diff --git a/.github/assets/project-lockup.svg b/.github/assets/project-lockup.svg
new file mode 100644
index 0000000..0bc54fa
--- /dev/null
+++ b/.github/assets/project-lockup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/assets/project-mark.svg b/.github/assets/project-mark.svg
new file mode 100644
index 0000000..4920640
--- /dev/null
+++ b/.github/assets/project-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/assets/readme-header.png b/.github/assets/readme-header.png
new file mode 100644
index 0000000..da3fbbb
Binary files /dev/null and b/.github/assets/readme-header.png differ
diff --git a/.github/assets/social-preview.png b/.github/assets/social-preview.png
new file mode 100644
index 0000000..c09747a
Binary files /dev/null and b/.github/assets/social-preview.png differ
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 46c41e0..64346c4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,6 +14,14 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Set up Node 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Verify O11 brand sources
+ run: node scripts/check-brand-assets.mjs
+
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5bb766e..a5add66 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,6 +13,14 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Set up Node 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Verify O11 brand sources
+ run: node scripts/check-brand-assets.mjs
+
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
diff --git a/README.ko.md b/README.ko.md
index 1a51299..8c93f5f 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -1,3 +1,5 @@
+[](https://devslab.kr/brand/open-source/)
+
# DataLinq
[English](README.md) | **한국어**
@@ -11,7 +13,7 @@ MariaDB/MySQL, PostgreSQL 드라이버는 **번들**되어 있고, 그 외(Oracl
명령 한 줄로 내려받습니다. 연결은 **DB 종류 + host/port/database**(또는 raw URL)로 설정하며,
`application.yml` 에 적거나 DB 연결 화면에서 바로 편집·저장할 수 있습니다.
-> **DevsLab Co., Ltd.** (주식회사 데브스랩) 제작 · https://devslab.kr · Apache-2.0
+> [DevsLab 오픈소스](https://devslab.kr/brand/open-source/) · **DevsLab Co., Ltd.** (주식회사 데브스랩) 제작 · https://devslab.kr · Apache-2.0
## 빠른 시작
diff --git a/README.md b/README.md
index 5bc63ed..03f4f26 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+[](https://devslab.kr/brand/open-source/)
+
# DataLinq
**English** | [한국어](README.ko.md)
@@ -11,7 +13,7 @@ MariaDB/MySQL and PostgreSQL drivers are **bundled**; others (Oracle, H2, SQLite
one-line `driver` download away. Connections are configured by **DB type + host/port/database**
(or a raw URL), in `application.yml` or live from the DB Connection screen.
-> By **DevsLab Co., Ltd.** (주식회사 데브스랩) · https://devslab.kr · Apache-2.0
+> [Open source by DevsLab](https://devslab.kr/brand/open-source/) · By **DevsLab Co., Ltd.** (주식회사 데브스랩) · https://devslab.kr · Apache-2.0
## Quick start
diff --git a/scripts/check-brand-assets.mjs b/scripts/check-brand-assets.mjs
new file mode 100644
index 0000000..e55cc5d
--- /dev/null
+++ b/scripts/check-brand-assets.mjs
@@ -0,0 +1,55 @@
+import assert from 'node:assert/strict';
+import { createHash } from 'node:crypto';
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
+const root = resolve(import.meta.dirname, '..');
+const guide = 'https://devslab.kr/brand/open-source/';
+const terminalLogo = 'DataLinq\n========\n[ source ] ===> [ target ]\nOpen source by DevsLab\n';
+const assets = new Map([
+ ['.github/assets/project-mark.svg', '6a5ddb30c2f0c6251d60a96f78b88f026e946aa7c5cda31284b29194f8482309'],
+ ['.github/assets/project-lockup.svg', '123d4746b874a955b6f4c4a9f1373a2b312b01016028703ff3257fcc0e0e2c6d'],
+ ['.github/assets/readme-header.png', 'cc5b10846a31c4c458ccd784fe158c83616368df0ffb645015561b059343267d'],
+ ['.github/assets/social-preview.png', '9b83784be89547d512a174853e7e8bbd56ea43975b076530ce930839b7067c3a'],
+ ['src/main/resources/branding/logo.txt', 'aa4aa8c9bcc2b07257f96854a481496b8229d28bc27057eedbadbce4fa1cc6d7'],
+]);
+
+const file = (relative) => resolve(root, relative);
+const text = (relative) => readFile(file(relative), 'utf8');
+const hash = async (relative) => createHash('sha256').update(await readFile(file(relative))).digest('hex');
+
+for (const [relative, expected] of assets) {
+ assert.equal(await hash(relative), expected, `${relative} must match the oss-brand v0.1.1 O11 asset`);
+}
+
+const mark = await text('.github/assets/project-mark.svg');
+assert.match(mark, /data-oss-project="O11"/, 'project mark must identify O11');
+assert.match(mark, /M5 6H11V12H5Z/, 'project mark must retain O11 source column geometry');
+assert.match(mark, /M17 12L21 16L17 20/, 'project mark must retain O11 transfer path geometry');
+
+assert.equal(await text('src/main/resources/branding/logo.txt'), terminalLogo, 'bundled terminal logo must be the stable UTF-8 no-color O11 asset');
+assert.ok(!terminalLogo.includes('\u001b'), 'bundled terminal logo must contain no ANSI escape character');
+assert.deepEqual(terminalLogo.trimEnd().split('\n').map((line) => line.length), [8, 8, 26, 22], 'terminal logo line widths must remain stable');
+
+for (const [relative, endorsement] of [
+ ['README.md', 'Open source by DevsLab'],
+ ['README.ko.md', 'DevsLab 오픈소스'],
+]) {
+ const readme = await text(relative);
+ assert.match(readme, /\.github\/assets\/readme-header\.png/, `${relative} must use the vendored O11 README header`);
+ assert.ok(readme.includes(guide), `${relative} must link to the canonical OSS brand guide`);
+ assert.ok(readme.includes(endorsement), `${relative} must include its localized DevsLab endorsement`);
+}
+
+const brandCommand = 'run: node scripts/check-brand-assets.mjs';
+for (const [relative, gradleCommand] of [
+ ['.github/workflows/ci.yml', 'run: ./gradlew build'],
+ ['.github/workflows/release.yml', 'run: ./gradlew shadowJar'],
+]) {
+ const workflow = await text(relative);
+ const brandIndex = workflow.indexOf(brandCommand);
+ assert.ok(workflow.includes('actions/setup-node@v4') && workflow.includes("node-version: '22'"), `${relative} must provide Node 22 for the brand checker`);
+ assert.ok(brandIndex >= 0 && brandIndex < workflow.indexOf(gradleCommand), `${relative} must verify O11 brand sources before Gradle`);
+}
+
+console.log(`O11 brand contract passed (${assets.size} exact v0.1.1 assets, terminal, README, CI, and release).`);
diff --git a/src/main/resources/branding/logo.txt b/src/main/resources/branding/logo.txt
index 9bab72a..fe97f59 100644
--- a/src/main/resources/branding/logo.txt
+++ b/src/main/resources/branding/logo.txt
@@ -1,6 +1,4 @@
- ____ __ __ _
- / __ \____ _/ /_____ _/ / (_)___ ____ _
- / / / / __ `/ __/ __ `/ / / / __ \/ __ `/
- / /_/ / /_/ / /_/ /_/ / /___/ / / / / /_/ /
-/_____/\__,_/\__/\__,_/_____/_/_/ /_/\__, /
- /_/
+DataLinq
+========
+[ source ] ===> [ target ]
+Open source by DevsLab
diff --git a/src/test/java/kr/devslab/datalinq/ui/LogoTest.java b/src/test/java/kr/devslab/datalinq/ui/LogoTest.java
new file mode 100644
index 0000000..3662678
--- /dev/null
+++ b/src/test/java/kr/devslab/datalinq/ui/LogoTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2026 DevsLab Co., Ltd.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package kr.devslab.datalinq.ui;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LogoTest {
+
+ @Test
+ void bundledNoColorLogoUsesStableAsciiUtf8Output(@TempDir Path tempDir) throws Exception {
+ List lines = Logo.load(tempDir.resolve("missing-logo.txt"));
+
+ assertEquals(List.of(
+ "DataLinq",
+ "========",
+ "[ source ] ===> [ target ]",
+ "Open source by DevsLab"), lines);
+ assertEquals(List.of(8, 8, 26, 22), lines.stream().map(String::length).toList());
+ assertEquals(26, lines.stream().mapToInt(String::length).max().orElseThrow());
+ assertTrue(lines.stream().flatMapToInt(String::chars).allMatch(codePoint -> codePoint <= 0x7f));
+ assertFalse(lines.stream().anyMatch(line -> line.indexOf('\u001b') >= 0));
+
+ String bundled = new String(Logo.class.getResourceAsStream("/branding/logo.txt").readAllBytes(), StandardCharsets.UTF_8);
+ assertEquals(String.join("\n", lines) + "\n", bundled);
+ }
+}