Merge pull request #2119 from deepseek-harness/worktree/gate-package-subsystem-pages
Require package groups to declare subsystem documentation
This commit is contained in:
commit
4ba47e665b
24 changed files with 340 additions and 14 deletions
|
|
@ -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: 71e4c2a28f3f556e0d3df2242b410899df763e48
|
||||
2026-08-03-package-anchored-subsystem-pages.zh.md: 0f29a26e456ad7883d00ad51ae60aadc4acb5358
|
||||
2026-08-03-package-anchored-subsystem-pages.md: e5337fba0c9a69dd786fb156572e978f87435b9b
|
||||
2026-08-03-package-anchored-subsystem-pages.zh.md: 25afe84faa43af59d4366b5cc0b6c2d2ad4da1ed
|
||||
|
|
|
|||
|
|
@ -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/<group>/README.md` pair is a thin entry point 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/<group>/README.md` pair is a thin entry point 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 that shape relocates to the 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 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 check 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 an added or merged page 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/<group>/`; the subsystems README is a complete index enforced by test.
|
||||
- Every package group makes its subsystem owner or justified absence reviewable, and `verify-subsystem-pages` 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.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ Status: implemented
|
|||
|
||||
生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给确实仅用于服务内部或来自 vendored 代码的类型结构。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.zh.md);core.md 概括并链接)。
|
||||
|
||||
每个 `packages/<group>/README.md` 配对都是统一形状的精简入口:一段先说明「为什么」的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向对应子系统页面的收尾链接。如果承载关键信息的正文超出这一结构所能容纳的范围,就将其迁移到对应的子系统页面,而非删除。
|
||||
每个 `packages/<group>/README.md` 配对都是统一形状的精简入口:一段先说明「为什么」的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向对应子系统页面的收尾链接。未声明独立子系统参考资料的分组会在 `GROUPS_WITHOUT_SUBSYSTEM_PAGE` 中附上非空理由。如果承载关键信息的正文超出这一结构所能容纳的范围,就将其迁移到对应的子系统页面,而非删除。
|
||||
|
||||
`verify-subsystem-pages` 同时从分组 README 和子包 manifest(元数据清单)发现分组。它会拒绝缺少分组 README、分组既没有面向读者且直接指向 `docs/subsystems/` 下某一个英文文件的链接也没有显式豁免、豁免为空或成为孤立项、已豁免分组新增链接,以及链接指向的页面不存在;代码、注释、图片、嵌套路径和路径穿越都不能满足所有权声明。该检查作为独立的 `doc-sync`(文档同步)叶节点运行,因此新增包分组时不能悄悄遗漏其文档拥有方。
|
||||
|
||||
[子系统 README](../../../../docs/subsystems/README.zh.md) 在中英文两侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每个页面对应一个表格行,因此新增或合并吸收的页面无法悄悄缺席索引。
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ Status: implemented
|
|||
## 后果
|
||||
|
||||
- 哪一页记录某类型可由 `packages/<group>/` 预测;子系统 README 是由测试强制的完整索引。
|
||||
- 每个包分组都会将其子系统拥有方或合理的缺席原因暴露给评审,且 `verify-subsystem-pages` 会拒绝未分类的新增项和陈旧豁免。
|
||||
- 生成的签名页脚链接 agent 所有权词汇,而不是静默豁免。
|
||||
- `verify-type-equiv` 的 1:1 manifest(元数据清单)保证每个粘贴单一归属;重复的 `SessionEvent` 粘贴已移除。
|
||||
- [原目录 Agent Note](2026-06-20-core-data-structures-catalog.zh.md) 仍拥有 `ts type-equiv` 漂移门禁机制;此处仅取代其页面范围界定规则。
|
||||
- [原目录 Agent Note](2026-06-20-core-data-structures-catalog.zh.md) 仍拥有 `ts type-equiv` 漂移检查机制;此处仅取代其页面范围界定规则。
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"verify-doc-site-fragments": "tsx scripts/verify-doc-site-fragments.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-dsh-package-licenses": "tsx scripts/verify-dsh-package-licenses.ts",
|
||||
"verify-config-source-ownership": "tsx scripts/verify-config-source-ownership.ts",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md
|
|||
- **Package tsconfig:** extends `tsconfig.base.json` (Client: `tsconfig.base.client.json`), uses `rootDir: src`, `outDir: lib/types`, and references each workspace dependency plus `runtime-diagnostics/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)).
|
||||
|
|
|
|||
|
|
@ -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/attachment/README.md
|
||||
README.md: 61b4e5c602f475f85bbe859b8483e30b518e06c8
|
||||
README.zh.md: ac93f4870a714d3131fc47789dac9cb5ae926b66
|
||||
README.md: 556c98b40f601a631e65ccb2121d30fa74b97399
|
||||
README.zh.md: 2915e8e207acfdc389b04c609d86ec2ef9c083b9
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ The durable binary attachment seam and its local filesystem implementation. Both
|
|||
| `attachment-local/` | Content-addressed private storage below `DSH_HOME` | (registers on `ctx.attachments`) |
|
||||
|
||||
Unsent browser drafts are intentionally outside this capability. Bytes enter durable storage only when a user prompt is submitted or when a provider adapter commits structured model output.
|
||||
|
||||
See [durable image attachments](../../docs/subsystems/attachment.md) for reference validation, storage, and verified-read contracts.
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@
|
|||
| `attachment-local/` | `DSH_HOME` 下的私有内容寻址存储 | (注册至 `ctx.attachments`) |
|
||||
|
||||
未发送的浏览器草稿刻意位于这项能力之外。只有用户提交提示词,或提供方适配器提交结构化模型输出时,字节才进入持久存储。
|
||||
|
||||
有关引用校验、存储和经过校验的读取约定,参见[持久图片附件](../../docs/subsystems/attachment.zh.md)。
|
||||
|
|
|
|||
|
|
@ -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/extensions/README.md
|
||||
README.md: 12717c43b0cf0a2a7958ed2e18f608da23c67963
|
||||
README.zh.md: df2d70d3e534b0dfefafe6e1192f20eb5f237d6a
|
||||
README.md: 27957a4683edf840cbd7bad00a809ceac8944c20
|
||||
README.zh.md: 173ec99b9418015fced23e72453bcd4d814d7009
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ Model-facing tools over the live cordis runtime the agent itself runs inside: in
|
|||
| [`cordis-host-runner/`](cordis-host-runner/README.md) | Definition registry, the `node:vm` sandbox for host halves, and the request-run round trip | provides `ctx.dynamicCordisRunner` |
|
||||
| [`cordis-client-runner/`](cordis-client-runner/README.md) | Browser half of a dual-half package: evaluates the definition into a live browser plugin and answers the run request | client face; provides the browser `ctx.dynamicCordisRunner` |
|
||||
| [`ui-cordis/`](ui-cordis/README.md) | Browser surfaces: the frame-wide panel that operates every definition, and the read-only define card | client face; registers slots |
|
||||
|
||||
See [Extensions](../../docs/subsystems/extensions.md) for package lifecycle, sandbox behavior, and the generated Cordis API.
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ agent 修改自身运行时:检查已加载的插件与服务接口、定义
|
|||
| [`cordis-host-runner/`](cordis-host-runner/README.zh.md) | 定义注册表、host 半的 `node:vm` 沙箱,以及 request-run 往返 | 提供 `ctx.dynamicCordisRunner` |
|
||||
| [`cordis-client-runner/`](cordis-client-runner/README.zh.md) | 双半包的浏览器半:把定义求值成活的浏览器插件,并应答运行请求 | client 面;提供浏览器侧 `ctx.dynamicCordisRunner` |
|
||||
| [`ui-cordis/`](ui-cordis/README.zh.md) | 浏览器面:操作全部定义的全局面板,与只读的 define 卡片 | client 面;注册 slot |
|
||||
|
||||
有关包生命周期、沙箱行为和生成的 Cordis API,参见[扩展](../../docs/subsystems/extensions.zh.md)。
|
||||
|
|
|
|||
|
|
@ -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/preset/README.md
|
||||
README.md: 5e7805eaa303b78c4d8a1a8972e1d031ee304fae
|
||||
README.zh.md: 217d807f40abc0908fb0c67fa0c94dc4d0806592
|
||||
README.md: 24d13075281689f5e3df8f91c4f13035fdb4acb9
|
||||
README.zh.md: a529eea6667da50204ccd4fa958b8c443af65e99
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@ The presets the deployment ships live in [`apps/cli/config/agent-presets/`](../.
|
|||
The composition split this group assumes: registries and cross-session facilities are process singletons and stay in the host composition, while a preset carries what one agent contributes to them. A preset that names a row publishing a process-global service is rejected at mount rather than allowed to collide with the next session.
|
||||
|
||||
Design: [the per-session agent-preset note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md).
|
||||
|
||||
See the [`AgentPresets` reference](../../docs/subsystems/core.md#ctxagentpresets--agentpresets) for discovery, mounting, inheritance, and recomposition.
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@
|
|||
本组假定的组装划分是:注册表与跨会话设施是进程单例,留在宿主组装中;preset 只承载单个 agent 对它们的贡献。若 preset 中某一行发布了进程级全局服务,挂载时即被拒绝,而不是留到与下一个会话相撞。
|
||||
|
||||
设计详见 [按会话组装 agent preset 的 Agent Note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md)。
|
||||
|
||||
有关发现、挂载、继承和重新组装的约定,参见 [`AgentPresets` 参考](../../docs/subsystems/core.zh.md#ctxagentpresets--agentpresets)。
|
||||
|
|
|
|||
6
packages/runtime-diagnostics/README.i18n.yaml
Normal file
6
packages/runtime-diagnostics/README.i18n.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/runtime-diagnostics/README.md
|
||||
README.md: f8df688f2dc053d579437dfdd04cee790973c1db
|
||||
README.zh.md: 294bf3369c4049c9c350713ed497ca4374a99c2f
|
||||
11
packages/runtime-diagnostics/README.md
Normal file
11
packages/runtime-diagnostics/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# runtime-diagnostics/ — package-owned runtime checks
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Configurable diagnostics that check package-owned runtime relationships without changing product behavior.
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`invariants/`](invariants/README.md) | Registers and runs package-owned runtime invariant companions | `ctx.invariants` |
|
||||
|
||||
See [runtime invariants](../../docs/subsystems/invariants.md) for selection, lifecycle, and package-attributed failure semantics.
|
||||
11
packages/runtime-diagnostics/README.zh.md
Normal file
11
packages/runtime-diagnostics/README.zh.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# runtime-diagnostics/:包自有运行时检查
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
可配置诊断用于检查包自有的运行时关系,而不改变产品行为。
|
||||
|
||||
| 包 | 角色 | ctx 键 |
|
||||
|---|---|---|
|
||||
| [`invariants/`](invariants/README.zh.md) | 注册并运行包自有的运行时不变式配套入口 | `ctx.invariants` |
|
||||
|
||||
有关选择、生命周期和归因到包的失败语义,参见[运行时不变式](../../docs/subsystems/invariants.zh.md)。
|
||||
|
|
@ -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: 9ebd00f71a509e24a0b6c674c56d87c411dd7794
|
||||
README.md: 36b97510ceb6524b396e53eb05b6d0e702fce9fe
|
||||
README.zh.md: 0bf6b83a606f7cd812772cdd116fd65825d66fd9
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -9,3 +9,5 @@ Typert 将源代码分析、运行时存储和 Loader 发现机制分离。
|
|||
| [`registry/`](registry/README.zh.md) | 存储运行时包反射和 schema | `ctx.typert` |
|
||||
| [`loader/`](loader/README.zh.md) | 发现 Loader 条目并注册生成的宿主产物 | 使用 `ctx.loader`、`ctx.typert` |
|
||||
| [`generator/`](generator/README.zh.md) | 从源代码类型生成运行时产物 | 构建时库 |
|
||||
|
||||
有关所生成的调用、schema 和传输三方面的约定,参见 [TypeRT 远程调用](../../docs/subsystems/typert.zh.md)。
|
||||
|
|
|
|||
|
|
@ -84,6 +84,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('keeps the hygiene aggregate aligned with the package script checks', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
||||
|
||||
|
|
|
|||
|
|
@ -674,6 +674,7 @@ function docSyncLeafGates(options: {
|
|||
pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }),
|
||||
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' }),
|
||||
|
|
|
|||
123
scripts/verify-subsystem-pages.spec.ts
Normal file
123
scripts/verify-subsystem-pages.spec.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/** 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 { describe, expect, it, onTestFinished } from 'vitest'
|
||||
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 })
|
||||
})
|
||||
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#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')
|
||||
|
||||
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 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 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 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) -->',
|
||||
'',
|
||||
'',
|
||||
].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',
|
||||
])
|
||||
})
|
||||
|
||||
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',
|
||||
])
|
||||
})
|
||||
})
|
||||
145
scripts/verify-subsystem-pages.ts
Normal file
145
scripts/verify-subsystem-pages.ts
Normal file
|
|
@ -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'
|
||||
import { parseMarkdown, visitMarkdown } from './markdown.ts'
|
||||
|
||||
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<Record<string, string>> = {
|
||||
acp: 'Protocol transport entry point; 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.',
|
||||
identity: 'Shared anonymous correlation values rather than authenticated account or authorization behavior.',
|
||||
mcp: 'Integration adapter that contributes external tools through the existing tool registry.',
|
||||
sdk: 'Out-of-process protocol and client packages whose package READMEs own the SDK 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<string>()
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Record<string, string>> = 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 reader-visible 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()
|
||||
Loading…
Add table
Reference in a new issue