gen-cordis-catalog.ts 43 KB

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