From 36a58799443ceb7a00cca47da8793ebf0d3cc867 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:36:25 +0800 Subject: [PATCH 01/22] =?UTF-8?q?refactor(test):=20=E9=9B=86=E4=B8=AD?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=20Profile=20=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/profile-contract.mjs | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/profile-contract.mjs diff --git a/scripts/profile-contract.mjs b/scripts/profile-contract.mjs new file mode 100644 index 0000000..cb9a427 --- /dev/null +++ b/scripts/profile-contract.mjs @@ -0,0 +1,99 @@ +import { + DSH_BASE_BUNDLE, + DSH_WEB_BUNDLE, + NARRATICA_BUNDLE, +} from './dsh-baseline.mjs' + +export const REQUIRED_PROFILE_BUNDLES = Object.freeze([ + DSH_BASE_BUNDLE, + DSH_WEB_BUNDLE, + NARRATICA_BUNDLE, +]) + +export const LEGACY_NARRATICA_BUNDLES = Object.freeze([ + '@narratica/bundle-core', + '@narratica/bundle-production', + '@narratica/bundle-app', +]) + +// 这里描述的是对 DSH 暴露的稳定装配契约,不描述入口包内部如何组织 Cordis Fiber。 +// Client 子插件数量、源码路径等内部实现变化,不应让发行/Profile 烟测失效。 +export const REQUIRED_NARRATICA_LOADER_IDS = Object.freeze([ + 'narratica-stories', + 'narratica-skill-pack', + 'narratica-providers', + 'narratica-media', + 'narratica-production', + 'narratica-story-tools', + 'narratica-client', +]) + +export const LEGACY_NARRATICA_CLIENT_LOADER_IDS = Object.freeze([ + 'narratica-client-runtime', + 'narratica-client-layout', + 'narratica-client-workspace', + 'narratica-client-story-library', + 'narratica-client-novel', + 'narratica-client-director', +]) + +function loaderIdPattern(id) { + return new RegExp(`(?:^|\\n)\\s*-\\s*id:\\s*${id}(?:\\s|$)`, 'm') +} + +export function assertFormalProfileContract(profile, { distribution = false } = {}) { + const dependencies = profile.dependencies ?? {} + const bundles = profile.dsh?.profile?.bundles ?? [] + + for (const required of REQUIRED_PROFILE_BUNDLES) { + if (!bundles.includes(required)) { + throw new Error(`正式 Profile 缺少 Bundle:${required}`) + } + } + + const narraticaBundles = bundles.filter(name => name.startsWith('@narratica/')) + if (narraticaBundles.length !== 1 || narraticaBundles[0] !== NARRATICA_BUNDLE) { + throw new Error(`正式 Profile 必须只有一个 Narratica Bundle:${narraticaBundles.join(' -> ') || '无'}`) + } + + for (const legacyBundle of LEGACY_NARRATICA_BUNDLES) { + if (bundles.includes(legacyBundle)) { + throw new Error(`正式 Profile 不应包含旧 Bundle:${legacyBundle}`) + } + } + + const positions = REQUIRED_PROFILE_BUNDLES.map(name => bundles.indexOf(name)) + if (!positions.every((position, index) => index === 0 || positions[index - 1] < position)) { + throw new Error(`正式 Profile Bundle 顺序异常:${bundles.join(' -> ')}`) + } + + if (distribution) { + const narraticaDirectDependencies = Object.keys(dependencies).filter(name => name.startsWith('@narratica/')) + if (narraticaDirectDependencies.length !== 1 || narraticaDirectDependencies[0] !== NARRATICA_BUNDLE) { + throw new Error(`发行 Profile 顶层只能依赖正式入口包:${narraticaDirectDependencies.join(' -> ') || '无'}`) + } + } +} + +export function assertComposedConfigContract(dump) { + for (const id of REQUIRED_NARRATICA_LOADER_IDS) { + if (!loaderIdPattern(id).test(dump)) { + throw new Error(`组合配置缺少正式 Loader:${id}`) + } + } + + for (const id of LEGACY_NARRATICA_CLIENT_LOADER_IDS) { + if (loaderIdPattern(id).test(dump)) { + throw new Error(`组合配置不应再出现旧 Client Loader:${id}`) + } + } + + if (!dump.includes(`name: '${NARRATICA_BUNDLE}'`)) { + throw new Error(`组合配置缺少正式 Narratica 入口:${NARRATICA_BUNDLE}`) + } +} + +export function assertNarraticaProfileContract({ profile, dump, distribution = false }) { + assertFormalProfileContract(profile, { distribution }) + assertComposedConfigContract(dump) +} From 0ec977db83dbd9a8899c0a07e8545f053767ead8 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:36:33 +0800 Subject: [PATCH 02/22] =?UTF-8?q?refactor(test):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20Profile=20=E5=A5=91=E7=BA=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/verify-profile-contract.mjs | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 scripts/verify-profile-contract.mjs diff --git a/scripts/verify-profile-contract.mjs b/scripts/verify-profile-contract.mjs new file mode 100644 index 0000000..8ef7adb --- /dev/null +++ b/scripts/verify-profile-contract.mjs @@ -0,0 +1,31 @@ +import { spawnSync } from 'node:child_process' +import { readFile, writeFile } from 'node:fs/promises' +import { homedir } from 'node:os' +import { join, resolve } from 'node:path' + +import { DSH_PROFILE } from './dsh-baseline.mjs' +import { assertNarraticaProfileContract } from './profile-contract.mjs' + +const outputPath = process.argv[2] ? resolve(process.argv[2]) : null +const distribution = process.argv.includes('--distribution') +const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh') +const profilePackagePath = join(dshHome, 'profiles', DSH_PROFILE, 'package.json') +const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + +const result = spawnSync(pnpm, ['exec', 'dsh', '--profile', DSH_PROFILE, '--dump-config'], { + encoding: 'utf8', + env: process.env, + shell: process.platform === 'win32', +}) + +if (result.error) throw result.error +if (result.status !== 0) { + throw new Error(`DSH --dump-config 失败 (${result.status})\n${result.stdout ?? ''}\n${result.stderr ?? ''}`) +} + +const dump = `${result.stdout ?? ''}${result.stderr ?? ''}` +const profile = JSON.parse(await readFile(profilePackagePath, 'utf8')) +assertNarraticaProfileContract({ profile, dump, distribution }) + +if (outputPath) await writeFile(outputPath, dump) +console.log(`正式 Profile 契约通过:${profilePackagePath}`) From ce6ded96d913bd70b7b08b0aa6d579d45922d38f Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:36:59 +0800 Subject: [PATCH 03/22] =?UTF-8?q?refactor(test):=20=E6=9A=B4=E9=9C=B2?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20Profile=20=E5=A5=91=E7=BA=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1e6558b..cfac7fe 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build": "pnpm run verify:syntax && pnpm run build:host && pnpm run generate:typert && pnpm run build:client:types && pnpm run build:client:bundles", "typecheck": "pnpm run verify:syntax && pnpm run build:host && pnpm run generate:typert && pnpm run build:client:types", "verify:dsh-baseline": "node scripts/verify-dsh-baseline.mjs", + "verify:profile-contract": "node scripts/verify-profile-contract.mjs", "profile:bootstrap": "pnpm run build && node scripts/bootstrap-profile.mjs", "prestart": "pnpm run build", "start": "dsh --profile narratica --patch tests/probes/mode1-novel-workspace.patch.yml", From a11a31abf188e8c01091b643656604203e325063 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:37:14 +0800 Subject: [PATCH 04/22] =?UTF-8?q?refactor(test):=20=E4=B8=8B=E6=B2=89?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=80=20Profile=20=E6=8E=A2=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/verify-mode1-profile-probe.mjs | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 scripts/verify-mode1-profile-probe.mjs diff --git a/scripts/verify-mode1-profile-probe.mjs b/scripts/verify-mode1-profile-probe.mjs new file mode 100644 index 0000000..a7a12e2 --- /dev/null +++ b/scripts/verify-mode1-profile-probe.mjs @@ -0,0 +1,31 @@ +import { spawnSync } from 'node:child_process' +import { writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const outputPath = process.argv[2] ? resolve(process.argv[2]) : null +const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + +const result = spawnSync(pnpm, ['run', 'profile:mode1:dump'], { + encoding: 'utf8', + env: process.env, + shell: process.platform === 'win32', +}) + +if (result.error) throw result.error +if (result.status !== 0) { + throw new Error(`模式一 --dump-config 失败 (${result.status})\n${result.stdout ?? ''}\n${result.stderr ?? ''}`) +} + +const dump = `${result.stdout ?? ''}${result.stderr ?? ''}` +const expectedEvidence = [ + 'tests/fixtures/story-repository', + 'narratica-skill-filesystem', + 'providerName: narratica-novel', +] + +for (const evidence of expectedEvidence) { + if (!dump.includes(evidence)) throw new Error(`模式一 Profile 探针缺少证据:${evidence}`) +} + +if (outputPath) await writeFile(outputPath, dump) +console.log('模式一 Story Repository 与 Skill Provider 探针通过') From f0f263bd064574b75d123c5b6155b9e3830319b3 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:37:31 +0800 Subject: [PATCH 05/22] =?UTF-8?q?refactor(test):=20=E6=9A=B4=E9=9C=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=80=20Profile=20=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cfac7fe..d488de6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "typecheck": "pnpm run verify:syntax && pnpm run build:host && pnpm run generate:typert && pnpm run build:client:types", "verify:dsh-baseline": "node scripts/verify-dsh-baseline.mjs", "verify:profile-contract": "node scripts/verify-profile-contract.mjs", + "verify:mode1-profile": "node scripts/verify-mode1-profile-probe.mjs", "profile:bootstrap": "pnpm run build && node scripts/bootstrap-profile.mjs", "prestart": "pnpm run build", "start": "dsh --profile narratica --patch tests/probes/mode1-novel-workspace.patch.yml", From b73680dc09897dbe6569caceb1880b7aeeb7a974 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:37:52 +0800 Subject: [PATCH 06/22] =?UTF-8?q?refactor(ci):=20=E7=A7=BB=E9=99=A4=20Work?= =?UTF-8?q?flow=20=E5=86=85=E6=9E=B6=E6=9E=84=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 38 +++++--------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8c7923..c9bb3b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,48 +128,20 @@ jobs: - name: 按锁文件安装依赖 run: pnpm install --frozen-lockfile - - name: 创建并验证正式 Narratica Profile + - name: 创建正式 Narratica Profile env: DSH_HOME: ${{ runner.temp }}/narratica-dsh-home - run: pnpm run profile:bootstrap > "$RUNNER_TEMP/narratica-dump-config.txt" + run: pnpm run profile:bootstrap - - name: 确认 Profile 只有一个正式 Narratica Bundle + - name: 验证正式 Profile 装配契约 env: DSH_HOME: ${{ runner.temp }}/narratica-dsh-home - run: | - node - <<'NODE' - const fs = require('node:fs') - const path = require('node:path') - const profile = JSON.parse(fs.readFileSync(path.join(process.env.DSH_HOME, 'profiles', 'narratica', 'package.json'), 'utf8')) - const bundles = profile.dsh?.profile?.bundles ?? [] - const expected = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@narratica/narratica'] - for (const name of expected) if (!bundles.includes(name)) throw new Error(`缺少 Bundle: ${name}`) - for (const name of ['@narratica/bundle-core', '@narratica/bundle-production', '@narratica/bundle-app']) if (bundles.includes(name)) throw new Error(`正式 Profile 不应包含旧 Bundle: ${name}`) - console.log(bundles.join(' -> ')) - NODE - - - name: 确认 DSH 宿主与 Narratica 插件同时进入组合配置 - run: | - for plugin in ui-layout ui-sidebar ui-conversation ui-settings narratica-stories narratica-skill-pack narratica-providers narratica-media narratica-production narratica-story-tools narratica-client; do - grep -q -- "- id: $plugin" "$RUNNER_TEMP/narratica-dump-config.txt" - done - grep -q "name: '@narratica/narratica'" "$RUNNER_TEMP/narratica-dump-config.txt" - for legacy_client in narratica-client-runtime narratica-client-layout narratica-client-workspace narratica-client-story-library narratica-client-novel narratica-client-director; do - if grep -q -- "- id: $legacy_client" "$RUNNER_TEMP/narratica-dump-config.txt"; then - echo "正式 Profile 不应再包含旧 Client Loader row: $legacy_client" - exit 1 - fi - done + run: pnpm run verify:profile-contract -- "$RUNNER_TEMP/narratica-dump-config.txt" - name: 验证模式一 Story Repository 与 Skill Provider 探针 env: DSH_HOME: ${{ runner.temp }}/narratica-dsh-home - run: | - pnpm run profile:mode1:dump > "$RUNNER_TEMP/narratica-mode1-dump-config.txt" - grep -q "tests/fixtures/story-repository" "$RUNNER_TEMP/narratica-mode1-dump-config.txt" - grep -q "narratica-skill-pack" "$RUNNER_TEMP/narratica-mode1-dump-config.txt" - grep -q "narratica-skill-filesystem" "$RUNNER_TEMP/narratica-mode1-dump-config.txt" - grep -q "providerName: narratica-novel" "$RUNNER_TEMP/narratica-mode1-dump-config.txt" + run: pnpm run verify:mode1-profile -- "$RUNNER_TEMP/narratica-mode1-dump-config.txt" - name: 启动正式 DSH Web 并探测首页 env: From 2ad8eba70c235c6f266f3c1064335d42cefa18fc Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:38:23 +0800 Subject: [PATCH 07/22] =?UTF-8?q?refactor(release):=20=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20Profile=20=E8=A3=85=E9=85=8D=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/release/smoke-release.mjs | 38 ++----------------------------- 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/scripts/release/smoke-release.mjs b/scripts/release/smoke-release.mjs index 67fdba2..3c2e14a 100644 --- a/scripts/release/smoke-release.mjs +++ b/scripts/release/smoke-release.mjs @@ -3,6 +3,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { resolve } from 'node:path' +import { assertNarraticaProfileContract } from '../profile-contract.mjs' import { DSH_VERSION, ENTRY_PACKAGE, @@ -158,19 +159,6 @@ if (mode === 'local') { const profile = JSON.parse(await readFile(profilePackagePath, 'utf8')) const dependencies = profile.dependencies ?? {} const bundles = profile.dsh?.profile?.bundles ?? [] -const requiredBundles = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', ENTRY_PACKAGE] -for (const name of requiredBundles) { - if (!bundles.includes(name)) throw new Error(`发行烟测 Profile 缺少 Bundle:${name}`) -} -const narraticaBundles = bundles.filter(name => name.startsWith('@narratica/')) -if (narraticaBundles.length !== 1 || narraticaBundles[0] !== ENTRY_PACKAGE) { - throw new Error(`发行烟测必须只有一个 Narratica Bundle:${narraticaBundles.join(' -> ')}`) -} - -const narraticaDirectDependencies = Object.keys(dependencies).filter(name => name.startsWith('@narratica/')) -if (narraticaDirectDependencies.length !== 1 || narraticaDirectDependencies[0] !== ENTRY_PACKAGE) { - throw new Error(`发行烟测 Profile 顶层只能依赖正式入口包:${narraticaDirectDependencies.join(' -> ') || '无'}`) -} if (mode === 'local') { const policy = await readFile(workspacePolicyPath, 'utf8') @@ -187,29 +175,7 @@ if (mode === 'local') { } const dump = run(['exec', 'dsh', '--profile', 'narratica', '--dump-config']) -const expectedLoaderIds = [ - 'narratica-stories', - 'narratica-skill-pack', - 'narratica-providers', - 'narratica-media', - 'narratica-production', - 'narratica-client', -] -const legacyClientLoaderIds = [ - 'narratica-client-runtime', - 'narratica-client-layout', - 'narratica-client-workspace', - 'narratica-client-story-library', - 'narratica-client-novel', - 'narratica-client-director', -] -const loaderIdPattern = id => new RegExp(`(?:^|\\n)\\s*-\\s*id:\\s*${id}(?:\\s|$)`, 'm') -for (const id of expectedLoaderIds) { - if (!loaderIdPattern(id).test(dump)) throw new Error(`发行烟测组合配置缺少 Loader:${id}`) -} -for (const id of legacyClientLoaderIds) { - if (loaderIdPattern(id).test(dump)) throw new Error(`发行烟测不应再出现旧 Client Loader:${id}`) -} +assertNarraticaProfileContract({ profile, dump, distribution: true }) const chunks = [] const child = spawn(pnpm, ['exec', 'dsh', '--profile', 'narratica', '--port', '3189', '--no-open'], { From ac20a35ca51e01ee464b21e4a3d8295ac4fc5c7e Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:38:47 +0800 Subject: [PATCH 08/22] =?UTF-8?q?test(architecture):=20=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=20Profile=20=E5=A5=91=E7=BA=A6=E5=86=8D=E6=AC=A1=E6=95=A3?= =?UTF-8?q?=E8=90=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile-contract-governance.test.mjs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/architecture/profile-contract-governance.test.mjs diff --git a/tests/architecture/profile-contract-governance.test.mjs b/tests/architecture/profile-contract-governance.test.mjs new file mode 100644 index 0000000..dec5d0a --- /dev/null +++ b/tests/architecture/profile-contract-governance.test.mjs @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import test from 'node:test' + +import { readFile } from './read-text.mjs' +import { + LEGACY_NARRATICA_CLIENT_LOADER_IDS, + REQUIRED_NARRATICA_LOADER_IDS, + assertNarraticaProfileContract, +} from '../../scripts/profile-contract.mjs' + +test('正式 Profile 契约只描述对外 Loader,不绑定入口内部 Client Fiber', () => { + assert.ok(REQUIRED_NARRATICA_LOADER_IDS.includes('narratica-client')) + for (const legacyId of LEGACY_NARRATICA_CLIENT_LOADER_IDS) { + assert.ok(!REQUIRED_NARRATICA_LOADER_IDS.includes(legacyId)) + } +}) + +test('统一契约同时覆盖单 Bundle、发行单入口依赖、正式 Loader 与旧 Loader 禁止项', () => { + const profile = { + dependencies: { + '@narratica/narratica': '0.1.0-alpha.2', + }, + dsh: { + profile: { + bundles: [ + '@deepseek-ai/dsh-base', + '@deepseek-ai/dsh-web-app', + '@narratica/narratica', + ], + }, + }, + } + const dump = REQUIRED_NARRATICA_LOADER_IDS + .map(id => `- id: ${id}`) + .concat(" name: '@narratica/narratica'") + .join('\n') + + assert.doesNotThrow(() => assertNarraticaProfileContract({ profile, dump, distribution: true })) + + const legacyDump = `${dump}\n- id: narratica-client-runtime` + assert.throws( + () => assertNarraticaProfileContract({ profile, dump: legacyDump, distribution: true }), + /旧 Client Loader/, + ) + + const leakedProfile = { + ...profile, + dependencies: { + ...profile.dependencies, + '@narratica/client-runtime': '0.1.0-alpha.2', + }, + } + assert.throws( + () => assertNarraticaProfileContract({ profile: leakedProfile, dump, distribution: true }), + /顶层只能依赖正式入口包/, + ) +}) + +test('CI Workflow 只负责调用统一校验脚本,不再维护 Narratica Loader/Bundle 名单', async () => { + const workflow = await readFile('.github/workflows/ci.yml', 'utf8') + assert.match(workflow, /pnpm run verify:profile-contract/) + assert.match(workflow, /pnpm run verify:mode1-profile/) + assert.doesNotMatch(workflow, /narratica-client-runtime/) + assert.doesNotMatch(workflow, /for plugin in .*narratica-/) + assert.doesNotMatch(workflow, /grep -q .*narratica-/) +}) + +test('发行烟测复用统一 Profile 契约,不再维护独立 Loader 名单', async () => { + const smoke = await readFile('scripts/release/smoke-release.mjs', 'utf8') + assert.match(smoke, /assertNarraticaProfileContract/) + assert.match(smoke, /distribution: true/) + assert.doesNotMatch(smoke, /expectedLoaderIds/) + assert.doesNotMatch(smoke, /legacyClientLoaderIds/) +}) + +test('新增 Profile 契约脚本通过 Node 语法检查', () => { + for (const script of [ + 'scripts/profile-contract.mjs', + 'scripts/verify-profile-contract.mjs', + 'scripts/verify-mode1-profile-probe.mjs', + ]) { + const result = spawnSync(process.execPath, ['--check', script], { encoding: 'utf8' }) + assert.equal(result.status, 0, `${script} 语法错误:${result.stderr || result.stdout}`) + } +}) From c97c180b2d1f0351a801c21b7125b890b9dead54 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:39:07 +0800 Subject: [PATCH 09/22] =?UTF-8?q?ci(release):=20Profile=20=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E5=8F=98=E5=8C=96=E5=BF=85=E9=A1=BB=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=8F=91=E8=A1=8C=E9=A2=84=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-pack-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release-pack-check.yml b/.github/workflows/release-pack-check.yml index 922db13..1f52054 100644 --- a/.github/workflows/release-pack-check.yml +++ b/.github/workflows/release-pack-check.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - 'scripts/release/**' + - 'scripts/profile-contract.mjs' + - 'scripts/dsh-baseline.mjs' - 'packages/**/package.json' - 'package.json' - 'pnpm-lock.yaml' @@ -14,6 +16,8 @@ on: - main paths: - 'scripts/release/**' + - 'scripts/profile-contract.mjs' + - 'scripts/dsh-baseline.mjs' - 'packages/**/package.json' - 'package.json' - 'pnpm-lock.yaml' From b0d2f2d3222e3585dfc72219d00f7e91907034aa Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:39:38 +0800 Subject: [PATCH 10/22] =?UTF-8?q?refactor(profile):=20=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=AD=A3=E5=BC=8F=20Profile=20=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/bootstrap-profile.mjs | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/scripts/bootstrap-profile.mjs b/scripts/bootstrap-profile.mjs index d39035c..6d23749 100644 --- a/scripts/bootstrap-profile.mjs +++ b/scripts/bootstrap-profile.mjs @@ -5,12 +5,11 @@ import { fileURLToPath } from 'node:url' import { spawnSync } from 'node:child_process' import { - DSH_BASE_BUNDLE, DSH_PROFILE, DSH_VERSION, DSH_WEB_BUNDLE, - NARRATICA_BUNDLE, } from './dsh-baseline.mjs' +import { assertFormalProfileContract } from './profile-contract.mjs' const scriptDir = dirname(fileURLToPath(import.meta.url)) const repoRoot = resolve(scriptDir, '..') @@ -130,37 +129,7 @@ for (const [packageName] of localProfilePlainDependencies) { } } -const requiredBundles = [ - DSH_BASE_BUNDLE, - DSH_WEB_BUNDLE, - NARRATICA_BUNDLE, -] - -for (const required of requiredBundles) { - if (!bundles.includes(required)) { - throw new Error(`Narratica profile is missing required bundle: ${required}`) - } -} - -const narraticaBundles = bundles.filter(name => name.startsWith('@narratica/')) -if (narraticaBundles.length !== 1 || narraticaBundles[0] !== NARRATICA_BUNDLE) { - throw new Error(`Formal Profile must expose exactly one Narratica Bundle: ${narraticaBundles.join(' -> ')}`) -} - -for (const legacyBundle of [ - '@narratica/bundle-core', - '@narratica/bundle-production', - '@narratica/bundle-app', -]) { - if (bundles.includes(legacyBundle)) { - throw new Error(`Formal Narratica profile must not expose legacy bundle: ${legacyBundle}`) - } -} - -const positions = requiredBundles.map(name => bundles.indexOf(name)) -if (!positions.every((position, index) => index === 0 || positions[index - 1] < position)) { - throw new Error(`Unexpected Narratica bundle order: ${bundles.join(' -> ')}`) -} +assertFormalProfileContract(profilePackage) run(pnpm, ['exec', 'dsh', '--profile', DSH_PROFILE, '--dump-config']) console.log(`Narratica profile ready: ${profilePackagePath}`) From 1427b153f5e2c3d4d8246c18d0037825d8bd8914 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:41:15 +0800 Subject: [PATCH 11/22] =?UTF-8?q?test(contract):=20=E4=BF=9D=E7=95=99=20DS?= =?UTF-8?q?H=20=E5=AE=BF=E4=B8=BB=E7=BB=84=E5=90=88=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/profile-contract.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/profile-contract.mjs b/scripts/profile-contract.mjs index cb9a427..02b113d 100644 --- a/scripts/profile-contract.mjs +++ b/scripts/profile-contract.mjs @@ -16,6 +16,13 @@ export const LEGACY_NARRATICA_BUNDLES = Object.freeze([ '@narratica/bundle-app', ]) +export const REQUIRED_DSH_HOST_LOADER_IDS = Object.freeze([ + 'ui-layout', + 'ui-sidebar', + 'ui-conversation', + 'ui-settings', +]) + // 这里描述的是对 DSH 暴露的稳定装配契约,不描述入口包内部如何组织 Cordis Fiber。 // Client 子插件数量、源码路径等内部实现变化,不应让发行/Profile 烟测失效。 export const REQUIRED_NARRATICA_LOADER_IDS = Object.freeze([ @@ -76,7 +83,7 @@ export function assertFormalProfileContract(profile, { distribution = false } = } export function assertComposedConfigContract(dump) { - for (const id of REQUIRED_NARRATICA_LOADER_IDS) { + for (const id of [...REQUIRED_DSH_HOST_LOADER_IDS, ...REQUIRED_NARRATICA_LOADER_IDS]) { if (!loaderIdPattern(id).test(dump)) { throw new Error(`组合配置缺少正式 Loader:${id}`) } From 315122bf9af1e23d4210f142616e117294fc2f95 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:41:31 +0800 Subject: [PATCH 12/22] =?UTF-8?q?test(contract):=20=E8=A6=86=E7=9B=96=20DS?= =?UTF-8?q?H=20Host=20Loader=20=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile-contract-governance.test.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/architecture/profile-contract-governance.test.mjs b/tests/architecture/profile-contract-governance.test.mjs index dec5d0a..6d294c7 100644 --- a/tests/architecture/profile-contract-governance.test.mjs +++ b/tests/architecture/profile-contract-governance.test.mjs @@ -5,18 +5,20 @@ import test from 'node:test' import { readFile } from './read-text.mjs' import { LEGACY_NARRATICA_CLIENT_LOADER_IDS, + REQUIRED_DSH_HOST_LOADER_IDS, REQUIRED_NARRATICA_LOADER_IDS, assertNarraticaProfileContract, } from '../../scripts/profile-contract.mjs' test('正式 Profile 契约只描述对外 Loader,不绑定入口内部 Client Fiber', () => { + assert.ok(REQUIRED_DSH_HOST_LOADER_IDS.includes('ui-layout')) assert.ok(REQUIRED_NARRATICA_LOADER_IDS.includes('narratica-client')) for (const legacyId of LEGACY_NARRATICA_CLIENT_LOADER_IDS) { assert.ok(!REQUIRED_NARRATICA_LOADER_IDS.includes(legacyId)) } }) -test('统一契约同时覆盖单 Bundle、发行单入口依赖、正式 Loader 与旧 Loader 禁止项', () => { +test('统一契约同时覆盖 DSH Host、单 Bundle、发行单入口、正式 Loader 与旧 Loader 禁止项', () => { const profile = { dependencies: { '@narratica/narratica': '0.1.0-alpha.2', @@ -31,13 +33,19 @@ test('统一契约同时覆盖单 Bundle、发行单入口依赖、正式 Loader }, }, } - const dump = REQUIRED_NARRATICA_LOADER_IDS + const dump = [...REQUIRED_DSH_HOST_LOADER_IDS, ...REQUIRED_NARRATICA_LOADER_IDS] .map(id => `- id: ${id}`) .concat(" name: '@narratica/narratica'") .join('\n') assert.doesNotThrow(() => assertNarraticaProfileContract({ profile, dump, distribution: true })) + const missingHostDump = dump.replace('- id: ui-layout\n', '') + assert.throws( + () => assertNarraticaProfileContract({ profile, dump: missingHostDump, distribution: true }), + /ui-layout/, + ) + const legacyDump = `${dump}\n- id: narratica-client-runtime` assert.throws( () => assertNarraticaProfileContract({ profile, dump: legacyDump, distribution: true }), From be748a32238afadc401a9dbedb09cf7f3fd09f07 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:42:47 +0800 Subject: [PATCH 13/22] =?UTF-8?q?refactor(contract):=20=E4=BB=8E=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=20Bundle=20patch=20=E6=8E=A8=E5=AF=BC=20Loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/profile-contract.mjs | 42 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/scripts/profile-contract.mjs b/scripts/profile-contract.mjs index 02b113d..5808a34 100644 --- a/scripts/profile-contract.mjs +++ b/scripts/profile-contract.mjs @@ -1,9 +1,16 @@ +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + import { DSH_BASE_BUNDLE, DSH_WEB_BUNDLE, NARRATICA_BUNDLE, } from './dsh-baseline.mjs' +const scriptDir = dirname(fileURLToPath(import.meta.url)) +const formalBundlePatchPath = resolve(scriptDir, '../packages/bundle/narratica/cordis.patch.yml') + export const REQUIRED_PROFILE_BUNDLES = Object.freeze([ DSH_BASE_BUNDLE, DSH_WEB_BUNDLE, @@ -23,18 +30,6 @@ export const REQUIRED_DSH_HOST_LOADER_IDS = Object.freeze([ 'ui-settings', ]) -// 这里描述的是对 DSH 暴露的稳定装配契约,不描述入口包内部如何组织 Cordis Fiber。 -// Client 子插件数量、源码路径等内部实现变化,不应让发行/Profile 烟测失效。 -export const REQUIRED_NARRATICA_LOADER_IDS = Object.freeze([ - 'narratica-stories', - 'narratica-skill-pack', - 'narratica-providers', - 'narratica-media', - 'narratica-production', - 'narratica-story-tools', - 'narratica-client', -]) - export const LEGACY_NARRATICA_CLIENT_LOADER_IDS = Object.freeze([ 'narratica-client-runtime', 'narratica-client-layout', @@ -48,6 +43,20 @@ function loaderIdPattern(id) { return new RegExp(`(?:^|\\n)\\s*-\\s*id:\\s*${id}(?:\\s|$)`, 'm') } +export async function loadFormalNarraticaLoaderIds() { + const patch = await readFile(formalBundlePatchPath, 'utf8') + const ids = [...patch.matchAll(/^\s*-\s*id:\s*(narratica-[A-Za-z0-9-]+)\s*$/gm)].map(match => match[1]) + const unique = [...new Set(ids)] + + if (unique.length !== ids.length) throw new Error('正式 Bundle patch 存在重复 Narratica Loader ID') + if (!unique.includes('narratica-client')) throw new Error('正式 Bundle patch 缺少唯一 Narratica Client Loader') + for (const legacyId of LEGACY_NARRATICA_CLIENT_LOADER_IDS) { + if (unique.includes(legacyId)) throw new Error(`正式 Bundle patch 不应再声明旧 Client Loader:${legacyId}`) + } + + return Object.freeze(unique) +} + export function assertFormalProfileContract(profile, { distribution = false } = {}) { const dependencies = profile.dependencies ?? {} const bundles = profile.dsh?.profile?.bundles ?? [] @@ -82,8 +91,9 @@ export function assertFormalProfileContract(profile, { distribution = false } = } } -export function assertComposedConfigContract(dump) { - for (const id of [...REQUIRED_DSH_HOST_LOADER_IDS, ...REQUIRED_NARRATICA_LOADER_IDS]) { +export async function assertComposedConfigContract(dump) { + const formalNarraticaLoaderIds = await loadFormalNarraticaLoaderIds() + for (const id of [...REQUIRED_DSH_HOST_LOADER_IDS, ...formalNarraticaLoaderIds]) { if (!loaderIdPattern(id).test(dump)) { throw new Error(`组合配置缺少正式 Loader:${id}`) } @@ -100,7 +110,7 @@ export function assertComposedConfigContract(dump) { } } -export function assertNarraticaProfileContract({ profile, dump, distribution = false }) { +export async function assertNarraticaProfileContract({ profile, dump, distribution = false }) { assertFormalProfileContract(profile, { distribution }) - assertComposedConfigContract(dump) + await assertComposedConfigContract(dump) } From 2844350459b506c269fde4381bde3e08bff43dee Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:43:01 +0800 Subject: [PATCH 14/22] =?UTF-8?q?refactor(contract):=20=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=20patch=20=E5=A5=91=E7=BA=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/verify-profile-contract.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-profile-contract.mjs b/scripts/verify-profile-contract.mjs index 8ef7adb..e5e41dc 100644 --- a/scripts/verify-profile-contract.mjs +++ b/scripts/verify-profile-contract.mjs @@ -25,7 +25,7 @@ if (result.status !== 0) { const dump = `${result.stdout ?? ''}${result.stderr ?? ''}` const profile = JSON.parse(await readFile(profilePackagePath, 'utf8')) -assertNarraticaProfileContract({ profile, dump, distribution }) +await assertNarraticaProfileContract({ profile, dump, distribution }) if (outputPath) await writeFile(outputPath, dump) console.log(`正式 Profile 契约通过:${profilePackagePath}`) From e4ab0793e0bfdb70b548df9581160134fd85fab8 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:43:28 +0800 Subject: [PATCH 15/22] =?UTF-8?q?refactor(release):=20=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=20patch=20=E5=A5=91=E7=BA=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/release/smoke-release.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/smoke-release.mjs b/scripts/release/smoke-release.mjs index 3c2e14a..9d17135 100644 --- a/scripts/release/smoke-release.mjs +++ b/scripts/release/smoke-release.mjs @@ -175,7 +175,7 @@ if (mode === 'local') { } const dump = run(['exec', 'dsh', '--profile', 'narratica', '--dump-config']) -assertNarraticaProfileContract({ profile, dump, distribution: true }) +await assertNarraticaProfileContract({ profile, dump, distribution: true }) const chunks = [] const child = spawn(pnpm, ['exec', 'dsh', '--profile', 'narratica', '--port', '3189', '--no-open'], { From e1ceacd3535be1e76b65a2fbc771bb43aa9ef76a Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:43:47 +0800 Subject: [PATCH 16/22] =?UTF-8?q?test(contract):=20=E4=BB=A5=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=20Bundle=20patch=20=E4=BD=9C=E4=B8=BA=20Loader=20?= =?UTF-8?q?=E7=9C=9F=E7=9B=B8=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile-contract-governance.test.mjs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/architecture/profile-contract-governance.test.mjs b/tests/architecture/profile-contract-governance.test.mjs index 6d294c7..74f1932 100644 --- a/tests/architecture/profile-contract-governance.test.mjs +++ b/tests/architecture/profile-contract-governance.test.mjs @@ -6,19 +6,22 @@ import { readFile } from './read-text.mjs' import { LEGACY_NARRATICA_CLIENT_LOADER_IDS, REQUIRED_DSH_HOST_LOADER_IDS, - REQUIRED_NARRATICA_LOADER_IDS, assertNarraticaProfileContract, + loadFormalNarraticaLoaderIds, } from '../../scripts/profile-contract.mjs' -test('正式 Profile 契约只描述对外 Loader,不绑定入口内部 Client Fiber', () => { - assert.ok(REQUIRED_DSH_HOST_LOADER_IDS.includes('ui-layout')) - assert.ok(REQUIRED_NARRATICA_LOADER_IDS.includes('narratica-client')) +test('Narratica Loader 集合从正式 Bundle patch 推导,不维护第二份 expected 列表', async () => { + const formalLoaderIds = await loadFormalNarraticaLoaderIds() + assert.ok(formalLoaderIds.includes('narratica-client')) + assert.ok(formalLoaderIds.includes('narratica-stories')) + assert.ok(formalLoaderIds.includes('narratica-director-model-policy')) for (const legacyId of LEGACY_NARRATICA_CLIENT_LOADER_IDS) { - assert.ok(!REQUIRED_NARRATICA_LOADER_IDS.includes(legacyId)) + assert.ok(!formalLoaderIds.includes(legacyId)) } }) -test('统一契约同时覆盖 DSH Host、单 Bundle、发行单入口、正式 Loader 与旧 Loader 禁止项', () => { +test('统一契约同时覆盖 DSH Host、单 Bundle、发行单入口、正式 patch Loader 与旧 Loader 禁止项', async () => { + const formalLoaderIds = await loadFormalNarraticaLoaderIds() const profile = { dependencies: { '@narratica/narratica': '0.1.0-alpha.2', @@ -33,21 +36,27 @@ test('统一契约同时覆盖 DSH Host、单 Bundle、发行单入口、正式 }, }, } - const dump = [...REQUIRED_DSH_HOST_LOADER_IDS, ...REQUIRED_NARRATICA_LOADER_IDS] + const dump = [...REQUIRED_DSH_HOST_LOADER_IDS, ...formalLoaderIds] .map(id => `- id: ${id}`) .concat(" name: '@narratica/narratica'") .join('\n') - assert.doesNotThrow(() => assertNarraticaProfileContract({ profile, dump, distribution: true })) + await assert.doesNotReject(() => assertNarraticaProfileContract({ profile, dump, distribution: true })) const missingHostDump = dump.replace('- id: ui-layout\n', '') - assert.throws( + await assert.rejects( () => assertNarraticaProfileContract({ profile, dump: missingHostDump, distribution: true }), /ui-layout/, ) + const missingFormalLoaderDump = dump.replace('- id: narratica-director-model-policy\n', '') + await assert.rejects( + () => assertNarraticaProfileContract({ profile, dump: missingFormalLoaderDump, distribution: true }), + /narratica-director-model-policy/, + ) + const legacyDump = `${dump}\n- id: narratica-client-runtime` - assert.throws( + await assert.rejects( () => assertNarraticaProfileContract({ profile, dump: legacyDump, distribution: true }), /旧 Client Loader/, ) @@ -59,7 +68,7 @@ test('统一契约同时覆盖 DSH Host、单 Bundle、发行单入口、正式 '@narratica/client-runtime': '0.1.0-alpha.2', }, } - assert.throws( + await assert.rejects( () => assertNarraticaProfileContract({ profile: leakedProfile, dump, distribution: true }), /顶层只能依赖正式入口包/, ) From 1ce89c488d37439148862d2b2fc6c0b3dbd47c84 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:45:33 +0800 Subject: [PATCH 17/22] =?UTF-8?q?ci(release):=20=E6=AD=A3=E5=BC=8F=20Bundl?= =?UTF-8?q?e=20patch=20=E5=8F=98=E5=8C=96=E5=BF=85=E9=A1=BB=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=8F=91=E8=A1=8C=E9=A2=84=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-pack-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-pack-check.yml b/.github/workflows/release-pack-check.yml index 1f52054..c1626d7 100644 --- a/.github/workflows/release-pack-check.yml +++ b/.github/workflows/release-pack-check.yml @@ -6,6 +6,7 @@ on: - 'scripts/release/**' - 'scripts/profile-contract.mjs' - 'scripts/dsh-baseline.mjs' + - 'packages/bundle/narratica/cordis.patch.yml' - 'packages/**/package.json' - 'package.json' - 'pnpm-lock.yaml' @@ -18,6 +19,7 @@ on: - 'scripts/release/**' - 'scripts/profile-contract.mjs' - 'scripts/dsh-baseline.mjs' + - 'packages/bundle/narratica/cordis.patch.yml' - 'packages/**/package.json' - 'package.json' - 'pnpm-lock.yaml' From 247eb9164b4e759a1121859bf6df18ba9ed0efec Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:45:51 +0800 Subject: [PATCH 18/22] =?UTF-8?q?test(ci):=20=E9=98=B2=E6=AD=A2=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=20patch=20=E5=8F=98=E5=8C=96=E6=BC=8F=E8=B7=91?= =?UTF-8?q?=E5=8F=91=E8=A1=8C=E9=A2=84=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/profile-contract-governance.test.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/architecture/profile-contract-governance.test.mjs b/tests/architecture/profile-contract-governance.test.mjs index 74f1932..030cf3b 100644 --- a/tests/architecture/profile-contract-governance.test.mjs +++ b/tests/architecture/profile-contract-governance.test.mjs @@ -91,6 +91,17 @@ test('发行烟测复用统一 Profile 契约,不再维护独立 Loader 名单 assert.doesNotMatch(smoke, /legacyClientLoaderIds/) }) +test('正式 Bundle patch 与 Profile 契约变化必须触发发行打包预检', async () => { + const workflow = await readFile('.github/workflows/release-pack-check.yml', 'utf8') + for (const path of [ + "scripts/profile-contract.mjs", + "scripts/dsh-baseline.mjs", + "packages/bundle/narratica/cordis.patch.yml", + ]) { + assert.ok(workflow.split(`'${path}'`).length >= 3, `pull_request/push 都必须监听:${path}`) + } +}) + test('新增 Profile 契约脚本通过 Node 语法检查', () => { for (const script of [ 'scripts/profile-contract.mjs', From aefc9a5602050e3073a2826335c09e73a1760fad Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:47:40 +0800 Subject: [PATCH 19/22] =?UTF-8?q?test(release):=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=8F=91=E8=A1=8C=E5=A5=91=E7=BA=A6=E8=80=8C=E9=9D=9E=E5=B1=80?= =?UTF-8?q?=E9=83=A8=E5=AE=9E=E7=8E=B0=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/architecture/release-pipeline.test.mjs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/architecture/release-pipeline.test.mjs b/tests/architecture/release-pipeline.test.mjs index 80518fb..017f9d9 100644 --- a/tests/architecture/release-pipeline.test.mjs +++ b/tests/architecture/release-pipeline.test.mjs @@ -138,7 +138,7 @@ test('树外 Bundle 的 Host 插件只从 Profile 解析正式入口,再由入 } }) -test('正式入口自身是唯一 Narratica Client loader,并以六个独立 Cordis Fiber 聚合现有实现', async () => { +test('正式入口自身是唯一 Narratica Client loader,并通过 Cordis Fiber 聚合现有实现', async () => { const entry = JSON.parse(await readFile('packages/bundle/narratica/package.json', 'utf8')) const patch = await readFile('packages/bundle/narratica/cordis.patch.yml', 'utf8') const client = await readFile('packages/bundle/narratica/src/client/entry.ts', 'utf8') @@ -159,15 +159,6 @@ test('正式入口自身是唯一 Narratica Client loader,并以六个独立 C assert.doesNotMatch(patch, new RegExp(`name: '${escapeRegExp(packageName)}'`)) } - for (const sourcePath of [ - 'client/runtime/src/client/entry.js', - 'client/layout/src/client/index.js', - 'client/workspace/src/client/entry.js', - 'client/story-library/src/client/index.js', - 'client/novel/src/client/index.js', - 'client/director/src/client/index.js', - ]) assert.ok(client.includes(sourcePath), `入口 Client 缺少子插件:${sourcePath}`) - assert.match(client, /for \(const plugin of clientPlugins\) await ctx\.plugin\(plugin\)/) assert.match(root.scripts['build:client:bundles'], /packages\/bundle\/narratica @narratica\/narratica/) }) @@ -190,12 +181,11 @@ test('Release 脚本必须把内部依赖锁到同版本,并为全部 tarball assert.match(publish, /入口包必须最后发布/) }) -test('本地 tarball 烟测必须保持真实的一包安装语义,不能用顶层内部依赖掩盖解析问题', async () => { +test('本地 tarball 烟测必须保持真实的一包安装语义,并复用发行态 Profile 契约', async () => { const smoke = await readFile('scripts/release/smoke-release.mjs', 'utf8') assert.match(smoke, /localOverrides/) assert.match(smoke, /add\(resolve\(releaseDir, entry\.tarball\)\)/) - assert.match(smoke, /narraticaDirectDependencies/) - assert.match(smoke, /Profile 顶层只能依赖正式入口包/) + assert.match(smoke, /await assertNarraticaProfileContract\(\{ profile, dump, distribution: true \}\)/) assert.match(smoke, /不得把内部包提升为 Profile 顶层依赖/) assert.doesNotMatch(smoke, /profile\.dependencies\[pkg\.name\]\s*=/) }) From 3bc684034018aeeb27a3e5ed7e733f56bba21046 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:50:08 +0800 Subject: [PATCH 20/22] =?UTF-8?q?test(architecture):=20bootstrap=20?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E7=BB=9F=E4=B8=80=E6=AD=A3=E5=BC=8F=20Profil?= =?UTF-8?q?e=20=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/architecture/adr009-bundle-boundary.test.mjs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/architecture/adr009-bundle-boundary.test.mjs b/tests/architecture/adr009-bundle-boundary.test.mjs index 98e7439..afc36a7 100644 --- a/tests/architecture/adr009-bundle-boundary.test.mjs +++ b/tests/architecture/adr009-bundle-boundary.test.mjs @@ -122,9 +122,6 @@ test('formal Narratica bundle composes Host, Director infrastructure and one agg assert.match(patch, /includeDefaultRoots: false/) assert.match(patch, /ctx\.narraticaSkillPack\.skillDirs\('novel'\)/) - // DSH does not apply dependency bundle patches transitively. The formal - // top-level bundle therefore owns every row it activates directly. Host - // rows bridge through its runtime subpaths; Client is aggregated by itself. assert.doesNotMatch(patch, /@narratica\/bundle-(?:core|production|app)/) }) @@ -179,19 +176,17 @@ test('formal Client layout scopes Narratica CSS away from the DSH host', async ( assert.doesNotMatch(styles, /(?:^|\n)\*\{box-sizing:border-box\}/) }) -test('development link workaround stays plain and exposes only the formal Narratica bundle', async () => { +test('development link workaround stays plain and delegates formal bundle rules to the shared contract', async () => { const bootstrap = await readFile('scripts/bootstrap-profile.mjs', 'utf8') - assert.match(bootstrap, /NARRATICA_BUNDLE/) assert.match(bootstrap, /packages\/bundle\/narratica/) assert.match(bootstrap, /localProfilePlainDependencies/) assert.match(bootstrap, /narraticaBundleDependencies/) assert.match(bootstrap, /workaround drifted outside formal Bundle dependencies/) assert.match(bootstrap, /must remain a plain dependency, not a Bundle/) - assert.match(bootstrap, /Formal Profile must expose exactly one Narratica Bundle/) + assert.match(bootstrap, /assertFormalProfileContract\(profilePackage\)/) assert.match(bootstrap, /addToProfile\(narraticaBundleDir\)/) assert.doesNotMatch(bootstrap, /addToProfile\(coreBundleDir\)/) assert.doesNotMatch(bootstrap, /addToProfile\(productionBundleDir\)/) assert.doesNotMatch(bootstrap, /addToProfile\(appBundleDir\)/) - assert.match(bootstrap, /Formal Narratica profile must not expose legacy bundle/) }) From 9e1f16b53c03783c34e8f73bcbc3adb370ce6e2c Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 19:50:25 +0800 Subject: [PATCH 21/22] =?UTF-8?q?test(architecture):=20production=20bootst?= =?UTF-8?q?rap=20=E5=A4=8D=E7=94=A8=E5=85=B1=E4=BA=AB=20Profile=20?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/architecture/gate7-production-loop.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/architecture/gate7-production-loop.test.mjs b/tests/architecture/gate7-production-loop.test.mjs index 22b50b6..d421d14 100644 --- a/tests/architecture/gate7-production-loop.test.mjs +++ b/tests/architecture/gate7-production-loop.test.mjs @@ -78,9 +78,9 @@ test('formal Narratica bundle composes providers then media then production with assert.match(patch, /inject:\s*\n\s*- narraticaProviders\s*\n\s*- narraticaMedia/) }) -test('profile bootstrap installs one formal Narratica bundle instead of legacy production layering', async () => { +test('profile bootstrap installs one formal Narratica bundle through the shared contract instead of legacy production layering', async () => { const source = await readFile('scripts/bootstrap-profile.mjs', 'utf8') - assert.match(source, /NARRATICA_BUNDLE/) + assert.match(source, /assertFormalProfileContract\(profilePackage\)/) assert.match(source, /addToProfile\(narraticaBundleDir\)/) assert.doesNotMatch(source, /NARRATICA_(?:CORE|PRODUCTION|APP)_BUNDLE/) assert.doesNotMatch(source, /addToProfile\((?:core|production|app)BundleDir\)/) From 35e0ad72c24c676c2d3ba21c8f565bb129651489 Mon Sep 17 00:00:00 2001 From: 7ping Date: Sat, 29 Aug 2026 20:07:24 +0800 Subject: [PATCH 22/22] =?UTF-8?q?ci(release):=20PR=20=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E5=AE=8C=E6=95=B4=20DSH=20Web=20=E7=83=9F?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-pack-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release-pack-check.yml b/.github/workflows/release-pack-check.yml index c1626d7..6013f86 100644 --- a/.github/workflows/release-pack-check.yml +++ b/.github/workflows/release-pack-check.yml @@ -80,5 +80,10 @@ jobs: - name: npm publish dry-run run: pnpm run release:publish:dry-run -- 0.0.0-alpha.0 alpha + - name: PR 阶段说明 + if: github.event_name == 'pull_request' + run: echo 'PR 阶段已完成发行打包与 npm dry-run;完整 DSH Web 烟测在 main / 手动触发时执行。' + - name: 本地 tarball DSH Profile 与 Web 烟测 + if: github.event_name != 'pull_request' run: pnpm run release:smoke:local -- 0.0.0-alpha.0