@lmzhen/dsh-evolution-core 0.26.26 → 0.27.0

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.
Files changed (107) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +66 -850
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.js +346 -0
  25. package/lib/types/diagnostics.d.ts +26 -0
  26. package/lib/types/diagnostics.js +23 -0
  27. package/lib/types/document-body.js +91 -0
  28. package/lib/types/drift-signals.js +319 -0
  29. package/lib/types/env.js +25 -0
  30. package/lib/types/errors.d.ts +4 -1
  31. package/lib/types/errors.js +59 -0
  32. package/lib/types/events.js +16 -0
  33. package/lib/types/evidence.js +65 -0
  34. package/lib/types/evolution-events.js +479 -0
  35. package/lib/types/fold-untrusted.js +44 -0
  36. package/lib/types/frontmatter.js +623 -0
  37. package/lib/types/fuzzy-match.js +135 -0
  38. package/lib/types/gates.js +51 -0
  39. package/lib/types/guards.js +27 -0
  40. package/lib/types/health-baseline.js +63 -0
  41. package/lib/types/http-routes.d.ts +67 -35
  42. package/lib/types/http-routes.js +135 -0
  43. package/lib/types/index.d.ts +7 -2
  44. package/lib/types/index.js +101 -0
  45. package/lib/types/instance-scope.js +69 -0
  46. package/lib/types/io.js +675 -0
  47. package/lib/types/learn-prompt.js +42 -0
  48. package/lib/types/limits.js +63 -0
  49. package/lib/types/live.js +107 -0
  50. package/lib/types/long-runs.js +96 -0
  51. package/lib/types/memory-store.d.ts +0 -16
  52. package/lib/types/memory-store.js +606 -0
  53. package/lib/types/mutations.js +95 -0
  54. package/lib/types/namespace-migration.js +156 -0
  55. package/lib/types/numeric.js +47 -0
  56. package/lib/types/opt-in.js +101 -0
  57. package/lib/types/package-identity.js +47 -0
  58. package/lib/types/params.js +328 -0
  59. package/lib/types/payload.js +36 -0
  60. package/lib/types/platform-line.generated.d.ts +10 -0
  61. package/lib/types/platform-line.generated.js +12 -0
  62. package/lib/types/platform-services.d.ts +2 -2
  63. package/lib/types/platform-services.js +52 -0
  64. package/lib/types/probe.js +122 -0
  65. package/lib/types/prompts.js +409 -0
  66. package/lib/types/quality.js +308 -0
  67. package/lib/types/reachability.js +122 -0
  68. package/lib/types/redact.js +257 -0
  69. package/lib/types/reference-rewrite.js +144 -0
  70. package/lib/types/reports.js +110 -0
  71. package/lib/types/restrictor.d.ts +11 -0
  72. package/lib/types/restrictor.js +48 -0
  73. package/lib/types/review-channel.js +59 -0
  74. package/lib/types/run-registry.js +345 -0
  75. package/lib/types/scope.js +7 -0
  76. package/lib/types/serial.js +17 -0
  77. package/lib/types/session-fold.d.ts +15 -0
  78. package/lib/types/session-fold.js +28 -0
  79. package/lib/types/session-projection.js +295 -0
  80. package/lib/types/signals.js +209 -0
  81. package/lib/types/skill-discovery.js +66 -0
  82. package/lib/types/skill-health.js +134 -0
  83. package/lib/types/skill-history.js +745 -0
  84. package/lib/types/skill-lint.js +112 -0
  85. package/lib/types/skill-reads.js +67 -0
  86. package/lib/types/skill-row-cells.js +123 -0
  87. package/lib/types/skill-store.js +3528 -0
  88. package/lib/types/state-store.js +60 -0
  89. package/lib/types/store-ledger.js +117 -0
  90. package/lib/types/subagent/channel.d.ts +4 -0
  91. package/lib/types/subagent/channel.js +131 -0
  92. package/lib/types/subagent/index.d.ts +4 -0
  93. package/lib/types/subagent/index.js +5 -0
  94. package/lib/types/subagent/readiness.d.ts +6 -0
  95. package/lib/types/subagent/readiness.js +35 -0
  96. package/lib/types/subagent/types.d.ts +58 -0
  97. package/lib/types/subagent/types.js +2 -0
  98. package/lib/types/threats.js +338 -0
  99. package/lib/types/tool-dispatch.d.ts +5 -0
  100. package/lib/types/tool-dispatch.js +535 -0
  101. package/lib/types/tree-write.js +40 -0
  102. package/lib/types/unit-members.js +121 -0
  103. package/lib/types/usage.js +474 -0
  104. package/lib/types/write-authority.js +23 -0
  105. package/lib/types/write-gates.js +344 -0
  106. package/lib/types/write-inventory.js +158 -0
  107. package/package.json +39 -11
@@ -0,0 +1,535 @@
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
+ import { CITATION_SUPPORT_DIRS } from "./citations.js";
38
+ // S1-E6 (0.3.80): the former third value 'program-root' (the run_code call
39
+ // itself) was UNREACHABLE — the platform appends the root's `tool/call` BEFORE
40
+ // any PTC sub-dispatch pair exists, so the root's signal always emitted as
41
+ // 'native'. Dead vocabulary removed rather than kept as a trap.
42
+ /** The platform event type carrying a PTC sub-dispatch start. */
43
+ export const PTC_DISPATCH_START_EVENT = 'tool/ptc-dispatch-start';
44
+ /** The platform event type carrying a PTC sub-dispatch settle. */
45
+ export const PTC_DISPATCH_EVENT = 'tool/ptc-dispatch';
46
+ /** The platform event type carrying a native tool call. */
47
+ export const NATIVE_CALL_EVENT = 'tool/call';
48
+ /** The platform event type carrying a native tool result. */
49
+ export const NATIVE_RESULT_EVENT = 'tool/result';
50
+ /**
51
+ * The dispatch vocabulary this module owns. A consumer must never compare
52
+ * against these literals itself (rule N11); it compares \`kind\` instead.
53
+ */
54
+ export const DISPATCH_EVENT_TYPES = [
55
+ NATIVE_CALL_EVENT,
56
+ NATIVE_RESULT_EVENT,
57
+ PTC_DISPATCH_START_EVENT,
58
+ PTC_DISPATCH_EVENT,
59
+ ];
60
+ /**
61
+ * Model-visible session frames: the message types plus the ONE dispatch vocabulary above.
62
+ * Consumers classify through this list instead of naming event literals (rule N11).
63
+ */
64
+ export const MODEL_SESSION_EVENT_TYPES = ['user/message', 'assistant/message', ...DISPATCH_EVENT_TYPES];
65
+ /**
66
+ * \`TypeError\` thrown when a payload that claims to be a dispatch event
67
+ * violates the platform's event declaration. Named so a deployment can tell this
68
+ * refusal apart from a generic failure in a log line.
69
+ */
70
+ export class ToolDispatchPayloadError extends TypeError {
71
+ name = 'ToolDispatchPayloadError';
72
+ }
73
+ /** The dispatch event types this module recognizes, as a set. */
74
+ const DISPATCH_EVENT_TYPE_SET = new Set(DISPATCH_EVENT_TYPES);
75
+ /** \`name\`-less payloads: an external emitter or a corrupt log can produce these. */
76
+ const MALFORMED = 'malformed';
77
+ /** One tool-result block of a native result payload, or \`null\` when the payload carries none. */
78
+ function firstResultBlock(data) {
79
+ const content = data.message?.content;
80
+ if (!Array.isArray(content))
81
+ return null;
82
+ for (const entry of content) {
83
+ if (entry === null || typeof entry !== 'object')
84
+ continue;
85
+ const block = entry;
86
+ if (block.type === 'tool-result')
87
+ return block;
88
+ }
89
+ return null;
90
+ }
91
+ /** Deterministic identity for a dispatch payload that carries no call id. */
92
+ function payloadIdentity(data) {
93
+ if (data === undefined)
94
+ return '';
95
+ try {
96
+ return JSON.stringify(data);
97
+ }
98
+ catch {
99
+ // A cyclic payload cannot be identified by value; the event TYPE is then the
100
+ // whole identity, so two undecidable frames collapse into one rather than
101
+ // inflating the count. The refusal is loud at the live boundary
102
+ // (`assertDispatchPayload`), which is where a producer bug is actionable.
103
+ return '';
104
+ }
105
+ }
106
+ /** Read the payload of a recognized dispatch event, or \`null\` for any other event. */
107
+ function readDispatchRecord(event) {
108
+ if (!DISPATCH_EVENT_TYPE_SET.has(event.type))
109
+ return null;
110
+ const data = (event.data ?? {});
111
+ if (event.type === NATIVE_RESULT_EVENT) {
112
+ // The call identity lives INSIDE the message (`message.source.callId`, the
113
+ // shape `createToolResultMessage` produces — llm/src/message.ts:258); the
114
+ // event payload has no top-level `callId`. A `toolCallId` on a result block
115
+ // is accepted as the same identity for a hand-built or legacy frame.
116
+ const owner = data.message?.source?.callId;
117
+ const block = firstResultBlock(data);
118
+ const callId = typeof owner === 'string' && owner !== ''
119
+ ? owner
120
+ : typeof block?.toolCallId === 'string' && block.toolCallId !== '' ? block.toolCallId : undefined;
121
+ if (callId === undefined)
122
+ return null;
123
+ return {
124
+ kind: 'native',
125
+ callId,
126
+ rootCallId: callId,
127
+ name: '',
128
+ arguments: undefined,
129
+ // A failure is marked by the payload-level `error` OR a result block's own
130
+ // `isError` (both shapes exist in persisted pre-rc.2 logs).
131
+ outcome: { ok: data.isError !== true && data.error === undefined && block?.isError !== true },
132
+ };
133
+ }
134
+ const isPtc = event.type === PTC_DISPATCH_START_EVENT || event.type === PTC_DISPATCH_EVENT;
135
+ const identity = isPtc ? data.subCallId : data.callId;
136
+ // An id-less payload is still evidence of a call the model made: a real log
137
+ // always carries the id, but a replayed pre-upgrade frame or an external
138
+ // emitter may not, and dropping those would silently under-count. The
139
+ // payload itself becomes the identity, so the same broken frame delivered
140
+ // twice is still ONE dispatch.
141
+ const callId = typeof identity === 'string' && identity !== '' ? identity : `${event.type}:${payloadIdentity(event.data)}`;
142
+ const parent = isPtc && typeof data.parentCallId === 'string' && data.parentCallId !== '' ? data.parentCallId : undefined;
143
+ const name = typeof data.name === 'string' && data.name !== '' ? data.name : MALFORMED;
144
+ return {
145
+ kind: isPtc ? 'program' : 'native',
146
+ callId,
147
+ rootCallId: typeof data.rootCallId === 'string' && data.rootCallId !== '' ? data.rootCallId : (parent ?? callId),
148
+ name,
149
+ arguments: data.arguments,
150
+ ...isPtc && event.type === PTC_DISPATCH_EVENT ? { outcome: { ok: data.isError !== true } } : {},
151
+ };
152
+ }
153
+ /**
154
+ * The ledger key of one dispatch: a scope-qualified call id.
155
+ *
156
+ * v43 audit (FLOW4-4): a normalizer shared by every session (skill-usage's live listener holds ONE
157
+ * process-wide instance) used the bare call id, so two sessions that produced the same id — PTC
158
+ * sub-call ids are short, and an id-less payload falls back to a type+payload key that is not
159
+ * unique by construction — collided: the second session's read was absorbed as "already seen" and
160
+ * never counted, and a settle in one session flipped the other's `ok`. Callers that span sessions
161
+ * pass the session id as `scope`.
162
+ * @param callId - the dispatch's call id (or its payload-derived fallback).
163
+ * @param scope - the owning session id, or '' for a fold that never leaves one session.
164
+ * @returns the key both the ledger and every projection fold dedupe on.
165
+ */
166
+ export function dispatchKeyOf(callId, scope = '') {
167
+ return scope === '' ? callId : `${scope}:${callId}`;
168
+ }
169
+ /**
170
+ * The pure half of the ledger: what one event says about one dispatch.
171
+ *
172
+ * The DECISION (which dispatch, and whether this event is the one that opens it) lives here and
173
+ * only here; carriers differ in what they keep — the class below keeps consumer-visible mutable
174
+ * signals, the family's `evolutionReads`/`evolutionSignals` projection units keep plain-JSON
175
+ * ledgers. `name` is empty on an event that only settles an already-opened dispatch (a native
176
+ * `tool/result`), and `outcome` is present exactly when the event carried one.
177
+ * @param event - one session event; any non-dispatch event answers `null`.
178
+ * @param scope - the owning session id, or '' for a single-session fold.
179
+ * @returns the step, or `null` when the event is not a dispatch.
180
+ */
181
+ export function dispatchStepOf(event, scope = '') {
182
+ const record = readDispatchRecord(event);
183
+ if (record === null)
184
+ return null;
185
+ return {
186
+ key: dispatchKeyOf(record.callId, scope),
187
+ kind: record.kind,
188
+ callId: record.callId,
189
+ rootCallId: record.rootCallId,
190
+ name: record.name,
191
+ arguments: record.arguments,
192
+ ...record.outcome === undefined ? {} : { outcome: record.outcome },
193
+ };
194
+ }
195
+ /**
196
+ * The family's single dispatch ledger: absorbs platform events in log order,
197
+ * emits one \`ToolDispatchSignal\` per dispatch, and folds later events of the
198
+ * same dispatch into the record it already emitted.
199
+ *
200
+ * Consumers keep the \`ToolDispatchSignal\` object they received and re-read it
201
+ * later; no consumer needs to correlate events itself.
202
+ */
203
+ export class ToolDispatchNormalizer {
204
+ records = new Map();
205
+ /** S2-P2-14: cap for the LEDGER (and the settle markers) — opt-in, meant for
206
+ * the long-lived live listener (skill-usage) whose ledger is pure dedup
207
+ * state and would otherwise grow with the process lifetime. `Infinity` (the
208
+ * default, used by whole-log folding) keeps every dispatch. Eviction is
209
+ * oldest-first: a call/result pair spans one tool call, so an evicted
210
+ * dispatch can never be re-paired within any realistic window. */
211
+ maxTracked;
212
+ /** S2-P2-12: call ids whose outcome has already been reported to the settle
213
+ * channel — a replayed result event must not settle the same dispatch twice. */
214
+ settledIds = new Set();
215
+ constructor(options = {}) {
216
+ this.maxTracked = options.maxTracked ?? Number.POSITIVE_INFINITY;
217
+ }
218
+ /**
219
+ * Absorb one session event.
220
+ * @param event - the event to absorb; any non-dispatch event is ignored.
221
+ * @returns the dispatch's signal when this event FIRST reveals the dispatch,
222
+ * otherwise \`null\` (the paired event of an already-emitted dispatch, or a
223
+ * non-dispatch event). A \`null\` return is never a dispatch to count again.
224
+ */
225
+ advance(event, scope = '') {
226
+ const step = dispatchStepOf(event, scope);
227
+ if (step === null)
228
+ return null;
229
+ const existing = this.records.get(step.key);
230
+ if (existing !== undefined) {
231
+ if (step.outcome !== undefined)
232
+ existing.ok = step.outcome.ok;
233
+ return null;
234
+ }
235
+ // An event that only settles a dispatch this ledger never saw open carries no name:
236
+ // there is no signal to emit (the same answer the pre-refactor branch gave).
237
+ if (step.name === '')
238
+ return null;
239
+ const signal = {
240
+ kind: step.kind,
241
+ callId: step.callId,
242
+ rootCallId: step.rootCallId,
243
+ name: step.name,
244
+ arguments: step.arguments,
245
+ // Explicitly `undefined` when the event carried no outcome: the signal type requires the key
246
+ // (`exactOptionalPropertyTypes`), and consumers read it as "pending" in exactly that case.
247
+ ok: step.outcome?.ok,
248
+ };
249
+ this.records.set(step.key, signal);
250
+ this.evict();
251
+ // S1-E6: a former 'program-root' upgrade lived here (a PTC sub-dispatch
252
+ // proving its parent was a run_code root) — but the platform appends the
253
+ // root's `tool/call` BEFORE any PTC pair, so the root's signal had already
254
+ // emitted as 'native' and the branch never ran. The root counting once as
255
+ // 'native' and its sub-dispatches as 'program' is the observable contract.
256
+ return signal;
257
+ }
258
+ /**
259
+ * S2-P2-12: the SETTLE channel. \`advance\` answers \`null\` for the paired
260
+ * event that settles an already-emitted dispatch, so a consumer that must act
261
+ * on the OUTCOME (e.g. count successful skill reads only) cannot tell which
262
+ * dispatch a \`null\` belonged to. Call this with the same event AFTER
263
+ * \`advance\`: it answers the settled dispatch exactly once (replayed settle
264
+ * events answer \`null\`).
265
+ * @param event - the event already absorbed by \`advance\`.
266
+ * @returns the dispatch this event settled, or \`null\`.
267
+ */
268
+ settledSignalOf(event, scope = '') {
269
+ const step = dispatchStepOf(event, scope);
270
+ if (step?.outcome === undefined)
271
+ return null;
272
+ if (this.settledIds.has(step.key))
273
+ return null;
274
+ const signal = this.records.get(step.key);
275
+ if (signal === undefined)
276
+ return null;
277
+ this.settledIds.add(step.key);
278
+ this.evict();
279
+ return signal;
280
+ }
281
+ /** Oldest-first eviction once the ledger (or the settle markers) overflows. */
282
+ evict() {
283
+ while (this.records.size > this.maxTracked) {
284
+ const oldest = this.records.keys().next().value;
285
+ if (oldest === undefined)
286
+ break;
287
+ this.records.delete(oldest);
288
+ this.settledIds.delete(oldest);
289
+ }
290
+ while (this.settledIds.size > this.maxTracked) {
291
+ const oldest = this.settledIds.values().next().value;
292
+ if (oldest === undefined)
293
+ break;
294
+ this.settledIds.delete(oldest);
295
+ }
296
+ }
297
+ /** Every emitted dispatch, in first-seen order. */
298
+ get signals() {
299
+ return [...this.records.values()];
300
+ }
301
+ /**
302
+ * Fold every event of a session log, in order.
303
+ * @param events - the log, oldest first.
304
+ * @returns one signal per dispatch, in first-seen order.
305
+ */
306
+ foldAll(events) {
307
+ const emitted = [];
308
+ for (const event of events) {
309
+ const signal = this.advance(event);
310
+ if (signal !== null)
311
+ emitted.push(signal);
312
+ }
313
+ return emitted;
314
+ }
315
+ }
316
+ /**
317
+ * Read one event into a dispatch record without a ledger (the pure half of the
318
+ * fold). Unlike the ledger it does NOT dedupe: a caller that walks a log and
319
+ * emits its own lines needs the call line on the event that opens the result.
320
+ * @param event - one session event; an absent event (an over-advanced index)
321
+ * answers \`null\` rather than throwing.
322
+ * @returns the dispatch this event opens, or \`null\` for any other event.
323
+ */
324
+ export function readDispatchSignal(event) {
325
+ if (event?.type === undefined)
326
+ return null;
327
+ const record = readDispatchRecord({ type: event.type, ...event.data === undefined ? {} : { data: event.data } });
328
+ if (record === null || record.name === '')
329
+ return null;
330
+ return {
331
+ kind: record.kind,
332
+ callId: record.callId,
333
+ rootCallId: record.rootCallId,
334
+ name: record.name,
335
+ arguments: record.arguments,
336
+ };
337
+ }
338
+ /** Tool names that READ one skill. The single authority for skill-read detection. */
339
+ const SKILL_READ_TOOL_NAMES = new Set(['skill']);
340
+ /**
341
+ * Is this dispatched tool name a single-skill read?
342
+ * @param name - the dispatched tool name.
343
+ * @returns \`true\` only for the read tool whose arguments name one skill.
344
+ */
345
+ export function isSkillReadToolName(name) {
346
+ return SKILL_READ_TOOL_NAMES.has(name);
347
+ }
348
+ /** Tool names whose dispatch opens a code program, i.e. whose sub-dispatches are \`program\`. */
349
+ const PROGRAM_TOOL_NAMES = new Set(['run_code']);
350
+ /**
351
+ * Does this signal read one skill, and did it not fail?
352
+ * @param signal - a normalized dispatch.
353
+ * @returns the skill name the dispatch read, or \`undefined\` when the dispatch
354
+ * is not a skill read or has failed. A dispatch whose outcome is still pending
355
+ * counts as a read: the platform settles every started sub-dispatch, so pending
356
+ * is a live-window state, not a failure.
357
+ */
358
+ /** Tool names whose dispatch reads one FILE — the deployment's file tool
359
+ * (default `read`). A differently named tool is configured, not guessed. */
360
+ export const DEFAULT_SUPPORT_READ_TOOL_NAMES = ['read'];
361
+ /** Dispatch arguments: a JSON string in native mode and an already-parsed
362
+ * object in PTC mode (core/tools/src/types.ts:11-23) — parsed defensively so no
363
+ * consumer has to trust one modality's encoding. */
364
+ function parsedArguments(signal) {
365
+ let parsed = signal.arguments;
366
+ if (typeof parsed === 'string') {
367
+ try {
368
+ parsed = JSON.parse(parsed);
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ }
374
+ if (parsed === null || typeof parsed !== 'object')
375
+ return null;
376
+ return parsed;
377
+ }
378
+ /** The path-shaped argument of a file tool, when it carries one. */
379
+ function readPathArgument(signal) {
380
+ const record = parsedArguments(signal);
381
+ if (record === null)
382
+ return null;
383
+ for (const key of ['file_path', 'path', 'file', 'target']) {
384
+ const value = record[key];
385
+ if (typeof value === 'string' && value.trim() !== '')
386
+ return value;
387
+ }
388
+ return null;
389
+ }
390
+ /**
391
+ * Read one dispatch as a SUPPORT-FILE read attributed to a skill: a read of
392
+ * `<root>/<skill>/<support dir>/…` is demand evidence for that one file, the
393
+ * way `skillReadNameOf` is demand evidence for a whole body.
394
+ * @param signal - a settled dispatch.
395
+ * @param options - the deployment's file-tool names and its skills root; an
396
+ * empty root falls back to the `/skills/` marker in the path.
397
+ * @returns the skill name and the skill-root-relative path, or `null` when the
398
+ * dispatch is not a support-file read. Pure: no IO, no service lookup.
399
+ */
400
+ export function supportFileReadOf(signal, options) {
401
+ if (!options.toolNames.includes(signal.name))
402
+ return null;
403
+ if (signal.ok === false)
404
+ return null;
405
+ const raw = readPathArgument(signal);
406
+ if (raw === null)
407
+ return null;
408
+ const normalized = raw.replace(/\\/g, '/').trim();
409
+ if (normalized === '' || normalized.split('/').includes('..'))
410
+ return null;
411
+ const root = options.root.replace(/\\/g, '/').replace(/\/+$/, '');
412
+ let relative = null;
413
+ if (root !== '') {
414
+ const lower = normalized.toLowerCase();
415
+ const lowerRoot = root.toLowerCase();
416
+ if (lower.startsWith(lowerRoot + '/'))
417
+ relative = normalized.slice(root.length + 1);
418
+ }
419
+ if (relative === null) {
420
+ const marker = '/skills/';
421
+ const at = normalized.toLowerCase().lastIndexOf(marker);
422
+ if (at === -1)
423
+ return null;
424
+ relative = normalized.slice(at + marker.length);
425
+ }
426
+ const parts = relative.split('/').filter(part => part !== '');
427
+ const skill = parts[0];
428
+ const dir = parts[1];
429
+ if (skill === undefined || dir === undefined || parts.length < 3)
430
+ return null;
431
+ if (!CITATION_SUPPORT_DIRS.includes(dir))
432
+ return null;
433
+ return { skill: skill.trim(), rel: parts.slice(1).join('/') };
434
+ }
435
+ export function skillReadNameOf(signal) {
436
+ if (!SKILL_READ_TOOL_NAMES.has(signal.name))
437
+ return undefined;
438
+ if (signal.ok === false)
439
+ return undefined;
440
+ // One defensive re-parse for both readers (parsedArguments): the encoding
441
+ // differs by runtime mode, so neither may trust the other's.
442
+ const candidate = parsedArguments(signal);
443
+ if (candidate === null)
444
+ return undefined;
445
+ const name = typeof candidate.name === 'string' ? candidate.name : typeof candidate.skill === 'string' ? candidate.skill : '';
446
+ return name === '' ? undefined : name;
447
+ }
448
+ /**
449
+ * Fold one session log into its deduplicated dispatches.
450
+ * @param events - the log, oldest first. Pass an array, not a live snapshot
451
+ * iterator, when the log can grow while folding.
452
+ * @returns one signal per dispatch, in first-seen order.
453
+ */
454
+ export function foldToolDispatches(events) {
455
+ return new ToolDispatchNormalizer().foldAll(events);
456
+ }
457
+ /**
458
+ * Every skill name this log read through a dispatch, deduplicated by dispatch.
459
+ * @param events - the log, oldest first.
460
+ * @returns the names read by at least one non-failed skill dispatch.
461
+ */
462
+ export function collectReadSkillNames(events) {
463
+ const names = new Set();
464
+ for (const signal of foldToolDispatches(events)) {
465
+ const name = skillReadNameOf(signal);
466
+ if (name !== undefined)
467
+ names.add(name);
468
+ }
469
+ return names;
470
+ }
471
+ /** Tool names that touch the skill library at all — the review's skill-signal set. */
472
+ const SKILL_TOOL_NAMES = new Set([...SKILL_READ_TOOL_NAMES, 'skill_manage']);
473
+ /**
474
+ * Does this dispatched tool name touch the skill library (read or mutate)?
475
+ * @param name - the dispatched tool name.
476
+ * @returns \`true\` for any skill-library tool. This is the review cadence's
477
+ * skill signal, which has always covered reads and writes alike.
478
+ */
479
+ export function isSkillToolName(name) {
480
+ return SKILL_TOOL_NAMES.has(name);
481
+ }
482
+ /**
483
+ * Does this dispatched tool name open a code program?
484
+ * @param name - the dispatched tool name.
485
+ * @returns \`true\` for the tool whose sub-dispatches are \`program\` kind.
486
+ */
487
+ export function isProgramToolName(name) {
488
+ return PROGRAM_TOOL_NAMES.has(name);
489
+ }
490
+ /**
491
+ * Assert that a payload really is the dispatch event it claims to be.
492
+ *
493
+ * The normalizer is deliberately lenient (it also folds persisted logs, where a
494
+ * payload may predate the current declaration). S1-E7 (honesty fix): this
495
+ * function is an OPT-IN gate — NO production consumer calls it; the shipped
496
+ * listeners fold tolerant (`'malformed'` synthesis) precisely because a
497
+ * throwing gate wired into every session event would brick the family on any
498
+ * upstream payload drift. Deployments that want fail-loud behavior call this
499
+ * from their own `session/event` listener. It never silently downgrades: an
500
+ * unrecognized event type or a payload missing a declared field throws a named
501
+ * \`ToolDispatchPayloadError\`.
502
+ * @param event - the session event to check.
503
+ * @returns nothing; throws when the payload violates the platform declaration.
504
+ */
505
+ export function assertDispatchPayload(event) {
506
+ const data = (event.data ?? {});
507
+ if (event.type === NATIVE_CALL_EVENT) {
508
+ if (typeof data.name !== 'string' || data.name === '' || typeof data.callId !== 'string' || data.callId === '') {
509
+ throw new ToolDispatchPayloadError('tool/call carries no name/callId: the agent-loop write site is broken');
510
+ }
511
+ return;
512
+ }
513
+ if (event.type === PTC_DISPATCH_START_EVENT || event.type === PTC_DISPATCH_EVENT) {
514
+ if (typeof data.subCallId !== 'string' || data.subCallId === '' || typeof data.name !== 'string' || data.name === '') {
515
+ throw new ToolDispatchPayloadError(`${event.type} carries no name/subCallId: the PTC bridge write site is broken`);
516
+ }
517
+ if (event.type === PTC_DISPATCH_EVENT && typeof data.isError !== 'boolean') {
518
+ throw new ToolDispatchPayloadError('tool/ptc-dispatch carries no boolean isError: the settle outcome is unusable');
519
+ }
520
+ return;
521
+ }
522
+ if (event.type === NATIVE_RESULT_EVENT)
523
+ return;
524
+ throw new ToolDispatchPayloadError(`${event.type} is not a platform dispatch event; compare signal.kind instead of event types`);
525
+ }
526
+ /**
527
+ * Total dispatches in a log, deduplicated.
528
+ * @param events - the log, oldest first.
529
+ * @returns the number of distinct dispatches, one per dispatch regardless of
530
+ * how many events carried it.
531
+ */
532
+ export function countDispatches(events) {
533
+ return foldToolDispatches(events).length;
534
+ }
535
+ //# sourceMappingURL=tool-dispatch.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * G1/1.5 (S7-09): the TYPED critical section around a tree write.
3
+ *
4
+ * The control-plane lock used to wrap a whole pass, LLM call and all: an operator undo/restore waited up to
5
+ * one `curatorReviewTimeoutMs` (~120s) for a review that was merely thinking. The fix is to give the critical
6
+ * section a shape that a review cannot be squeezed into.
7
+ *
8
+ * WHAT THIS TYPE CAN AND CANNOT DO (the plan wording says "cannot be put inside the lock by TYPE" - that is
9
+ * only half true and the half that is false is the half people rely on): a callback is a CLOSURE, so even
10
+ * with `view` as its only parameter it can still reach `this.io` / `this.ctx` / `this.llm` from the enclosing
11
+ * scope. TypeScript cannot stop that, and a criterion claiming it would never go red.
12
+ *
13
+ * What the type DOES give: new code has no other entry - the callback is handed a `SnapshotView` and nothing
14
+ * else, so a phase that needs `io` or `llm` has to stay OUTSIDE and be passed its result in. What the type
15
+ * CANNOT give is covered by the companion source guard (see the family gates): an `io` / `llm` / `this.`
16
+ * / `ctx.` reference inside a `withTreeWrite` callback is a violation, and that guard is the executable form
17
+ * of "cannot be put inside the lock".
18
+ *
19
+ * @module @lmzhen/dsh-evolution-core/tree-write
20
+ */
21
+ /**
22
+ * Run one tree-writing section under a caller-owned serial queue, exposing ONLY a {@link SnapshotView}.
23
+ *
24
+ * The queue is passed in rather than owned here: the lock belongs to whoever serializes the control plane
25
+ * (the curator), and a second lock would be a second answer to one question.
26
+ * @param acquire - the caller serial queue (e.g. the curator mutex), returning its release.
27
+ * @param view - what this section sees while it holds the lock.
28
+ * @param fn - the section. It may only use the view.
29
+ * @returns whatever the section returns.
30
+ */
31
+ export async function withTreeWrite(acquire, view, fn) {
32
+ const release = await acquire();
33
+ try {
34
+ return await fn(view);
35
+ }
36
+ finally {
37
+ release();
38
+ }
39
+ }
40
+ //# sourceMappingURL=tree-write.js.map