Merge remote-tracking branch 'origin/worktree/ci-native-windows-20260808' into worktree/ci-native-windows-coverage-20260808

This commit is contained in:
Tianyi Cui
2026-08-09 02:15:46 +08:00
1062 changed files with 17432 additions and 9494 deletions

View File

@@ -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}](../core-data-structures/${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 [core-data-structures/](../core-data-structures/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 [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. 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')
}

View File

@@ -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 = [
@@ -152,10 +151,11 @@ describe.skip('gen-cordis-catalog collectEvents', { timeout: 60_000 }, () => {
it('accepts linked, foundation, generic-parameter, and explicitly exempt signature types', () => {
const events = collectEvents(make(
' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata outside the core catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'<T extends SessionEvent>(value: Readonly<T>, preset: PresetSpec, signal: AbortSignal): Promise<T>',
' /**\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](../core-data-structures/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]')
})

View File

@@ -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,10 +15,24 @@ 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/cordis/tool-cordis/src/api-catalog.ts'),
expected('packages/self-modification/tool-cordis/src/api-catalog.ts'),
)
})
})

View File

@@ -424,7 +424,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('rejects relative imports across face boundaries', () => {
const root = copyFixture('typert-relative-face-')
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
const source = readFileSync(sourcePath, 'utf8')
.replace("from '@fixture/host'", "from '../../host/src/index.ts'")
writeFileSync(sourcePath, source)
@@ -440,7 +440,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
join(root, 'packages/host/src/private.ts'),
'export interface PrivateHost { readonly value: string }\n',
)
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
const source = readFileSync(sourcePath, 'utf8')
.replace(
"import type { HostAgent, Payload } from '@fixture/host'",
@@ -463,7 +463,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
join(root, 'packages/host/src/private.ts'),
'export interface PrivateHost { readonly value: string }\n',
)
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
writeFileSync(sourcePath, [
readFileSync(sourcePath, 'utf8'),
"export type { PrivateHost } from '@fixture/host/private'",
@@ -477,7 +477,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('rejects cross-face namespace re-exports until the model has a namespace target', () => {
const root = copyFixture('typert-namespace-reexport-')
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
writeFileSync(sourcePath, [
readFileSync(sourcePath, 'utf8'),
"export type * as HostNamespace from '@fixture/host'",
@@ -492,10 +492,10 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('ignores cross-face namespace exports that are not package exports', () => {
const root = copyFixture('typert-private-namespace-reexport-')
writeFileSync(
join(root, 'packages/client/src/internal.ts'),
join(root, 'packages/client', 'src/internal.ts'),
"export type * as HiddenHostNamespace from '@fixture/host'\n",
)
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
writeFileSync(sourcePath, [
"import './internal.ts'",
readFileSync(sourcePath, 'utf8'),
@@ -508,7 +508,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('records public symbols from explicit cross-face star re-exports', () => {
const root = copyFixture('typert-star-reexport-')
const sourcePath = join(root, 'packages/client/src/index.ts')
const sourcePath = join(root, 'packages/client', 'src/index.ts')
writeFileSync(
sourcePath,
readFileSync(sourcePath, 'utf8')
@@ -1143,7 +1143,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => {
it('rejects a public Typert subpath that points outside the root-level face artifact', () => {
const root = copyFixture('typert-artifact-path-')
const manifestPath = join(root, 'packages/client/package.json')
const manifestPath = join(root, 'packages/client', 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
exports: Record<string, { types: string; default: string }>
}
@@ -1159,7 +1159,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => {
it('rejects absent Typert exports and package file entries', () => {
const noSubpathRoot = copyFixture('typert-missing-artifact-export-')
const noSubpathManifest = join(noSubpathRoot, 'packages/client/package.json')
const noSubpathManifest = join(noSubpathRoot, 'packages/client', 'package.json')
const noSubpath = JSON.parse(readFileSync(noSubpathManifest, 'utf8')) as Record<string, unknown>
noSubpath.exports = './lib/index.js'
writeFileSync(noSubpathManifest, `${JSON.stringify(noSubpath, null, 2)}\n`)
@@ -1168,7 +1168,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => {
)
const invalidSubpathRoot = copyFixture('typert-invalid-artifact-export-')
const invalidSubpathManifest = join(invalidSubpathRoot, 'packages/client/package.json')
const invalidSubpathManifest = join(invalidSubpathRoot, 'packages/client', 'package.json')
const invalidSubpath = JSON.parse(readFileSync(invalidSubpathManifest, 'utf8')) as {
exports: Record<string, unknown>
}
@@ -1179,7 +1179,7 @@ describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => {
)
const noFilesRoot = copyFixture('typert-missing-artifact-files-')
const noFilesManifest = join(noFilesRoot, 'packages/client/package.json')
const noFilesManifest = join(noFilesRoot, 'packages/client', 'package.json')
const noFiles = JSON.parse(readFileSync(noFilesManifest, 'utf8')) as Record<string, unknown>
delete noFiles.files
writeFileSync(noFilesManifest, `${JSON.stringify(noFiles, null, 2)}\n`)
@@ -1269,14 +1269,14 @@ function configureDualRuntimeClient(root: string, splitProjects: boolean): void
const hostAggregate = JSON.parse(readFileSync(hostAggregatePath, 'utf8')) as {
references: { path: string }[]
}
hostAggregate.references.push({ path: './packages/client/tsconfig.host.json' })
hostAggregate.references.push({ path: ['.', 'packages', 'client', 'tsconfig.host.json'].join('/') })
writeFileSync(hostAggregatePath, `${JSON.stringify(hostAggregate, null, 2)}\n`)
const clientAggregatePath = join(root, 'tsconfig.client.json')
const clientAggregate = JSON.parse(readFileSync(clientAggregatePath, 'utf8')) as {
references: { path: string }[]
}
clientAggregate.references = [{ path: './packages/client/tsconfig.client.json' }]
clientAggregate.references = [{ path: ['.', 'packages', 'client', 'tsconfig.client.json'].join('/') }]
writeFileSync(clientAggregatePath, `${JSON.stringify(clientAggregate, null, 2)}\n`)
}