| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- /**
- * Quick comprehensive documentation-standard tests: the reference example
- * stays valid, the consolidated `dsh-doc` skill carries no stale copied
- * website values or prototype-era language, and the kind system maps each
- * label to exactly one skill template. These run in `pnpm run test` and
- * `pnpm run test:docs` to guard the standard between heavier corpus gates.
- * @module scripts/doc-standard.spec
- */
- import { existsSync, globSync, readFileSync } from 'node:fs'
- import { resolve, sep } from 'node:path'
- import { load } from 'js-yaml'
- import { describe, expect, it } from 'vitest'
- const root = resolve(import.meta.dirname, '..')
- const PACKAGE_README_GLOBS = [
- 'packages/README.md',
- 'packages/README.zh.md',
- 'packages/*/README.md',
- 'packages/*/README.zh.md',
- 'packages/*/*/README.md',
- 'packages/*/*/README.zh.md',
- ] as const
- function packageReadmes(): string[] {
- return PACKAGE_README_GLOBS
- .flatMap(pattern => globSync(pattern, { cwd: root, exclude: ['**/node_modules/**'] }))
- .map(file => file.replaceAll('\\', '/'))
- .sort()
- }
- /**
- * The kind system: each label maps to exactly one template in the dsh-doc
- * skill. The check derives the expected kind from the same mechanical facts
- * the skill documents; a kind without a template, a template without a kind,
- * or a document whose kind does not match its position fails here.
- */
- const KIND_TEMPLATES: Readonly<Record<string, string>> = {
- 'package-group': '.agents/skills/dsh-doc/templates/package-group.md',
- 'package-reference': '.agents/skills/dsh-doc/templates/package-reference.md',
- 'package-library': '.agents/skills/dsh-doc/templates/package-library.md',
- 'package-bundle': '.agents/skills/dsh-doc/templates/package-bundle.md',
- }
- /**
- * Audited packages whose entry is a plain module API rather than a Cordis
- * plugin (`apply` export or a default service export) or an installable
- * bundle (`dsh.bundle.patch`). Each entry names why the package is a
- * library; the check re-derives the entry shape so a stale entry fails loud.
- */
- const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
- 'packages/boot/app-boot': 'Boot library the app bins import; plain helper exports.',
- 'packages/boot/cmdline': 'Command-line library the app bins import; plain module exports.',
- 'packages/client/store': 'Browser-side state primitives; plain function/type exports.',
- 'packages/client/ui-primitives': 'Browser-side UI component library; plain component exports.',
- 'packages/client/ui-slots': 'Browser-side slot-map declarations; plain type exports.',
- 'packages/client/web': 'Browser application boot library; exports the app entry and static module table.',
- 'packages/core/scope': 'Scoped-context primitives; exports functions and types without a plugin entry.',
- 'packages/experimental/webworker-packer': 'Build-time VFS image packer and command library.',
- 'packages/experimental/webworker-runtime': 'Browser worker runtime library with explicit host entry points.',
- 'packages/hooks/hook-protocol': 'Shared wire-protocol library between the hook bridges.',
- 'packages/identity/anonymous-user-id': 'Harness-home identity helper with no plugin registration.',
- 'packages/sandbox/sandbox-windows-acl': 'Windows ACL sandbox library consumed by sandbox-local.',
- 'packages/sdk/client': 'Client-process library; the spawned runtime owns plugin behavior.',
- 'packages/sdk/protocol': 'Wire-protocol library with type declarations only.',
- 'packages/session/session-format': 'Pure Session format planning, codec dispatch, and lossless JSON library.',
- 'packages/session/session-format-catalog': 'Generated build-static Session format inventory with no plugin registration.',
- 'packages/session/session-format-v0-to-v1': 'Pure released-v0 codec and adjacent migration library.',
- 'packages/session/session-telemetry': 'Telemetry Service Definition and capture library; providers mount the backend.',
- 'packages/session/session-title-llm': 'Shared LLM title-provider registration and request policy.',
- 'packages/subagent/subagent-in-process-driver': 'Shared one-shot child-agent driver used by provider plugins.',
- 'packages/subprocess/win32-process': 'Low-level Win32 process and Job Object primitives.',
- 'packages/test-support/session-snapshot': 'Test infrastructure; mounts nothing into a product composition.',
- 'packages/test-support/agent-loop-testkit': 'Test helper library; mounts nothing into a product composition.',
- 'packages/test-support/client-runtime': 'Browser-side test infrastructure.',
- 'packages/test-support/llm-mock-server': 'Test server library; substitutes provider wire behavior.',
- 'packages/test-support/loader-smoke': 'Test harness library; mounts nothing into a product composition.',
- 'packages/typert/generator': 'Build-time generator run outside any agent runtime.',
- 'packages/typert/protocol': 'Compiler-independent protocol declarations.',
- 'packages/util/atomic-write': 'Zero-dependency filesystem write utility.',
- 'packages/util/brand': 'Stateless nominal-string and canonical-key constructors.',
- 'packages/util/crypto': 'Zero-dependency identifier minting utility.',
- 'packages/util/deque': 'Zero-dependency circular deque utility.',
- 'packages/util/home-paths': 'Zero-dependency harness-home path resolver.',
- 'packages/util/launch-environment': 'Zero-dependency environment resolver.',
- 'packages/util/native-command': 'Host-side subprocess runner utility.',
- 'packages/util/output-retention': 'Zero-dependency retention utility.',
- 'packages/util/time': 'Zero-dependency time-zone canonicalization utility.',
- 'packages/util/timeout': 'Zero-dependency timeout utility.',
- 'packages/util/values': 'Stateless lossless-JSON and immutable-value helpers.',
- 'packages/util/workspace-path': 'Zero-dependency Workspace path formatter.',
- }
- function readFrontmatter(file: string): Record<string, unknown> {
- const source = readFileSync(resolve(root, file), 'utf8')
- const match = /^---\n([\s\S]*?)\n---\n/u.exec(source)
- expect(match, `${file}: YAML frontmatter`).not.toBeNull()
- const metadata = load(match?.[1] ?? '')
- expect(metadata, `${file}: frontmatter object`).toBeTypeOf('object')
- expect(Array.isArray(metadata), `${file}: frontmatter object`).toBe(false)
- return metadata as Record<string, unknown>
- }
- function packageDir(file: string): string {
- return file.replaceAll('\\', '/').replace(/\/README\.zh\.md$/, '').replace(/\/README\.md$/, '')
- }
- /** Whether the package manifest declares `dsh.bundle.patch`. */
- function declaresBundle(dir: string): boolean {
- const manifest = resolve(root, dir, 'package.json')
- if (!existsSync(manifest)) return false
- const metadata = JSON.parse(readFileSync(manifest, 'utf8')) as { dsh?: { bundle?: { patch?: string } } }
- return metadata.dsh?.bundle?.patch !== undefined
- }
- /** The expected kind for one package README, from the facts the skill documents. */
- function expectedKind(file: string): string {
- const normalized = file.replaceAll('\\', '/')
- if (normalized.split('/').length <= 3) return 'package-group'
- const dir = packageDir(normalized)
- if (declaresBundle(dir)) return 'package-bundle'
- if (Object.hasOwn(PACKAGE_LIBRARIES, dir)) return 'package-library'
- return 'package-reference'
- }
- function packageReadmeMetadataErrors(file: string, metadata: Record<string, unknown>): string[] {
- const errors: string[] = []
- if (metadata.kind !== expectedKind(file)) errors.push(`kind must be ${expectedKind(file)}`)
- if (typeof metadata.description !== 'string' || metadata.description.trim() === '') {
- errors.push('description must be a non-empty string')
- }
- for (const field of ['name', 'audience', 'tags', 'i18n']) {
- if (field in metadata) errors.push(`${field} is redundant or has no governed consumer`)
- }
- return errors
- }
- function packageReadmeStructureErrors(file: string, source: string): string[] {
- const chinese = file.endsWith('.zh.md')
- const required = chinese
- ? [[/^## 概述$/m, '概述'], [/^## 目录$/m, '目录'], [/^#{2,3} 开发备注$/m, '开发备注']] as const
- : [[/^## Summary$/m, 'Summary'], [/^## Table of Contents$/m, 'Table of Contents'], [/^#{2,3} Dev Note$/m, 'Dev Note']] as const
- return required.flatMap(([pattern, label]) => pattern.test(source) ? [] : [`missing ${label}`])
- }
- describe('dsh-doc skill consolidation', () => {
- it('carries no prototype-era language', () => {
- const files = [
- '.agents/skills/dsh-doc/SKILL.md',
- '.agents/skills/dsh-doc/references/metadata-links-i18n.md',
- '.agents/skills/dsh-doc/references/structure-hierarchy.md',
- '.agents/skills/dsh-doc/references/style.md',
- '.agents/skills/dsh-doc/references/review.md',
- '.agents/skills/dsh-doc/references/website-sync.md',
- ]
- for (const file of files) {
- const source = readFileSync(resolve(root, file), 'utf8')
- expect(source, file).not.toMatch(/\bprototype\b/i)
- }
- })
- it('copies no stale website sidebar or section-owner values', () => {
- const source = readFileSync(resolve(root, '.agents/skills/dsh-doc/references/website-sync.md'), 'utf8')
- expect(source).not.toContain('en-docs')
- expect(source).not.toContain('sectionOrder')
- })
- it('keeps the reference example linked from the skill', () => {
- const skill = readFileSync(resolve(root, '.agents/skills/dsh-doc/SKILL.md'), 'utf8')
- expect(skill).toContain('session-persistence-jsonl/README.md')
- expect(skill).toContain('session-persistence-jsonl/README.zh.md')
- })
- it('defines controlled English as a precision-preserving review discipline', () => {
- const skill = readFileSync(resolve(root, '.agents/skills/dsh-doc/SKILL.md'), 'utf8')
- const style = readFileSync(resolve(root, '.agents/skills/dsh-doc/references/style.md'), 'utf8')
- expect(skill).toContain('references/style.md#controlled-technical-english')
- expect(style).toContain('not certified ASD-STE100 compliance')
- expect(style).toContain('review prompts, not mechanical gates')
- expect(style).toContain('Never remove or strengthen `must`, `may`, `never`')
- })
- it('maps every kind label to exactly one skill template that exists', () => {
- const templateFiles = globSync('.agents/skills/dsh-doc/templates/*.md', { cwd: root }).map(path => path.split(sep).join('/')).sort()
- const registered = Object.values(KIND_TEMPLATES).sort()
- expect(templateFiles).toEqual(registered)
- for (const [kind, template] of Object.entries(KIND_TEMPLATES)) {
- expect(existsSync(resolve(root, template)), `${kind}: template ${template}`).toBe(true)
- }
- })
- it('maps package README kinds to their documentation standards', () => {
- const files = packageReadmes()
- expect(files.length).toBeGreaterThan(0)
- for (const file of files) {
- const metadata = readFrontmatter(file)
- expect(packageReadmeMetadataErrors(file, metadata), file).toEqual([])
- }
- })
- it('keeps the audited library registry accurate: every entry has a plain module entry and no bundle declaration', () => {
- for (const [dir, reason] of Object.entries(PACKAGE_LIBRARIES)) {
- expect(reason.trim().length, `${dir}: library justification`).toBeGreaterThan(0)
- expect(declaresBundle(dir), `${dir}: a bundle declaration makes this package-bundle, not a library`).toBe(false)
- const entry = resolve(root, dir, 'src/index.ts')
- expect(existsSync(entry), `${dir}: library entry`).toBe(true)
- const source = readFileSync(entry, 'utf8')
- expect(source, `${dir}: entry must be a plain module, not a plugin`).not.toMatch(/export (?:default|\{[^}]*default[^}]*\} from)/u)
- expect(source, `${dir}: entry must be a plain module, not a plugin`).not.toMatch(/export (?:async )?(?:function|const) apply\b/u)
- }
- })
- it('keeps every package README on the summary, contents, and Dev Note skeleton', () => {
- for (const file of packageReadmes().filter(file => file.split('/').length === 4)) {
- const source = readFileSync(resolve(root, file), 'utf8')
- expect(packageReadmeStructureErrors(file, source), file).toEqual([])
- }
- })
- it('rejects redundant fields and a kind that does not match the README position', () => {
- expect(packageReadmeMetadataErrors('packages/example/README.md', {
- description: 'Example group.',
- kind: 'package-reference',
- name: 'example',
- audience: ['developer'],
- tags: ['example'],
- i18n: { counterpart: 'README.zh.md' },
- })).toEqual([
- 'kind must be package-group',
- 'name is redundant or has no governed consumer',
- 'audience is redundant or has no governed consumer',
- 'tags is redundant or has no governed consumer',
- 'i18n is redundant or has no governed consumer',
- ])
- expect(packageReadmeMetadataErrors('packages\\example\\package\\README.md', {
- description: 'Example package.',
- kind: 'package-reference',
- })).toEqual([])
- })
- it('rejects README-local i18n metadata', () => {
- expect(packageReadmeMetadataErrors('packages\\example\\package\\README.md', {
- description: 'Example package.',
- kind: 'package-reference',
- i18n: {
- 'counterpart': 'packages/example/package/README.zh.md',
- 'line-aligned': true,
- },
- })).toEqual([
- 'i18n is redundant or has no governed consumer',
- ])
- })
- })
- describe('reference-example README pair', () => {
- const dir = 'packages/session/session-persistence-jsonl'
- it('keeps exact English/Chinese physical line alignment', () => {
- const sourceLines = readFileSync(resolve(root, dir, 'README.md'), 'utf8').split('\n').length
- const zhLines = readFileSync(resolve(root, dir, 'README.zh.md'), 'utf8').split('\n').length
- expect(sourceLines).toBe(zhLines)
- })
- it('keeps the sidecar consistency record present', () => {
- const sidecar = readFileSync(resolve(root, dir, 'README.i18n.yaml'), 'utf8')
- expect(sidecar).toMatch(/^README\.md: [0-9a-f]{40}$/m)
- expect(sidecar).toMatch(/^README\.zh\.md: [0-9a-f]{40}$/m)
- })
- })
|