2026-06-20 19:47:09 +08:00
/**
2026-07-30 21:40:58 +08:00
* Generate the per-subsystem Cordis service/event reference regions from the
* Typert catalog projection. Every harness `ctx.<key>` service and event scope
* maps to exactly one `docs/subsystems/` page through the curated tables below;
* the generator injects each page's surface between its GENERATED markers —
* byte-identically into both language sides of the pair — and re-records a
* pair's `.i18n.yaml` only when nothing outside the region changed. The
* projection enforces event modes, JSDoc parameter/return completeness, and
* signature type-link coverage; the inherited (vendor) tier renders to
* `docs/cordis-api/inherited.md`. `--check` verifies every generated artifact.
2026-06-20 19:47:09 +08:00
*/
2026-07-28 23:48:35 +08:00
import { mkdirSync , readFileSync , writeFileSync } from 'node:fs'
import { dirname , resolve } from 'node:path'
import {
projectCordisCatalog ,
2026-07-30 21:40:58 +08:00
renderInheritedPage ,
renderPageRegion ,
REGION_BEGIN ,
REGION_END ,
2026-07-28 23:48:35 +08:00
} from '@deepseek-ai/dsh-typert-generator'
import type { CordisCatalogPolicy } from '@deepseek-ai/dsh-typert-generator'
2026-07-20 16:32:08 +08:00
import { renderCordisCoreApiPages } from './cordis-core-api.ts'
2026-08-09 02:39:12 +08:00
import { contextKeyMap , contextMergeFiles , eventNameList } from './cordis-walk.ts'
2026-07-30 21:40:58 +08:00
import {
blobHash ,
parsePairMeta ,
partitionGeneratedRegions ,
renderPairMeta ,
} from './translation-pairing.ts'
2026-06-20 19:47:09 +08:00
const root = resolve ( import . meta . dirname , '..' )
2026-07-30 21:40:58 +08:00
const SUBSYSTEMS_DIR = 'docs/subsystems'
const OUT_INHERITED = 'docs/cordis-api/inherited.md'
2026-07-28 14:11:18 +08:00
const OUT_RUNTIME_API = 'packages/self-modification/tool-cordis/src/api-catalog.ts'
2026-06-20 19:47:09 +08:00
2026-07-30 21:40:58 +08:00
export { REGION_BEGIN , REGION_END }
/**
* The owning subsystems page for every harness `ctx.<key>` service the
* projection discovers. Fail-closed both ways: a discovered key absent here
* and an entry whose key the projection no longer discovers are both hard
* errors, so the partition can never silently drift from the service surface.
*/
export const SERVICE_PAGE : Record < string , string > = {
agentLoop : 'core.md' ,
2026-08-09 12:13:58 +08:00
agentDefaultModel : 'core.md' ,
2026-08-09 02:50:45 +08:00
agentPresets : 'core.md' ,
2026-07-30 21:40:58 +08:00
agents : 'core.md' ,
approval : 'approval.md' ,
2026-08-09 23:33:35 +08:00
attachments : 'attachment.md' ,
2026-07-30 21:40:58 +08:00
bash : 'bash.md' ,
bashEnv : 'bash.md' ,
clientModuleHost : 'client-modules.md' ,
codeRuntime : 'code-runtime.md' ,
commands : 'commands.md' ,
compact : 'compaction.md' ,
credentials : 'credentials.md' ,
directoryPicker : 'workspace.md' ,
e2b : 'subprocess.md' ,
fs : 'filesystem.md' ,
goals : 'goal.md' ,
httpServer : 'http-server.md' ,
invariants : 'invariants.md' ,
llm : 'llm-streaming.md' ,
permission : 'permission.md' ,
planMode : 'plan.md' ,
pty : 'pty.md' ,
sandbox : 'sandbox.md' ,
sandboxPolicy : 'sandbox.md' ,
sessionPersistence : 'persistence.md' ,
sessionQuery : 'session-query.md' ,
sessionReferences : 'session-reference.md' ,
sessionProjectionCache : 'session-projection.md' ,
sessionProjections : 'session-projection.md' ,
sessions : 'session.md' ,
settings : 'settings.md' ,
sessionTitle : 'session-title.md' ,
skills : 'skills.md' ,
spillStore : 'spill.md' ,
storage : 'storage.md' ,
storageDomain : 'storage.md' ,
subagents : 'subagent.md' ,
subprocess : 'subprocess.md' ,
systemPrompt : 'system-prompt.md' ,
tasks : 'tasks.md' ,
telemetry : 'telemetry.md' ,
tokenMeter : 'token-meter.md' ,
toolResultPrune : 'compaction.md' ,
tools : 'tools.md' ,
typert : 'typert.md' ,
typertGateway : 'typert.md' ,
userInteraction : 'user-interaction.md' ,
web : 'web.md' ,
workflows : 'workflow.md' ,
workspace : 'workspace.md' ,
}
/**
* Context keys declared in `interface Context` merges that the rendering
* projection cannot see, each with the reason and its documentation owner.
* The scan that enforces this list reads EVERY `declare module 'cordis'`
2026-08-09 02:39:12 +08:00
* Context merge under `packages/x/x/src/**` — any depth, not only root
* `index.ts` files with a same-named service class — so a new service can
* never silently join this blind spot: it either enters {@link SERVICE_PAGE}
* or names itself here. Client-face keys (the projection analyzes the host
* face only) name the package README that owns their surface.
2026-07-30 21:40:58 +08:00
* TODO(cordis-catalog-interface-services): the interface-typed and
* non-index-declared entries would all render once the projection resolves a
* Context key through its declaring file's imports to the class declaration.
*/
export const SERVICE_WALK_EXEMPTIONS : Record < string , string > = {
agent : 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle' ,
2026-08-09 15:27:21 +08:00
configuredAgentIdentities : 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract' ,
launcherSessionQueryPath : 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract' ,
2026-07-30 21:40:58 +08:00
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' ,
2026-08-09 15:27:21 +08:00
headlessIo : 'not a service: launcher-provided root accessor value (HeadlessIo | undefined) for the headless bundle runner — packages/bundle/headless/README.md owns this launcher contract' ,
launcherEnvironment : 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns this launcher contract' ,
2026-07-30 21:40:58 +08:00
lsp : 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the surface' ,
apiProxy : 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the surface' ,
appShell : 'client-side interface-typed browser service — packages/client/web/README.md owns the surface' ,
connection : 'client-side interface-typed browser service — packages/client/connection/README.md owns the surface' ,
2026-08-09 15:27:21 +08:00
chatFileMentions : 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API' ,
2026-08-09 02:39:12 +08:00
command : 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the surface' ,
conversation : 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the surface' ,
2026-08-09 17:02:20 +08:00
conversationEvents : 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface' ,
conversationViews : 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface' ,
2026-08-09 02:39:12 +08:00
layout : 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the surface' ,
locale : 'client-side interface-typed browser service — packages/client/locale/README.md owns the surface' ,
models : 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the surface' ,
modules : 'client-side interface-typed browser service — packages/client/modules/README.md owns the surface' ,
remote : 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the surface' ,
sessionHistory : 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface' ,
slash : 'client-side interface-typed browser service — packages/client/ui-slash/README.md owns the surface' ,
slots : 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface' ,
theme : 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the surface' ,
workspaces : 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface' ,
2026-07-30 21:40:58 +08:00
}
/**
* The owning subsystems page for every harness event scope (the segment
2026-08-09 02:39:12 +08:00
* before the first `/`) the projection renders. Fail-closed exactly like
* {@link SERVICE_PAGE}. Client-face events (`slash/*`, `theme/change`, …) are
* invisible to the host-face projection and therefore never reach this map;
* {@link EVENT_WALK_EXEMPTIONS} names each one with its documentation owner.
2026-07-30 21:40:58 +08:00
*/
export const EVENT_SCOPE_PAGE : Record < string , string > = {
'agent' : 'core.md' ,
'agent-loop' : 'core.md' ,
'approval' : 'approval.md' ,
'commands' : 'commands.md' ,
'credentials' : 'credentials.md' ,
'domain' : 'storage.md' ,
'fs' : 'filesystem.md' ,
'goal' : 'goal.md' ,
'llm' : 'llm-streaming.md' ,
'session' : 'session.md' ,
'settings' : 'settings.md' ,
'skills' : 'skills.md' ,
'subagent' : 'subagent.md' ,
'system-prompt' : 'system-prompt.md' ,
'telemetry' : 'telemetry.md' ,
'tools' : 'tools.md' ,
'workflow' : 'workflow.md' ,
}
2026-06-20 19:47:09 +08:00
2026-08-09 02:39:12 +08:00
/**
* Event names declared in `interface Events` merges that the rendering
* projection cannot see, each with the reason and its documentation owner.
* The mirror of {@link SERVICE_WALK_EXEMPTIONS} for events: an independent
* scan reads EVERY `declare module 'cordis'` Events merge under
* `packages/x/x/src/**`, so a declared event either renders onto a subsystems
* page (via {@link EVENT_SCOPE_PAGE}) or names itself here — never vanishes
* silently. Keys are full event names, not scopes: client-face events share
* scopes with rendered host events (`commands/changed` beside `commands/*`),
* so a scope-level exemption would mask a host-face regression.
*/
export const EVENT_WALK_EXEMPTIONS : Record < string , string > = {
'commands/changed' : 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface' ,
'connection/reset' : 'client-face transport signal — packages/client/runtime/README.md owns the surface' ,
'credentials/changed' : 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface' ,
'locale/change' : 'client-face locale switch signal — packages/client/locale/README.md owns the surface' ,
'models/changed' : 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface' ,
2026-08-10 14:35:13 +08:00
'session/preset-changed' : 'client-face per-session catalog invalidation signal — packages/client/runtime/README.md owns the surface' ,
2026-08-09 02:39:12 +08:00
'settings/changed' : 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface' ,
'slash/input-begin-command' : 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface' ,
'slash/input-consume-token' : 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface' ,
'slash/input-insert-reference' : 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface' ,
'slash/input-insert-text' : 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface' ,
'slots/changed' : 'client-face slot invalidation signal — packages/client/runtime/README.md owns the surface' ,
'theme/change' : 'client-face theme switch signal — packages/client/ui-theme/README.md owns the surface' ,
}
2026-07-30 21:40:58 +08:00
/**
* One primary subsystems page per project type used by a generated
* signature. This stays curated because union names intentionally do not
* reuse the type-equivalence manifest's map-symbol entries and some symbols
* appear on more than one page.
*/
2026-07-28 23:48:35 +08:00
export const LINK_MAP : Readonly < Record < string , string > > = {
2026-06-20 19:47:09 +08:00
Agent : 'core.md' ,
2026-07-21 12:48:46 +08:00
AgentCancelCause : 'core.md' ,
2026-08-03 16:34:00 +08:00
AgentFactory : 'core.md' ,
AgentHandle : 'core.md' ,
2026-08-09 12:13:58 +08:00
ModelSelection : 'core.md' ,
2026-07-19 14:57:52 +08:00
AgentOptions : 'core.md' ,
AgentStatus : 'core.md' ,
2026-08-02 05:54:15 +08:00
ContentBlock : 'llm-streaming.md' ,
2026-08-03 16:34:00 +08:00
CreateAgentOptions : 'core.md' ,
2026-08-02 05:54:15 +08:00
GenerateOptions : 'llm-streaming.md' ,
2026-07-28 14:11:18 +08:00
InboxItem : 'core.md' ,
2026-07-27 22:48:20 +08:00
InboxPlacement : 'core.md' ,
2026-08-02 05:54:15 +08:00
MessageId : 'llm-streaming.md' ,
2026-08-03 16:34:00 +08:00
ResumeAgentOptions : 'core.md' ,
2026-07-27 17:38:42 +08:00
SettleReason : 'core.md' ,
2026-08-02 05:54:15 +08:00
AdapterRegistrationHandle : 'llm-streaming.md' ,
DirectoryRegistrationHandle : 'llm-streaming.md' ,
LlmCallConfig : 'llm-streaming.md' ,
LlmModelContext : 'llm-streaming.md' ,
LlmModelReasoningInfo : 'llm-streaming.md' ,
LlmResolvedModelInfo : 'llm-streaming.md' ,
2026-07-20 03:34:19 +08:00
LlmFailure : 'llm-streaming.md' ,
2026-08-02 05:54:15 +08:00
LlmModelInfo : 'llm-streaming.md' ,
LlmProviderInfo : 'llm-streaming.md' ,
LlmConfigurableProvider : 'llm-streaming.md' ,
LlmModelDiscoveryRequest : 'llm-streaming.md' ,
LlmDiscoveredModel : 'llm-streaming.md' ,
2026-07-25 10:18:16 +08:00
ResolvedRetryPolicy : 'llm-streaming.md' ,
2026-08-02 05:54:15 +08:00
Message : 'llm-streaming.md' ,
MessageSource : 'llm-streaming.md' ,
2026-07-28 13:55:59 +08:00
UserMessage : 'session.md' ,
2026-07-31 19:21:16 +08:00
PreStepDecision : 'core.md' ,
PreStepContext : 'core.md' ,
2026-07-28 23:48:35 +08:00
RequestErrorAction : 'core.md' ,
2026-07-30 13:49:57 +08:00
RequestFailureContext : 'core.md' ,
2026-07-21 16:46:48 +08:00
PreparedReferencedMessage : 'session-reference.md' ,
SessionReferenceCandidate : 'session-reference.md' ,
SessionReferenceInput : 'session-reference.md' ,
2026-08-03 16:34:00 +08:00
SessionEvent : 'session.md' ,
2026-07-19 14:57:52 +08:00
SessionId : 'core.md' ,
2026-07-12 08:57:05 +08:00
SessionStartSource : 'core.md' ,
2026-07-24 12:31:26 +08:00
SessionLogSnapshot : 'session-query.md' ,
2026-07-21 16:46:48 +08:00
SessionSurfaceSnapshot : 'session-query.md' ,
2026-07-11 21:37:38 +08:00
ApprovalOutcome : 'approval.md' ,
ApprovalPolicy : 'approval.md' ,
ApprovalRequest : 'approval.md' ,
2026-07-19 14:57:52 +08:00
ApprovalService : 'approval.md' ,
2026-07-23 15:20:47 +08:00
ImageAttachmentRef : 'attachment.md' ,
SaveImageAttachment : 'attachment.md' ,
StoredImageAttachment : 'attachment.md' ,
2026-06-20 19:47:09 +08:00
BashExecRequest : 'bash.md' ,
BashExecSpec : 'bash.md' ,
2026-07-19 14:57:52 +08:00
BashProcess : 'bash.md' ,
2026-06-20 19:47:09 +08:00
BashRunResult : 'bash.md' ,
2026-07-26 12:43:14 +08:00
DshEnvironment : 'subprocess.md' ,
SubprocessHandle : 'subprocess.md' ,
SubprocessOutcome : 'subprocess.md' ,
SubprocessOutputRead : 'subprocess.md' ,
SubprocessOutputReader : 'subprocess.md' ,
SubprocessSpawnSpec : 'subprocess.md' ,
2026-07-28 23:00:00 +08:00
SubprocessTerminalHandle : 'subprocess.md' ,
SubprocessTerminalSpawnSpec : 'subprocess.md' ,
2026-07-08 02:38:47 +08:00
CodeRunRequest : 'code-runtime.md' ,
CodeRunResult : 'code-runtime.md' ,
2026-07-19 14:57:52 +08:00
CompactionResult : 'compaction.md' ,
2026-07-19 16:41:51 +08:00
CompactionTrigger : 'compaction.md' ,
2026-07-19 17:54:55 +08:00
PruneResult : 'compaction.md' ,
2026-07-19 14:57:52 +08:00
FileReadOutcome : 'filesystem.md' ,
FsDirEntry : 'filesystem.md' ,
2026-06-22 14:53:36 +08:00
FsEditOutcome : 'filesystem.md' ,
FsEditRequest : 'filesystem.md' ,
2026-06-26 18:14:30 +08:00
FsInfo : 'filesystem.md' ,
2026-08-09 15:22:50 +08:00
FsObservation : 'filesystem.md' ,
2026-07-19 14:57:52 +08:00
FsPathInfo : 'filesystem.md' ,
FsPolicyExec : 'filesystem.md' ,
2026-06-22 14:53:36 +08:00
FsTarget : 'filesystem.md' ,
FsVersion : 'filesystem.md' ,
2026-07-02 03:12:38 +08:00
FsWriteIntent : 'filesystem.md' ,
2026-06-22 14:53:36 +08:00
FsWriteOutcome : 'filesystem.md' ,
2026-07-19 18:47:34 +08:00
CreateGoalRequest : 'goal.md' ,
EditGoalRequest : 'goal.md' ,
2026-07-20 16:39:40 +08:00
GoalBlockReason : 'goal.md' ,
2026-07-19 18:47:34 +08:00
GoalChanged : 'goal.md' ,
GoalRef : 'goal.md' ,
GoalView : 'goal.md' ,
2026-07-30 21:40:58 +08:00
CreateGoalResult : 'goal.md' ,
2026-07-19 22:11:59 +08:00
CommandDefinition : 'commands.md' ,
CommandDescriptor : 'commands.md' ,
2026-08-09 15:48:56 +08:00
CommandId : 'commands.md' ,
2026-07-19 22:11:59 +08:00
CommandResult : 'commands.md' ,
CommandSurface : 'commands.md' ,
2026-07-19 14:57:52 +08:00
LlmAdapter : 'llm-streaming.md' ,
2026-07-25 22:59:45 +08:00
PreparedLlmCall : 'llm-streaming.md' ,
2026-07-19 14:57:52 +08:00
LlmService : 'llm-streaming.md' ,
StreamChunk : 'llm-streaming.md' ,
2026-07-29 02:06:07 +08:00
SkillProviderControl : 'skills.md' ,
2026-07-19 14:57:52 +08:00
CreateSessionOptions : 'persistence.md' ,
2026-08-05 22:54:55 +08:00
PrepareSessionOptions : 'persistence.md' ,
2026-07-19 14:57:52 +08:00
SessionHeader : 'persistence.md' ,
2026-08-05 22:54:55 +08:00
SessionInspection : 'persistence.md' ,
2026-07-19 14:57:52 +08:00
SessionLocation : 'persistence.md' ,
2026-08-05 22:54:55 +08:00
SessionPreparation : 'persistence.md' ,
2026-07-23 13:56:56 +08:00
SessionPersistenceSnapshot : 'persistence.md' ,
2026-07-19 14:57:52 +08:00
ConfinedArgv : 'sandbox.md' ,
2026-07-21 00:44:28 +08:00
SandboxExecutionPolicy : 'sandbox.md' ,
2026-07-19 14:57:52 +08:00
SandboxMode : 'sandbox.md' ,
SandboxPolicy : 'sandbox.md' ,
2026-07-21 16:01:00 +08:00
PtyBackend : 'pty.md' ,
PtyReadRequest : 'pty.md' ,
PtyReadResult : 'pty.md' ,
PtySendOperation : 'pty.md' ,
PtySendRequest : 'pty.md' ,
PtySessionId : 'pty.md' ,
PtySessionSnapshot : 'pty.md' ,
PtySignal : 'pty.md' ,
PtySignalResult : 'pty.md' ,
PtySpawnRequest : 'pty.md' ,
PtySpawnResult : 'pty.md' ,
2026-07-21 00:44:28 +08:00
SandboxPolicyRequest : 'sandbox.md' ,
2026-07-19 14:57:52 +08:00
ScopeKey : 'scope.md' ,
Scoped : 'scope.md' ,
EpochHeader : 'session.md' ,
Session : 'session.md' ,
2026-07-21 01:54:00 +08:00
SessionEventMap : 'session.md' ,
2026-07-19 14:57:52 +08:00
TurnEndReason : 'session.md' ,
2026-07-21 01:54:00 +08:00
TurnTrigger : 'session.md' ,
2026-07-19 14:57:52 +08:00
SessionEventReadRequest : 'session-query.md' ,
SessionEventRecord : 'session-query.md' ,
2026-07-23 13:56:56 +08:00
SessionEventResultFilter : 'session-query.md' ,
SessionEventSearchDocument : 'session-query.md' ,
SessionEventSearchHit : 'session-query.md' ,
2026-07-24 16:40:08 +08:00
SessionEventSearchPage : 'session-query.md' ,
2026-07-23 13:56:56 +08:00
SessionEventSearchRequest : 'session-query.md' ,
2026-07-19 14:57:52 +08:00
SessionEventTrace : 'session-query.md' ,
2026-07-24 16:40:08 +08:00
SessionEventTraceObservation : 'session-query.md' ,
2026-07-19 14:57:52 +08:00
SessionEventTraceRequest : 'session-query.md' ,
SessionEventWindow : 'session-query.md' ,
SessionLineageTrace : 'session-query.md' ,
SessionRecord : 'session-query.md' ,
2026-07-23 13:56:56 +08:00
SessionResultFilter : 'session-query.md' ,
SessionSearchExecContext : 'session-query.md' ,
SessionSearchHit : 'session-query.md' ,
SessionSearchPage : 'session-query.md' ,
SessionSearchRequest : 'session-query.md' ,
2026-07-24 16:40:08 +08:00
SessionTitleObservation : 'session-query.md' ,
2026-07-24 18:13:11 +08:00
SessionTitleObservationResult : 'session-query.md' ,
2026-07-21 01:54:00 +08:00
SessionTitleProvider : 'session-title.md' ,
SessionTitleSnapshot : 'session-title.md' ,
2026-07-27 16:50:56 +08:00
SkillCatalogSnapshot : 'skills.md' ,
2026-07-19 14:57:52 +08:00
SkillDefinition : 'skills.md' ,
SkillLookupOptions : 'skills.md' ,
SkillProvider : 'skills.md' ,
2026-07-29 21:08:10 +08:00
SkillProviderObservation : 'skills.md' ,
2026-07-19 14:57:52 +08:00
SkillRegistration : 'skills.md' ,
2026-08-09 22:29:53 +08:00
SkillViewOptions : 'skills.md' ,
2026-07-19 14:57:52 +08:00
SkillSummary : 'skills.md' ,
SaveTextSpill : 'spill.md' ,
SpillRef : 'spill.md' ,
2026-07-30 13:41:59 +08:00
ContinuableCreateRequest : 'subagent.md' ,
ContinuableCreateSpec : 'subagent.md' ,
2026-07-31 22:45:21 +08:00
ContinuableSetupContribution : 'subagent.md' ,
2026-07-23 17:07:38 +08:00
ContinuableStart : 'subagent.md' ,
ContinuableStartSpec : 'subagent.md' ,
2026-07-24 13:18:35 +08:00
CoordinatorMessageSource : 'subagent.md' ,
2026-08-06 13:46:55 +08:00
SubagentDescendantListEntry : 'subagent.md' ,
2026-07-28 00:10:24 +08:00
SubagentFollowupOptions : 'subagent.md' ,
2026-08-06 11:59:19 +08:00
SubagentInterruptAuthority : 'subagent.md' ,
2026-07-26 02:32:34 +08:00
SubagentListEntry : 'subagent.md' ,
2026-07-19 14:57:52 +08:00
SubagentProvider : 'subagent.md' ,
2026-07-31 22:45:21 +08:00
SubagentReportDelivery : 'subagent.md' ,
SubagentReportMessageSource : 'subagent.md' ,
SubagentReportOptions : 'subagent.md' ,
2026-07-19 14:57:52 +08:00
SubagentRun : 'subagent.md' ,
SubagentService : 'subagent.md' ,
SubagentStartRequest : 'subagent.md' ,
AssembleContext : 'system-prompt.md' ,
2026-07-30 22:09:15 +08:00
PromptContext : 'system-prompt.md' ,
2026-07-19 14:57:52 +08:00
PromptSection : 'system-prompt.md' ,
SystemPrompt : 'system-prompt.md' ,
ToolProviderResult : 'system-prompt.md' ,
TaskDoneListener : 'tasks.md' ,
TaskId : 'tasks.md' ,
TaskRead : 'tasks.md' ,
TaskSnapshot : 'tasks.md' ,
TaskStart : 'tasks.md' ,
TokenMeasurement : 'token-meter.md' ,
2026-07-26 09:01:03 +08:00
CodeDispatchLog : 'tools.md' ,
2026-07-19 14:57:52 +08:00
PostToolDecision : 'tools.md' ,
PreToolDecision : 'tools.md' ,
ToolDefinition : 'tools.md' ,
ToolExecution : 'tools.md' ,
2026-07-19 23:38:54 +08:00
ToolDispatchExecution : 'tools.md' ,
2026-07-19 14:57:52 +08:00
ToolExecutionInput : 'tools.md' ,
ToolExecutionMode : 'tools.md' ,
ToolExecutionResult : 'tools.md' ,
ToolExecutionToken : 'tools.md' ,
ToolGuard : 'tools.md' ,
2026-08-05 20:31:52 +08:00
ToolPresentationMode : 'tools.md' ,
2026-07-19 14:57:52 +08:00
ToolRegistry : 'tools.md' ,
ToolRestriction : 'tools.md' ,
ToolSchema : 'tools.md' ,
2026-07-28 18:18:34 +08:00
SettingsNamespace : 'settings.md' ,
SettingsRegisterOptions : 'settings.md' ,
SettingsScope : 'settings.md' ,
SettingsDescriptor : 'settings.md' ,
2026-07-30 19:24:21 +08:00
SettingsPathOp : 'settings.md' ,
2026-07-30 10:53:39 +08:00
SettingsDescribeOptions : 'settings.md' ,
2026-07-28 18:18:34 +08:00
SettingsUpdateSource : 'settings.md' ,
2026-07-29 14:20:06 +08:00
CredentialRef : 'credentials.md' ,
CredentialInfo : 'credentials.md' ,
ResolvedCredential : 'credentials.md' ,
2026-07-19 14:57:52 +08:00
AskUserQuestionAnswer : 'user-interaction.md' ,
AskUserQuestionRequest : 'user-interaction.md' ,
UserInteractionProvider : 'user-interaction.md' ,
WebFetchProvider : 'web.md' ,
WebFetchRequest : 'web.md' ,
WebFetchResult : 'web.md' ,
WebSearchProvider : 'web.md' ,
WebSearchRequest : 'web.md' ,
WebSearchResult : 'web.md' ,
WorkflowRun : 'workflow.md' ,
2026-07-27 22:48:20 +08:00
PresetOption : 'permission.md' ,
PresetSpec : 'permission.md' ,
InvariantInstaller : 'invariants.md' ,
WebRoute : 'http-server.md' ,
StorageBackend : 'storage.md' ,
StorageForms : 'storage.md' ,
Domain : 'storage.md' ,
DomainSpec : 'storage.md' ,
DomainChanged : 'storage.md' ,
DomainFacility : 'storage.md' ,
Workspace : 'workspace.md' ,
WorkspaceId : 'workspace.md' ,
WebBootGraph : 'client-modules.md' ,
2026-07-27 23:29:15 +08:00
TelemetryRecord : 'telemetry.md' ,
2026-07-19 14:57:52 +08:00
WorkflowRunInfo : 'workflow.md' ,
WorkflowStartRequest : 'workflow.md' ,
2026-07-30 21:40:58 +08:00
ProjectionDefinition : 'session-projection.md' ,
SessionProjectionMap : 'session-projection.md' ,
ProjectionChangeListener : 'session-projection.md' ,
ProjectionSnapshot : 'session-projection.md' ,
ProjectionCheckpoint : 'session-projection.md' ,
DirectoryPickerCapability : 'workspace.md' ,
TypertContribution : 'invariants.md' ,
TypertFace : 'invariants.md' ,
TypertPackageFilter : 'invariants.md' ,
TypertPackageRecord : 'invariants.md' ,
TypertSchemaFilter : 'invariants.md' ,
TypertSchemaRecord : 'invariants.md' ,
2026-07-19 14:57:52 +08:00
}
2026-07-28 23:48:35 +08:00
/** TypeScript lib and pinned framework types with no repository-owned data page. */
export const FOUNDATION_TYPE_NAMES : ReadonlySet < string > = new Set ( [
2026-07-19 14:57:52 +08:00
'AbortSignal' ,
'AsyncIterable' ,
'Context' ,
'Error' ,
2026-07-28 23:48:35 +08:00
'Map' ,
2026-07-28 02:11:31 +08:00
'Partial' ,
2026-07-19 14:57:52 +08:00
'Pick' ,
'Promise' ,
2026-07-28 23:00:00 +08:00
'Record' ,
2026-07-19 14:57:52 +08:00
'Readonly' ,
] )
2026-07-27 22:48:20 +08:00
/** Project types deliberately documented outside the subsystems catalog. */
2026-07-28 23:48:35 +08:00
export const TYPE_LINK_EXEMPTIONS : Readonly < Record < string , string > > = {
2026-07-28 17:30:12 +08:00
z : 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)' ,
2026-07-27 03:17:52 +08:00
BeginCommandRequest : 'event-local request contract is owned by packages/client/ui-slash/src/types.ts' ,
InsertReferenceRequest : 'event-local request contract is owned by packages/client/ui-slash/src/types.ts' ,
ConsumeTokenRequest : 'event-local request contract is owned by packages/client/ui-slash/src/types.ts' ,
InsertTextRequest : 'event-local request contract is owned by packages/client/ui-slash/src/types.ts' ,
2026-07-19 14:57:52 +08:00
AgentHandle : 'agent ownership handle is owned by packages/core/agent/README.md' ,
2026-08-03 20:30:33 +08:00
AgentPreset : 'discovered preset record is owned by packages/preset/agent-presets/README.md' ,
2026-08-05 16:30:14 +08:00
PresetMetadata : 'preset display text is owned by packages/preset/agent-presets/README.md' ,
2026-07-19 14:57:52 +08:00
BashEnvContributor : 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts' ,
BashEnvVariableInfo : 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts' ,
CompactAgentContext : 'compaction service input is owned by packages/compact/compact/src/index.ts' ,
2026-07-30 17:40:25 +08:00
ManualCompactAgentContext : 'manual compaction service input is owned by packages/compact/compact/src/index.ts' ,
2026-07-25 16:04:48 +08:00
DomainImpl : 'domain implementation contract is owned by packages/storage/storage-domain/README.md' ,
2026-07-30 03:13:49 +08:00
CommandExecution : 'executor return contract is owned by packages/interaction/commands/src/index.ts' ,
2026-07-28 23:48:35 +08:00
'z.core.JSONSchema.BaseSchema' : 'zod projection output is owned by the zod v4 API' ,
'z.core.ToJSONSchemaParams' : 'zod projection parameters are owned by the zod v4 API' ,
2026-07-30 21:40:58 +08:00
TypeRTDisposer : 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md' ,
InvokeRemoteRequest : 'gateway invocation contract is owned by packages/api/gateway/README.md' ,
2026-08-09 15:27:21 +08:00
LocaleDict : 'service-local dictionary fields are owned by packages/client/i18n/src/index.ts' ,
2026-07-19 21:17:57 +08:00
ThemeTokens : 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts' ,
Translate : 'service-local bound translator is owned by packages/client/i18n/src/index.ts' ,
2026-07-27 22:48:20 +08:00
WebUpgradeRoute :
'upgrade route registration contract is owned by packages/host/webserver/src/index.ts' ,
2026-07-19 19:19:57 +08:00
InvariantRegistration : 'service-local lifecycle handle is owned by packages/support/invariants/README.md' ,
2026-08-09 15:27:21 +08:00
KnobState : 'projection unit state fields are owned by packages/interaction/permission/README.md' ,
2026-07-30 03:13:49 +08:00
PermissionSelect : 'permissions projection payload is owned by packages/interaction/permission/src/types.ts' ,
2026-07-19 14:57:52 +08:00
PromptAssembly : 'assembly result is owned by packages/core/system-prompt/README.md' ,
2026-07-28 10:05:30 +08:00
Sandbox : 'external E2B SDK handle is owned by packages/e2b/e2b/README.md' ,
2026-07-19 14:57:52 +08:00
SessionForkSource : 'service-local fork input is owned by packages/core/session/src/index.ts' ,
2026-07-30 17:47:48 +08:00
SubagentRunEndInfo : 'event payload contract is owned by packages/subagent/subagent/src/types.ts' ,
SubagentRunInfo : 'event payload contract is owned by packages/subagent/subagent/src/types.ts' ,
2026-07-19 14:57:52 +08:00
WorkflowAgentEndInfo : 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts' ,
WorkflowAgentInfo : 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts' ,
WorkflowResultInfo : 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts' ,
}
2026-07-28 23:48:35 +08:00
/** Repository data policy consumed by the Cordis catalog projector. */
export const CORDIS_CATALOG_POLICY : CordisCatalogPolicy = {
linkedTypePages : LINK_MAP ,
foundationTypeNames : FOUNDATION_TYPE_NAMES ,
typeLinkExemptions : TYPE_LINK_EXEMPTIONS ,
inheritedEvents : [
{ name : 'internal/plugin' , summary : 'A plugin fiber was created.' , source : 'vendor/cordis/src/events.ts:328' } ,
{ name : 'internal/status' , summary : 'A fiber changed lifecycle state.' , source : 'vendor/cordis/src/events.ts:330' } ,
{ name : 'internal/service' , summary : 'Interception hook for a service binding (no core producer).' , source : 'vendor/cordis/src/events.ts:332' } ,
{ name : 'internal/update' , summary : 'Waterfall: a fiber config update is being applied.' , source : 'vendor/cordis/src/events.ts:334' } ,
{ name : 'internal/get' , summary : 'Waterfall: a service is being read from the store.' , source : 'vendor/cordis/src/events.ts:336' } ,
{ name : 'internal/set' , summary : 'Waterfall: a service is being written to the store.' , source : 'vendor/cordis/src/events.ts:338' } ,
{ name : 'internal/listener' , summary : 'A listener was registered.' , source : 'vendor/cordis/src/events.ts:340' } ,
{ name : 'internal/dispatch' , summary : 'An event is being dispatched to listeners.' , source : 'vendor/cordis/src/events.ts:342' } ,
{ name : 'hmr/change' , summary : 'A watched source file changed on disk.' , source : 'vendor/hmr/src/index.ts:20' } ,
2026-07-28 14:11:18 +08:00
{ name : 'hmr/reload' , summary : 'Plugins are being reloaded after a change.' , source : 'vendor/hmr/src/index.ts:21' } ,
2026-07-28 23:48:35 +08:00
{ name : 'exit' , summary : 'The process is exiting on a signal.' , source : 'vendor/loader/src/index.ts:23' } ,
{ name : 'loader/config-update' , summary : 'The loader config tree changed.' , source : 'vendor/loader/src/index.ts:24' } ,
{ name : 'loader/entry-init' , summary : 'A config entry is being initialized.' , source : 'vendor/loader/src/index.ts:25' } ,
{ name : 'loader/partial-dispose' , summary : 'An entry is being partially disposed on reload.' , source : 'vendor/loader/src/index.ts:26' } ,
{ name : 'loader/patch-context' , summary : 'A context is being patched during a reload.' , source : 'vendor/loader/src/index.ts:27' } ,
] ,
inheritedServices : [
{ name : 'ctx.on / ctx.once' , summary : 'Register an event listener (disposable).' , source : 'vendor/cordis/src/events.ts:34' } ,
2026-07-22 18:02:26 +08:00
{ 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' } ,
2026-07-28 23:48:35 +08:00
{ name : 'ctx.plugin / ctx.inject' , summary : 'Load a plugin / declare required services.' , source : 'vendor/cordis/src/registry.ts:164' } ,
{ name : 'ctx.effect' , summary : 'Register a disposable side effect tied to the fiber.' , source : 'vendor/cordis/src/fiber.ts:9' } ,
{ 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' } ,
{ name : 'ctx.extend / ctx.isolate / ctx.intercept' , summary : 'Derive a child context (scoped services / isolation / interception).' , source : 'vendor/cordis/src/context.ts:42' } ,
{ 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' } ,
{ 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' } ,
{ name : 'ctx.loader' , summary : 'The config Loader that booted the app (present under the loader).' , source : 'vendor/loader/src/index.ts:30' } ,
{ name : 'ctx.hmr' , summary : 'The hot-module-reload watcher (present under the hmr plugin).' , source : 'vendor/hmr/src/index.ts:15' } ,
] ,
2026-06-20 19:47:09 +08:00
}
2026-07-30 21:40:58 +08:00
/**
* Splice a page's generated cordis-surface region into its Markdown content.
* The page must contain exactly one cordis-surface region (the markers are
* part of the hand-owned page skeleton once, then owned by the generator);
* zero or several is a partition error the caller reports with the page path.
* The match is on THIS generator's exact markers, not the generic region
* grammar, so a page carrying only some other generator's region fails loud
* instead of having that region overwritten.
* @param content - the page's current full Markdown text.
* @param region - the freshly rendered marker-delimited region.
* @returns the page text with the region replaced.
2026-06-20 19:47:09 +08:00
*/
2026-07-30 21:40:58 +08:00
export function spliceRegion ( content : string , region : string ) : string {
const lines = content . split ( '\n' )
const begins = lines . flatMap ( ( line , index ) = > ( line === REGION_BEGIN ? [ index ] : [ ] ) )
const ends = lines . flatMap ( ( line , index ) = > ( line === REGION_END ? [ index ] : [ ] ) )
if ( begins . length !== 1 || ends . length !== 1 ) {
throw new Error ( ` expected exactly 1 cordis-surface region, found ${ begins . length } BEGIN/ ${ ends . length } END; add the BEGIN/END cordis-surface markers once ` )
}
const begin = begins [ 0 ] ? ? - 1
const end = ends [ 0 ] ? ? - 1
if ( end < begin ) throw new Error ( 'cordis-surface END marker precedes its BEGIN' )
return [ . . . lines . slice ( 0 , begin ) , . . . region . split ( '\n' ) , . . . lines . slice ( end + 1 ) ] . join ( '\n' )
}
2026-08-09 02:39:12 +08:00
/** The declared-vs-rendered inputs {@link walkPartitionProblems} judges. */
export interface WalkPartitionInput {
/** Service key → source pointer, as the rendering projection produced them. */
readonly renderedKeys : ReadonlyMap < string , string >
/** Event scopes the rendering projection produced. */
readonly renderedScopes : ReadonlySet < string >
/** Event names the rendering projection produced. */
readonly renderedEventNames : ReadonlySet < string >
/** Context key → first declaring file, from the independent AST scan. */
readonly declaredKeys : ReadonlyMap < string , string >
/** Event name → first declaring file, from the independent AST scan. */
readonly declaredEvents : ReadonlyMap < string , string >
}
/** The curated partition maps {@link walkPartitionProblems} enforces. */
export interface WalkPartitionMaps {
readonly servicePage : Readonly < Record < string , string > >
readonly serviceWalkExemptions : Readonly < Record < string , string > >
readonly eventScopePage : Readonly < Record < string , string > >
readonly eventWalkExemptions : Readonly < Record < string , string > >
}
/**
* Judge the rendered surface and the independent AST scan against the curated
* partition maps, fail-closed in both directions for services AND events: a
* rendered key/scope must be mapped to a page, a mapped key/scope must still
* render, and — the backstop — a DECLARED key/event the projection cannot see
2026-08-09 10:29:13 +08:00
* must carry a named walk exemption (a rendered one must not). A third
* direction guards the scan itself: everything rendered must also be declared
* to the scan, so a scan blind spot cannot decay silently. Pure so the
2026-08-09 02:39:12 +08:00
* acceptance paths are provable without running the projection.
* @param input - rendered surface plus the declared-key/event scans.
* @param maps - the curated page maps and walk exemptions.
* @returns one message per violation, empty when the partition holds.
*/
export function walkPartitionProblems ( input : WalkPartitionInput , maps : WalkPartitionMaps ) : string [ ] {
const problems : string [ ] = [ ]
for ( const [ key , source ] of input . renderedKeys ) {
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. ` )
}
for ( const scope of [ . . . input . renderedScopes ] . sort ( ) ) {
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. ` )
}
for ( const key of Object . keys ( maps . servicePage ) ) {
if ( ! input . renderedKeys . has ( key ) ) problems . push ( ` SERVICE_PAGE maps 'ctx. ${ key } ' but the projection discovers no such service; remove the stale entry. ` )
}
for ( const scope of Object . keys ( maps . eventScopePage ) ) {
if ( ! input . renderedScopes . has ( scope ) ) problems . push ( ` EVENT_SCOPE_PAGE maps ' ${ scope } /*' but the projection discovers no such scope; remove the stale entry. ` )
}
// The rendering projection only sees a Context key it can resolve to a
// documented service class. The independent scan reads EVERY Context merge
// so a key the projection cannot render must either be rendered (mapped) or
// carry a named SERVICE_WALK_EXEMPTIONS reason — never vanish silently.
for ( const [ key , rel ] of input . declaredKeys ) {
const rendered = input . renderedKeys . has ( key )
const exempt = Object . hasOwn ( maps . serviceWalkExemptions , key )
if ( ! rendered && ! exempt ) {
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. ` )
}
if ( rendered && exempt ) problems . push ( ` ctx. ${ key } is rendered by the projection but still listed in SERVICE_WALK_EXEMPTIONS; remove the stale exemption. ` )
}
for ( const key of Object . keys ( maps . serviceWalkExemptions ) ) {
if ( ! input . declaredKeys . has ( key ) ) problems . push ( ` SERVICE_WALK_EXEMPTIONS names 'ctx. ${ key } ' but no Context merge declares it; remove the stale exemption. ` )
}
// The event mirror of the service backstop: the projection walks only files
// reachable from host-face package exports, so a client-face or unreachable
// Events merge would otherwise vanish without a trace.
for ( const [ name , rel ] of input . declaredEvents ) {
const rendered = input . renderedEventNames . has ( name )
const exempt = Object . hasOwn ( maps . eventWalkExemptions , name )
if ( ! rendered && ! exempt ) {
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. ` )
}
if ( rendered && exempt ) problems . push ( ` event ' ${ name } ' is rendered by the projection but still listed in EVENT_WALK_EXEMPTIONS; remove the stale exemption. ` )
}
for ( const name of Object . keys ( maps . eventWalkExemptions ) ) {
if ( ! input . declaredEvents . has ( name ) ) problems . push ( ` EVENT_WALK_EXEMPTIONS names ' ${ name } ' but no Events merge declares it; remove the stale exemption. ` )
}
2026-08-09 10:29:13 +08:00
// Self-check the scan itself: everything the projection renders is declared
// in a Context/Events merge the scan must also reach, so a rendered key or
// event the scan cannot see means the SCAN regressed (glob, prefilter, or
// block walk) — a partial blind spot that exemption staleness alone would
// never surface.
for ( const key of input . renderedKeys . keys ( ) ) {
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. ` )
}
for ( const name of input . renderedEventNames ) {
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. ` )
}
2026-08-09 02:39:12 +08:00
return problems
}
2026-07-30 21:40:58 +08:00
/**
* Compute every generated artifact: the inherited-tier page, the model-facing
* runtime API module, plus, per mapped subsystems page, the pair's two updated
* documents with the injected region. Fail-loud partition checks live here: an
* unmapped service/event scope, a mapping whose page file does not exist, a
2026-08-09 02:39:12 +08:00
* curated entry whose key/scope the projection no longer discovers, a declared
* Context key or Events member the projection cannot see without a named walk
* exemption, and a mapped page missing its markers are all aggregated errors.
2026-07-30 21:40:58 +08:00
* @returns `[repo-relative path, exact content]` for every generated artifact.
*/
export function computeOutputs ( ) : [ string , string ] [ ] {
2026-07-28 23:48:35 +08:00
const { projector , model } = projectCordisCatalog ( root , CORDIS_CATALOG_POLICY )
2026-07-30 21:40:58 +08:00
const services = [ . . . model . services ]
const events = [ . . . model . events ]
const declaredKeys = new Map < string , string > ( )
2026-08-09 02:39:12 +08:00
const declaredEvents = new Map < string , string > ( )
2026-08-09 10:29:13 +08:00
for ( const { rel , sf , body } of contextMergeFiles ( root , [ 'packages/*/*/src/**/*.ts' , 'packages/*/*/src/**/*.tsx' ] ) ) {
2026-07-30 21:40:58 +08:00
for ( const key of contextKeyMap ( body , sf ) . keys ( ) ) {
if ( ! declaredKeys . has ( key ) ) declaredKeys . set ( key , rel )
}
2026-08-09 02:39:12 +08:00
for ( const name of eventNameList ( body , sf ) ) {
if ( ! declaredEvents . has ( name ) ) declaredEvents . set ( name , rel )
2026-07-30 21:40:58 +08:00
}
}
2026-08-09 02:39:12 +08:00
const problems = walkPartitionProblems ( {
renderedKeys : new Map ( services . map ( s = > [ s . key , s . source ] ) ) ,
renderedScopes : new Set ( events . map ( e = > e . scope ) ) ,
renderedEventNames : new Set ( events . map ( e = > e . name ) ) ,
declaredKeys ,
declaredEvents ,
} , {
servicePage : SERVICE_PAGE ,
serviceWalkExemptions : SERVICE_WALK_EXEMPTIONS ,
eventScopePage : EVENT_SCOPE_PAGE ,
eventWalkExemptions : EVENT_WALK_EXEMPTIONS ,
} )
2026-07-30 21:40:58 +08:00
if ( problems . length > 0 ) throw new Error ( ` gen-cordis-catalog: ${ problems . length } partition violation(s): \ n ${ problems . map ( p = > ` ${ p } ` ) . join ( '\n' ) } ` )
const pages = [ . . . new Set ( [ . . . Object . values ( SERVICE_PAGE ) , . . . Object . values ( EVENT_SCOPE_PAGE ) ] ) ] . sort ( )
2026-07-05 00:45:06 +08:00
const outputs : [ string , string ] [ ] = [
2026-07-30 21:40:58 +08:00
[ OUT_INHERITED , renderInheritedPage ( CORDIS_CATALOG_POLICY ) ] ,
2026-07-28 23:48:35 +08:00
[ OUT_RUNTIME_API , projector . renderRuntimeApi ( model ) ] ,
2026-07-30 21:40:58 +08:00
]
for ( const page of pages ) {
const region = renderPageRegion (
page ,
services . filter ( s = > SERVICE_PAGE [ s . key ] === page ) ,
events . filter ( e = > EVENT_SCOPE_PAGE [ e . scope ] === page ) ,
CORDIS_CATALOG_POLICY ,
)
for ( const side of [ page , page . replace ( /\.md$/ , '.zh.md' ) ] ) {
const rel = ` ${ SUBSYSTEMS_DIR } / ${ side } `
let current : string
try {
current = readFileSync ( resolve ( root , rel ) , 'utf8' )
} catch {
// Both pair sides must exist before a region can be injected; the
// pairing gate owns pair completeness, this generator names the miss.
problems . push ( ` ${ rel } : mapped subsystems page does not exist. ` )
continue
}
try {
outputs . push ( [ rel , spliceRegion ( current , region ) ] )
} catch ( error ) {
problems . push ( ` ${ rel } : ${ error instanceof Error ? error.message : String ( error ) } ` )
}
}
}
if ( problems . length > 0 ) throw new Error ( ` gen-cordis-catalog: ${ problems . length } page violation(s): \ n ${ problems . map ( p = > ` ${ p } ` ) . join ( '\n' ) } ` )
return outputs
}
/**
* Re-record a pair's `.i18n.yaml` after a region write ONLY when the write is
* region-confined: both sides' region-stripped content must be byte-equal to
* the region-stripped previous content whose hashes the record holds. The
* caller supplies the previous bytes (read before writing); human-content
* drift leaves the record untouched so the pairing gate still demands the
* normal translation flow.
* @param pageRel - repo-relative English page path (`docs/subsystems/x.md`).
* @param before - pre-write bytes per repo-relative path.
* @param scanRoot - repository root override for tests.
* @returns true when the record was refreshed.
*/
export function maybeRecordPair ( pageRel : string , before : Map < string , Buffer > , scanRoot : string = root ) : boolean {
const zhRel = pageRel . replace ( /\.md$/ , '.zh.md' )
const metaRel = pageRel . replace ( /\.md$/ , '.i18n.yaml' )
const metaAbs = resolve ( scanRoot , metaRel )
let meta : string
try {
meta = readFileSync ( metaAbs , 'utf8' )
} catch {
// No record yet: a brand-new pair is recorded by the author's --write
// after review, never silently by regeneration.
return false
}
2026-08-09 15:27:21 +08:00
// The record must contain exactly the two valid entries for THIS pair;
2026-07-30 21:40:58 +08:00
// a malformed or renamed-key sidecar is the pairing gate's problem to
// report, never something regeneration silently repairs into validity.
const recorded = parsePairMeta ( meta )
const names = [ pageRel , zhRel ] . map ( rel = > rel . split ( '/' ) . at ( - 1 ) ? ? rel )
if ( ! recorded || recorded . size !== 2 || ! names . every ( name = > recorded . has ( name ) ) ) return false
for ( const rel of [ pageRel , zhRel ] ) {
const previous = before . get ( rel )
if ( ! previous ) return false
if ( recorded . get ( rel . split ( '/' ) . at ( - 1 ) ? ? rel ) !== blobHash ( previous ) ) return false
const current = readFileSync ( resolve ( scanRoot , rel ) )
const strippedBefore = partitionGeneratedRegions ( previous . toString ( 'utf8' ) ) . stripped
const strippedAfter = partitionGeneratedRegions ( current . toString ( 'utf8' ) ) . stripped
if ( strippedBefore !== strippedAfter ) return false
}
const source = readFileSync ( resolve ( scanRoot , pageRel ) )
const zh = readFileSync ( resolve ( scanRoot , zhRel ) )
writeFileSync ( metaAbs , renderPairMeta ( pageRel , blobHash ( source ) , zhRel , blobHash ( zh ) ) )
return true
}
/** CLI entry: default regenerates every artifact, `--check` fails if any is
* stale. Guarded behind an entry-point check so importing this module for
* tests neither regenerates the committed files nor calls process.exit.
* @returns nothing; writes files or reports freshness through the process.
*/
export function main ( ) : void {
const outputs : [ string , string ] [ ] = [
. . . computeOutputs ( ) ,
2026-07-20 16:32:08 +08:00
. . . renderCordisCoreApiPages ( ) ,
2026-07-05 00:45:06 +08:00
]
2026-06-20 19:47:09 +08:00
if ( process . argv . includes ( '--check' ) ) {
2026-07-05 00:45:06 +08:00
const stale : string [ ] = [ ]
for ( const [ out , content ] of outputs ) {
let committed : string | null = null
try {
committed = readFileSync ( resolve ( root , out ) , 'utf8' )
} catch {
2026-07-30 21:40:58 +08:00
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
2026-07-05 00:45:06 +08:00
committed = null
}
if ( committed !== content ) stale . push ( out )
2026-06-20 19:47:09 +08:00
}
2026-07-05 00:45:06 +08:00
if ( stale . length === 0 ) {
2026-07-30 21:40:58 +08:00
console . log ( ` gen-cordis-catalog: ${ outputs . length } generated file(s)/region(s) are up to date. ` )
2026-06-20 19:47:09 +08:00
process . exit ( 0 )
}
2026-07-30 21:40:58 +08:00
console . error ( ` gen-cordis-catalog: stale — ${ stale . join ( ', ' ) } . Run \` pnpm run gen-cordis-catalog \` and commit the result. ` )
2026-06-20 19:47:09 +08:00
process . exit ( 1 )
}
2026-07-30 21:40:58 +08:00
const before = new Map < string , Buffer > ( )
for ( const [ out ] of outputs ) {
try {
before . set ( out , readFileSync ( resolve ( root , out ) ) )
} catch {
// First generation of this artifact; nothing to guard, nothing to record.
}
}
let changedPages = 0
let recorded = 0
2026-07-20 16:32:08 +08:00
for ( const [ out , content ] of outputs ) {
const destination = resolve ( root , out )
2026-07-30 21:40:58 +08:00
if ( before . get ( out ) ? . toString ( 'utf8' ) === content ) continue
2026-07-20 16:32:08 +08:00
mkdirSync ( dirname ( destination ) , { recursive : true } )
writeFileSync ( destination , content )
2026-07-30 21:40:58 +08:00
changedPages ++
2026-07-20 16:32:08 +08:00
}
2026-07-30 21:40:58 +08:00
for ( const page of [ . . . new Set ( [ . . . Object . values ( SERVICE_PAGE ) , . . . Object . values ( EVENT_SCOPE_PAGE ) ] ) ] ) {
const rel = ` ${ SUBSYSTEMS_DIR } / ${ page } `
const zhRel = rel . replace ( /\.md$/ , '.zh.md' )
const wroteEither = [ rel , zhRel ] . some ( ( side ) = > {
const previous = before . get ( side )
return previous !== undefined && previous . toString ( 'utf8' ) !== readFileSync ( resolve ( root , side ) , 'utf8' )
} )
if ( wroteEither && maybeRecordPair ( rel , before ) ) recorded ++
}
console . log ( ` gen-cordis-catalog: ${ outputs . length } artifact(s) computed, ${ changedPages } written, ${ recorded } pair record(s) refreshed. ` )
2026-06-20 19:47:09 +08:00
}
2026-07-30 21:40:58 +08:00
// Run only when invoked as a script, not when imported by a test.
if ( process . argv [ 1 ] && import . meta . filename === resolve ( process . argv [ 1 ] ) ) {
main ( )
}