docs: generate each subsystem's cordis surface into its own page; delete the flat catalogs
Rebuild of the region machinery (PR3) on the post-#904 Typert projection: renderPageRegion/renderInheritedPage live in dsh-typert-generator beside the projection; scripts/gen-cordis-catalog.ts owns the curated SERVICE_PAGE / EVENT_SCOPE_PAGE / SERVICE_WALK_EXEMPTIONS / LINK_MAP partition (fail-loud in both directions, with the independent Context-merge scan backstopping the projection's blind spot), spliceRegion, and the guarded pair auto-record. docs/cordis-catalog/ is deleted: the flat events/services catalogs dissolve into per-page regions and docs/cordis-catalog/core moves to docs/cordis-api/ with the inherited tier as its own generated page. The partition absorbs the post-regrouping surface: ctx.typert → invariants.md, ctx.directoryPicker → workspace.md, skills/* events → skills.md, and the four launcher-provided tui accessor values join the named exemptions.
This commit is contained in:
@@ -694,32 +694,61 @@ function renderRuntimeApi(
|
||||
lines.push(']', '')
|
||||
return lines.join('\n')
|
||||
}
|
||||
/** Render the cross-link "Types:" line for a signature, or '' if none apply. */
|
||||
function typeLinks(signature: string, linkedTypePages: Readonly<Record<string, string>>): string {
|
||||
/** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
|
||||
export const REGION_BEGIN = '<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->'
|
||||
/** Closing region delimiter matching {@link REGION_BEGIN}. */
|
||||
export const REGION_END = '<!-- END GENERATED cordis-surface -->'
|
||||
|
||||
/**
|
||||
* Render the cross-link "Types:" line for a signature relative to one
|
||||
* subsystems page, or '' if none apply. A type whose primary page IS the
|
||||
* rendering page would link as a fragmentless self-link readers already sit
|
||||
* on, so it is dropped instead.
|
||||
*/
|
||||
function typeLinks(signature: string, onPage: string, linkedTypePages: Readonly<Record<string, string>>): string {
|
||||
const seen = new Set<string>()
|
||||
for (const name of Object.keys(linkedTypePages)) {
|
||||
if (new RegExp(`\\b${name}\\b`).test(signature)) seen.add(name)
|
||||
}
|
||||
if (seen.size === 0) return ''
|
||||
const links = [...seen].sort().map(n => `[${n}](../subsystems/${linkedTypePages[n]})`)
|
||||
const links = [...seen].sort()
|
||||
.filter(name => linkedTypePages[name] !== onPage)
|
||||
.map(name => `[${name}](${linkedTypePages[name]})`)
|
||||
if (links.length === 0) return ''
|
||||
return `Types: ${links.join(' · ')}`
|
||||
}
|
||||
|
||||
/** Render one harness event entry. */
|
||||
function renderEvent(e: EventEntry, linkedTypePages: Readonly<Record<string, string>>): string[] {
|
||||
const out = [`### \`${e.name}\` — ${e.mode}`, '']
|
||||
/**
|
||||
* GitHub's heading-slug algorithm (lowercase; drop everything but letters,
|
||||
* numbers, spaces, hyphens; spaces become hyphens). Region headings carry
|
||||
* backticks and em-dashes, which VitePress slugifies differently, so each
|
||||
* generated heading is preceded by an explicit `<a id>` carrying this slug —
|
||||
* the historical flat-catalog anchor — making `#ctx<key>--<class>` fragments
|
||||
* resolve identically on GitHub and the published site.
|
||||
*/
|
||||
function githubSlug(heading: string): string {
|
||||
return heading.toLowerCase().replace(/[^\p{L}\p{N} -]/gu, '').replaceAll(' ', '-')
|
||||
}
|
||||
|
||||
/** The explicit-anchor line emitted before one generated heading. */
|
||||
function anchorFor(headingText: string): string[] {
|
||||
return [`<a id="${githubSlug(headingText)}"></a>`, '']
|
||||
}
|
||||
|
||||
/** Render one harness event entry onto its owning page, nested under its scope heading. */
|
||||
function renderEvent(e: EventEntry, onPage: string, linkedTypePages: Readonly<Record<string, string>>): string[] {
|
||||
const out = [...anchorFor(`${e.name} — ${e.mode}`), `#### \`${e.name}\` — ${e.mode}`, '']
|
||||
if (e.doc) out.push(e.doc, '')
|
||||
out.push('```' + FENCE, e.jsDoc, e.signature, '```', '')
|
||||
const links = typeLinks(e.signature, linkedTypePages)
|
||||
const links = typeLinks(e.signature, onPage, linkedTypePages)
|
||||
if (links) out.push(links, '')
|
||||
out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '')
|
||||
return out
|
||||
}
|
||||
|
||||
/** Render one harness service entry. */
|
||||
function renderService(s: ServiceEntry, linkedTypePages: Readonly<Record<string, string>>): string[] {
|
||||
/** Render one harness service entry onto its owning page. */
|
||||
function renderService(s: ServiceEntry, onPage: string, linkedTypePages: Readonly<Record<string, string>>): string[] {
|
||||
const kind = s.abstract ? ' (abstract seam)' : ''
|
||||
const out = [`## \`ctx.${s.key}\` — \`${s.type}\`${kind}`, '']
|
||||
const out = [...anchorFor(`ctx.${s.key} — ${s.type}${kind}`), `### \`ctx.${s.key}\` — \`${s.type}\`${kind}`, '']
|
||||
if (s.doc) out.push(s.doc, '')
|
||||
if (s.methods.length) {
|
||||
const declarations = s.methods.flatMap((method, index) => [
|
||||
@@ -728,7 +757,7 @@ function renderService(s: ServiceEntry, linkedTypePages: Readonly<Record<string,
|
||||
method.signature,
|
||||
])
|
||||
out.push('```' + FENCE, ...declarations, '```', '')
|
||||
const links = typeLinks(s.methods.map(method => method.signature).join('\n'), linkedTypePages)
|
||||
const links = typeLinks(s.methods.map(method => method.signature).join('\n'), onPage, linkedTypePages)
|
||||
if (links) out.push(links, '')
|
||||
}
|
||||
out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '')
|
||||
@@ -746,36 +775,62 @@ const BANNER = [
|
||||
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 and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.'
|
||||
|
||||
/**
|
||||
* Render the events catalog deterministically.
|
||||
* @param events - validated event entries to render.
|
||||
* @param policy - type links and inherited events supplied by the caller.
|
||||
* Render one page's generated `cordis-surface` region: the services mapped to
|
||||
* the page, then the event scopes mapped to it, markers included. Pure and
|
||||
* deterministic given sorted inputs; identical bytes land in both pair sides.
|
||||
* @param page - the owning `docs/subsystems/` page basename, e.g. `core.md`.
|
||||
* @param services - validated services mapped to this page.
|
||||
* @param events - validated events whose scopes map to this page.
|
||||
* @param policy - type links supplied by the caller.
|
||||
* @returns the complete marker-delimited region text.
|
||||
*/
|
||||
export function renderPageRegion(page: string, services: ServiceEntry[], events: EventEntry[], policy: CordisCatalogPolicy): string {
|
||||
const lines: string[] = [
|
||||
REGION_BEGIN,
|
||||
'',
|
||||
'<a id="cordis-surface"></a>',
|
||||
'',
|
||||
'## Cordis surface',
|
||||
'',
|
||||
'Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).',
|
||||
'',
|
||||
]
|
||||
for (const s of services) lines.push(...renderService(s, page, policy.linkedTypePages))
|
||||
const scopes = [...new Set(events.map(e => e.scope))].sort()
|
||||
for (const scope of scopes) {
|
||||
lines.push(...anchorFor(`${scope}/* events`), `### \`${scope}/*\` events`, '')
|
||||
for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
lines.push(...renderEvent(e, page, policy.linkedTypePages))
|
||||
}
|
||||
}
|
||||
while (lines.at(-1) === '') lines.pop()
|
||||
lines.push(REGION_END)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the inherited (pinned vendor) tier as its own generated page.
|
||||
* @param policy - inherited events and services supplied by the caller.
|
||||
* @returns the complete generated Markdown document.
|
||||
*/
|
||||
export function renderEvents(events: EventEntry[], policy: CordisCatalogPolicy): string {
|
||||
export function renderInheritedPage(policy: CordisCatalogPolicy): string {
|
||||
const lines: string[] = [
|
||||
...BANNER,
|
||||
'# Cordis Events Catalog',
|
||||
'# Inherited Cordis Surface',
|
||||
'',
|
||||
'Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration 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 [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around.',
|
||||
'The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md).',
|
||||
'',
|
||||
GATE_NOTICE,
|
||||
'',
|
||||
'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. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).',
|
||||
'',
|
||||
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **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`).',
|
||||
'## Inherited `ctx` members (cordis core + loader/hmr/timer)',
|
||||
'',
|
||||
]
|
||||
const scopes = [...new Set(events.map(e => e.scope))].sort()
|
||||
for (const scope of scopes) {
|
||||
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, policy.linkedTypePages))
|
||||
}
|
||||
for (const s of policy.inheritedServices) {
|
||||
lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`)
|
||||
}
|
||||
lines.push(
|
||||
'## Inherited events (cordis core + loader/hmr/timer)',
|
||||
'',
|
||||
'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.',
|
||||
'## Inherited events (cordis core + loader/hmr/timer)',
|
||||
'',
|
||||
)
|
||||
for (const e of policy.inheritedEvents) {
|
||||
@@ -784,35 +839,3 @@ export function renderEvents(events: EventEntry[], policy: CordisCatalogPolicy):
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the services catalog deterministically.
|
||||
* @param services - validated service entries to render.
|
||||
* @param policy - type links and inherited services supplied by the caller.
|
||||
* @returns the complete generated Markdown document.
|
||||
*/
|
||||
export function renderServices(services: ServiceEntry[], policy: CordisCatalogPolicy): string {
|
||||
const lines: string[] = [
|
||||
...BANNER,
|
||||
'# Cordis Services Catalog',
|
||||
'',
|
||||
'Every `ctx.<key>` service a plugin can call: the exact public interface with original method JSDoc, 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 [subsystems/](../subsystems/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. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).',
|
||||
'',
|
||||
]
|
||||
for (const s of services) lines.push(...renderService(s, policy.linkedTypePages))
|
||||
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 policy.inheritedServices) {
|
||||
lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`)
|
||||
}
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
collectEvents as collectEventsWithPolicy,
|
||||
collectServices as collectServicesWithPolicy,
|
||||
renderEvents as renderEventsWithPolicy,
|
||||
renderServices as renderServicesWithPolicy,
|
||||
renderPageRegion,
|
||||
} from '../src/cordis-catalog.ts'
|
||||
import type {
|
||||
CordisCatalogPolicy,
|
||||
@@ -35,12 +34,12 @@ function collectServices(root: string): ServiceEntry[] {
|
||||
return collectServicesWithPolicy(root, TEST_POLICY)
|
||||
}
|
||||
|
||||
function renderEvents(events: EventEntry[]): string {
|
||||
return renderEventsWithPolicy(events, TEST_POLICY)
|
||||
function renderEvents(events: EventEntry[], onPage = 'bash.md'): string {
|
||||
return renderPageRegion(onPage, [], events, TEST_POLICY)
|
||||
}
|
||||
|
||||
function renderServices(services: ServiceEntry[]): string {
|
||||
return renderServicesWithPolicy(services, TEST_POLICY)
|
||||
function renderServices(services: ServiceEntry[], onPage = 'bash.md'): string {
|
||||
return renderPageRegion(onPage, services, [], TEST_POLICY)
|
||||
}
|
||||
|
||||
const TYPE_FIXTURES = [
|
||||
@@ -155,7 +154,8 @@ describe.skip('gen-cordis-catalog collectEvents', { timeout: 60_000 }, () => {
|
||||
' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata documented outside the subsystems catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'<T extends SessionEvent>(value: Readonly<T>, preset: PresetSpec, signal: AbortSignal): Promise<T>',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
expect(renderEvents(events)).toContain('Types: [SessionEvent](../subsystems/core.md)')
|
||||
expect(renderEvents(events)).toContain('Types: [SessionEvent](core.md)')
|
||||
expect(renderEvents(events, 'core.md')).not.toContain('Types: [SessionEvent]')
|
||||
expect(renderEvents(events)).not.toContain('[PresetSpec]')
|
||||
})
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
projectCordisCatalog,
|
||||
renderEvents,
|
||||
renderServices,
|
||||
renderInheritedPage,
|
||||
renderPageRegion,
|
||||
} from '../src/cordis-catalog.ts'
|
||||
import { CORDIS_CATALOG_POLICY } from '../../../../scripts/gen-cordis-catalog.ts'
|
||||
import { CORDIS_CATALOG_POLICY, EVENT_SCOPE_PAGE, REGION_BEGIN, REGION_END, SERVICE_PAGE } from '../../../../scripts/gen-cordis-catalog.ts'
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, '../../../..')
|
||||
|
||||
@@ -15,8 +15,22 @@ describe('Typert-backed Cordis catalog', () => {
|
||||
const { projector, model } = projectCordisCatalog(workspaceRoot, CORDIS_CATALOG_POLICY)
|
||||
const expected = (path: string): string => readFileSync(join(workspaceRoot, path), 'utf8')
|
||||
|
||||
expect(renderEvents([...model.events], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/events.md'))
|
||||
expect(renderServices([...model.services], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/services.md'))
|
||||
expect(renderInheritedPage(CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-api/inherited.md'))
|
||||
for (const page of [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])].sort()) {
|
||||
const region = renderPageRegion(
|
||||
page,
|
||||
[...model.services].filter(s => SERVICE_PAGE[s.key] === page),
|
||||
[...model.events].filter(e => EVENT_SCOPE_PAGE[e.scope] === page),
|
||||
CORDIS_CATALOG_POLICY,
|
||||
)
|
||||
for (const side of [page, page.replace(/\.md$/, '.zh.md')]) {
|
||||
const committed = expected(`docs/subsystems/${side}`)
|
||||
const begin = committed.indexOf(REGION_BEGIN)
|
||||
const end = committed.indexOf(REGION_END)
|
||||
expect(begin, `docs/subsystems/${side} carries the region`).toBeGreaterThanOrEqual(0)
|
||||
expect(committed.slice(begin, end + REGION_END.length)).toBe(region)
|
||||
}
|
||||
}
|
||||
expect(projector.renderRuntimeApi(model)).toBe(
|
||||
expected('packages/self-modification/tool-cordis/src/api-catalog.ts'),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user