fix(docs): harden subsystem ownership links

This commit is contained in:
Tianyi Cui committed 2026-08-09 23:30:03 +08:00
1 parent fb4554a9be
commit 4125dac22d
6 files changed
+48 -16

No files matched your search

@@ -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
@@ -16,7 +16,7 @@ Every type a generated signature references must resolve somewhere in the folder
Every `packages/<group>/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.
@@ -16,7 +16,7 @@ Status: implemented
每个 `packages/<group>/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 新增(或合并吸收)的页面无法悄悄缺席索引。
+1 -1
View File
@@ -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)).
+35 -3
View File
@@ -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)',
'```',
'<!-- [comment](../../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',
])
})
+8 -8
View File
@@ -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<string>()
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',
)
}