docs: generate each subsystem's cordis surface into its own page; delete the flat catalogs
Rebuild of the region machinery (PR3) on the post-#904 Typert projection: renderPageRegion/renderInheritedPage live in dsh-typert-generator beside the projection; scripts/gen-cordis-catalog.ts owns the curated SERVICE_PAGE / EVENT_SCOPE_PAGE / SERVICE_WALK_EXEMPTIONS / LINK_MAP partition (fail-loud in both directions, with the independent Context-merge scan backstopping the projection's blind spot), spliceRegion, and the guarded pair auto-record. docs/cordis-catalog/ is deleted: the flat events/services catalogs dissolve into per-page regions and docs/cordis-catalog/core moves to docs/cordis-api/ with the inherited tier as its own generated page. The partition absorbs the post-regrouping surface: ctx.typert → invariants.md, ctx.directoryPicker → workspace.md, skills/* events → skills.md, and the four launcher-provided tui accessor values join the named exemptions.
This commit is contained in:
@@ -181,7 +181,7 @@ interface FsEditOutcome {
|
||||
|
||||
`dsh-fs` owns three events the tool dispatches and the policy plugin listens for, so the emitter (`dsh-tool-fs`) and the listener (`dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
|
||||
|
||||
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated catalog shows the exact signatures on [events.md](../cordis-catalog/events.md).
|
||||
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated [cordis surface](#cordis-surface) below shows the exact signatures.
|
||||
|
||||
## Execution context (policy plugin)
|
||||
|
||||
@@ -258,4 +258,208 @@ type FsErrorCode =
|
||||
|
||||
## The service and the plugin
|
||||
|
||||
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated wiring catalog shows the exact `ctx.fs` signatures on [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam).
|
||||
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
<a id="cordis-surface"></a>
|
||||
|
||||
## Cordis surface
|
||||
|
||||
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxfs--filesystem-abstract-seam"></a>
|
||||
|
||||
### `ctx.fs` — `FileSystem` (abstract seam)
|
||||
|
||||
Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a
|
||||
* remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence
|
||||
* async even though the local backend only normalizes + realpaths.
|
||||
*
|
||||
* @param path - the path to resolve; relative paths resolve against `opts.cwd`.
|
||||
* @param opts - optional cwd override and cancellation signal.
|
||||
* @returns the stable target; the same file yields the same `targetKey`.
|
||||
*/
|
||||
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
|
||||
|
||||
/**
|
||||
* Return the canonical absolute path a subprocess in this filesystem's
|
||||
* execution world can open. The path is deliberately separate from
|
||||
* {@link FsTarget.targetKey}: consumers may pass this value to another OS
|
||||
* capability, but must continue treating the target key as opaque.
|
||||
* @param target - the resolved target whose process path is required.
|
||||
* @returns an absolute path in the backend's execution world.
|
||||
*/
|
||||
abstract processPath(target: FsTarget): string
|
||||
|
||||
/**
|
||||
* Return the canonical `file:` URI for a target in this filesystem's
|
||||
* execution world. Backends own URI encoding because the host platform may
|
||||
* differ from the execution platform.
|
||||
* @param target - the resolved target to encode.
|
||||
* @returns the target's canonical file URI.
|
||||
*/
|
||||
abstract fileUrl(target: FsTarget): string
|
||||
|
||||
/**
|
||||
* Test canonical containment without exposing or parsing backend target
|
||||
* keys. Both targets must come from this provider.
|
||||
* @param parent - canonical directory target.
|
||||
* @param child - canonical candidate target.
|
||||
* @returns true when `child` is `parent` or a descendant of it.
|
||||
*/
|
||||
abstract contains(parent: FsTarget, child: FsTarget): boolean
|
||||
|
||||
/**
|
||||
* Return target metadata, or `undefined` when the target does not exist.
|
||||
* @param target - the resolved target to stat.
|
||||
* @param signal - aborts the metadata round-trip.
|
||||
* @returns metadata only, never content; undefined for an absent target.
|
||||
*/
|
||||
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
|
||||
|
||||
/**
|
||||
* Return path metadata without following the final path component when it is a
|
||||
* symbolic link. This is intentionally path-shaped, not target-shaped:
|
||||
* {@link resolve} follows symlinks to produce the stable identity used by
|
||||
* normal reads/writes, while `lstat` lets a consumer reject the path itself
|
||||
* before that follow happens.
|
||||
*
|
||||
* `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is
|
||||
* absent.
|
||||
* @param path - the path to inspect; relative paths resolve against `opts.cwd`.
|
||||
* @param opts - `cwd` overrides the backend's default base for relative paths.
|
||||
* @param signal - aborts the metadata round-trip.
|
||||
* @returns metadata only, never content; undefined for an absent path.
|
||||
*/
|
||||
abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined>
|
||||
|
||||
/**
|
||||
* Read the whole regular text file as a single decoded string.
|
||||
* @param target - the resolved target to read.
|
||||
* @param signal - aborts the read.
|
||||
* @returns the full decoded UTF-8 content.
|
||||
*/
|
||||
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
|
||||
|
||||
/**
|
||||
* Stream the whole regular text file as decoded text chunks (same text
|
||||
* semantics as {@link readText}, for large files). The backend owns
|
||||
* cross-chunk UTF-8 decoding and binary rejection so the policy layer never
|
||||
* touches raw bytes.
|
||||
* @param target - the resolved target to read.
|
||||
* @param signal - aborts the stream, including between chunks.
|
||||
* @returns the chunk iterable, decoded and validated like {@link readText}.
|
||||
*/
|
||||
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
|
||||
|
||||
/**
|
||||
* List direct children of a directory in stable name order. Returns resolved
|
||||
* child targets plus cheap metadata only; never reads file contents.
|
||||
* @param target - the resolved directory target.
|
||||
* @param signal - aborts the listing.
|
||||
* @returns one entry per direct child, in stable name order.
|
||||
*/
|
||||
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
|
||||
|
||||
/**
|
||||
* Atomically create or replace UTF-8 text. `expected` guards intent and
|
||||
* staleness; omission allows unconditional overwrite.
|
||||
* @param target - the resolved target to write.
|
||||
* @param content - the full new file content.
|
||||
* @param expected - the write intent guarding the write; omit for unconditional.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this write
|
||||
* runs under; a sandboxing backend fences the write by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
* @returns the outcome, including the version the write produced.
|
||||
*/
|
||||
abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsWriteOutcome>
|
||||
|
||||
/**
|
||||
* Atomically edit literal text. When supplied, the version guard is checked
|
||||
* before matching so stale content reports `FS_STALE_VERSION`; omission edits
|
||||
* the current content without a freshness precondition.
|
||||
* @param target - the resolved target to edit.
|
||||
* @param edit - the literal search/replace request.
|
||||
* @param expected - the version guard; omit for an unconditional edit.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this edit runs
|
||||
* under; a sandboxing backend fences the edit by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
* @returns the outcome, including the version the edit produced.
|
||||
*/
|
||||
abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsEditOutcome>
|
||||
```
|
||||
|
||||
Types: [SandboxExecutionPolicy](sandbox.md)
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fs-events"></a>
|
||||
|
||||
### `fs/*` events
|
||||
|
||||
<a id="fsedit-intent--waterfall"></a>
|
||||
|
||||
#### `fs/edit-intent` — waterfall
|
||||
|
||||
Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Single-slot decision for the next {@link FileSystem.editText}. Calling
|
||||
* `next()` yields an unconditional edit; the first returned guard wins.
|
||||
* @param target - the resolved target about to be edited.
|
||||
* @param actor - the opaque tool-execution context the decider keys off.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fsobserved--emit"></a>
|
||||
|
||||
#### `fs/observed` — emit
|
||||
|
||||
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Record a successful observation. Listeners must be synchronous recorders:
|
||||
* throws fail the tool call and returned promises are not awaited.
|
||||
* @param target - the target that was read/written/edited.
|
||||
* @param version - the version the actor now holds as its observation.
|
||||
* @param actor - the observing tool-execution context; undefined records nothing useful.
|
||||
* @mode emit
|
||||
*/
|
||||
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fswrite-intent--waterfall"></a>
|
||||
|
||||
#### `fs/write-intent` — waterfall
|
||||
|
||||
Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Single-slot decision for the next {@link FileSystem.writeText}. Calling
|
||||
* `next()` yields the bare provider's unconditional write; the first listener
|
||||
* that returns an intent owns the decision rather than composing with peers.
|
||||
* @param target - the resolved target about to be written.
|
||||
* @param actor - the opaque tool-execution context the decider keys off.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
Reference in New Issue
Block a user