From 9d37d7155ac3b42ad8ec227d1a25d4b5aa523e0a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:50:01 +0800 Subject: [PATCH 1/4] test(docs): require package subsystem ownership --- ...package-anchored-subsystem-pages.i18n.yaml | 4 +- ...-08-03-package-anchored-subsystem-pages.md | 5 +- ...-03-package-anchored-subsystem-pages.zh.md | 5 +- package.json | 1 + packages/typert/README.i18n.yaml | 4 +- packages/typert/README.md | 2 + packages/typert/README.zh.md | 2 + scripts/run-gates.spec.ts | 6 + scripts/run-gates.ts | 1 + scripts/verify-subsystem-pages.spec.ts | 95 ++++++++++++ scripts/verify-subsystem-pages.ts | 145 ++++++++++++++++++ 11 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 scripts/verify-subsystem-pages.spec.ts create mode 100644 scripts/verify-subsystem-pages.ts diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml index b124dbfd9e..6bfa2c83dc 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md -2026-08-03-package-anchored-subsystem-pages.md: f429f3d41c1f152e83faeb12c379d221627e767f -2026-08-03-package-anchored-subsystem-pages.zh.md: 3a56fa39357591197606a28b49cf0eac8f58963e +2026-08-03-package-anchored-subsystem-pages.md: 7326d1cc9432555b07a3b5a047fdddb2cbb6d1d2 +2026-08-03-package-anchored-subsystem-pages.zh.md: ba771c586cc121be69f8e864a8ed632059f2b668 diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md index f429f3d41c..7326d1cc94 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md @@ -14,7 +14,9 @@ Every `docs/subsystems/` page anchors to the package or package group that decla Every type a generated signature references must resolve somewhere in the folder: the agent ownership vocabulary moved from the generator's `TYPE_LINK_EXEMPTIONS` into `LINK_MAP → core.md`, so exemptions are reserved for genuinely service-local or vendored shapes. Each pasted declaration has one home (`SessionEvent` lives on [session.md](../../../../docs/subsystems/session.md); core.md summarizes and links). -Every `packages//README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted. +Every `packages//README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. A group that declares no standalone subsystem reference is instead classified with a non-empty rationale in `GROUPS_WITHOUT_SUBSYSTEM_PAGE`; load-bearing prose that outgrows the group README relocates to an owning subsystems page rather than being deleted. + +`verify-subsystem-pages` discovers groups from both group READMEs and child package manifests. It rejects a missing group README, a group with neither a direct subsystem-page link nor an explicit exemption, a blank or orphaned exemption, an exempt group that gains a link, and a link whose page is absent. The gate runs as an independent `doc-sync` leaf, so adding a package group cannot silently omit its documentation owner. The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index. @@ -29,6 +31,7 @@ The [subsystems README](../../../../docs/subsystems/README.md) indexes every pag ## Consequences - Which page documents a type is predictable from `packages//`; the subsystems README is a complete index enforced by test. +- Every package group makes its subsystem owner or justified absence reviewable, and the repository gate rejects unclassified additions and stale exemptions. - Generated signature footers link the agent ownership vocabulary instead of silently exempting it. - `verify-type-equiv`'s 1:1 manifest keeps each paste single-homed; the duplicate `SessionEvent` paste is gone. - The [original catalog note](2026-06-20-core-data-structures-catalog.md) remains the owner of the `ts type-equiv` drift-gate mechanism; only its page-scoping rule is superseded here. diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md index 3a56fa3935..ba771c586c 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md @@ -14,7 +14,9 @@ Status: implemented 生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给真正服务本地或 vendored 的形状。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.md);core.md 概括并链接)。 -每个 `packages//README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。 +每个 `packages//README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。未声明独立子系统参考资料的分组,改为在 `GROUPS_WITHOUT_SUBSYSTEM_PAGE` 中以非空理由分类;超出分组 README 体量的承重散文迁移到拥有方子系统页面,而非删除。 + +`verify-subsystem-pages` 同时从分组 README 和子包 manifest(元数据清单)发现分组。它会拒绝缺少分组 README、分组既没有直接子系统页面链接也没有显式豁免、豁免为空或成为孤立项、已豁免分组新增链接,以及链接指向的页面不存在。该门禁作为独立的 `doc-sync`(文档同步门禁)叶节点运行,因此新增包分组时不能悄悄遗漏其文档拥有方。 [子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。 @@ -29,6 +31,7 @@ Status: implemented ## Consequences - 哪一页记录某类型可由 `packages//` 预测;子系统 README 是由测试强制的完整索引。 +- 每个包分组都会将其子系统拥有方或合理的缺席原因暴露给评审,且仓库门禁会拒绝未分类的新增项和陈旧豁免。 - 生成的签名页脚链接 agent 所有权词汇,而不是静默豁免。 - `verify-type-equiv` 的 1:1 manifest 保证每个粘贴单一归属;重复的 `SessionEvent` 粘贴已移除。 - [原目录 note](2026-06-20-core-data-structures-catalog.md) 仍拥有 `ts type-equiv` 漂移检查机制;此处仅取代其页面范围界定规则。 diff --git a/package.json b/package.json index d417354739..aa9a742204 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "verify-md-links": "tsx scripts/verify-md-links.ts", "verify-public-repository-links": "tsx scripts/verify-public-repository-links.ts", "verify-doc-refs": "tsx scripts/verify-doc-refs.ts", + "verify-subsystem-pages": "tsx scripts/verify-subsystem-pages.ts", "verify-package-paths": "tsx scripts/verify-package-paths.ts", "verify-config-source-ownership": "tsx scripts/verify-config-source-ownership.ts", "verify-package-invariants": "tsx scripts/verify-package-invariants.ts", diff --git a/packages/typert/README.i18n.yaml b/packages/typert/README.i18n.yaml index 3a061e43fd..451db06529 100644 --- a/packages/typert/README.i18n.yaml +++ b/packages/typert/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/typert/README.md -README.md: ad9f843e48be0e3be85921ed8fd3ca4e2c327160 -README.zh.md: aae74bf89f657f50f4519b2f8a0628fd391f8de3 +README.md: 36b97510ceb6524b396e53eb05b6d0e702fce9fe +README.zh.md: 0dc82bf69d58af3d4ccd51c37b546da414af8fcf diff --git a/packages/typert/README.md b/packages/typert/README.md index ad9f843e48..36b97510ce 100644 --- a/packages/typert/README.md +++ b/packages/typert/README.md @@ -9,3 +9,5 @@ Typert separates source analysis, runtime storage, and Loader discovery. | [`registry/`](registry/README.md) | Stores runtime package reflection and schemas | `ctx.typert` | | [`loader/`](loader/README.md) | Discovers Loader entries and registers generated host artifacts | consumes `ctx.loader` and `ctx.typert` | | [`generator/`](generator/README.md) | Generates runtime artifacts from source types | build-time library | + +See [TypeRT remote calls](../../docs/subsystems/typert.md) for the generated invocation, schema, and transport contracts. diff --git a/packages/typert/README.zh.md b/packages/typert/README.zh.md index aae74bf89f..0dc82bf69d 100644 --- a/packages/typert/README.zh.md +++ b/packages/typert/README.zh.md @@ -9,3 +9,5 @@ Typert 将源代码分析、运行时存储和 Loader 发现机制分离。 | [`registry/`](registry/README.md) | 存储运行时包反射和 schema | `ctx.typert` | | [`loader/`](loader/README.md) | 发现 Loader 条目并注册生成的宿主产物 | 使用 `ctx.loader`、`ctx.typert` | | [`generator/`](generator/README.md) | 从源代码类型生成运行时产物 | 构建时库 | + +有关所生成的调用、schema 和传输三方面的约定,参见 [TypeRT 远程调用](../../docs/subsystems/typert.md)。 diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 5c4ba9899a..83504e5e3e 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -83,6 +83,12 @@ describe('gate graph validation', () => { expect(ids).toContain('public-repository-links') }) + it('keeps package-group subsystem ownership in the documentation gate', () => { + const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) + + expect(ids).toContain('subsystem-pages') + }) + it.each([ ['empty', [], /gate graph has no gates/], ['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/], diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 3bd0e11987..9d5545605c 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -590,6 +590,7 @@ function docSyncLeafGates(options: { pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }), pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }), + pnpmScript('subsystem-pages', 'verify-subsystem-pages', { label: 'subsystem pages' }), pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }), pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }), pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }), diff --git a/scripts/verify-subsystem-pages.spec.ts b/scripts/verify-subsystem-pages.spec.ts new file mode 100644 index 0000000000..e7dbc87fc2 --- /dev/null +++ b/scripts/verify-subsystem-pages.spec.ts @@ -0,0 +1,95 @@ +/** Regression coverage for package-group subsystem-page ownership. */ + +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { auditSubsystemPages } from './verify-subsystem-pages.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-subsystem-pages-')) + roots.push(root) + return root +} + +function write(root: string, path: string, source: string): void { + const absolute = join(root, path) + mkdirSync(dirname(absolute), { recursive: true }) + writeFileSync(absolute, source) +} + +describe('package-group subsystem pages', () => { + it('accepts a direct page link and a justified no-page group', () => { + const root = fixture() + write(root, 'packages/alpha/README.md', '[types](../../docs/subsystems/alpha.md)\n') + write(root, 'packages/alpha/alpha/package.json', '{}\n') + write(root, 'docs/subsystems/alpha.md', '# Alpha\n') + write(root, 'packages/adapter/README.md', '# Adapter\n') + + expect(auditSubsystemPages(root, { adapter: 'Adapter over an existing subsystem.' })).toEqual({ + groups: 2, + linked: 1, + exempt: 1, + violations: [], + }) + }) + + it('rejects a new group whose README never declares subsystem ownership', () => { + const root = fixture() + write(root, 'packages/schedule/README.md', '# Schedule\n') + write(root, 'packages/schedule/tool-schedule/package.json', '{}\n') + + expect(auditSubsystemPages(root, {}).violations).toEqual([ + 'packages/schedule/README.md: no direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + ]) + }) + + it('does not treat the subsystem index or a Chinese counterpart as an owning page', () => { + const root = fixture() + write( + root, + 'packages/wrong/README.md', + '[index](../../docs/subsystems/README.md) [Chinese](../../docs/subsystems/wrong.zh.md)\n', + ) + write(root, 'docs/subsystems/README.md', '# Subsystems\n') + write(root, 'docs/subsystems/wrong.zh.md', '# Wrong\n') + + expect(auditSubsystemPages(root, {}).violations).toEqual([ + 'packages/wrong/README.md: no direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + ]) + }) + + it('rejects missing group READMEs and missing linked pages', () => { + const root = fixture() + write(root, 'packages/no-readme/pkg/package.json', '{}\n') + write(root, 'packages/broken/README.md', '[missing](../../docs/subsystems/missing.md)\n') + + expect(auditSubsystemPages(root, {}).violations).toEqual([ + 'packages/broken/README.md: linked subsystem page does not exist: docs/subsystems/missing.md', + 'packages/no-readme/README.md: package group has no group README declaring subsystem ownership', + ]) + }) + + it('rejects blank, orphaned, and stale exemptions', () => { + const root = fixture() + write(root, 'packages/linked/README.md', '[types](../../docs/subsystems/linked.md)\n') + write(root, 'docs/subsystems/linked.md', '# Linked\n') + write(root, 'packages/blank/README.md', '# Blank\n') + + expect(auditSubsystemPages(root, { + blank: ' ', + linked: 'No page.', + orphan: 'Removed group.', + }).violations).toEqual([ + 'exemption blank: missing justification for omitting a subsystem page', + 'exemption orphan: no matching package group; remove the stale entry', + 'packages/linked/README.md: links a subsystem page but remains exempt; remove the stale exemption', + ]) + }) +}) diff --git a/scripts/verify-subsystem-pages.ts b/scripts/verify-subsystem-pages.ts new file mode 100644 index 0000000000..403a3c5035 --- /dev/null +++ b/scripts/verify-subsystem-pages.ts @@ -0,0 +1,145 @@ +/** + * Doc-sync gate for package-group subsystem references. Every package group + * either links at least one existing `docs/subsystems/` page from its English + * group README or carries an explicit, justified exemption below. + */ + +import { existsSync, globSync, readFileSync } from 'node:fs' +import { resolve, sep } from 'node:path' + +const root = resolve(import.meta.dirname, '..') + +/** + * Package groups that do not own a standalone subsystem reference. Reasons + * are reviewable policy: a new group cannot silently inherit an exemption. + */ +export const GROUPS_WITHOUT_SUBSYSTEM_PAGE: Readonly> = { + acp: 'Protocol transport front door; the server package README owns its interoperability contract.', + api: 'Remote transport and BFF assembly; Typert and the package READMEs own the underlying contracts.', + boot: 'Shared application-bin boot library rather than a runtime subsystem.', + bundle: 'Composition patch carriers whose mounted packages own all runtime contracts.', + e2b: 'Provider implementations of the filesystem and subprocess subsystems, not a new capability contract.', + examples: 'Non-product demonstration compositions whose mounted packages own all runtime contracts.', + experimental: 'Empty staging group; promoted packages move to their product-role group before release.', + feedback: 'One command producer and inline log-event payload; its package README and persistence catalog own the complete contract.', + hooks: 'External hook-protocol bridges over existing interception points, not a new Harness service.', + mcp: 'Integration adapter that contributes external tools through the existing tool registry.', + scaffold: 'Developer tooling and out-of-process SDK transport rather than an in-process Harness subsystem.', + 'self-modification': 'Model-facing consumers of the existing tool and Cordis runtime contracts.', + util: 'Low-level primitives whose business semantics remain with their consuming subsystems.', +} + +/** Result of auditing package-group subsystem documentation. */ +export interface SubsystemPageAudit { + /** Package groups discovered from group READMEs or child package manifests. */ + readonly groups: number + /** Groups carrying at least one direct subsystem-page link. */ + readonly linked: number + /** Groups covered by an explicit no-page policy. */ + readonly exempt: number + /** Actionable contract violations. */ + readonly violations: readonly string[] +} + +/** Normalize one filesystem glob result to repository slash form. */ +function normalize(path: string): string { + return path.split(sep).join('/') +} + +/** Extract the package-group segment from a repository-relative path. */ +function groupOf(path: string): string { + const group = path.split('/')[1] + if (group === undefined || group.length === 0) throw new Error(`invalid package path: ${path}`) + return group +} + +/** Return canonical subsystem-page targets linked by one group README. */ +function subsystemLinks(source: string): string[] { + const links = new Set() + const pattern = /\]\(\.\.\/\.\.\/docs\/subsystems\/([^\s)#]+\.md)(?:#[^)]+)?\)/g + for (const match of source.matchAll(pattern)) { + const page = match[1] + if (page !== undefined && page !== 'README.md' && !page.endsWith('.zh.md')) { + links.add(`docs/subsystems/${page}`) + } + } + return [...links].sort() +} + +/** + * Audit package-group subsystem ownership for one repository tree. + * @param scanRoot - repository root containing `packages/` and `docs/`. + * @param exemptions - groups intentionally carrying no subsystem-page link. + * @returns counts plus every actionable violation. + */ +export function auditSubsystemPages( + scanRoot: string = root, + exemptions: Readonly> = GROUPS_WITHOUT_SUBSYSTEM_PAGE, +): SubsystemPageAudit { + const readmes = globSync('packages/*/README.md', { cwd: scanRoot }).map(normalize).sort() + const manifests = globSync('packages/*/*/package.json', { cwd: scanRoot }).map(normalize).sort() + const groups = new Set([...readmes, ...manifests].map(groupOf)) + const violations: string[] = [] + let linked = 0 + let exempt = 0 + + for (const [group, reason] of Object.entries(exemptions)) { + if (!groups.has(group)) { + violations.push(`exemption ${group}: no matching package group; remove the stale entry`) + } + if (reason.trim().length === 0) { + violations.push(`exemption ${group}: missing justification for omitting a subsystem page`) + } + } + + for (const group of [...groups].sort()) { + const readme = `packages/${group}/README.md` + const readmePath = resolve(scanRoot, readme) + if (!existsSync(readmePath)) { + violations.push(`${readme}: package group has no group README declaring subsystem ownership`) + continue + } + + const links = subsystemLinks(readFileSync(readmePath, 'utf8')) + const isExempt = Object.hasOwn(exemptions, group) + if (links.length === 0) { + if (isExempt) { + exempt += 1 + } else { + violations.push( + `${readme}: no direct docs/subsystems/*.md link; add the owning page and link,` + + ' or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + ) + } + continue + } + + linked += 1 + if (isExempt) { + violations.push(`${readme}: links a subsystem page but remains exempt; remove the stale exemption`) + } + for (const page of links) { + if (!existsSync(resolve(scanRoot, page))) { + violations.push(`${readme}: linked subsystem page does not exist: ${page}`) + } + } + } + + return { groups: groups.size, linked, exempt, violations } +} + +/** Run the repository audit as a standalone doc-sync gate. */ +function main(): void { + const audit = auditSubsystemPages() + if (audit.violations.length > 0) { + console.error('verify-subsystem-pages: package-group documentation violations found:') + for (const violation of audit.violations) console.error(` ${violation}`) + process.exit(1) + } + console.log( + `verify-subsystem-pages: ${String(audit.groups)} group(s) checked` + + ` (${String(audit.linked)} linked, ${String(audit.exempt)} explicitly exempt), all conform.`, + ) +} + +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) main() From 4125dac22dabf0453d861a91e39974b6458b9844 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:30:03 +0800 Subject: [PATCH 2/4] fix(docs): harden subsystem ownership links --- ...package-anchored-subsystem-pages.i18n.yaml | 4 +- ...-08-03-package-anchored-subsystem-pages.md | 2 +- ...-03-package-anchored-subsystem-pages.zh.md | 2 +- packages/AGENTS.md | 2 +- scripts/verify-subsystem-pages.spec.ts | 38 +++++++++++++++++-- scripts/verify-subsystem-pages.ts | 16 ++++---- 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml index 6bfa2c83dc..6c7548e4ce 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md -2026-08-03-package-anchored-subsystem-pages.md: 7326d1cc9432555b07a3b5a047fdddb2cbb6d1d2 -2026-08-03-package-anchored-subsystem-pages.zh.md: ba771c586cc121be69f8e864a8ed632059f2b668 +2026-08-03-package-anchored-subsystem-pages.md: e5ec7f561a4f4725c2662415f23144c0182dce80 +2026-08-03-package-anchored-subsystem-pages.zh.md: 409d0f62e026275787f02ab781153739108a107f diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md index 7326d1cc94..e5ec7f561a 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md @@ -16,7 +16,7 @@ Every type a generated signature references must resolve somewhere in the folder Every `packages//README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. A group that declares no standalone subsystem reference is instead classified with a non-empty rationale in `GROUPS_WITHOUT_SUBSYSTEM_PAGE`; load-bearing prose that outgrows the group README relocates to an owning subsystems page rather than being deleted. -`verify-subsystem-pages` discovers groups from both group READMEs and child package manifests. It rejects a missing group README, a group with neither a direct subsystem-page link nor an explicit exemption, a blank or orphaned exemption, an exempt group that gains a link, and a link whose page is absent. The gate runs as an independent `doc-sync` leaf, so adding a package group cannot silently omit its documentation owner. +`verify-subsystem-pages` discovers groups from both group READMEs and child package manifests. It rejects a missing group README, a group with neither a reader-visible direct link to one English file under `docs/subsystems/` nor an explicit exemption, a blank or orphaned exemption, an exempt group that gains a link, and a link whose page is absent; code, comments, images, nested paths, and traversal do not satisfy ownership. The gate runs as an independent `doc-sync` leaf, so adding a package group cannot silently omit its documentation owner. The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index. diff --git a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md index ba771c586c..409d0f62e0 100644 --- a/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md +++ b/.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.zh.md @@ -16,7 +16,7 @@ Status: implemented 每个 `packages//README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。未声明独立子系统参考资料的分组,改为在 `GROUPS_WITHOUT_SUBSYSTEM_PAGE` 中以非空理由分类;超出分组 README 体量的承重散文迁移到拥有方子系统页面,而非删除。 -`verify-subsystem-pages` 同时从分组 README 和子包 manifest(元数据清单)发现分组。它会拒绝缺少分组 README、分组既没有直接子系统页面链接也没有显式豁免、豁免为空或成为孤立项、已豁免分组新增链接,以及链接指向的页面不存在。该门禁作为独立的 `doc-sync`(文档同步门禁)叶节点运行,因此新增包分组时不能悄悄遗漏其文档拥有方。 +`verify-subsystem-pages` 同时从分组 README 和子包 manifest(元数据清单)发现分组。它会拒绝缺少分组 README、分组既没有面向读者且直接指向 `docs/subsystems/` 下某一个英文文件的链接也没有显式豁免、豁免为空或成为孤立项、已豁免分组新增链接,以及链接指向的页面不存在;代码、注释、图片、嵌套路径和路径穿越都不能满足所有权声明。该门禁作为独立的 `doc-sync`(文档同步门禁)叶节点运行,因此新增包分组时不能悄悄遗漏其文档拥有方。 [子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。 diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 6cff1b6327..98aac095df 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -22,6 +22,6 @@ Naming notes: - **Package tsconfig:** extends `tsconfig.base.json` (Client: `tsconfig.base.client.json`), uses `rootDir: src`, `outDir: lib/types`, and references each workspace dependency plus `support/invariants`; registers in exactly one aggregate. Only `api/remotes` splits for generated contracts; ordinary two-entry Client plugins do not ([layout](../docs/development.md#typescript-project-layout)). - `src/types.ts` contains only types — no runtime code. - Tests live at package level under `tests/`, not `src/__tests__/`. -- A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; apply [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for complete, concise prose and verify accuracy against code. +- Update package README and JSDoc contracts in the same commit as behavior, and verify them against code with [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md). Group READMEs declare subsystem ownership through a canonical English page link or justified [exemption](../scripts/verify-subsystem-pages.ts). - Package READMEs document model, token, and KV-cache effects using the [canonical Model Experience format](../docs/cookbook/adding-a-package.md#4-write-the-package-readme). - Package READMEs put durable consumer gaps and non-obvious maintainer constraints under `## Known Limitations and Deferred Work`; ordinary cleanup stays in its TODO or Agent Note. Packages with none use a justified [allowlist entry](../scripts/verify-package-readme-limitations.ts) ([rationale](../.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md)). diff --git a/scripts/verify-subsystem-pages.spec.ts b/scripts/verify-subsystem-pages.spec.ts index e7dbc87fc2..fa8fbc8553 100644 --- a/scripts/verify-subsystem-pages.spec.ts +++ b/scripts/verify-subsystem-pages.spec.ts @@ -27,7 +27,7 @@ function write(root: string, path: string, source: string): void { describe('package-group subsystem pages', () => { it('accepts a direct page link and a justified no-page group', () => { const root = fixture() - write(root, 'packages/alpha/README.md', '[types](../../docs/subsystems/alpha.md)\n') + write(root, 'packages/alpha/README.md', '[types](../../docs/subsystems/alpha.md#contract)\n') write(root, 'packages/alpha/alpha/package.json', '{}\n') write(root, 'docs/subsystems/alpha.md', '# Alpha\n') write(root, 'packages/adapter/README.md', '# Adapter\n') @@ -46,7 +46,7 @@ describe('package-group subsystem pages', () => { write(root, 'packages/schedule/tool-schedule/package.json', '{}\n') expect(auditSubsystemPages(root, {}).violations).toEqual([ - 'packages/schedule/README.md: no direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + 'packages/schedule/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', ]) }) @@ -61,7 +61,39 @@ describe('package-group subsystem pages', () => { write(root, 'docs/subsystems/wrong.zh.md', '# Wrong\n') expect(auditSubsystemPages(root, {}).violations).toEqual([ - 'packages/wrong/README.md: no direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + 'packages/wrong/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + ]) + }) + + it('does not count links hidden in code, comments, or image syntax', () => { + const root = fixture() + write( + root, + 'packages/hidden/README.md', + [ + '`[inline](../../docs/subsystems/hidden.md)`', + '```md', + '[fenced](../../docs/subsystems/hidden.md)', + '```', + '', + '![image](../../docs/subsystems/hidden.md)', + '', + ].join('\n'), + ) + write(root, 'docs/subsystems/hidden.md', '# Hidden\n') + + expect(auditSubsystemPages(root, {}).violations).toEqual([ + 'packages/hidden/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', + ]) + }) + + it('rejects a link that escapes the subsystem directory', () => { + const root = fixture() + write(root, 'packages/escape/README.md', '[escape](../../docs/subsystems/../architecture.md)\n') + write(root, 'docs/architecture.md', '# Architecture\n') + + expect(auditSubsystemPages(root, {}).violations).toEqual([ + 'packages/escape/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', ]) }) diff --git a/scripts/verify-subsystem-pages.ts b/scripts/verify-subsystem-pages.ts index 403a3c5035..6401cfdae8 100644 --- a/scripts/verify-subsystem-pages.ts +++ b/scripts/verify-subsystem-pages.ts @@ -6,6 +6,7 @@ import { existsSync, globSync, readFileSync } from 'node:fs' import { resolve, sep } from 'node:path' +import { parseMarkdown, visitMarkdown } from './markdown.ts' const root = resolve(import.meta.dirname, '..') @@ -56,13 +57,12 @@ function groupOf(path: string): string { /** Return canonical subsystem-page targets linked by one group README. */ function subsystemLinks(source: string): string[] { const links = new Set() - const pattern = /\]\(\.\.\/\.\.\/docs\/subsystems\/([^\s)#]+\.md)(?:#[^)]+)?\)/g - for (const match of source.matchAll(pattern)) { - const page = match[1] - if (page !== undefined && page !== 'README.md' && !page.endsWith('.zh.md')) { - links.add(`docs/subsystems/${page}`) - } - } + visitMarkdown(parseMarkdown(source), (node) => { + if (node.type !== 'link') return + const match = /^\.\.\/\.\.\/docs\/subsystems\/([^/#?]+\.md)(?:#[^?#]*)?$/.exec(node.url) + const page = match?.[1] + if (page !== undefined && page !== 'README.md' && !page.endsWith('.zh.md')) links.add(`docs/subsystems/${page}`) + }) return [...links].sort() } @@ -107,7 +107,7 @@ export function auditSubsystemPages( exempt += 1 } else { violations.push( - `${readme}: no direct docs/subsystems/*.md link; add the owning page and link,` + `${readme}: no reader-visible direct docs/subsystems/*.md link; add the owning page and link,` + ' or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry', ) } From 97938582e573704c86471c7f386b29cafadbe5a7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:55:46 +0800 Subject: [PATCH 3/4] test(docs): avoid duplicate fixture cleanup --- scripts/verify-subsystem-pages.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/verify-subsystem-pages.spec.ts b/scripts/verify-subsystem-pages.spec.ts index fa8fbc8553..65e696468c 100644 --- a/scripts/verify-subsystem-pages.spec.ts +++ b/scripts/verify-subsystem-pages.spec.ts @@ -3,18 +3,12 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it, onTestFinished } from 'vitest' import { auditSubsystemPages } from './verify-subsystem-pages.ts' -const roots: string[] = [] - -afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) -}) - function fixture(): string { const root = mkdtempSync(join(tmpdir(), 'dsh-subsystem-pages-')) - roots.push(root) + onTestFinished(() => rmSync(root, { recursive: true, force: true })) return root } From 1d469469630fc71504a82ddf8cf43ffb266fa7c7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:02:32 +0800 Subject: [PATCH 4/4] test(docs): use a block cleanup callback --- scripts/verify-subsystem-pages.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/verify-subsystem-pages.spec.ts b/scripts/verify-subsystem-pages.spec.ts index 65e696468c..45eeedb35e 100644 --- a/scripts/verify-subsystem-pages.spec.ts +++ b/scripts/verify-subsystem-pages.spec.ts @@ -8,7 +8,9 @@ import { auditSubsystemPages } from './verify-subsystem-pages.ts' function fixture(): string { const root = mkdtempSync(join(tmpdir(), 'dsh-subsystem-pages-')) - onTestFinished(() => rmSync(root, { recursive: true, force: true })) + onTestFinished(() => { + rmSync(root, { recursive: true, force: true }) + }) return root }