|
|
|
|
@@ -1,25 +1,28 @@
|
|
|
|
|
/**
|
|
|
|
|
* Generate (and verify) the cordis events + services catalog in
|
|
|
|
|
* docs/cordis-catalog/events-and-services.md.
|
|
|
|
|
* Generate (and verify) the cordis events and services catalogs in
|
|
|
|
|
* docs/cordis-catalog/events.md and docs/cordis-catalog/services.md.
|
|
|
|
|
*
|
|
|
|
|
* The catalog is the WIRING-axis reference: every cordis event a plugin can
|
|
|
|
|
* listen to (exact signature + dispatch mode) and every `ctx.<key>` service it
|
|
|
|
|
* can call (exact public interface). It complements the core-data-structures
|
|
|
|
|
* catalog (the VOCABULARY axis — the types these signatures move around).
|
|
|
|
|
* The two pages are the WIRING-axis reference, one axis each: every cordis
|
|
|
|
|
* event a plugin can listen to (exact signature + dispatch mode) and every
|
|
|
|
|
* `ctx.<key>` service it can call (exact public interface). They complement the
|
|
|
|
|
* core-data-structures catalog (the VOCABULARY axis — the types these
|
|
|
|
|
* signatures move around).
|
|
|
|
|
*
|
|
|
|
|
* The catalog is FULLY GENERATED from source — never hand-edit it. The codebase
|
|
|
|
|
* is disciplined enough that a pure-AST pass captures the whole truthful
|
|
|
|
|
* surface: every event/service is a string literal that round-trips to a static
|
|
|
|
|
* `interface Events` / `interface Context` declaration (no dynamically-named
|
|
|
|
|
* events, no runtime-only services). So the committed file is a build artifact
|
|
|
|
|
* and a regenerate-and-diff freshness check (`--check`) makes drift structurally
|
|
|
|
|
* impossible. Because generation enumerates source rather than checking a
|
|
|
|
|
* hand-written subset, a brand-new event cannot be silently undocumented — it
|
|
|
|
|
* appears in the next regenerate, and an un-regenerated file fails `--check`.
|
|
|
|
|
* The catalogs are FULLY GENERATED from source — never hand-edit them. The
|
|
|
|
|
* codebase is disciplined enough that a pure-AST pass captures the whole
|
|
|
|
|
* truthful surface: every event/service is a string literal that round-trips
|
|
|
|
|
* to a static `interface Events` / `interface Context` declaration (no
|
|
|
|
|
* dynamically-named events, no runtime-only services). So the committed files
|
|
|
|
|
* are build artifacts and a regenerate-and-diff freshness check (`--check`)
|
|
|
|
|
* makes drift structurally impossible. Because generation enumerates source
|
|
|
|
|
* rather than checking a hand-written subset, a brand-new event cannot be
|
|
|
|
|
* silently undocumented — it appears in the next regenerate, and an
|
|
|
|
|
* un-regenerated file fails `--check`.
|
|
|
|
|
*
|
|
|
|
|
* `tsx scripts/gen-cordis-catalog.ts` → write the catalog
|
|
|
|
|
* `tsx scripts/gen-cordis-catalog.ts --check` → exit 1 if the committed file
|
|
|
|
|
* is stale (CI / pre-push gate)
|
|
|
|
|
* `tsx scripts/gen-cordis-catalog.ts` → write both catalogs
|
|
|
|
|
* `tsx scripts/gen-cordis-catalog.ts --check` → exit 1 if a committed
|
|
|
|
|
* catalog is stale (CI /
|
|
|
|
|
* pre-push gate)
|
|
|
|
|
*
|
|
|
|
|
* The HARNESS tier (the `@deepseek-ai/dsh-*` events + services) is rendered in
|
|
|
|
|
* full from source: signature, the `@mode` badge, and the declaration's JSDoc.
|
|
|
|
|
@@ -52,7 +55,8 @@ import { resolve } from 'node:path'
|
|
|
|
|
import ts from 'typescript'
|
|
|
|
|
|
|
|
|
|
const root = resolve(import.meta.dirname, '..')
|
|
|
|
|
const OUT = 'docs/cordis-catalog/events-and-services.md'
|
|
|
|
|
const OUT_EVENTS = 'docs/cordis-catalog/events.md'
|
|
|
|
|
const OUT_SERVICES = 'docs/cordis-catalog/services.md'
|
|
|
|
|
|
|
|
|
|
/** The fenced-block info string for generated signature blocks (skipped by
|
|
|
|
|
* doc-typecheck, since a bare signature fragment is not standalone-compilable). */
|
|
|
|
|
@@ -63,7 +67,8 @@ type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cross-link map: a type name that appears in a signature → the
|
|
|
|
|
* core-data-structures page that documents it (path relative to OUT's folder).
|
|
|
|
|
* core-data-structures page that documents it (path relative to the catalogs'
|
|
|
|
|
* folder).
|
|
|
|
|
* Hand-curated and catalog-owned, NOT derived from type-equiv.manifest.json —
|
|
|
|
|
* that manifest documents the `…Map` symbols (`ContentBlockMap`) while
|
|
|
|
|
* signatures reference the derived UNION names (`ContentBlock`), and it lists a
|
|
|
|
|
@@ -506,7 +511,7 @@ function typeLinks(signature: string): string {
|
|
|
|
|
|
|
|
|
|
/** Render one harness event entry. */
|
|
|
|
|
function renderEvent(e: EventEntry): string[] {
|
|
|
|
|
const out = [`#### \`${e.name}\` — ${e.mode}`, '']
|
|
|
|
|
const out = [`### \`${e.name}\` — ${e.mode}`, '']
|
|
|
|
|
if (e.doc) out.push(e.doc, '')
|
|
|
|
|
out.push('```' + FENCE, e.signature, '```', '')
|
|
|
|
|
const links = typeLinks(e.signature)
|
|
|
|
|
@@ -518,7 +523,7 @@ function renderEvent(e: EventEntry): string[] {
|
|
|
|
|
/** Render one harness service entry. */
|
|
|
|
|
function renderService(s: ServiceEntry): string[] {
|
|
|
|
|
const kind = s.abstract ? ' (abstract seam)' : ''
|
|
|
|
|
const out = [`### \`ctx.${s.key}\` — \`${s.type}\`${kind}`, '']
|
|
|
|
|
const out = [`## \`ctx.${s.key}\` — \`${s.type}\`${kind}`, '']
|
|
|
|
|
if (s.doc) out.push(s.doc, '')
|
|
|
|
|
if (s.methods.length) {
|
|
|
|
|
out.push('```' + FENCE, ...s.methods, '```', '')
|
|
|
|
|
@@ -529,51 +534,71 @@ function renderService(s: ServiceEntry): string[] {
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Render the full catalog (pure, deterministic given sorted inputs). */
|
|
|
|
|
function render(events: EventEntry[], services: ServiceEntry[]): string {
|
|
|
|
|
/** The shared generated-file banner comment. */
|
|
|
|
|
const BANNER = [
|
|
|
|
|
'<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.',
|
|
|
|
|
' Run `pnpm run gen-cordis-catalog` to regenerate. -->',
|
|
|
|
|
'',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
/** The shared GENERATED + freshness-gate + fence notice paragraph. */
|
|
|
|
|
const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them.'
|
|
|
|
|
|
|
|
|
|
/** Render the events catalog (pure, deterministic given sorted inputs). */
|
|
|
|
|
function renderEvents(events: EventEntry[]): string {
|
|
|
|
|
const lines: string[] = [
|
|
|
|
|
'<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.',
|
|
|
|
|
' Run `pnpm run gen-cordis-catalog` to regenerate. -->',
|
|
|
|
|
...BANNER,
|
|
|
|
|
'# Cordis Events Catalog',
|
|
|
|
|
'',
|
|
|
|
|
'# Cordis Events & Services Catalog',
|
|
|
|
|
'Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration\'s JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.<key>` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.',
|
|
|
|
|
'',
|
|
|
|
|
'An index reference to the **wiring** a plugin author works against: every cordis event you can listen to (exact signature + dispatch mode) and every `ctx.<key>` service you can call (exact public interface). It complements [core-data-structures/](../core-data-structures/core.md), which catalogs the *data structures* these signatures move around — this page is the verbs, that page is the nouns.',
|
|
|
|
|
GATE_NOTICE,
|
|
|
|
|
'',
|
|
|
|
|
'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them.',
|
|
|
|
|
'',
|
|
|
|
|
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer surface a plugin also sees — pinned vendor source, summarized tersely.',
|
|
|
|
|
'',
|
|
|
|
|
'## Events',
|
|
|
|
|
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.',
|
|
|
|
|
'',
|
|
|
|
|
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../architecture.md#cordis-waterfall-semantics-important)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).',
|
|
|
|
|
'',
|
|
|
|
|
]
|
|
|
|
|
const scopes = [...new Set(events.map(e => e.scope))].sort()
|
|
|
|
|
for (const scope of scopes) {
|
|
|
|
|
lines.push(`### \`${scope}/*\``, '')
|
|
|
|
|
lines.push(`## \`${scope}/*\``, '')
|
|
|
|
|
for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
|
|
|
lines.push(...renderEvent(e))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lines.push(
|
|
|
|
|
'## Services',
|
|
|
|
|
'## Inherited events (cordis core + loader/hmr/timer)',
|
|
|
|
|
'',
|
|
|
|
|
'The `ctx.<key>` services the harness provides. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.',
|
|
|
|
|
'',
|
|
|
|
|
)
|
|
|
|
|
for (const s of services) lines.push(...renderService(s))
|
|
|
|
|
lines.push(
|
|
|
|
|
'## Inherited tier (cordis core + loader/hmr/timer)',
|
|
|
|
|
'',
|
|
|
|
|
'The framework surface every plugin inherits, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the catalog is a complete picture of what `ctx` and the event bus offer, without elevating framework internals to the harness tier\'s prominence.',
|
|
|
|
|
'',
|
|
|
|
|
'### Inherited events',
|
|
|
|
|
'The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier\'s prominence.',
|
|
|
|
|
'',
|
|
|
|
|
)
|
|
|
|
|
for (const e of INHERITED_EVENTS) {
|
|
|
|
|
lines.push(`- \`${e.name}\` — ${e.summary} ([\`${e.source}\`](../../${e.source.split(':')[0]}))`)
|
|
|
|
|
}
|
|
|
|
|
lines.push('', '### Inherited `ctx` members', '')
|
|
|
|
|
lines.push('')
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Render the services catalog (pure, deterministic given sorted inputs). */
|
|
|
|
|
function renderServices(services: ServiceEntry[]): string {
|
|
|
|
|
const lines: string[] = [
|
|
|
|
|
...BANNER,
|
|
|
|
|
'# Cordis Services Catalog',
|
|
|
|
|
'',
|
|
|
|
|
'Every `ctx.<key>` service a plugin can call: the exact public interface plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.',
|
|
|
|
|
'',
|
|
|
|
|
GATE_NOTICE,
|
|
|
|
|
'',
|
|
|
|
|
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.',
|
|
|
|
|
'',
|
|
|
|
|
]
|
|
|
|
|
for (const s of services) lines.push(...renderService(s))
|
|
|
|
|
lines.push(
|
|
|
|
|
'## Inherited `ctx` members (cordis core + loader/hmr/timer)',
|
|
|
|
|
'',
|
|
|
|
|
'The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier\'s prominence.',
|
|
|
|
|
'',
|
|
|
|
|
)
|
|
|
|
|
for (const s of INHERITED_SERVICES) {
|
|
|
|
|
lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`)
|
|
|
|
|
}
|
|
|
|
|
@@ -581,31 +606,38 @@ function render(events: EventEntry[], services: ServiceEntry[]): string {
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** CLI entry: `--write` (default) writes the catalog, `--check` fails if stale.
|
|
|
|
|
* Guarded behind an entry-point check so importing this module for tests neither
|
|
|
|
|
* regenerates the committed file nor calls process.exit. */
|
|
|
|
|
/** CLI entry: `--write` (default) writes both catalogs, `--check` fails if
|
|
|
|
|
* either is stale. Guarded behind an entry-point check so importing this module
|
|
|
|
|
* for tests neither regenerates the committed files nor calls process.exit. */
|
|
|
|
|
function main(): void {
|
|
|
|
|
const content = render(collectEvents(), collectServices())
|
|
|
|
|
const outputs: [string, string][] = [
|
|
|
|
|
[OUT_EVENTS, renderEvents(collectEvents())],
|
|
|
|
|
[OUT_SERVICES, renderServices(collectServices())],
|
|
|
|
|
]
|
|
|
|
|
if (process.argv.includes('--check')) {
|
|
|
|
|
let committed: string | null = null
|
|
|
|
|
try {
|
|
|
|
|
committed = readFileSync(resolve(root, OUT), 'utf8')
|
|
|
|
|
} catch {
|
|
|
|
|
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
|
|
|
|
|
// file is not a state this repo produces. Either way the remedy is the
|
|
|
|
|
// same — regenerate — so treat a read failure as "stale".
|
|
|
|
|
committed = null
|
|
|
|
|
const stale: string[] = []
|
|
|
|
|
for (const [out, content] of outputs) {
|
|
|
|
|
let committed: string | null = null
|
|
|
|
|
try {
|
|
|
|
|
committed = readFileSync(resolve(root, out), 'utf8')
|
|
|
|
|
} catch {
|
|
|
|
|
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
|
|
|
|
|
// file is not a state this repo produces. Either way the remedy is the
|
|
|
|
|
// same — regenerate — so treat a read failure as "stale".
|
|
|
|
|
committed = null
|
|
|
|
|
}
|
|
|
|
|
if (committed !== content) stale.push(out)
|
|
|
|
|
}
|
|
|
|
|
if (committed === content) {
|
|
|
|
|
console.log(`gen-cordis-catalog: ${OUT} is up to date.`)
|
|
|
|
|
if (stale.length === 0) {
|
|
|
|
|
console.log(`gen-cordis-catalog: ${OUT_EVENTS} and ${OUT_SERVICES} are up to date.`)
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
console.error(`gen-cordis-catalog: ${OUT} is stale. Run \`pnpm run gen-cordis-catalog\` and commit ${OUT}.`)
|
|
|
|
|
console.error(`gen-cordis-catalog: ${stale.join(' and ')} ${stale.length === 1 ? 'is' : 'are'} stale. Run \`pnpm run gen-cordis-catalog\` and commit the result.`)
|
|
|
|
|
process.exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeFileSync(resolve(root, OUT), content)
|
|
|
|
|
console.log(`gen-cordis-catalog: wrote ${OUT}.`)
|
|
|
|
|
for (const [out, content] of outputs) writeFileSync(resolve(root, out), content)
|
|
|
|
|
console.log(`gen-cordis-catalog: wrote ${OUT_EVENTS} and ${OUT_SERVICES}.`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run only when invoked as a script, not when imported by a test.
|
|
|
|
|
|