gen-cordis-catalog.ts 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  1. /**
  2. * Generate the per-subsystem Cordis service/event reference regions from the
  3. * Typert catalog projection. Every harness `ctx.<key>` service and event scope
  4. * maps to exactly one `docs/subsystems/` page through the curated tables below;
  5. * the generator injects each page's Cordis API reference between its GENERATED markers —
  6. * byte-identically into both language sides of the pair — and re-records a
  7. * pair's `.i18n.yaml` only when nothing outside the region changed. The
  8. * projection enforces event modes, JSDoc parameter/return completeness, and
  9. * signature type-link coverage; the inherited (vendor) tier renders to
  10. * `docs/cordis-api/inherited.md`. `--check` verifies every generated artifact.
  11. *
  12. * Generated regions embed `file:line` source pointers, so inserting lines ABOVE a
  13. * recorded symbol makes the committed output stale even though nothing about the
  14. * symbol changed. Regenerate after editing any file this projection records — the
  15. * failure otherwise surfaces as the "reproduces every committed catalog artifact
  16. * byte for byte" test failing, which reads like a snapshot regression rather than
  17. * a missing regeneration.
  18. */
  19. import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
  20. import { dirname, resolve } from 'node:path'
  21. import {
  22. projectCordisCatalog,
  23. renderInheritedPage,
  24. renderPageRegion,
  25. REGION_BEGIN,
  26. REGION_END,
  27. } from '@deepseek-ai/dsh-typert-generator'
  28. import type { CordisCatalogPolicy } from '@deepseek-ai/dsh-typert-generator'
  29. import { renderCordisCoreApiPages } from './cordis-core-api.ts'
  30. import { contextKeyMap, contextMergeFiles, eventNameList } from './cordis-walk.ts'
  31. import {
  32. blobHash,
  33. parsePairMeta,
  34. partitionGeneratedRegions,
  35. renderPairMeta,
  36. } from './translation-pairing.ts'
  37. const root = resolve(import.meta.dirname, '..')
  38. const SUBSYSTEMS_DIR = 'docs/subsystems'
  39. const OUT_INHERITED = 'docs/cordis-api/inherited.md'
  40. const OUT_RUNTIME_API = 'packages/extensions/tool-cordis/src/api-catalog.ts'
  41. export { REGION_BEGIN, REGION_END }
  42. /**
  43. * The owning subsystems page for every harness `ctx.<key>` service the
  44. * projection discovers. Fail-closed both ways: a discovered key absent here
  45. * and an entry whose key the projection no longer discovers are both hard
  46. * errors, so the partition can never silently drift from the service API.
  47. */
  48. export const SERVICE_PAGE: Record<string, string> = {
  49. agentLoop: 'core.md',
  50. agentDefaultModel: 'core.md',
  51. agentPresets: 'core.md',
  52. agents: 'core.md',
  53. apiProxy: 'typert.md',
  54. approval: 'approval.md',
  55. attachments: 'attachment.md',
  56. shell: 'shell.md',
  57. shellEnv: 'shell.md',
  58. clientModules: 'client-modules.md',
  59. codeRuntime: 'code-runtime.md',
  60. commands: 'commands.md',
  61. compaction: 'compaction.md',
  62. cordisInspect: 'extensions.md',
  63. credentials: 'credentials.md',
  64. directoryPicker: 'workspace.md',
  65. dynamicCordisRunner: 'extensions.md',
  66. e2b: 'subprocess.md',
  67. fs: 'filesystem.md',
  68. goals: 'goal.md',
  69. webServer: 'web-server.md',
  70. invariants: 'invariants.md',
  71. llm: 'llm-streaming.md',
  72. lsp: 'lsp.md',
  73. messageFeedback: 'feedback.md',
  74. permissionPresets: 'permission-presets.md',
  75. planMode: 'plan.md',
  76. terminals: 'terminal.md',
  77. sandbox: 'sandbox.md',
  78. sandboxPolicy: 'sandbox.md',
  79. sessionPersistence: 'persistence.md',
  80. sessionQuery: 'session-query.md',
  81. sessionReferenceResolver: 'session-reference.md',
  82. sessionProjectionCache: 'session-projection.md',
  83. sessionProjections: 'session-projection.md',
  84. sessions: 'session.md',
  85. settings: 'settings.md',
  86. sessionTitle: 'session-title.md',
  87. skills: 'skills.md',
  88. spillStore: 'spill.md',
  89. storage: 'storage.md',
  90. storageDomain: 'storage.md',
  91. subagents: 'subagent.md',
  92. subprocess: 'subprocess.md',
  93. systemPrompt: 'system-prompt.md',
  94. jobs: 'jobs.md',
  95. sessionTelemetry: 'session-telemetry.md',
  96. tokenMeter: 'token-meter.md',
  97. toolResultPruner: 'compaction.md',
  98. tools: 'tools.md',
  99. typert: 'typert.md',
  100. typertGateway: 'typert.md',
  101. userQuestions: 'user-questions.md',
  102. web: 'web.md',
  103. workflowEngine: 'workflow.md',
  104. workspaceRegistry: 'workspace.md',
  105. }
  106. /**
  107. * Context keys declared in `interface Context` merges that the rendering
  108. * projection cannot see, each with the reason and its documentation owner.
  109. * The scan that enforces this list reads EVERY `declare module '@deepseek-ai/cordis'`
  110. * Context merge under `packages/x/x/src/**` — any depth, not only root
  111. * `index.ts` files with a same-named service class — so a new service can
  112. * never silently join this blind spot: it either enters {@link SERVICE_PAGE}
  113. * or names itself here. Client-face keys (the projection analyzes the host
  114. * face only) name the package README that owns their surface.
  115. *
  116. * Two categories remain, and neither is a projection gap a scanning rule could
  117. * close. An OPTIONAL key (`key?: X`) is a value the launcher or boot code
  118. * installs before the tree mounts, which the analyzer skips by rule because no
  119. * plugin provides it and `inject` cannot reach it. A client-face key belongs to
  120. * the browser Context, which this host-face program never sees; the browser
  121. * surface has its own generated catalog (`scripts/gen-client-catalog.ts`, served
  122. * to a model as `cordis_runtime_inspect what:"client"`).
  123. */
  124. export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
  125. agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle',
  126. appExit: 'not a service: launcher-provided bounded process-exit callback — packages/boot/cmdline/README.md owns the launcher contract',
  127. cmdlineArgs: 'not a service: launcher-provided immutable app argument accessor — packages/boot/cmdline/README.md owns the launcher contract',
  128. configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract',
  129. launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract',
  130. dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract',
  131. launchEnvironment: 'not a service: launcher-provided root accessor value (LaunchEnvironmentSnapshot | undefined) — packages/util/launch-environment/README.md owns this launcher contract',
  132. connection: 'interface-typed (HostConnectionHandle); implementing class HostConnectionService is declared in rpc-host.ts — packages/client/connection/README.md owns the API',
  133. appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the API',
  134. settingsScope: 'client-side settings-namespace transport service — packages/client/ui-settings/README.md owns the API',
  135. chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API',
  136. commandUi: 'client-side interface-typed browser service — packages/client/ui-commands/README.md owns the API',
  137. conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API',
  138. conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
  139. conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
  140. layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API',
  141. locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the API',
  142. modelDirectories: 'client-side interface-typed browser service — packages/client/ui-model-selection/README.md owns the API',
  143. modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the API',
  144. remote: 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the API',
  145. sessionLogDownload: 'client-side browser download controller — packages/session-query/session-log-export/README.md owns the API',
  146. inputTriggers: 'client-side interface-typed browser service — packages/client/ui-input-trigger/README.md owns the API',
  147. timer: 'client-side dynamic-package timer service — packages/extensions/cordis-client-runner/README.md owns the API',
  148. slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
  149. theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
  150. workspaces: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
  151. }
  152. /**
  153. * The owning subsystems page for every harness event scope (the segment
  154. * before the first `/`) the projection renders. Fail-closed exactly like
  155. * {@link SERVICE_PAGE}. Client-face events (`slash/*`, `theme/change`, …) are
  156. * invisible to the host-face projection and therefore never reach this map;
  157. * {@link EVENT_WALK_EXEMPTIONS} names each one with its documentation owner.
  158. */
  159. export const EVENT_SCOPE_PAGE: Record<string, string> = {
  160. 'agent': 'core.md',
  161. 'agent-loop': 'core.md',
  162. 'agent-preset': 'core.md',
  163. 'approval': 'approval.md',
  164. 'commands': 'commands.md',
  165. 'cordis': 'extensions.md',
  166. 'credentials': 'credentials.md',
  167. 'domain': 'storage.md',
  168. 'fs': 'filesystem.md',
  169. 'goal': 'goal.md',
  170. 'llm': 'llm-streaming.md',
  171. 'session': 'session.md',
  172. 'settings': 'settings.md',
  173. 'skills': 'skills.md',
  174. 'subagent': 'subagent.md',
  175. 'system-prompt': 'system-prompt.md',
  176. 'session-telemetry': 'session-telemetry.md',
  177. 'tools': 'tools.md',
  178. 'workflow': 'workflow.md',
  179. }
  180. /**
  181. * Event names declared in `interface Events` merges that the rendering
  182. * projection cannot see, each with the reason and its documentation owner.
  183. * The mirror of {@link SERVICE_WALK_EXEMPTIONS} for events: an independent
  184. * scan reads EVERY `declare module '@deepseek-ai/cordis'` Events merge under
  185. * `packages/x/x/src/**`, so a declared event either renders onto a subsystems
  186. * page (via {@link EVENT_SCOPE_PAGE}) or names itself here — never vanishes
  187. * silently. Keys are full event names rather than scopes, so a scope-level
  188. * exemption cannot mask another declaration in that scope.
  189. */
  190. export const EVENT_WALK_EXEMPTIONS: Record<string, string> = {
  191. 'command/executed': 'client-face local command acknowledgment — packages/client/ui-commands/README.md owns the API',
  192. 'connection/reset': 'client-face transport signal — packages/client/runtime/README.md owns the API',
  193. 'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the API',
  194. 'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
  195. 'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
  196. 'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
  197. 'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
  198. 'slots/changed': 'client-face slot invalidation signal — packages/client/runtime/README.md owns the API',
  199. 'theme/change': 'client-face theme switch signal — packages/client/ui-theme/README.md owns the API',
  200. }
  201. /**
  202. * One primary subsystems page per project type used by a generated
  203. * signature. This stays curated because union names intentionally do not
  204. * reuse the type-equivalence manifest's map-symbol entries and some symbols
  205. * appear on more than one page.
  206. */
  207. export const LINK_MAP: Readonly<Record<string, string>> = {
  208. Agent: 'core.md',
  209. AgentCancelCause: 'core.md',
  210. AgentFactory: 'core.md',
  211. AgentHandle: 'core.md',
  212. ModelSelection: 'core.md',
  213. AgentOptions: 'core.md',
  214. AgentStatus: 'core.md',
  215. ContentBlock: 'llm-streaming.md',
  216. CreateAgentOptions: 'core.md',
  217. GenerateOptions: 'llm-streaming.md',
  218. InboxItem: 'core.md',
  219. InboxPlacement: 'core.md',
  220. MessageId: 'llm-streaming.md',
  221. ResumeAgentOptions: 'core.md',
  222. SettleReason: 'core.md',
  223. AdapterRegistrationHandle: 'llm-streaming.md',
  224. DirectoryRegistrationHandle: 'llm-streaming.md',
  225. LlmCallConfig: 'llm-streaming.md',
  226. LlmModelContext: 'llm-streaming.md',
  227. LlmModelReasoningInfo: 'llm-streaming.md',
  228. LlmResolvedModelInfo: 'llm-streaming.md',
  229. LlmFailure: 'llm-streaming.md',
  230. LlmModelInfo: 'llm-streaming.md',
  231. LlmProviderInfo: 'llm-streaming.md',
  232. LlmConfigurableProvider: 'llm-streaming.md',
  233. LlmModelDiscoveryRequest: 'llm-streaming.md',
  234. LlmDiscoveredModel: 'llm-streaming.md',
  235. ResolvedRetryPolicy: 'llm-streaming.md',
  236. Message: 'llm-streaming.md',
  237. MessageSource: 'llm-streaming.md',
  238. MessageFeedbackDeleteRequest: 'feedback.md',
  239. MessageFeedbackDeleteResult: 'feedback.md',
  240. MessageFeedbackDeleteValue: 'feedback.md',
  241. MessageFeedbackFailure: 'feedback.md',
  242. MessageFeedbackItem: 'feedback.md',
  243. MessageFeedbackListRequest: 'feedback.md',
  244. MessageFeedbackListResult: 'feedback.md',
  245. MessageFeedbackListValue: 'feedback.md',
  246. MessageFeedbackNoteBlank: 'feedback.md',
  247. MessageFeedbackNoteTooLarge: 'feedback.md',
  248. MessageFeedbackPutRequest: 'feedback.md',
  249. MessageFeedbackPutResult: 'feedback.md',
  250. MessageFeedbackRating: 'feedback.md',
  251. MessageFeedbackRejected: 'feedback.md',
  252. MessageFeedbackSessionNotFound: 'feedback.md',
  253. MessageFeedbackSuccess: 'feedback.md',
  254. MessageFeedbackTargetNotFound: 'feedback.md',
  255. MessageFeedbackVersion: 'feedback.md',
  256. MessageFeedbackVersionConflict: 'feedback.md',
  257. UserMessage: 'session.md',
  258. PreStepDecision: 'core.md',
  259. PreStepContext: 'core.md',
  260. RequestErrorAction: 'core.md',
  261. RequestFailureContext: 'core.md',
  262. PreparedReferencedMessage: 'session-reference.md',
  263. SessionReferenceCandidate: 'session-reference.md',
  264. SessionReferenceInput: 'session-reference.md',
  265. SessionEvent: 'session.md',
  266. SessionId: 'core.md',
  267. SessionStartSource: 'core.md',
  268. SessionLogSnapshot: 'session-query.md',
  269. SessionSurfaceSnapshot: 'session-query.md',
  270. ApprovalOutcome: 'approval.md',
  271. ApprovalPolicy: 'approval.md',
  272. ApprovalRequest: 'approval.md',
  273. ApprovalService: 'approval.md',
  274. ImageAttachmentRef: 'attachment.md',
  275. SaveImageAttachment: 'attachment.md',
  276. StoredImageAttachment: 'attachment.md',
  277. ShellExecRequest: 'shell.md',
  278. ShellExecSpec: 'shell.md',
  279. ShellProcess: 'shell.md',
  280. ShellRunResult: 'shell.md',
  281. DshEnvironment: 'subprocess.md',
  282. SubprocessHandle: 'subprocess.md',
  283. SubprocessOutcome: 'subprocess.md',
  284. SubprocessOutputRead: 'subprocess.md',
  285. SubprocessOutputReader: 'subprocess.md',
  286. SubprocessSpawnSpec: 'subprocess.md',
  287. SubprocessTerminalHandle: 'subprocess.md',
  288. SubprocessTerminalSpawnSpec: 'subprocess.md',
  289. CodeRunRequest: 'code-runtime.md',
  290. CodeRunResult: 'code-runtime.md',
  291. CompactionResult: 'compaction.md',
  292. CompactionTrigger: 'compaction.md',
  293. PruneResult: 'compaction.md',
  294. FileReadOutcome: 'filesystem.md',
  295. FsDirEntry: 'filesystem.md',
  296. FsEditOutcome: 'filesystem.md',
  297. FsEditRequest: 'filesystem.md',
  298. FsInfo: 'filesystem.md',
  299. FsObservation: 'filesystem.md',
  300. FsPathInfo: 'filesystem.md',
  301. FsObservationActor: 'filesystem.md',
  302. FsTarget: 'filesystem.md',
  303. FsVersion: 'filesystem.md',
  304. FsWriteIntent: 'filesystem.md',
  305. FsWriteOutcome: 'filesystem.md',
  306. CreateGoalRequest: 'goal.md',
  307. EditGoalRequest: 'goal.md',
  308. GoalBlockReason: 'goal.md',
  309. GoalChanged: 'goal.md',
  310. GoalRef: 'goal.md',
  311. GoalView: 'goal.md',
  312. CreateGoalResult: 'goal.md',
  313. CommandDefinition: 'commands.md',
  314. CommandDescriptor: 'commands.md',
  315. CommandId: 'commands.md',
  316. CommandResult: 'commands.md',
  317. CommandSurface: 'commands.md',
  318. LspProvider: 'lsp.md',
  319. LspQueryRequest: 'lsp.md',
  320. LspQueryResult: 'lsp.md',
  321. LlmAdapter: 'llm-streaming.md',
  322. PreparedLlmCall: 'llm-streaming.md',
  323. LlmRuntime: 'llm-streaming.md',
  324. StreamChunk: 'llm-streaming.md',
  325. SkillProviderControl: 'skills.md',
  326. CreateSessionOptions: 'persistence.md',
  327. PrepareSessionOptions: 'persistence.md',
  328. SessionHeader: 'persistence.md',
  329. SessionInspection: 'persistence.md',
  330. SessionLocation: 'persistence.md',
  331. SessionPreparation: 'persistence.md',
  332. SessionPersistenceSnapshot: 'persistence.md',
  333. SessionRawArtifact: 'persistence.md',
  334. ConfinedArgv: 'sandbox.md',
  335. SandboxExecutionPolicy: 'sandbox.md',
  336. SandboxMode: 'sandbox.md',
  337. SandboxPolicy: 'sandbox.md',
  338. TerminalBackend: 'terminal.md',
  339. TerminalReadRequest: 'terminal.md',
  340. TerminalReadResult: 'terminal.md',
  341. TerminalSendOperation: 'terminal.md',
  342. TerminalSendRequest: 'terminal.md',
  343. TerminalSessionId: 'terminal.md',
  344. TerminalSessionSnapshot: 'terminal.md',
  345. TerminalSignal: 'terminal.md',
  346. TerminalSignalResult: 'terminal.md',
  347. TerminalSpawnRequest: 'terminal.md',
  348. TerminalSpawnResult: 'terminal.md',
  349. SandboxPolicyRequest: 'sandbox.md',
  350. ScopeKey: 'scope.md',
  351. Scoped: 'scope.md',
  352. EpochHeader: 'session.md',
  353. Session: 'session.md',
  354. SessionEventMap: 'session.md',
  355. TurnEndReason: 'session.md',
  356. TurnTrigger: 'session.md',
  357. SessionEventReadRequest: 'session-query.md',
  358. SessionEventRecord: 'session-query.md',
  359. SessionEventResultFilter: 'session-query.md',
  360. SessionEventSearchDocument: 'session-query.md',
  361. SessionEventSearchHit: 'session-query.md',
  362. SessionEventSearchPage: 'session-query.md',
  363. SessionEventSearchRequest: 'session-query.md',
  364. SessionEventTrace: 'session-query.md',
  365. SessionEventTraceObservation: 'session-query.md',
  366. SessionEventTraceRequest: 'session-query.md',
  367. SessionEventWindow: 'session-query.md',
  368. SessionLineageTrace: 'session-query.md',
  369. SessionRecord: 'session-query.md',
  370. SessionResultFilter: 'session-query.md',
  371. SessionSearchExecContext: 'session-query.md',
  372. SessionSearchHit: 'session-query.md',
  373. SessionSearchPage: 'session-query.md',
  374. SessionSearchRequest: 'session-query.md',
  375. SessionTitleObservation: 'session-query.md',
  376. SessionTitleObservationResult: 'session-query.md',
  377. SessionTitleProvider: 'session-title.md',
  378. SessionTitleSnapshot: 'session-title.md',
  379. SkillCatalogSnapshot: 'skills.md',
  380. SkillDefinition: 'skills.md',
  381. SkillLookupOptions: 'skills.md',
  382. SkillProvider: 'skills.md',
  383. SkillProviderObservation: 'skills.md',
  384. SkillRegistration: 'skills.md',
  385. SkillViewOptions: 'skills.md',
  386. SkillSummary: 'skills.md',
  387. SaveTextSpill: 'spill.md',
  388. SpillRef: 'spill.md',
  389. ContinuableCreateRequest: 'subagent.md',
  390. ContinuableCreateSpec: 'subagent.md',
  391. ContinuableSetupContribution: 'subagent.md',
  392. ContinuableStart: 'subagent.md',
  393. ContinuableStartSpec: 'subagent.md',
  394. CoordinatorMessageSource: 'subagent.md',
  395. SubagentDescendantListEntry: 'subagent.md',
  396. SubagentFollowupOptions: 'subagent.md',
  397. SubagentInterruptAuthority: 'subagent.md',
  398. SubagentListEntry: 'subagent.md',
  399. SubagentProvider: 'subagent.md',
  400. SubagentReportDelivery: 'subagent.md',
  401. SubagentReportMessageSource: 'subagent.md',
  402. SubagentReportOptions: 'subagent.md',
  403. SubagentRun: 'subagent.md',
  404. SubagentRuntime: 'subagent.md',
  405. SubagentStartRequest: 'subagent.md',
  406. AssembleContext: 'system-prompt.md',
  407. PromptContext: 'system-prompt.md',
  408. PromptSection: 'system-prompt.md',
  409. SystemPrompt: 'system-prompt.md',
  410. ToolProviderResult: 'system-prompt.md',
  411. JobDoneListener: 'jobs.md',
  412. JobId: 'jobs.md',
  413. JobRead: 'jobs.md',
  414. JobSnapshot: 'jobs.md',
  415. JobStart: 'jobs.md',
  416. JobsChangedListener: 'jobs.md',
  417. TokenMeasurement: 'token-meter.md',
  418. CodeDispatchLog: 'tools.md',
  419. PostToolDecision: 'tools.md',
  420. PreToolDecision: 'tools.md',
  421. ToolDefinition: 'tools.md',
  422. ToolExecution: 'tools.md',
  423. ToolDispatchExecution: 'tools.md',
  424. ToolExecutionInput: 'tools.md',
  425. ToolExecutionMode: 'tools.md',
  426. ToolExecutionResult: 'tools.md',
  427. ToolExecutionToken: 'tools.md',
  428. ToolGuard: 'tools.md',
  429. ToolPresentationMode: 'tools.md',
  430. ToolRuntime: 'tools.md',
  431. ToolRestriction: 'tools.md',
  432. ToolSchema: 'tools.md',
  433. SettingsNamespace: 'settings.md',
  434. SettingsRegisterOptions: 'settings.md',
  435. SettingsScope: 'settings.md',
  436. SettingsDescriptor: 'settings.md',
  437. SettingsPathOp: 'settings.md',
  438. SettingsDescribeOptions: 'settings.md',
  439. SettingsUpdateSource: 'settings.md',
  440. CredentialRef: 'credentials.md',
  441. CredentialInfo: 'credentials.md',
  442. ResolvedCredential: 'credentials.md',
  443. AskUserQuestionAnswer: 'user-questions.md',
  444. AskUserQuestionRequest: 'user-questions.md',
  445. UserQuestionProvider: 'user-questions.md',
  446. WebFetchProvider: 'web.md',
  447. WebFetchRequest: 'web.md',
  448. WebFetchResult: 'web.md',
  449. WebSearchProvider: 'web.md',
  450. WebSearchRequest: 'web.md',
  451. WebSearchResult: 'web.md',
  452. WorkflowRun: 'workflow.md',
  453. PresetOption: 'permission-presets.md',
  454. PresetSpec: 'permission-presets.md',
  455. InvariantInstaller: 'invariants.md',
  456. WebRoute: 'web-server.md',
  457. StorageBackend: 'storage.md',
  458. StorageForms: 'storage.md',
  459. Domain: 'storage.md',
  460. DomainSpec: 'storage.md',
  461. DomainChanged: 'storage.md',
  462. DomainFacility: 'storage.md',
  463. Workspace: 'workspace.md',
  464. WorkspaceId: 'workspace.md',
  465. WebBootGraph: 'client-modules.md',
  466. SessionTelemetryRecord: 'session-telemetry.md',
  467. WorkflowRunInfo: 'workflow.md',
  468. WorkflowStartRequest: 'workflow.md',
  469. ProjectionDefinition: 'session-projection.md',
  470. SessionProjectionMap: 'session-projection.md',
  471. ProjectionChangeListener: 'session-projection.md',
  472. ProjectionSnapshot: 'session-projection.md',
  473. ProjectionCheckpoint: 'session-projection.md',
  474. DirectoryPickerCapability: 'workspace.md',
  475. TypertContribution: 'invariants.md',
  476. TypertFace: 'invariants.md',
  477. TypertPackageFilter: 'invariants.md',
  478. TypertPackageRecord: 'invariants.md',
  479. TypertSchemaFilter: 'invariants.md',
  480. TypertSchemaRecord: 'invariants.md',
  481. }
  482. /** TypeScript lib and pinned framework types with no repository-owned data page. */
  483. export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
  484. 'AbortSignal',
  485. 'AsyncIterable',
  486. 'Context',
  487. 'Error',
  488. 'Map',
  489. 'Partial',
  490. 'Pick',
  491. 'Promise',
  492. 'Record',
  493. 'Readonly',
  494. 'Uint8Array',
  495. ])
  496. /** Project types deliberately documented outside the subsystems catalog. */
  497. export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
  498. z: 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)',
  499. BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
  500. InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
  501. ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
  502. InsertTextRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
  503. AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
  504. AgentPreset: 'discovered preset record is owned by packages/preset/agent-presets/README.md',
  505. PresetMetadata: 'preset display text is owned by packages/preset/agent-presets/README.md',
  506. BashEnvContributor: 'service-local extension type is owned by packages/shell/tool-bash/src/index.ts',
  507. BashEnvVariableInfo: 'service-local metadata type is owned by packages/shell/tool-bash/src/index.ts',
  508. CompactionAgentContext: 'compaction service input is owned by packages/compaction/compaction/src/index.ts',
  509. ManualCompactAgentContext: 'manual compaction service input is owned by packages/compaction/compaction/src/index.ts',
  510. ClientResponse: 'wire response message is owned by packages/host/apiproxy/src/api/rpc.ts',
  511. ApprovalRequestId: 'dynamic Plugin approval identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  512. CordisErrorDetails: 'Cordis runtime error payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
  513. CordisInspectPlatform: 'Cordis inspect platform identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  514. CordisInspectProviderManifest: 'Cordis inspect provider manifest is owned by packages/extensions/cordis-host-runner/src/types.ts',
  515. CordisInspectProviderView: 'Cordis inspect provider view is owned by packages/extensions/cordis-host-runner/src/types.ts',
  516. CordisInspectQueryRequest: 'Cordis inspect transport payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
  517. CordisInspectQueryResolution: 'Cordis inspect query result is owned by packages/extensions/cordis-host-runner/src/types.ts',
  518. CordisInspectQueryResolved: 'Cordis inspect transport payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
  519. CordisInspectRequestId: 'Cordis inspect request identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  520. CordisInspectResolveAck: 'Cordis inspect resolution acknowledgement is owned by packages/extensions/cordis-host-runner/src/types.ts',
  521. CordisDynamicPackageId: 'dynamic Package identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  522. CordisDynamicPluginId: 'dynamic Plugin identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  523. CordisDynamicPluginRunId: 'dynamic Plugin run identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
  524. CordisDynamicRunMode: 'dynamic Plugin activation mode is owned by packages/extensions/cordis-host-runner/src/types.ts',
  525. DynamicCordisClientSource: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  526. DynamicCordisDefineReceipt: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  527. DynamicCordisDefineRequest: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  528. DynamicCordisHostHalfResult: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  529. DynamicCordisInventoryRow: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  530. DynamicCordisInvokeResult: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  531. DynamicCordisPackageInspection: 'dynamic Package source inspection is owned by packages/extensions/cordis-host-runner/src/registry.ts',
  532. DynamicCordisPluginInspection: 'dynamic Plugin inspection is owned by packages/extensions/cordis-host-runner/src/registry.ts',
  533. DynamicCordisRequestResolved: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  534. DynamicCordisRetracted: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  535. DynamicCordisRunRequest: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  536. DynamicCordisPackage: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  537. DynamicCordisReference: 'dynamic Plugin reference is owned by packages/extensions/cordis-host-runner/src/registry.ts',
  538. DynamicCordisRenderFailure: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  539. DynamicCordisResolveAck: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  540. DynamicCordisRunResolution: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  541. DynamicCordisRunResponse: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  542. DynamicCordisSnapshotRow: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  543. DynamicCordisStopResponse: 'dynamic Plugin stop result is owned by packages/extensions/cordis-host-runner/src/types.ts',
  544. DynamicCordisUndefineReceipt: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  545. HostCordisInspectProviderRegistration: 'Host inspect provider registration is owned by packages/extensions/cordis-host-runner/src/inspect-registry.ts',
  546. DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md',
  547. CommandExecution: 'executor return contract is owned by packages/interaction/commands/src/index.ts',
  548. 'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API',
  549. 'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API',
  550. TypertDisposer: 'Typert lifecycle contract is owned by packages/typert/protocol/README.md',
  551. InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
  552. LocaleDict: 'service-local dictionary fields are owned by packages/client/i18n/src/index.ts',
  553. ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
  554. Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
  555. WebUpgradeRoute:
  556. 'upgrade route registration contract is owned by packages/host/webserver/src/index.ts',
  557. InvariantRegistration: 'service-local lifecycle handle is owned by packages/runtime-diagnostics/invariants/README.md',
  558. JsonValue: 'JSON value union is owned by packages/core/session/src/json.ts',
  559. KnobState: 'projection unit state fields are owned by packages/interaction/permission-presets/README.md',
  560. PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission-presets/src/types.ts',
  561. PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
  562. RequestRunId: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
  563. RpcReceipt: 'carrier-layer receipt is owned by packages/host/apiproxy/src/api/rpc.ts',
  564. Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md',
  565. SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
  566. SubagentRunEndInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',
  567. SubagentRunInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',
  568. WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
  569. WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
  570. WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
  571. }
  572. /** Repository data policy consumed by the Cordis catalog projector. */
  573. export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = {
  574. linkedTypePages: LINK_MAP,
  575. foundationTypeNames: FOUNDATION_TYPE_NAMES,
  576. typeLinkExemptions: TYPE_LINK_EXEMPTIONS,
  577. runtimeServiceExclusions: new Set(['cordisInspect', 'dynamicCordisRunner']),
  578. runtimeServices: [{
  579. key: 'timer',
  580. type: 'TimerService',
  581. abstract: false,
  582. doc: 'Disposable timer helpers mixed into Cordis contexts.',
  583. source: 'vendor/timer/src/index.ts:12',
  584. methods: [
  585. {
  586. signature: 'timeout(callback: () => void, delay: number): () => void',
  587. jsDoc: '/** Run a callback once and return its disposer. */',
  588. },
  589. {
  590. signature: 'timeout(delay: number): Promise<void>',
  591. jsDoc: '/** Resolve after a delay; disposal rejects the pending promise. */',
  592. },
  593. {
  594. signature: 'interval(callback: () => void, delay: number): () => void',
  595. jsDoc: '/** Run a callback repeatedly and return its disposer. */',
  596. },
  597. {
  598. signature: 'interval<R = any>(delay: number): AsyncIterableIterator<void, R, void>',
  599. jsDoc: '/** Return an async iterator of timer ticks. */',
  600. },
  601. {
  602. signature: 'throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): F & { dispose: () => void }',
  603. jsDoc: '/** Return a throttled function whose timer is disposed with the current fiber. */',
  604. },
  605. {
  606. signature: 'debounce<F extends (...args: any[]) => void>(callback: F, delay: number): F & { dispose: () => void }',
  607. jsDoc: '/** Return a debounced function whose timer is disposed with the current fiber. */',
  608. },
  609. ],
  610. }],
  611. inheritedEvents: [
  612. { name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' },
  613. { name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' },
  614. { name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:332' },
  615. { name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:334' },
  616. { name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:336' },
  617. { name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:338' },
  618. { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' },
  619. { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' },
  620. { name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' },
  621. { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' },
  622. { name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' },
  623. { name: 'loader/config-update', summary: 'The loader config tree changed.', source: 'vendor/loader/src/index.ts:24' },
  624. { name: 'loader/entry-init', summary: 'A config entry is being initialized.', source: 'vendor/loader/src/index.ts:25' },
  625. { name: 'loader/partial-dispose', summary: 'An entry is being partially disposed on reload.', source: 'vendor/loader/src/index.ts:26' },
  626. { name: 'loader/patch-context', summary: 'A context is being patched during a reload.', source: 'vendor/loader/src/index.ts:27' },
  627. ],
  628. inheritedServices: [
  629. { name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' },
  630. { name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / short-circuit chain).', source: 'vendor/cordis/src/events.ts:34' },
  631. { name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' },
  632. { name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' },
  633. { name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' },
  634. { name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' },
  635. { name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' },
  636. { name: 'ctx.timer (+ interval / timeout / throttle / debounce)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the four supported helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' },
  637. { name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' },
  638. { name: 'ctx.hmr', summary: 'The hot-module-reload watcher (present under the hmr plugin).', source: 'vendor/hmr/src/index.ts:15' },
  639. ],
  640. }
  641. /**
  642. * Splice a page's generated Cordis API region into its Markdown content.
  643. * The page must contain exactly one `cordis-surface` marker region (the markers are
  644. * part of the hand-owned page skeleton once, then owned by the generator);
  645. * zero or several is a partition error the caller reports with the page path.
  646. * The match is on THIS generator's exact markers, not the generic region
  647. * grammar, so a page carrying only some other generator's region fails loud
  648. * instead of having that region overwritten.
  649. * @param content - the page's current full Markdown text.
  650. * @param region - the freshly rendered marker-delimited region.
  651. * @returns the page text with the region replaced.
  652. */
  653. export function spliceRegion(content: string, region: string): string {
  654. const lines = content.split('\n')
  655. const begins = lines.flatMap((line, index) => (line === REGION_BEGIN ? [index] : []))
  656. const ends = lines.flatMap((line, index) => (line === REGION_END ? [index] : []))
  657. if (begins.length !== 1 || ends.length !== 1) {
  658. throw new Error(`expected exactly 1 cordis-surface region, found ${begins.length} BEGIN/${ends.length} END; add the BEGIN/END cordis-surface markers once`)
  659. }
  660. const begin = begins[0] ?? -1
  661. const end = ends[0] ?? -1
  662. if (end < begin) throw new Error('cordis-surface END marker precedes its BEGIN')
  663. return [...lines.slice(0, begin), ...region.split('\n'), ...lines.slice(end + 1)].join('\n')
  664. }
  665. /** The declared-vs-rendered inputs {@link walkPartitionProblems} judges. */
  666. export interface WalkPartitionInput {
  667. /** Service key → source pointer, as the rendering projection produced them. */
  668. readonly renderedKeys: ReadonlyMap<string, string>
  669. /** Event scopes the rendering projection produced. */
  670. readonly renderedScopes: ReadonlySet<string>
  671. /** Event names the rendering projection produced. */
  672. readonly renderedEventNames: ReadonlySet<string>
  673. /** Context key → first declaring file, from the independent AST scan. */
  674. readonly declaredKeys: ReadonlyMap<string, string>
  675. /** Event name → first declaring file, from the independent AST scan. */
  676. readonly declaredEvents: ReadonlyMap<string, string>
  677. }
  678. /** The curated partition maps {@link walkPartitionProblems} enforces. */
  679. export interface WalkPartitionMaps {
  680. readonly servicePage: Readonly<Record<string, string>>
  681. readonly serviceWalkExemptions: Readonly<Record<string, string>>
  682. readonly eventScopePage: Readonly<Record<string, string>>
  683. readonly eventWalkExemptions: Readonly<Record<string, string>>
  684. }
  685. /**
  686. * Judge the rendered API and the independent AST scan against the curated
  687. * partition maps, fail-closed in both directions for services AND events: a
  688. * rendered key/scope must be mapped to a page, a mapped key/scope must still
  689. * render, and — the backstop — a DECLARED key/event the projection cannot see
  690. * must carry a named walk exemption (a rendered one must not). A third
  691. * direction guards the scan itself: everything rendered must also be declared
  692. * to the scan, so a scan blind spot cannot decay silently. Pure so the
  693. * acceptance paths are provable without running the projection.
  694. * @param input - rendered API plus the declared-key/event scans.
  695. * @param maps - the curated page maps and walk exemptions.
  696. * @returns one message per violation, empty when the partition holds.
  697. */
  698. export function walkPartitionProblems(input: WalkPartitionInput, maps: WalkPartitionMaps): string[] {
  699. const problems: string[] = []
  700. for (const [key, source] of input.renderedKeys) {
  701. if (!Object.hasOwn(maps.servicePage, key)) problems.push(`service ctx.${key} (${source}) has no SERVICE_PAGE entry; every service maps to exactly one subsystems page.`)
  702. }
  703. for (const scope of [...input.renderedScopes].sort()) {
  704. if (!Object.hasOwn(maps.eventScopePage, scope)) problems.push(`event scope '${scope}/*' has no EVENT_SCOPE_PAGE entry; every event scope maps to exactly one subsystems page.`)
  705. }
  706. for (const key of Object.keys(maps.servicePage)) {
  707. if (!input.renderedKeys.has(key)) problems.push(`SERVICE_PAGE maps 'ctx.${key}' but the projection discovers no such service; remove the stale entry.`)
  708. }
  709. for (const scope of Object.keys(maps.eventScopePage)) {
  710. if (!input.renderedScopes.has(scope)) problems.push(`EVENT_SCOPE_PAGE maps '${scope}/*' but the projection discovers no such scope; remove the stale entry.`)
  711. }
  712. // The rendering projection only sees a Context key it can resolve to a
  713. // documented service class. The independent scan reads EVERY Context merge
  714. // so a key the projection cannot render must either be rendered (mapped) or
  715. // carry a named SERVICE_WALK_EXEMPTIONS reason — never vanish silently.
  716. for (const [key, rel] of input.declaredKeys) {
  717. const rendered = input.renderedKeys.has(key)
  718. const exempt = Object.hasOwn(maps.serviceWalkExemptions, key)
  719. if (!rendered && !exempt) {
  720. problems.push(`ctx.${key} (${rel}) is declared in a Context merge but invisible to the rendering projection; map it in SERVICE_PAGE (after making it renderable) or name it in SERVICE_WALK_EXEMPTIONS with its documentation owner.`)
  721. }
  722. if (rendered && exempt) problems.push(`ctx.${key} is rendered by the projection but still listed in SERVICE_WALK_EXEMPTIONS; remove the stale exemption.`)
  723. }
  724. for (const key of Object.keys(maps.serviceWalkExemptions)) {
  725. if (!input.declaredKeys.has(key)) problems.push(`SERVICE_WALK_EXEMPTIONS names 'ctx.${key}' but no Context merge declares it; remove the stale exemption.`)
  726. }
  727. // The event mirror of the service backstop: the projection walks only files
  728. // reachable from host-face package exports, so a client-face or unreachable
  729. // Events merge would otherwise vanish without a trace.
  730. for (const [name, rel] of input.declaredEvents) {
  731. const rendered = input.renderedEventNames.has(name)
  732. const exempt = Object.hasOwn(maps.eventWalkExemptions, name)
  733. if (!rendered && !exempt) {
  734. problems.push(`event '${name}' (${rel}) is declared in an Events merge but invisible to the rendering projection; make it renderable (mapped via EVENT_SCOPE_PAGE) or name it in EVENT_WALK_EXEMPTIONS with its documentation owner.`)
  735. }
  736. if (rendered && exempt) problems.push(`event '${name}' is rendered by the projection but still listed in EVENT_WALK_EXEMPTIONS; remove the stale exemption.`)
  737. }
  738. for (const name of Object.keys(maps.eventWalkExemptions)) {
  739. if (!input.declaredEvents.has(name)) problems.push(`EVENT_WALK_EXEMPTIONS names '${name}' but no Events merge declares it; remove the stale exemption.`)
  740. }
  741. // Self-check the scan itself: everything the projection renders is declared
  742. // in a Context/Events merge the scan must also reach, so a rendered key or
  743. // event the scan cannot see means the SCAN regressed (glob, prefilter, or
  744. // block walk) — a partial blind spot that exemption staleness alone would
  745. // never appear.
  746. for (const key of input.renderedKeys.keys()) {
  747. if (!input.declaredKeys.has(key)) problems.push(`ctx.${key} is rendered by the projection but the independent scan finds no Context merge declaring it; the scan has a blind spot (glob, prefilter, or module-block walk) — fix the scan, not the maps.`)
  748. }
  749. for (const name of input.renderedEventNames) {
  750. if (!input.declaredEvents.has(name)) problems.push(`event '${name}' is rendered by the projection but the independent scan finds no Events merge declaring it; the scan has a blind spot (glob, prefilter, or module-block walk) — fix the scan, not the maps.`)
  751. }
  752. return problems
  753. }
  754. /**
  755. * Compute every generated artifact: the inherited-tier page, the model-facing
  756. * runtime API module, plus, per mapped subsystems page, the pair's two updated
  757. * documents with the injected region. Fail-loud partition checks live here: an
  758. * unmapped service/event scope, a mapping whose page file does not exist, a
  759. * curated entry whose key/scope the projection no longer discovers, a declared
  760. * Context key or Events member the projection cannot see without a named walk
  761. * exemption, and a mapped page missing its markers are all aggregated errors.
  762. * @returns `[repo-relative path, exact content]` for every generated artifact.
  763. */
  764. export function computeOutputs(): [string, string][] {
  765. const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY)
  766. const services = [...model.services]
  767. const events = [...model.events]
  768. const declaredKeys = new Map<string, string>()
  769. const declaredEvents = new Map<string, string>()
  770. for (const { rel, sf, body } of contextMergeFiles(root, ['packages/*/*/src/**/*.ts', 'packages/*/*/src/**/*.tsx'])) {
  771. for (const key of contextKeyMap(body, sf).keys()) {
  772. if (!declaredKeys.has(key)) declaredKeys.set(key, rel)
  773. }
  774. for (const name of eventNameList(body, sf)) {
  775. if (!declaredEvents.has(name)) declaredEvents.set(name, rel)
  776. }
  777. }
  778. const problems = walkPartitionProblems({
  779. renderedKeys: new Map(services.map(s => [s.key, s.source])),
  780. renderedScopes: new Set(events.map(e => e.scope)),
  781. renderedEventNames: new Set(events.map(e => e.name)),
  782. declaredKeys,
  783. declaredEvents,
  784. }, {
  785. servicePage: SERVICE_PAGE,
  786. serviceWalkExemptions: SERVICE_WALK_EXEMPTIONS,
  787. eventScopePage: EVENT_SCOPE_PAGE,
  788. eventWalkExemptions: EVENT_WALK_EXEMPTIONS,
  789. })
  790. if (problems.length > 0) throw new Error(`gen-cordis-catalog: ${problems.length} partition violation(s):\n${problems.map(p => ` ${p}`).join('\n')}`)
  791. const pages = [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])].sort()
  792. const outputs: [string, string][] = [
  793. [OUT_INHERITED, renderInheritedPage(CORDIS_CATALOG_POLICY)],
  794. [OUT_RUNTIME_API, projector.renderRuntimeApi(model)],
  795. ]
  796. for (const page of pages) {
  797. const region = renderPageRegion(
  798. page,
  799. services.filter(s => SERVICE_PAGE[s.key] === page),
  800. events.filter(e => EVENT_SCOPE_PAGE[e.scope] === page),
  801. CORDIS_CATALOG_POLICY,
  802. )
  803. for (const side of [page, page.replace(/\.md$/, '.zh.md')]) {
  804. const rel = `${SUBSYSTEMS_DIR}/${side}`
  805. let current: string
  806. try {
  807. current = readFileSync(resolve(root, rel), 'utf8')
  808. } catch {
  809. // Both pair sides must exist before a region can be injected; the
  810. // pairing gate owns pair completeness, this generator names the miss.
  811. problems.push(`${rel}: mapped subsystems page does not exist.`)
  812. continue
  813. }
  814. try {
  815. outputs.push([rel, spliceRegion(current, region)])
  816. } catch (error) {
  817. problems.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`)
  818. }
  819. }
  820. }
  821. if (problems.length > 0) throw new Error(`gen-cordis-catalog: ${problems.length} page violation(s):\n${problems.map(p => ` ${p}`).join('\n')}`)
  822. return outputs
  823. }
  824. /**
  825. * Re-record a pair's `.i18n.yaml` after a region write ONLY when the write is
  826. * region-confined: both sides' region-stripped content must be byte-equal to
  827. * the region-stripped previous content whose hashes the record holds. The
  828. * caller supplies the previous bytes (read before writing); human-content
  829. * drift leaves the record untouched so the pairing gate still demands the
  830. * normal translation flow.
  831. * @param pageRel - repo-relative English page path (`docs/subsystems/x.md`).
  832. * @param before - pre-write bytes per repo-relative path.
  833. * @param scanRoot - repository root override for tests.
  834. * @returns true when the record was refreshed.
  835. */
  836. export function maybeRecordPair(pageRel: string, before: Map<string, Buffer>, scanRoot: string = root): boolean {
  837. const zhRel = pageRel.replace(/\.md$/, '.zh.md')
  838. const metaRel = pageRel.replace(/\.md$/, '.i18n.yaml')
  839. const metaAbs = resolve(scanRoot, metaRel)
  840. let meta: string
  841. try {
  842. meta = readFileSync(metaAbs, 'utf8')
  843. } catch {
  844. // No record yet: a brand-new pair is recorded by the author's --write
  845. // after review, never silently by regeneration.
  846. return false
  847. }
  848. // The record must contain exactly the two valid entries for THIS pair;
  849. // a malformed or renamed-key sidecar is the pairing gate's problem to
  850. // report, never something regeneration silently repairs into validity.
  851. const recorded = parsePairMeta(meta)
  852. const names = [pageRel, zhRel].map(rel => rel.split('/').at(-1) ?? rel)
  853. if (!recorded || recorded.size !== 2 || !names.every(name => recorded.has(name))) return false
  854. for (const rel of [pageRel, zhRel]) {
  855. const previous = before.get(rel)
  856. if (!previous) return false
  857. if (recorded.get(rel.split('/').at(-1) ?? rel) !== blobHash(previous)) return false
  858. const current = readFileSync(resolve(scanRoot, rel))
  859. const strippedBefore = partitionGeneratedRegions(previous.toString('utf8')).stripped
  860. const strippedAfter = partitionGeneratedRegions(current.toString('utf8')).stripped
  861. if (strippedBefore !== strippedAfter) return false
  862. }
  863. const source = readFileSync(resolve(scanRoot, pageRel))
  864. const zh = readFileSync(resolve(scanRoot, zhRel))
  865. writeFileSync(metaAbs, renderPairMeta(pageRel, blobHash(source), zhRel, blobHash(zh)))
  866. return true
  867. }
  868. /** CLI entry: default regenerates every artifact, `--check` fails if any is
  869. * stale. Guarded behind an entry-point check so importing this module for
  870. * tests neither regenerates the committed files nor calls process.exit.
  871. * @returns nothing; writes files or reports freshness through the process.
  872. */
  873. export function main(): void {
  874. const outputs: [string, string][] = [
  875. ...computeOutputs(),
  876. ...renderCordisCoreApiPages(),
  877. ]
  878. if (process.argv.includes('--check')) {
  879. const stale: string[] = []
  880. for (const [out, content] of outputs) {
  881. let committed: string | null = null
  882. try {
  883. committed = readFileSync(resolve(root, out), 'utf8')
  884. } catch {
  885. // Only ENOENT (not yet generated) is expected; a present-but-unreadable
  886. // file is not a state this repo produces. Either way the remedy is the
  887. // same — regenerate — so treat a read failure as "stale".
  888. committed = null
  889. }
  890. if (committed !== content) stale.push(out)
  891. }
  892. if (stale.length === 0) {
  893. console.log(`gen-cordis-catalog: ${outputs.length} generated file(s)/region(s) are up to date.`)
  894. process.exit(0)
  895. }
  896. console.error(`gen-cordis-catalog: stale — ${stale.join(', ')}. Run \`pnpm run gen-cordis-catalog\` and commit the result.`)
  897. process.exit(1)
  898. }
  899. const before = new Map<string, Buffer>()
  900. for (const [out] of outputs) {
  901. try {
  902. before.set(out, readFileSync(resolve(root, out)))
  903. } catch {
  904. // First generation of this artifact; nothing to guard, nothing to record.
  905. }
  906. }
  907. let changedPages = 0
  908. let recorded = 0
  909. for (const [out, content] of outputs) {
  910. const destination = resolve(root, out)
  911. if (before.get(out)?.toString('utf8') === content) continue
  912. mkdirSync(dirname(destination), { recursive: true })
  913. writeFileSync(destination, content)
  914. changedPages++
  915. }
  916. for (const page of [...new Set([...Object.values(SERVICE_PAGE), ...Object.values(EVENT_SCOPE_PAGE)])]) {
  917. const rel = `${SUBSYSTEMS_DIR}/${page}`
  918. const zhRel = rel.replace(/\.md$/, '.zh.md')
  919. const wroteEither = [rel, zhRel].some((side) => {
  920. const previous = before.get(side)
  921. return previous !== undefined && previous.toString('utf8') !== readFileSync(resolve(root, side), 'utf8')
  922. })
  923. if (wroteEither && maybeRecordPair(rel, before)) recorded++
  924. }
  925. console.log(`gen-cordis-catalog: ${outputs.length} artifact(s) computed, ${changedPages} written, ${recorded} pair record(s) refreshed.`)
  926. }
  927. // Run only when invoked as a script, not when imported by a test.
  928. if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
  929. main()
  930. }