docs: rebalance prose cleanup and add trimming skill

This commit is contained in:
Tianyi Cui
2026-07-13 23:27:00 +08:00
parent fcdc318dda
commit 148046b9c8
392 changed files with 2801 additions and 1754 deletions

View File

@@ -175,8 +175,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
}
/**
* Enforce the packages/ hierarchy SHAPE: every package lives at exactly
* `packages/<group>/<pkg>`.
* Enforce `packages/<group>/<pkg>`: groups are open-named containers without a
* package.json, and packages may be neither flat nor more deeply nested.
*/
function checkHierarchyShape(): string[] {
const errors: string[] = []

View File

@@ -1,6 +1,7 @@
/**
* Boot the REPL or ACP Code Mode overlay, defaulting to REPL. Both require a
* DeepSeek API key; unsupported arguments fail with usage.
* Boot the REPL or ACP Code Mode overlay, defaulting to REPL. Each overlay
* includes its base example, selects Code Mode, and adds the worker runtime.
* Both require a DeepSeek API key; unsupported arguments fail with usage.
*/
import { spawn } from 'node:child_process'

View File

@@ -11,7 +11,11 @@ import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
/** Classification of a TypeScript fence and the gate that owns it. */
/**
* TypeScript-fence ownership. `check` compiles; `ignore` is an unchecked sketch
* counted in the opt-out ratio; the catalog and type-equivalence variants are
* excluded from that ratio because their owning gates verify them.
*/
type BlockKind = 'check' | 'ignore' | 'type-equiv' | 'cordis-catalog' | 'persistence-catalog' | 'config-catalog'
/** One extracted code block. */
@@ -101,10 +105,8 @@ files.sort()
const all = files.flatMap(extractBlocks)
const checked = all.filter(b => b.kind === 'check')
const ignored = all.filter(b => b.kind === 'ignore')
// `type-equiv`, `cordis-catalog`, and `persistence-catalog` blocks are verified elsewhere
// (verify-type-equiv.ts and each catalog generator's `--check` freshness gate), not here:
// neither compiled nor counted toward the opt-out ratio (each is a separate fully-checked
// category, not an unchecked sketch).
// Only compile-eligible fences belong in the opt-out ratio; every other skipped
// kind has an independent verifier named in BlockKind's contract above.
const ratioDenominator = checked.length + ignored.length
if (checked.length === 0) {

View File

@@ -1,8 +1,10 @@
/**
* Generate `docs/config-catalog.md` from package entry points, config types,
* JSDoc, and static Schemastery schemas. Every package must classify, referenced
* types must resolve without collisions, and schema paths must exist on the
* declared config type. `--check` verifies the committed artifact.
* types must resolve without collisions, and every enumerable schema path must
* exist on the declared config type. External and dynamic shapes stay unknown;
* declared runtime-only fields need not appear in the schema. `--check` verifies
* the committed artifact.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -293,7 +295,7 @@ function declForTypeName(world: World, ctx: FileCtx, name: string): { decl: Type
entry = loadFile(resolve(world.scanRoot, entryRel), entryRel, world.cache)
} catch {
// A workspace package without a readable entry is reported by its own
// classification pass; for a lookup it is merely out of reach.
// classification pass; for a lookup it is out of reach.
return 'unknown'
}
return findExportedTypeDecl(world, entry, imp.imported) ?? 'unknown'
@@ -312,8 +314,9 @@ const PASSTHROUGH_WRAPPERS = new Set(['Partial', 'Required', 'Readonly', 'NonNul
*/
function lookupPath(world: World, ctx: FileCtx, node: ts.Node, steps: PathStep[], seen: Set<string>): PathLookup {
if (steps.length === 0) return 'found'
// Guard recursion at NAMED declarations only — the sole way a walk can loop (a recursive
// interface/alias).
// Guard only named declarations, where recursive types can loop. Structural
// children can share a source position with their parent, so guarding them
// would mistake ordinary descent for a cycle.
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
const key = `${ctx.abs}:${node.pos}:${steps.length}`
if (seen.has(key)) return 'unknown' // recursive type — bail rather than loop
@@ -713,8 +716,8 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
}
}
// Second phase: fold composed schemas' key paths in, then walk every schema-validated path
// against the declared config type.
// Fold composed schemas' key paths in, then check each path against the type.
// Only a definite miss fails; shapes the walk cannot enumerate stay unknown.
const byName = new Map(entries.map(e => [e.pkg, e]))
for (const entry of entries) {
if (entry.kind !== 'config' || entry.schemaKeys === null || entry.schemaKeys === undefined) continue

View File

@@ -1,7 +1,8 @@
/**
* Generate the model-facing Cordis API data module from the same event/service
* collector as the documentation catalogs. Output includes concise docs,
* signatures, and referenced public type shapes; `--check` verifies freshness.
* collector as the documentation catalogs. It emits first-sentence docs, raw
* signatures, transitive public type shapes, and inherited context entries,
* without source pointers; output is deterministic and `--check` verifies it.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -27,7 +28,10 @@ function quote(value: string): string {
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'').replace(/\n/g, '\\n')}'`
}
/** Collect uniquely named exported interface and type shapes. */
/**
* Collect exported interface and type shapes; omit names declared in multiple
* packages rather than risk serving the wrong package's shape.
*/
function collectTypeDecls(scanRoot: string = root): Map<string, string> {
const printer = ts.createPrinter({ removeComments: true })
const decls = new Map<string, string>()
@@ -53,7 +57,7 @@ function collectTypeDecls(scanRoot: string = root): Map<string, string> {
return decls
}
/** Resolve the transitive public type shapes referenced by seed text. */
/** Resolve and sort the word-bounded transitive type closure referenced by seed text. */
function referencedTypes(seeds: string[], decls: Map<string, string>): { name: string; declaration: string }[] {
const included = new Map<string, string>()
let frontier = seeds

View File

@@ -18,7 +18,11 @@ const OUT_SERVICES = 'docs/cordis-catalog/services.md'
* doc-typecheck, since a bare signature fragment is not standalone-compilable). */
const FENCE = 'ts cordis-catalog'
/** Primary core-data-structures page for signature types shared by both catalog generators. */
/**
* One primary core-data-structures page per signature type, shared by the
* Cordis and config catalogs; union names intentionally do not reuse the
* type-equivalence manifest's map-symbol entries.
*/
// TODO(catalog-type-links): verify or generate link-map coverage.
export const LINK_MAP: Record<string, string> = {
Agent: 'core.md',
@@ -157,7 +161,8 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
violations.push(`${where} is tagged '@mode waterfall' but has no trailing 'next' parameter. A waterfall delegates via next().`)
}
if (!doc) violations.push(`${where} has no description prose. Say what happened / what a listener may do, above the block tags.`)
// Payload parameters need a non-empty @param each.
// Payload parameters need a non-empty @param. The `this` receiver is not
// payload, and a waterfall's trailing `next` is covered by its mode.
const { params } = parseTags(raw)
checkParams(where, 'event', member.parameters, params, sf,
p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations)
@@ -208,7 +213,8 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] {
const methods: string[] = []
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
// Only the PUBLIC callable surface a `ctx.<key>` consumer sees.
// Only instance methods callable through `ctx.<key>` are surface;
// private, protected, and static methods are not.
const nonPublic = member.modifiers?.some(m =>
m.kind === ts.SyntaxKind.PrivateKeyword
|| m.kind === ts.SyntaxKind.ProtectedKeyword

View File

@@ -1,5 +1,8 @@
/**
* Generate (and verify) the relationship-diagram docs.
* Generate the relationship layer above the module, Cordis, and tool catalogs.
* Enumerable facts come from source; hybrid graphs add manifests for policy the
* source cannot infer, while curated graphs explain flow and ownership.
* `--check` verifies the generated set.
*/
import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
@@ -606,10 +609,8 @@ function isCordisContextReceiver(expr: ts.PropertyAccessExpression, sf: ts.Sourc
}
const target = expr.expression.getText(sf)
if (target === 'ctx' || target === 'this.ctx') return true
// Scoped-dispatch spellings (the agent-scoping seam): the loop's fused dispatcher (`events`
// from `agentEvents(ctx, agent)`), an agent's setup context (`childCtx`), the agent's own
// context handle (`this.loopCtx`), and the session store's captured dispatch context
// (`emitCtx`).
// Scoped-dispatch spellings are conventional names. Keep this list in sync
// with renames or the relationship matrix can silently lose an edge.
return target === 'events' || target === 'childCtx' || target === 'this.loopCtx' || target === 'emitCtx'
}
@@ -656,9 +657,8 @@ function renderEventRelations(pkgs: Pkg[]): string {
const relation = relations.get(event.name) ?? { dispatchers: new Map<string, Set<string>>(), listeners: new Set<string>() }
lines.push(`| \`${event.name}\` | \`${event.mode}\` | ${sourceLink(event.source)} | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
}
// Completeness guard: every DECLARED event must have at least one dispatcher edge — a
// zero-dispatcher row is either dead vocabulary or (the observed failure mode) a dispatch
// spelling the AST scan does not recognize, silently dropping the producer from the matrix.
// Every declared event needs a dispatcher: zero means dead vocabulary or an
// unrecognized dispatch spelling. Listener-free extension points remain valid.
const undispatched = [...events]
.filter(event => (relations.get(event.name)?.dispatchers.size ?? 0) === 0)
.map(event => event.name)
@@ -785,7 +785,7 @@ function renderToolPipeline(): string {
' allResults --> context',
'```',
'',
'Filesystem read-before-edit policy stays on `fs/*` events. Generic pre/post waterfalls host hook and approval policy, `ctx.approval` resolves asks before guards, and `tools/execute` hosts around-dispatch concerns such as timeouts. `tools/result` observes the immutable final outcome. Code Mode sends both `run_code` and its serialized sub-calls through this pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContext` to preserve call/result adjacency.',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`, while `tools/result` observes the immutable outcome after transforms, lossless-JSON validation, and outer error normalization. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContext` to preserve call/result adjacency.',
'',
...maintenanceFooter(maintenance),
].join('\n')

View File

@@ -1,5 +1,7 @@
/**
* Generate (and verify) the module dependency graph in docs/module-graph.md.
* Generate `docs/module-graph.md` from in-repo `peerDependencies`, the canonical
* runtime edges. The deterministic output groups packages by directory and
* renders both Mermaid and a dependency table; `--check` verifies freshness.
*/
import { dirname, resolve } from 'node:path'
@@ -161,8 +163,8 @@ if (process.argv.includes('--check')) {
try {
committed = readFileSync(resolve(root, OUT), 'utf8')
} catch {
// Only an ENOENT (file not yet generated) is expected here; readFileSync of a
// present-but-unreadable file is not a state this repo produces.
// A missing artifact is the expected read failure. Any read failure has the
// same remedy here—regenerate—so it is reported as stale below.
committed = null
}
if (committed === content) {

View File

@@ -1,8 +1,9 @@
/**
* Generate `docs/persistence-catalog.md` from every `SessionEventMap` merge and
* the owning `SurfaceEventType` union. Event declarations must be unique,
* explicitly typed, documented, and free of Cordis-only `@mode` tags. `--check`
* verifies the committed artifact.
* the owning `SurfaceEventType` union. This is the durable-record vocabulary,
* not the live Cordis bus. Event declarations must be unique, explicitly typed,
* documented, inheritance-free, and free of Cordis-only `@mode` tags; every
* surface-union member must resolve to one. `--check` verifies the artifact.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -81,7 +82,10 @@ function rawJsDoc(text: string, node: ts.Node): string {
return jsdoc ? text.slice(jsdoc.pos, jsdoc.end) : ''
}
/** Parse pre-tag JSDoc prose into one-line paragraphs and bullets for the catalog. */
/**
* Parse pre-tag JSDoc prose into one-line paragraphs and bullets, unwrap
* `{@link ...}`, and report whether the forbidden `@mode` tag appears.
*/
function parseJsDoc(raw: string): { doc: string; hasMode: boolean } {
const inner = raw
.replace(/^\/\*\*/, '')
@@ -188,7 +192,10 @@ function packageNameFor(rel: string, scanRoot: string): string | null {
}
}
/** Collect and validate every `SessionEventMap` declaration merge. */
/**
* Collect every `SessionEventMap` merge, rejecting inherited, non-literal,
* untyped, undocumented, duplicate, or incorrectly owned members in one report.
*/
export function collectLogEvents(scanRoot: string = root): LogEventEntry[] {
const entries: LogEventEntry[] = []
const violations: string[] = []
@@ -202,8 +209,9 @@ export function collectLogEvents(scanRoot: string = root): LogEventEntry[] {
for (const { decl, topLevel } of sessionEventMapDecls(sf)) {
const declSrc = pointer(rel, sf, decl)
if (topLevel) {
// The top-level form is the OWNING vocabulary, and it has exactly one home: the single
// EXPORTED declaration in the owning package.
// The top-level form has one home: the single exported declaration in
// the owning package. Same-named interfaces elsewhere are different
// types and must not enter the on-disk catalog.
const pkg = packageNameFor(rel, scanRoot)
if (pkg !== SESSION_MODULE) {
violations.push(`top-level interface SessionEventMap (${declSrc}) is outside ${SESSION_MODULE} (package ${pkg ?? 'unknown'}). Rename the interface, or contribute events via declare module '${SESSION_MODULE}'.`)

View File

@@ -2,7 +2,8 @@
* Generate `docs/tool-catalog.md` from schemas collected by booting each tool
* plugin. Runtime registration is the source of truth for computed schemas;
* the manifest is checked against every on-disk `tool-*` package. `--check`
* verifies the committed artifact.
* verifies the committed artifact. Rationale and ownership live in
* `docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md`.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -35,7 +36,11 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
/** Tool package plus the non-default dependencies needed to boot it. */
/**
* Tool package plus its hand-maintained boot recipe. The caller mounts the
* prompt and registry; each recipe supplies only package-specific seams and
* config, while `dir` participates in the completeness check.
*/
interface ToolPackage {
/** The npm package name, used as the catalog section heading. */
pkg: string
@@ -134,7 +139,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt'],
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after successful file operations', 'tool/result'],
async mount(ctx) {
// The tool injects `fs`; boot the local backend to satisfy it.
// The tool needs `fs`; the bare provider is sufficient because policy
// changes behavior, not schema shape.
await ctx.plugin(LocalFileSystem)
await ctx.plugin(ToolFs)
},
@@ -207,8 +213,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
requires: ['ctx.tools', 'ctx.web', 'ctx.systemPrompt'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// The tools inject `web`; boot the seam plus one search and one fetch provider so both
// `web_search` and `web_fetch` register.
// Mount search and fetch providers so both tools register. Their schemas
// do not depend on provider identity or availability.
await ctx.plugin(WebService)
await ctx.plugin(WebSearchExa)
await ctx.plugin(WebFetchLocal)

View File

@@ -3,7 +3,8 @@
* catalog generator (`scripts/gen-cordis-catalog.ts` — the events + `ctx.<key>` service
* surface), the plugin config catalog generator (`scripts/gen-config-catalog.ts`, which
* renders the parsed prose), and the export-surface gate (`scripts/verify-export-jsdoc.ts` —
* every module-level export).
* every module-level export). This is the single definition of description,
* parameter, return, and stale-tag completeness across those surfaces.
*/
import ts from 'typescript'
@@ -25,8 +26,9 @@ export function rawJsDoc(text: string, node: ts.Node): string {
export type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial'
/**
* Parse a raw JSDoc block into description prose + the `@mode` tag (when present).
*
* Parse a raw JSDoc block into description prose and an optional `@mode`. Prose
* ends at the first block tag, paragraphs collapse to one line, bullet items
* remain separate lines, and `{@link X}` renders as `X`.
* @param raw - the raw comment text including the JSDoc delimiters.
* @returns the collapsed description prose plus the parsed `@mode` (or null).
*/
@@ -80,9 +82,8 @@ export function parseJsDoc(raw: string): { doc: string; mode: Mode | null } {
}
/**
* Parse the block tags of a raw JSDoc comment for the completeness checks: every `@param name
* — description` entry plus the `@returns` description.
*
* Parse `@param` and `@returns` descriptions, including continuation lines.
* Parameter separators are optional and `[optional]` names unwrap.
* @param raw - the raw comment text including the JSDoc delimiters.
* @returns the `@param` name→description map plus the `@returns` description
* (null when the tag is absent, '' when present but empty).
@@ -119,13 +120,15 @@ export function parseTags(raw: string): { params: Map<string, string>; returns:
}
/**
* Check that required parameter tags exist and no stale tag remains.
* Require a non-empty tag for each non-exempt identifier parameter, reject
* binding-pattern parameters, and reject stale tags. Exempt parameters may
* still be documented.
* @param where - the offender label violations open with, e.g. `event 'x' (file:1)`.
* @param surface - surface noun used in diagnostics.
* @param surface - surface noun used in binding-pattern diagnostics.
* @param parameters - the declaration's parameter list.
* @param tags - the parsed `@param` name→description map from parseTags.
* @param sf - source file used to render binding patterns.
* @param isExempt - parameters that need no tag.
* @param isExempt - parameters whose tag is optional, such as `this` or waterfall `next`.
* @param violations - the aggregate list violations append to.
*/
export function checkParams(
@@ -157,8 +160,8 @@ export function checkParams(
/**
* Check the `@returns` half of the completeness contract: a non-`void` / `Promise<void>`
* return needs a non-empty `@returns`, and the return type must be ANNOTATED — a pure-AST
* walk cannot classify an inferred return.
*
* walk cannot classify an inferred return. Void returns may still carry an
* optional tag, for example to document resolution timing.
* @param where - the offender label violations open with.
* @param typeNode - the declared return type annotation, or undefined when inferred.
* @param returns - the parsed `@returns` description from parseTags (null when absent).

View File

@@ -7,7 +7,8 @@ import { promisify } from 'node:util'
const execFileAsync = promisify(execFile)
const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
// publint every harness package.
// Discover harness packages at packages/<group>/<pkg>; group containers,
// examples, and private vendored sources are not package targets.
const root = resolve(import.meta.dirname, '..')
const packagesRoot = resolve(root, 'packages')

View File

@@ -2,7 +2,8 @@
* Shared source of truth for the RFC index: the tree walker (structure rules) and the README
* table renderer. `gen-rfc-index.ts` writes the generated regions;
* `verify-rfc-classification.ts` checks structure and asserts the committed regions are fresh.
* Pure module — no side effects on import.
* Lifecycle and class sets are closed under `docs/rfc/README.md`; rows derive
* from path, H1, and filename date and sort deterministically. Import is pure.
*/
import { readFileSync, readdirSync } from 'node:fs'

View File

@@ -1,7 +1,9 @@
/**
* Enforce `wc -w`-style ceilings from `scripts/doc-budgets.manifest.json`.
* Missing files and invalid ceilings fail; `--list` reports current usage.
* Ceiling changes remain reviewable manifest edits.
* Only listed standing docs are budgeted. Ceilings ratchet down with at least
* 5% headroom; raising one requires the justification defined in
* `docs/AGENTS.md`.
*/
import { existsSync, readFileSync } from 'node:fs'

View File

@@ -1,6 +1,7 @@
/**
* Verify root-relative `docs/*.md` tokens in repo-authored TypeScript. The
* textual scan requires the extension and excludes built and vendored source.
* textual scan requires the extension, checks matching string literals too,
* and excludes built declarations and vendored source.
*/
import { existsSync, globSync, readFileSync } from 'node:fs'

View File

@@ -1,9 +1,10 @@
/**
* Enforce JSDoc on every non-vendored package export. Functions and public
* class methods require parameter and non-void return documentation; exported
* declarations require description prose. Framework protocol slots,
* constructors, inherited members, augmentations, and source re-exports keep
* their documentation at the declaring contract. Unknown export forms fail.
* declarations require description prose. Inline callable types, overload
* signatures, namespace members, and public class members are included;
* framework slots, constructors, inherited contracts, augmentations, and source
* re-exports keep their docs at the declaring contract. Unknown forms fail closed.
*/
import { existsSync, globSync } from 'node:fs'

View File

@@ -1,7 +1,8 @@
/**
* Verify that relative Markdown links, images, and definitions resolve. URL,
* root-absolute, and in-page targets are excluded; query strings and fragments
* do not affect the filesystem check. Symlinked instruction files are deduped.
* do not affect resolution against the source file. The checker never rewrites,
* and symlinked instruction files are deduped.
*/
import { existsSync, globSync, readFileSync, realpathSync } from 'node:fs'

View File

@@ -1,7 +1,8 @@
/**
* Reject Markdown prose paragraphs spanning multiple physical lines. The GFM
* AST distinguishes paragraphs from multiline structural nodes; symlinked
* instruction files are deduped.
* AST distinguishes paragraphs—including those in lists and blockquotes—from
* multiline structural nodes. The checker never rewrites; symlinked instruction
* files are deduped. The owning convention is in `docs/AGENTS.md`.
*/
import { globSync, readFileSync, realpathSync } from 'node:fs'

View File

@@ -1,6 +1,7 @@
/**
* Parse every repo-authored Mermaid fence with Mermaid itself. Scope matches the
* Markdown link gate. Run with `tsx scripts/verify-mermaid.ts`.
* Parse every repo-authored Mermaid fence with Mermaid itself, catching syntax that link and fence
* checks cannot. Scope intentionally matches the Markdown link gate, including standing docs,
* package/example docs, and agent skills. Run with `tsx scripts/verify-mermaid.ts`.
*/
import { globSync, readFileSync, realpathSync } from 'node:fs'

View File

@@ -83,9 +83,8 @@ function findViolations(absPath: string): Violation[] {
// class may have swallowed (`packages/core/tools.` / `…/tools/`).
const ref = m[0].replace(/[./]+$/, '')
if (existsSync(resolve(root, ref))) continue
// A reference INTO a package's built `lib/` is a build OUTPUT, not an authored-source
// location: it does not exist until `pnpm run build` emits it, and CI runs this gate
// before the build step.
// Skip unbuilt `lib/` only below a real depth-two package root. A stale
// group-less path still fails; `lib` is not a blanket escape hatch.
const parts = ref.split('/')
const libAt = parts.indexOf('lib')
if (libAt === 3 && existsSync(resolve(root, parts.slice(0, 3).join('/')))) continue

View File

@@ -1,7 +1,8 @@
/**
* Enforce RFC lifecycle/class paths, dated filenames, and titles; verify the
* generated index and reject index rows in the curated README. Structural rules
* and rendering are shared with `rfc-index.ts`.
* and rendering are shared with `rfc-index.ts`; the closed classification
* contract lives in `docs/rfc/README.md`.
*/
import { readFileSync } from 'node:fs'

View File

@@ -1,7 +1,8 @@
/**
* Enforce RFC headers, lifecycle-specific sections, alternatives, and retired
* marker rules. Classification and filenames belong to the sibling tree gate;
* translation structure belongs to the pairing gate.
* translation structure belongs to the pairing gate. Exact format and
* grandfathering rules live in `docs/rfc/README.md`.
*/
import { readFileSync } from 'node:fs'

View File

@@ -4,6 +4,7 @@
* `packages/support/invariants/src/index.ts`, which enforces carriers at dispatch time) and
* the event declarations' JSDoc (the "Scope-filtered dispatch" sentence rendered into the
* events catalog, which tells plugin authors what a scoped listener will and won't hear).
* Registry-subject notifications are intentionally unfiltered and belong in neither set.
*/
import { globSync, readFileSync } from 'node:fs'

View File

@@ -2,7 +2,9 @@
* Enforce complete English/Chinese pairs, matching structure, and recorded git
* blob hashes under the bilingual manifest. `--list` reports state; `--write`
* records both sides after human review. Translation quality remains a review
* responsibility.
* responsibility. A complete pair includes both documents and its sidecar;
* headings, fences, tables, lists, and link targets must align, while excluded
* documents may have neither counterpart nor sidecar. See `docs/i18n/README.md`.
*/
import { createHash } from 'node:crypto'

View File

@@ -10,7 +10,7 @@ import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
/** Markdown scope shared with doc-typecheck. */
/** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
/** One manifest entry: a documented type-equiv block and its source symbol. */
@@ -34,7 +34,11 @@ interface EquivBlock {
code: string
}
/** Remove comments and normalize whitespace for structural comparison. */
/**
* Remove comments and normalize whitespace so prose-only edits do not drift
* structural copies. This is intentionally not a general tokenizer: repo type
* declarations do not contain comment delimiters inside string literals.
*/
function normalize(code: string): string {
return code
.replace(/\/\*[\s\S]*?\*\//g, '')