@lmzhen/dsh-evolution-core 0.3.74 → 0.3.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,8 +88,11 @@ export declare const DEFAULT_CURATOR_BOOT_GRACE_SECONDS = 10;
88
88
  export declare const DEFAULT_CURATOR_REVIEW_MAX_TOKENS = 2048;
89
89
  /** 0.3.17 (S3.10, T-1): control-plane fields a model-facing write call may
90
90
  * never carry — single source for plan-validator, evolution-policy and the
91
- * threat scanner (they used to each hardcode the list). */
92
- export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config"];
91
+ * threat scanner (they used to each hardcode the list).
92
+ * P2-13 (v37): staged_from_sha256 joins the list it is the replay's own
93
+ * staleness anchor, and the tool-arguments root is an OPEN object, so a model
94
+ * could otherwise choose the anchor that decides the write's outcome. */
95
+ export declare const FORBIDDEN_CONTROL_KEYS: readonly ["policy", "threshold", "prompt_hash", "model_route", "evolution_config", "staged_from_sha256"];
93
96
  /** 0.3.17 (S3.10): the model-facing write tools the policy guard and threat
94
97
  * scanner cover. */
95
98
  export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
@@ -49,6 +49,8 @@ export interface EvolutionEvent {
49
49
  * count snapshot at the moment the observation window opened), and
50
50
  * maintain scans (011: verdict + recommendation count + runId). */
51
51
  type: 'feedback' | 'learn' | 'usage' | 'maintain';
52
+ /** Fold key for a `feedback` event: required and non-empty there (P2-10),
53
+ * optional on the other tags and on a record this version only reads. */
52
54
  target?: string | undefined;
53
55
  kind?: 'skill' | 'session' | undefined;
54
56
  rating?: 'positive' | 'negative' | undefined;
@@ -70,6 +72,15 @@ export interface EvolutionEvent {
70
72
  opened?: string;
71
73
  } | undefined;
72
74
  }
75
+ /** The durable-write shape of one event: `feedback` REQUIRES a `target` —
76
+ * the fold key the aggregate is keyed by (P2-10). The other tags keep every
77
+ * field optional, exactly as the runtime payload gate treats them. */
78
+ export type EvolutionEventInput = (Omit<EvolutionEvent, 'seq' | 'at' | 'target'> & {
79
+ type: 'feedback';
80
+ target: string;
81
+ }) | (Omit<EvolutionEvent, 'seq' | 'at'> & {
82
+ type: 'learn' | 'usage' | 'maintain';
83
+ });
73
84
  export declare function eventsFile(home: string): string;
74
85
  /** I-5 (v18): one lightweight description of the durable event payload
75
86
  * contract. The log is a FILE boundary (a host, a script or an older version
@@ -108,7 +119,7 @@ export declare function listEventArchives(io: EvolutionIoLike, path: string): Pr
108
119
  * — the active restarts AFTER the highest archived seq, never at 1, so a new
109
120
  * event can never shadow an archived one in the seq-deduped timeline.
110
121
  */
111
- export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>, rotateAt?: number): Promise<number>;
122
+ export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: EvolutionEventInput, rotateAt?: number): Promise<number>;
112
123
  /**
113
124
  * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
114
125
  * The name's numeric part is the last archived seq, so ordering is NUMERIC —
@@ -3,10 +3,9 @@
3
3
  *
4
4
  * Pure stores, prompts, signals, lifecycle logic, threat scanning, IO seam
5
5
  * types, and session-event augmentations. This package ships no runtime
6
- * plugin beyond the invariant companion entry (`./invariant` the subpath
7
- * the host auto-assembles to register this package's no-op invariant);
8
- * consumers import named exports from the package root so published npm
9
- * bundles never depend on source subpaths.
6
+ * plugin and no `./invariant` companion (v37 S2.1); consumers import named
7
+ * exports from the package root so published npm bundles never depend on
8
+ * source subpaths.
10
9
  *
11
10
  * ## Layer map (OPT-28, 2026-09) — locate code by LAYER, not by directory
12
11
  *
@@ -39,14 +38,19 @@ export * from './preset-composition.ts';
39
38
  export * from './prompts.ts';
40
39
  export * from './quality.ts';
41
40
  export * from './redact.ts';
41
+ export * from './review-channel.ts';
42
42
  export * from './serial.ts';
43
+ export * from './probe.ts';
44
+ export * from './scope.ts';
43
45
  export * from './skill-health.ts';
44
46
  export * from './signals.ts';
45
47
  export * from './drift-signals.ts';
46
48
  export * from './skill-store.ts';
47
49
  export * from './state-store.ts';
48
50
  export * from './threats.ts';
51
+ export * from './tool-dispatch.ts';
49
52
  export * from './usage.ts';
50
53
  export * from './constants.ts';
51
54
  export * from './numeric.ts';
55
+ export * from './opt-in.ts';
52
56
  //# sourceMappingURL=index.d.ts.map
@@ -18,6 +18,22 @@ export interface MemoryApplyResult {
18
18
  limit: number;
19
19
  }
20
20
  export declare function memoryRoot(env?: NodeJS.ProcessEnv): string;
21
+ /**
22
+ * Neutralize the platform's `{{name}}` prompt-variable syntax in text that is
23
+ * about to be registered as a `systemPrompt.context` contribution.
24
+ *
25
+ * The platform interpolates every context/section text once per model step and
26
+ * THROWS on an unknown or malformed reference — so one stored memory entry
27
+ * containing `{{...}}` (a CI expression, a Jinja/Helm/Vue template, a doc
28
+ * placeholder) used to fail `assemble()` on EVERY later pre-step of every
29
+ * session under this DSH_HOME, while a registered name such as `{{cwd}}` was
30
+ * silently substituted into the "memory" the model reads. Only the INJECTED
31
+ * text is neutralized: stored entries keep their original bytes.
32
+ *
33
+ * @param text - rendered context text about to leave for the prompt surface.
34
+ * @returns the same text with every `{{` split so it cannot start a reference.
35
+ */
36
+ export declare function neutralizePromptVariables(text: string): string;
21
37
  export interface MemoryStoreOptions {
22
38
  memoryCharLimit?: number;
23
39
  userCharLimit?: number;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Variant-form opt-in: WHICH sessions the family's cross-session consumers act on.
3
+ *
4
+ * The family ships in two install forms. Mounted onto the original presets (the
5
+ * profile-level bundle), EVERY session is a family session and the consumers
6
+ * below keep acting on all of them. In the VARIANT form the family's model tools
7
+ * exist only inside a variant preset, so a session that did not select one never
8
+ * opted in — yet the cross-session consumers (review cadence and injection,
9
+ * skill-usage telemetry) live at profile root and used to act on every session
10
+ * anyway, including the platform's original presets: a user who installed a
11
+ * variant was still injected with review prompts and still counted in usage.
12
+ *
13
+ * The two forms are told apart WITHOUT a list of preset ids and WITHOUT a second
14
+ * source of truth: a session is a family session exactly when the family's own
15
+ * model tools are visible in its scope. Mounted at profile root they are visible
16
+ * to every session; mounted inside a variant preset they are visible only to the
17
+ * agents that joined that preset's standing scope — every registration inside a
18
+ * preset files into that agent's layer
19
+ * (packages/preset/agent-presets/src/mount.ts:4-13).
20
+ *
21
+ * `sessionScoped` is what a deployment declares: true means "act only on a
22
+ * session that carries the family's model tools", which is the right question in
23
+ * BOTH install forms — at profile root every session carries them, inside a
24
+ * variant preset only the sessions that selected one do. The shipped bundles set
25
+ * it; a bare library mount and the host-only infrastructure mode leave it false
26
+ * and keep the historical "every session" behavior.
27
+ * @module
28
+ */
29
+ import type { Context } from '@deepseek-ai/cordis';
30
+ /**
31
+ * Tool names only a session that mounted the family's MODEL rows can see.
32
+ *
33
+ * Both are family packages (`tool-skill-manage`, `tool-memory`), so a session
34
+ * that merely carries the platform's own session-query tool is not mistaken for
35
+ * a family session. Two names, not one: a deployment may disable either row.
36
+ */
37
+ export declare const FAMILY_SESSION_TOOL_NAMES: readonly string[];
38
+ /**
39
+ * Does this session's scope see the family's model tools?
40
+ *
41
+ * The scope is the live agent's — the platform's own addressing for "what does
42
+ * this session see" (`tools.get(name, scope)`, packages/core/tools/src/index.ts:1194).
43
+ * A session with no live agent (a finished or cold session) has no scope to ask
44
+ * in, so it resolves to false; the consumers this gate serves only run for live
45
+ * agents.
46
+ * @param ctx - a context of the runtime (any plane; the services are read from the global store).
47
+ * @param sessionId - the session to ask about.
48
+ * @returns true when at least one family model tool is visible to that session.
49
+ */
50
+ export declare function sessionSeesFamilyTools(ctx: Context, sessionId: string): boolean;
51
+ /**
52
+ * The one decision every cross-session consumer calls before it acts.
53
+ * @param ctx - a context of the runtime.
54
+ * @param sessionId - the session the event belongs to.
55
+ * @param sessionScoped - the row's `sessionScoped` config.
56
+ * @returns true when the consumer may act on this session. A deployment that did
57
+ * not declare session scoping always answers true (the historical behavior);
58
+ * a scoped one answers true only for a session that carries the family's model
59
+ * tools.
60
+ */
61
+ export declare function sessionAudited(ctx: Context, sessionId: string, sessionScoped: boolean | undefined): boolean;
62
+ //# sourceMappingURL=opt-in.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * V41 phase-1 follow-up: the read THREE-state (P2-25 sibling of N14).
3
+ *
4
+ * A read has three outcomes, not two: the value is there, it is genuinely not
5
+ * there, or we could not decide (IO error, unreadable store, missing
6
+ * provider). Collapsing the third into either of the others is how a broken
7
+ * store reads as "empty" and a guard silently passes — the class N14 registers.
8
+ * Migration target for the pre-existing two-state patches (protectionUnknown /
9
+ * unverifiable): constructors are prefixed so the type can never be confused
10
+ * with a value, and only `present` yields a value.
11
+ */
12
+ export type Probe<T> = {
13
+ kind: 'present';
14
+ value: T;
15
+ } | {
16
+ kind: 'absent';
17
+ } | {
18
+ kind: 'unknown';
19
+ reason: string;
20
+ };
21
+ export declare function probePresent<T>(value: T): Probe<T>;
22
+ export declare function probeAbsent<T>(): Probe<T>;
23
+ export declare function probeUnknown<T>(reason: string): Probe<T>;
24
+ export declare function isPresent<T>(probe: Probe<T>): probe is {
25
+ kind: 'present';
26
+ value: T;
27
+ };
28
+ export declare function isAbsent<T>(probe: Probe<T>): probe is {
29
+ kind: 'absent';
30
+ };
31
+ export declare function isUnknown<T>(probe: Probe<T>): probe is {
32
+ kind: 'unknown';
33
+ reason: string;
34
+ };
35
+ /** Only a PRESENT probe yields a value; absent and unknown both fall back. */
36
+ export declare function valueOr<T>(probe: Probe<T>, fallback: T): T;
37
+ export declare function mapProbe<T, U>(probe: Probe<T>, transform: (value: T) => U): Probe<U>;
38
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Family-internal mark for the review channel's INJECT delivery (v37 S2.2).
3
+ *
4
+ * With the default `reviewMode: 'inject'` the review prompt runs in the PARENT
5
+ * session, so the parent model's own `skill_manage` calls carry no origin the
6
+ * platform could attribute: `.pinned` and the `.hermes-managed` authorship mark
7
+ * were skipped for exactly the autonomous writes they exist for. The delivery
8
+ * marks the session here and the write path reads the mark.
9
+ *
10
+ * Window: after the review prompt, until the next REAL user message (a
11
+ * `user/message` with `source.kind === 'user'` clears it; plugin-sourced notices
12
+ * never do). Process-wide and keyed by session on purpose — the same
13
+ * "restart is a fresh conversation boundary" discipline as the review plugin's
14
+ * own per-session counters. This module is the ONE owner of the marker.
15
+ * @module @lmzhen/dsh-evolution-core/review-channel
16
+ */
17
+ /**
18
+ * Mark one session as running the review channel's prompt; idempotent, and
19
+ * keyed by session so a mark can never leak into another session.
20
+ * @param sessionId - the session the review prompt was delivered to.
21
+ */
22
+ export declare function markReviewChannel(sessionId: string): void;
23
+ /**
24
+ * Clear the mark: the session's next prompt is human input again.
25
+ * @param sessionId - the session whose mark is dropped.
26
+ */
27
+ export declare function clearReviewChannel(sessionId: string): void;
28
+ /**
29
+ * Whether this session's current prompt came from the review channel. An
30
+ * execution without a session is never the review channel.
31
+ * @param sessionId - the executing session id, when the caller has one.
32
+ * @returns true only for a marked session.
33
+ */
34
+ export declare function isReviewChannelSession(sessionId: string | undefined): boolean;
35
+ /**
36
+ * Drop marks whose session is gone — a dead session cannot execute a write.
37
+ * @param isAlive - liveness probe for one session id.
38
+ * @returns the number of removed marks.
39
+ */
40
+ export declare function sweepReviewChannelSessions(isAlive: (sessionId: string) => boolean): number;
41
+ //# sourceMappingURL=review-channel.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * V41 scope alignment: the ONE place that answers "in whose scope do we ask?".
3
+ *
4
+ * The platform's registry reads are scope-sensitive: `skills.list(options)`
5
+ * documents its `scope` as "the calling agent; OMITTED READS THE GLOBAL LAYER
6
+ * ALONE", and `tools.get(name, scope?)` resolves visibility the same way
7
+ * (packages/skill/skill/src/index.ts:113-120, packages/core/tools/src/index.ts:1194).
8
+ * A preset-mounted family row therefore lives in its PRESET'S STANDING SCOPE,
9
+ * so a scope-less read cannot see it — the asymmetry between "evolution mounted
10
+ * onto the original preset" and "the evolution variant preset".
11
+ *
12
+ * Resolution order (the single verdict; do not re-derive at call sites):
13
+ * 1. the scope the caller already holds (a tool invocation carrying one);
14
+ * 2. this plugin context's own scope — for a preset-mounted row that IS the
15
+ * preset's standing scope, i.e. exactly where the family's provider lives;
16
+ * 3. undefined, which callers must treat as an EXPLICIT global-layer read
17
+ * (registered per the arch guard's scope rule), never as a silent default.
18
+ */
19
+ import type { Context } from '@deepseek-ai/cordis';
20
+ /** The platform's scope identity, kept structural so callers do not need the
21
+ * platform type (`ScopeKey` is `object`). */
22
+ export type OpaqueScopeKey = object;
23
+ export declare function callingScope(ctx: Context, held?: OpaqueScopeKey): OpaqueScopeKey | undefined;
24
+ /** True when a read is deliberately scope-less (global layer only). Callers
25
+ * pass this to the register so the choice is reviewed, not accidental. */
26
+ export declare function isGlobalRead(scope: OpaqueScopeKey | undefined): boolean;
27
+ //# sourceMappingURL=scope.d.ts.map
@@ -50,7 +50,12 @@ export interface SkillSummary {
50
50
  protectedBy: string | null;
51
51
  /** A1-17 (v18): the marker probe itself failed (EACCES/EIO), so "no marker"
52
52
  * cannot be told apart from "directory unreadable". Consumers must treat this
53
- * as protected, never as unprotected. */
53
+ * as protected, never as unprotected.
54
+ * v41: this flag and `protectedBy` ARE the read three-state —
55
+ * (null, false) = no marker, (X, false) = present, (_, true) = unreadable.
56
+ * Read them together, never this flag alone; it is a published field, so the
57
+ * encoding stays (core's `Probe<T>` is for internal decisions, not for this
58
+ * contract). */
54
59
  protectionUnknown: boolean;
55
60
  managed: boolean;
56
61
  /** E-11 (v18): the frontmatter `whenToUse` routing hint, published so the
@@ -221,20 +226,6 @@ export interface Frontmatter {
221
226
  description?: string;
222
227
  [key: string]: unknown;
223
228
  }
224
- /**
225
- * Shared frontmatter block detection (P3-3 single owner): opening line `---`
226
- * and closing line exactly `---`. Used by `parseFrontmatter`,
227
- * `frontmatterCatalogInvalid` and `normalizeFrontmatter` so the three can
228
- * never disagree about where the block ends (the loose `indexOf('\n---')`
229
- * form matched `\n----` and was replaced by this strict line rule).
230
- *
231
- * V27 G2.1: both fence lines are matched EXACTLY, tolerating only a trailing
232
- * `\r` — the same rule the upstream filesystem catalog uses
233
- * (`skill-filesystem.parseFrontmatter`). The former `.trim()` comparison
234
- * accepted ` --- `, so an indented fence loaded in the family while the
235
- * platform ignored the file: family visibility split from platform visibility,
236
- * which is exactly what a strict-YAML frontmatter is supposed to prevent.
237
- */
238
229
  export declare function frontmatterBlock(content: string): {
239
230
  block: string;
240
231
  lines: string[];
@@ -260,9 +251,23 @@ export interface FrontmatterRead {
260
251
  /** Whether the frontmatter is not valid AS WRITTEN for the strict platform
261
252
  * catalog: the strict parser rejects the block, or an unquoted value would
262
253
  * read as something other than its text (a dropped ` # ` comment, a
263
- * number/bool shorthand the catalog refuses as a string field). The write
264
- * path quotes such a value on its next edit. */
254
+ * number/bool shorthand the catalog refuses as a string field), or a
255
+ * platform string field carries a non-string value. The write path quotes
256
+ * such a value on its next edit. */
265
257
  catalogInvalid: boolean;
258
+ /** Platform string fields (`name`/`description`/`whenToUse`) whose YAML value
259
+ * is not a string: the strict catalog reads such a field as ABSENT and, for an
260
+ * absent name/description, ignores the whole file (P2-9/v37). */
261
+ platformStringSplit: PlatformStringSplit[];
262
+ }
263
+ /** One `name`/`description`/`whenToUse` entry the strict catalog cannot read as
264
+ * a string, with the YAML kind the parser found. */
265
+ export interface PlatformStringSplit {
266
+ key: string;
267
+ /** The YAML type read for this field. `scalar` covers number/boolean (the
268
+ * E-47 auto-quote repair handles those); `sequence`/`mapping` are the shapes
269
+ * no rewrite can repair without inventing text — see `validateFrontmatter`. */
270
+ kind: 'sequence' | 'mapping' | 'scalar' | 'null';
266
271
  }
267
272
  /**
268
273
  * Parse a SKILL.md: its frontmatter values and body, or `null` when the file
@@ -341,7 +346,9 @@ export declare function normalizeFrontmatter(content: string): FrontmatterNormal
341
346
  * excluded. Pure and deduplicated.
342
347
  */
343
348
  export declare function relatedSkillNames(content: string, exclude?: string): string[];
344
- export declare function validateFrontmatter(content: string, expectedName?: string, limits?: SkillLimits): string | null;
349
+ export declare function validateFrontmatter(content: string, expectedName?: string, limits?: SkillLimits,
350
+ /** On-disk bytes, so a NET SHRINK of an over-limit file is allowed (S1.2). */
351
+ current?: string | null): string | null;
345
352
  /** Hermes authoring quality bar for descriptions — see constants.ts
346
353
  * (0.3.16 T-4 moved the single source there; the public re-export sits behind
347
354
  * the package root, which re-exports constants anyway). */
@@ -531,15 +538,16 @@ export declare class SkillLibrary {
531
538
  * each support dir (write_file's lock sits next to its file). Residual:
532
539
  * NESTED support-subdir locks and the probe→rename TOCTOU itself remain
533
540
  * fail-safe (renameWithRetry rides the write out; the writer's locked
534
- * re-read refuses on the moved-away file), and a residue `.lock` from a
535
- * CRASHED writer also refusescorrect: inspect, don't archive.
541
+ * re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
542
+ * holder is GONE is not a writer `decideTakeover` owns that verdict.
536
543
  */
537
544
  private hasWriteLock;
538
- /** P2 (v17): a file only counts as a writer lock when its body has the
539
- * `pid:token` shape the io layer writes. User support files legitimately
540
- * named `*.lock` (allowed by SUPPORT_FILE_NAME_RE) must not trip the probe
541
- * or be swept as residue the v16 first cut matched on suffix alone,
542
- * which permanently refused archiving and deleted user content on restore. */
545
+ /** P2 (v17): a `*.lock` file counts as a writer lock only when its body
546
+ * carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
547
+ * (a creator between create and body write); anything else is user content
548
+ * (suffix-only matching refused archiving and deleted user files on restore).
549
+ * v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
550
+ * the lock the io layer refuses to reclaim, the 30s empty window included. */
543
551
  private isWriterLock;
544
552
  /** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
545
553
  * A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
@@ -684,4 +692,30 @@ export declare class SkillLibrary {
684
692
  */
685
693
  private restoreSnapshotIntoRoot;
686
694
  }
695
+ /** The emit surface the helper needs — structurally the platform context's
696
+ * `emit`, so core states the contract without importing the platform type. */
697
+ export interface SkillMutationSink {
698
+ emit: (type: 'evolution/skill-mutated', event: EvolutionSkillMutatedEvent) => unknown;
699
+ }
700
+ /** V41 P2-26 (0.3.75): the ONE SkillLibrary construction point.
701
+ *
702
+ * Eleven call sites used to re-derive the root and hand-roll the
703
+ * `evolution/skill-mutated` arrow (the catalog invalidates its cache on that
704
+ * exact string, so a second spelling is a silent divergence), and each one
705
+ * re-decided its own limits. `config` stays the RAW family config: resolving
706
+ * an empty `root` to the family default is `resolveSkillsRoot`'s policy, not
707
+ * a per-caller detail. */
708
+ export interface NewSkillLibraryOptions {
709
+ /** Raw family config; only `root` is read, through resolveSkillsRoot. */
710
+ config?: {
711
+ root?: string | undefined;
712
+ } | undefined;
713
+ io: EvolutionIoLike;
714
+ limits?: SkillLimits | undefined;
715
+ /** Supplying it wires the mutation event HERE — never at the call site. */
716
+ ctx?: SkillMutationSink | undefined;
717
+ transact?: typeof transactIo | undefined;
718
+ threatExemptLabels?: readonly string[] | undefined;
719
+ }
720
+ export declare function newSkillLibrary(options: NewSkillLibraryOptions): SkillLibrary;
687
721
  //# sourceMappingURL=skill-store.d.ts.map
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The family's ONE reader of platform tool-dispatch events.
3
+ *
4
+ * The platform records a finished tool call under two different event
5
+ * vocabularies and only one of them is written per dispatch, chosen by the
6
+ * mounted tool runtime's mode:
7
+ *
8
+ * - native mode: \`tool/call\` (with \`callId\`) settled by \`tool/result\`, the
9
+ * write sites being core/agent-loop/src/tool-calls.ts:264 and :282.
10
+ * - PTC mode: \`tool/ptc-dispatch-start\` and \`tool/ptc-dispatch\` (with
11
+ * \`subCallId\`), the write sites being core/tools/src/ptc.ts:534 and :509.
12
+ *
13
+ * A consumer that matches a vocabulary directly therefore goes blind in the
14
+ * other mode while still appearing to work — the defect this module exists to
15
+ * make impossible. Every family consumer reads dispatches through
16
+ * \`ToolDispatchNormalizer\` (or the pure helpers below) and matches on
17
+ * \`ToolDispatchSignal\` fields only; \`verify-arch-guards\` rule N11 rejects a
18
+ * dispatch event-type comparison anywhere else.
19
+ *
20
+ * ## The one-dispatch invariant
21
+ *
22
+ * One dispatch produces exactly one \`ToolDispatchSignal\`, no matter how many
23
+ * events carry it: a start/settle pair for a PTC sub-dispatch, a call/result
24
+ * pair for a native call, or both vocabularies for the same call. Deduplication
25
+ * and outcome folding are keyed by the platform's own per-vocabulary call
26
+ * identity, which is stable across the pair (the platform's event JSDoc:
27
+ * "the pairing ids (matching the \`tool/ptc-dispatch-start\` with the same
28
+ * \`subCallId\`)").
29
+ *
30
+ * ## Layer
31
+ *
32
+ * Cross-cutting normalization: the reader half of the dispatch vocabulary,
33
+ * next to \`signals.ts\` (which folds the signals this module emits). Both are
34
+ * consumers; neither subscribes to the platform bus itself.
35
+ * @module
36
+ */
37
+ /** How the platform delivered one dispatch. */
38
+ export type ToolDispatchKind =
39
+ /** A model-authored call, logged as \`tool/call\` and settled by \`tool/result\`. */
40
+ 'native'
41
+ /** A sub-dispatch of a code program, logged as the PTC dispatch pair. */
42
+ | 'program'
43
+ /** A \`run_code\` call itself: a native call whose program dispatches sub-calls. */
44
+ | 'program-root';
45
+ /** The platform event type carrying a PTC sub-dispatch start. */
46
+ export declare const PTC_DISPATCH_START_EVENT = "tool/ptc-dispatch-start";
47
+ /** The platform event type carrying a PTC sub-dispatch settle. */
48
+ export declare const PTC_DISPATCH_EVENT = "tool/ptc-dispatch";
49
+ /** The platform event type carrying a native tool call. */
50
+ export declare const NATIVE_CALL_EVENT = "tool/call";
51
+ /** The platform event type carrying a native tool result. */
52
+ export declare const NATIVE_RESULT_EVENT = "tool/result";
53
+ /**
54
+ * The dispatch vocabulary this module owns. A consumer must never compare
55
+ * against these literals itself (rule N11); it compares \`kind\` instead.
56
+ */
57
+ export declare const DISPATCH_EVENT_TYPES: readonly string[];
58
+ /**
59
+ * One normalized tool dispatch.
60
+ *
61
+ * Every field is derived from a platform event payload; the record is mutable
62
+ * and is updated in place when a later event of the same dispatch settles it,
63
+ * so a consumer that reacted at start already holds the outcome.
64
+ */
65
+ export interface ToolDispatchSignal {
66
+ /** \`native\` | \`program\` (a code-program sub-dispatch) | \`program-root\` (the \`run_code\` call itself). */
67
+ readonly kind: ToolDispatchKind;
68
+ /** \`callId\` for a native call, \`subCallId\` for a PTC sub-dispatch — the platform's own identity. */
69
+ readonly callId: string;
70
+ /** The outer call: \`parentCallId\` for a PTC sub-dispatch, the call's own id for a native call. */
71
+ readonly rootCallId: string;
72
+ /** Dispatched tool name, always a non-empty string. */
73
+ readonly name: string;
74
+ /** Arguments as dispatched: a JSON string in native mode, the normalized object in PTC mode. */
75
+ readonly arguments: unknown;
76
+ /** Settled outcome: \`true\` succeeded, \`false\` failed, \`undefined\` still in flight. */
77
+ ok: boolean | undefined;
78
+ }
79
+ /**
80
+ * \`TypeError\` thrown when a payload that claims to be a dispatch event
81
+ * violates the platform's event declaration. Named so a deployment can tell this
82
+ * refusal apart from a generic failure in a log line.
83
+ */
84
+ export declare class ToolDispatchPayloadError extends TypeError {
85
+ name: string;
86
+ }
87
+ /**
88
+ * The family's single dispatch ledger: absorbs platform events in log order,
89
+ * emits one \`ToolDispatchSignal\` per dispatch, and folds later events of the
90
+ * same dispatch into the record it already emitted.
91
+ *
92
+ * Consumers keep the \`ToolDispatchSignal\` object they received and re-read it
93
+ * later; no consumer needs to correlate events itself.
94
+ */
95
+ export declare class ToolDispatchNormalizer {
96
+ private readonly records;
97
+ /** Call ids of \`run_code\`-class calls, i.e. of dispatches whose sub-dispatches are \`program\`. */
98
+ private readonly programRoots;
99
+ /**
100
+ * Absorb one session event.
101
+ * @param event - the event to absorb; any non-dispatch event is ignored.
102
+ * @returns the dispatch's signal when this event FIRST reveals the dispatch,
103
+ * otherwise \`null\` (the paired event of an already-emitted dispatch, or a
104
+ * non-dispatch event). A \`null\` return is never a dispatch to count again.
105
+ */
106
+ advance(event: {
107
+ type: string;
108
+ data?: unknown;
109
+ }): ToolDispatchSignal | null;
110
+ /** Every emitted dispatch, in first-seen order. */
111
+ get signals(): readonly ToolDispatchSignal[];
112
+ /**
113
+ * Fold every event of a session log, in order.
114
+ * @param events - the log, oldest first.
115
+ * @returns one signal per dispatch, in first-seen order.
116
+ */
117
+ foldAll(events: Iterable<{
118
+ type: string;
119
+ data?: unknown;
120
+ }>): ToolDispatchSignal[];
121
+ }
122
+ /**
123
+ * Read one event into a dispatch record without a ledger (the pure half of the
124
+ * fold). Unlike the ledger it does NOT dedupe: a caller that walks a log and
125
+ * emits its own lines needs the call line on the event that opens the result.
126
+ * @param event - one session event; an absent event (an over-advanced index)
127
+ * answers \`null\` rather than throwing.
128
+ * @returns the dispatch this event opens, or \`null\` for any other event.
129
+ */
130
+ export declare function readDispatchSignal(event: {
131
+ type?: string;
132
+ data?: unknown;
133
+ } | undefined): {
134
+ kind: ToolDispatchKind;
135
+ callId: string;
136
+ rootCallId: string;
137
+ name: string;
138
+ arguments: unknown;
139
+ } | null;
140
+ /**
141
+ * Is this dispatched tool name a single-skill read?
142
+ * @param name - the dispatched tool name.
143
+ * @returns \`true\` only for the read tool whose arguments name one skill.
144
+ */
145
+ export declare function isSkillReadToolName(name: string): boolean;
146
+ /**
147
+ * Does this signal read one skill, and did it not fail?
148
+ * @param signal - a normalized dispatch.
149
+ * @returns the skill name the dispatch read, or \`undefined\` when the dispatch
150
+ * is not a skill read or has failed. A dispatch whose outcome is still pending
151
+ * counts as a read: the platform settles every started sub-dispatch, so pending
152
+ * is a live-window state, not a failure.
153
+ */
154
+ export declare function skillReadNameOf(signal: ToolDispatchSignal): string | undefined;
155
+ /**
156
+ * Fold one session log into its deduplicated dispatches.
157
+ * @param events - the log, oldest first. Pass an array, not a live snapshot
158
+ * iterator, when the log can grow while folding.
159
+ * @returns one signal per dispatch, in first-seen order.
160
+ */
161
+ export declare function foldToolDispatches(events: Iterable<{
162
+ type: string;
163
+ data?: unknown;
164
+ }>): ToolDispatchSignal[];
165
+ /**
166
+ * Every skill name this log read through a dispatch, deduplicated by dispatch.
167
+ * @param events - the log, oldest first.
168
+ * @returns the names read by at least one non-failed skill dispatch.
169
+ */
170
+ export declare function collectReadSkillNames(events: Iterable<{
171
+ type: string;
172
+ data?: unknown;
173
+ }>): Set<string>;
174
+ /**
175
+ * Does this dispatched tool name touch the skill library (read or mutate)?
176
+ * @param name - the dispatched tool name.
177
+ * @returns \`true\` for any skill-library tool. This is the review cadence's
178
+ * skill signal, which has always covered reads and writes alike.
179
+ */
180
+ export declare function isSkillToolName(name: string): boolean;
181
+ /**
182
+ * Does this dispatched tool name open a code program?
183
+ * @param name - the dispatched tool name.
184
+ * @returns \`true\` for the tool whose sub-dispatches are \`program\` kind.
185
+ */
186
+ export declare function isProgramToolName(name: string): boolean;
187
+ /**
188
+ * Assert that a payload really is the dispatch event it claims to be.
189
+ *
190
+ * The normalizer is deliberately lenient (it also folds persisted logs, where a
191
+ * payload may predate the current declaration); this is the loud gate for the
192
+ * live path, where a malformed payload means the producer is broken. It never
193
+ * silently downgrades: an unrecognized event type or a payload missing a
194
+ * declared field throws a named \`ToolDispatchPayloadError\`.
195
+ * @param event - the session event to check.
196
+ * @returns nothing; throws when the payload violates the platform declaration.
197
+ */
198
+ export declare function assertDispatchPayload(event: {
199
+ type: string;
200
+ data?: unknown;
201
+ }): void;
202
+ /**
203
+ * Total dispatches in a log, deduplicated.
204
+ * @param events - the log, oldest first.
205
+ * @returns the number of distinct dispatches, one per dispatch regardless of
206
+ * how many events carried it.
207
+ */
208
+ export declare function countDispatches(events: Iterable<{
209
+ type: string;
210
+ data?: unknown;
211
+ }>): number;
212
+ //# sourceMappingURL=tool-dispatch.d.ts.map
@@ -59,18 +59,15 @@ export interface UsageMutateOptions {
59
59
  onQuarantine?: ((message: string) => void) | undefined;
60
60
  }
61
61
  export declare function mutateUsage(root: string, io: EvolutionIoLike, task: (map: UsageMap) => void | Promise<void>, options?: UsageMutateOptions): Promise<void>;
62
- /**
63
- * Curator-owned usage fields (rc.67 K-2): the curator writes ONLY this set —
62
+ /** Curator-owned usage fields (rc.67 K-2): the curator writes ONLY this set —
64
63
  * lifecycle state, archive stamp, the six-factor quality pair, and the
65
- * marker-mirrored pin flag. Counter and activity-stamp fields belong to the
66
- * tool-telemetry side (skill-usage / tool-skill-manage), which bumps them
67
- * through its own transact-backed RMW. A whole-record overwrite by either
68
- * side would clobber the other side's concurrent increment, so cross-side
69
- * folds copy this set only.
70
- */
71
- export declare function applyCuratorFields(disk: UsageRecord, curated: UsageRecord): void;
64
+ * marker-mirrored pin flag; counters and activity stamps belong to the
65
+ * tool-telemetry side and are never copied by a fold. P2-12 (v39): the
66
+ * combined `applyCuratorFields` wrapper had no production caller (folds go
67
+ * through {@link foldCuratorFields}), so the two field copies below are the
68
+ * whole contract. */
72
69
  /** Copy only the lifecycle pair (state/archived_at) — see the ownership split
73
- * rationale on {@link applyCuratorMetaFields}. */
70
+ * rationale above. */
74
71
  export declare function applyCuratorLifecycleFields(disk: UsageRecord, curated: UsageRecord): void;
75
72
  /**
76
73
  * Copy the recomputed meta pair (quality_score/quality_warn + the