@crewhaus/spec-patch 0.3.2 → 0.4.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.
- package/dist/index.d.ts +53 -0
- package/dist/index.js +230 -9
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -91,6 +91,59 @@ export declare function applySpecPatch(yamlText: string, patch: SpecPatch): Appl
|
|
|
91
91
|
* via `parseSpec`.
|
|
92
92
|
*/
|
|
93
93
|
export declare function validatePatch(spec: Spec, patch: SpecPatch): void;
|
|
94
|
+
/**
|
|
95
|
+
* Loop contract 0.4 (Batch B, G40) — one edit in an `applySpecEdits` batch.
|
|
96
|
+
*
|
|
97
|
+
* Where `SpecPatch` is the optimizer's IMPERATIVE op (`add` vs `replace` vs
|
|
98
|
+
* `remove`, each with a strict presence pre-check), `SpecEdit` is the
|
|
99
|
+
* DECLARATIVE surface shared by spec authors (the studio's form fields, the
|
|
100
|
+
* compiler-worker) and the optimizer: "make `path` carry `value`" (upsert —
|
|
101
|
+
* creates missing intermediate collections) or, when `value` is `undefined`,
|
|
102
|
+
* "make `path` absent" (idempotent — deleting an already-absent path is a
|
|
103
|
+
* no-op, so a cleared form field never needs to track prior presence).
|
|
104
|
+
*
|
|
105
|
+
* Paths may address sequence items by non-negative integer index (numeric
|
|
106
|
+
* strings work too — the CST coerces them for sequences): `["steps", 0,
|
|
107
|
+
* "instructions"]`. Index `length` appends; anything past that is an error
|
|
108
|
+
* (never null-padding), and a brand-new sequence can only start at index 0.
|
|
109
|
+
*/
|
|
110
|
+
export type SpecEditPathSegment = string | number;
|
|
111
|
+
export type SpecEdit = {
|
|
112
|
+
readonly path: ReadonlyArray<SpecEditPathSegment>;
|
|
113
|
+
/** New value at `path` (upsert). `undefined` — or omitting the key — DELETES the path. */
|
|
114
|
+
readonly value?: unknown;
|
|
115
|
+
/** Optional rationale string for audit / write-back commit messages. */
|
|
116
|
+
readonly rationale?: string;
|
|
117
|
+
};
|
|
118
|
+
export type ApplySpecEditsOptions = {
|
|
119
|
+
/**
|
|
120
|
+
* The optimizer surface: every edit path must fall under the spec target's
|
|
121
|
+
* `OPTIMIZABLE_PATHS` whitelist (exact or prefix match, same rule as
|
|
122
|
+
* `validatePatch`). Author surfaces leave this off and may edit any field —
|
|
123
|
+
* the atomic `parseSpec` re-validation is their safety floor.
|
|
124
|
+
*/
|
|
125
|
+
readonly restrictToOptimizable?: boolean;
|
|
126
|
+
};
|
|
127
|
+
export type ApplySpecEditsResult = {
|
|
128
|
+
/** The mutated YAML text, with comments and key order preserved. */
|
|
129
|
+
readonly yaml: string;
|
|
130
|
+
/** The re-parsed Spec after the whole batch. */
|
|
131
|
+
readonly spec: Spec;
|
|
132
|
+
/** Edits that changed the document — idempotent deletes of absent paths don't count. */
|
|
133
|
+
readonly applied: number;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Apply a batch of `SpecEdit`s to a YAML spec source ATOMICALLY: all edits
|
|
137
|
+
* mutate one CST in order, the result is re-validated via `parseSpec` once,
|
|
138
|
+
* and any failure — a malformed edit, an out-of-bounds sequence index, or a
|
|
139
|
+
* batch that produces an invalid spec — throws `SpecPatchError` without
|
|
140
|
+
* yielding a partially-edited text. Comments and key order survive exactly
|
|
141
|
+
* as in `applySpecPatch` (same `yaml`-package CST underneath).
|
|
142
|
+
*
|
|
143
|
+
* A zero-edit batch is a validated no-op: the input text is returned
|
|
144
|
+
* byte-identical (no CST round-trip reformatting).
|
|
145
|
+
*/
|
|
146
|
+
export declare function applySpecEdits(yamlText: string, edits: ReadonlyArray<SpecEdit>, opts?: ApplySpecEditsOptions): ApplySpecEditsResult;
|
|
94
147
|
/**
|
|
95
148
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
96
149
|
* allowed to touch. Adding a new field here is the explicit signal that
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { CrewhausError } from "@crewhaus/errors";
|
|
34
34
|
import { parseSpec } from "@crewhaus/spec";
|
|
35
|
-
import { parse, parseDocument } from "yaml";
|
|
35
|
+
import { isSeq, parse, parseDocument } from "yaml";
|
|
36
36
|
import { z } from "zod";
|
|
37
37
|
import { REDACTED_VALUE, isCredentialKey, maskCredentialTokens } from "./redact";
|
|
38
38
|
// Re-exported so downstream renderers (the CLI changelog, future reporters)
|
|
@@ -152,6 +152,135 @@ export function validatePatch(spec, patch) {
|
|
|
152
152
|
function formatPath(path) {
|
|
153
153
|
return path.join(".");
|
|
154
154
|
}
|
|
155
|
+
const specEditSchema = z.object({
|
|
156
|
+
path: z.array(z.union([z.string().min(1), z.number().int().nonnegative()])).min(1),
|
|
157
|
+
value: z.unknown().optional(),
|
|
158
|
+
rationale: z.string().optional(),
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Apply a batch of `SpecEdit`s to a YAML spec source ATOMICALLY: all edits
|
|
162
|
+
* mutate one CST in order, the result is re-validated via `parseSpec` once,
|
|
163
|
+
* and any failure — a malformed edit, an out-of-bounds sequence index, or a
|
|
164
|
+
* batch that produces an invalid spec — throws `SpecPatchError` without
|
|
165
|
+
* yielding a partially-edited text. Comments and key order survive exactly
|
|
166
|
+
* as in `applySpecPatch` (same `yaml`-package CST underneath).
|
|
167
|
+
*
|
|
168
|
+
* A zero-edit batch is a validated no-op: the input text is returned
|
|
169
|
+
* byte-identical (no CST round-trip reformatting).
|
|
170
|
+
*/
|
|
171
|
+
export function applySpecEdits(yamlText, edits, opts = {}) {
|
|
172
|
+
edits.forEach((edit, i) => {
|
|
173
|
+
const parsed = specEditSchema.safeParse(edit);
|
|
174
|
+
if (!parsed.success) {
|
|
175
|
+
throw new SpecPatchError(`edit #${i} shape is invalid: ${parsed.error.message}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (edits.length === 0) {
|
|
179
|
+
let spec;
|
|
180
|
+
try {
|
|
181
|
+
spec = parseSpec(yamlText);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
throw new SpecPatchError(`input YAML failed spec validation: ${err.message}`, err);
|
|
185
|
+
}
|
|
186
|
+
return { yaml: yamlText, spec, applied: 0 };
|
|
187
|
+
}
|
|
188
|
+
let doc;
|
|
189
|
+
try {
|
|
190
|
+
doc = parseDocument(yamlText);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
throw new SpecPatchError("input YAML is not parseable", err);
|
|
194
|
+
}
|
|
195
|
+
// `parseDocument` collects syntax errors on `doc.errors` instead of
|
|
196
|
+
// throwing — without this check they'd only surface as the CST's opaque
|
|
197
|
+
// "Document with errors cannot be stringified" at toString() time.
|
|
198
|
+
if (doc.errors.length > 0) {
|
|
199
|
+
throw new SpecPatchError(`input YAML is not parseable: ${doc.errors[0]?.message ?? "unknown error"}`);
|
|
200
|
+
}
|
|
201
|
+
if (opts.restrictToOptimizable === true) {
|
|
202
|
+
const docTarget = doc.getIn(["target"]);
|
|
203
|
+
if (typeof docTarget !== "string" || !(docTarget in OPTIMIZABLE_PATHS)) {
|
|
204
|
+
throw new SpecPatchError(`cannot restrict to optimizable paths: spec target ${JSON.stringify(docTarget)} is not a known target`);
|
|
205
|
+
}
|
|
206
|
+
edits.forEach((edit, i) => {
|
|
207
|
+
if (!isOptimizable(docTarget, edit.path)) {
|
|
208
|
+
throw new SpecPatchError(`edit #${i}: path ${formatEditPath(edit.path)} is not listed in OPTIMIZABLE_PATHS for target "${docTarget}"; add it to packages/spec-patch/src/index.ts if it's intended to be tunable`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
let applied = 0;
|
|
213
|
+
edits.forEach((edit, i) => {
|
|
214
|
+
const path = [...edit.path];
|
|
215
|
+
const label = `edit #${i} (${formatEditPath(path)})`;
|
|
216
|
+
if (edit.value === undefined) {
|
|
217
|
+
// Delete-on-undefined. hasIn is false both for an absent leaf and for a
|
|
218
|
+
// scalar intermediate, so the deleteIn below can never hit the CST's
|
|
219
|
+
// "Expected YAML collection" throw — absent paths are a clean no-op.
|
|
220
|
+
if (doc.hasIn(path)) {
|
|
221
|
+
doc.deleteIn(path);
|
|
222
|
+
applied += 1;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
guardSequenceIndices(doc, path, label);
|
|
227
|
+
try {
|
|
228
|
+
doc.setIn(path, edit.value);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
throw new SpecPatchError(`${label} failed: ${err.message}`, err);
|
|
232
|
+
}
|
|
233
|
+
applied += 1;
|
|
234
|
+
});
|
|
235
|
+
const newYaml = doc.toString();
|
|
236
|
+
let spec;
|
|
237
|
+
try {
|
|
238
|
+
spec = parseSpec(newYaml);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
throw new SpecPatchError(`edited YAML failed spec validation: ${err.message}`, err);
|
|
242
|
+
}
|
|
243
|
+
return { yaml: newYaml, spec, applied };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Pre-flight for `setIn` along a path that may address sequences: the CST
|
|
247
|
+
* happily null-pads a sequence up to any index (`steps[5]` on a 2-step list
|
|
248
|
+
* yields three `null` steps), which is never what an author or optimizer
|
|
249
|
+
* meant. Reject indices past `length` (index === `length` appends) and
|
|
250
|
+
* refuse to CREATE a sequence anywhere but at index 0. Map keys and the
|
|
251
|
+
* CST's own errors (negative / non-integer index, scalar intermediates)
|
|
252
|
+
* pass through untouched — the caller wraps those with the edit label.
|
|
253
|
+
*/
|
|
254
|
+
function guardSequenceIndices(doc, path, label) {
|
|
255
|
+
for (let i = 0; i < path.length; i++) {
|
|
256
|
+
const seg = path[i];
|
|
257
|
+
const parent = i === 0 ? doc.contents : doc.getIn(path.slice(0, i), true);
|
|
258
|
+
const asIndex = typeof seg === "number" ? seg : /^\d+$/.test(seg) ? Number.parseInt(seg, 10) : undefined;
|
|
259
|
+
if (isSeq(parent)) {
|
|
260
|
+
if (asIndex !== undefined && asIndex > parent.items.length) {
|
|
261
|
+
throw new SpecPatchError(`${label}: index ${asIndex} is out of bounds for the sequence at ${i === 0 ? "(root)" : formatEditPath(path.slice(0, i))} (length ${parent.items.length}; index ${parent.items.length} appends)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (parent === undefined && typeof seg === "number" && seg !== 0) {
|
|
265
|
+
throw new SpecPatchError(`${label}: cannot create a new sequence at ${i === 0 ? "(root)" : formatEditPath(path.slice(0, i))} starting at index ${seg} — a new sequence starts at index 0`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Render an edit path for messages: string keys dot-joined, integer
|
|
270
|
+
* sequence indices as `[i]` — `["steps", 0, "instructions"]` →
|
|
271
|
+
* `steps[0].instructions`. */
|
|
272
|
+
function formatEditPath(path) {
|
|
273
|
+
let out = "";
|
|
274
|
+
for (const seg of path) {
|
|
275
|
+
if (typeof seg === "number") {
|
|
276
|
+
out += `[${seg}]`;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
out += out === "" ? seg : `.${seg}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
155
284
|
/**
|
|
156
285
|
* Per-target whitelist of mutation paths the active optimizer is
|
|
157
286
|
* allowed to touch. Adding a new field here is the explicit signal that
|
|
@@ -167,7 +296,15 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
167
296
|
// per-turn output-token cap when max_tokens truncations recur; safe to
|
|
168
297
|
// autotune (a bigger cap can only trade cost for completeness).
|
|
169
298
|
Object.freeze(["agent", "max_tokens"]),
|
|
299
|
+
// Loop contract 0.4 (Batch A) — the explicit thinking-token budget is a
|
|
300
|
+
// pure quality/cost dial (>= 1024 enforced by the spec); the optimizer
|
|
301
|
+
// may raise/lower it but never flips the thinking FORM (effort presets
|
|
302
|
+
// stay human-owned via the exactly-one-form superRefine).
|
|
303
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
170
304
|
Object.freeze(["failure_taxonomy"]),
|
|
305
|
+
// Loop contract 0.4 (Batch A) — real as of the `compaction.threshold`
|
|
306
|
+
// spec field (0.5–0.99): when to trigger autocompaction is a classic
|
|
307
|
+
// token-cost vs context-completeness dial.
|
|
171
308
|
Object.freeze(["compaction", "threshold"]),
|
|
172
309
|
// Pillar 2 active context curation — eval-optimizer can flip the
|
|
173
310
|
// semantic-dedupe + relevance-reorder pass on/off and tune its
|
|
@@ -175,10 +312,16 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
175
312
|
Object.freeze(["compaction", "curate"]),
|
|
176
313
|
Object.freeze(["compaction", "dedupeThreshold"]),
|
|
177
314
|
Object.freeze(["compaction", "relevanceTopK"]),
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
315
|
+
// Loop contract 0.4 (Batch A) — tool-iteration ceiling: quality (task
|
|
316
|
+
// completion) vs runaway-cost dial. NOTE ["security","egressPolicy"]
|
|
317
|
+
// was REMOVED here: the spec never grew that key (strict schemas
|
|
318
|
+
// reject it), and the OPTIMIZABLE_PATHS guard test requires every
|
|
319
|
+
// listed path to round-trip through parseSpec. Re-add it WITH the spec
|
|
320
|
+
// field when the egress-fabric FRs ship it.
|
|
321
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
322
|
+
// Pillar 3 intent gate — tunable so the eval-optimizer can find the
|
|
323
|
+
// sweet spot between false-positive denials and false-negative exfil
|
|
324
|
+
// bypasses.
|
|
182
325
|
Object.freeze(["security", "justification"]),
|
|
183
326
|
// §47 blockchain subsystem (slice 0). Whole-block replacement so the
|
|
184
327
|
// optimizer can tune `chains[*].finality.count`, `chains[*].rpcPolicy`,
|
|
@@ -212,26 +355,63 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
212
355
|
// the volatile tail block (token cost vs
|
|
213
356
|
// plan/ledger completeness)
|
|
214
357
|
// Deliberately EXCLUDED — the optimizer tunes quality, never semantics:
|
|
215
|
-
// thredz.* (credentials
|
|
216
|
-
//
|
|
358
|
+
// thredz.* (credentials; thredz.messaging = a destructive send-tool
|
|
359
|
+
// switch), memory.backend (store flip), memory.dream.every + .mode
|
|
360
|
+
// (side-effecting schedule / model-spend switch), continuity
|
|
217
361
|
// .proof/.scope/.enabled (behavioral switches), compaction
|
|
218
362
|
// .preserve_user_messages (safety), learning.sources (allowlist =
|
|
219
|
-
// security).
|
|
363
|
+
// security). Batch G adds more of the same category: expose.* (G30 — the
|
|
364
|
+
// deployment/exposure surface, not a quality knob), plugins (G32 — a
|
|
365
|
+
// capability allowlist = supply-chain security, human-owned like
|
|
366
|
+
// learning.sources / the model roster), and sub_agents.*.federation (G31
|
|
367
|
+
// — cross-deployment wiring / trust boundary). None are optimizer-reachable.
|
|
220
368
|
Object.freeze(["memory", "recallK"]),
|
|
221
369
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
222
370
|
Object.freeze(["memory", "ttl"]),
|
|
223
371
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
224
372
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
225
373
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
374
|
+
// Loop contract 0.4 (Batch B, G40) — the in-loop evaluation dials. The
|
|
375
|
+
// pass bar (llm_judge graders only — the spec rejects threshold on
|
|
376
|
+
// deterministic graders, so a mis-aimed patch fails the re-parse) and
|
|
377
|
+
// the retry cap are classic quality-vs-cost knobs. Deliberately NOT
|
|
378
|
+
// ["evaluation"] wholesale and NOT ["evaluation","grader"]: the grader
|
|
379
|
+
// (criteria/type) and on_fail behaviour are human-owned semantics —
|
|
380
|
+
// the optimizer tunes how strict the gate is, never what it judges.
|
|
381
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
382
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
383
|
+
// Loop contract 0.4 (Batch E) — the agent-shape RAG (`knowledge:`) dials
|
|
384
|
+
// + the per-turn recall cadence. All scalars that survive lower() 1:1
|
|
385
|
+
// with their bounds owned by the spec schema, so an out-of-bounds patch
|
|
386
|
+
// fails applySpecPatch's re-parse. `default_k` (hits/turn) and the chunker
|
|
387
|
+
// window are classic recall-quality vs token-cost dials; `refreshEvery`
|
|
388
|
+
// trades recall freshness against per-turn recall cost. Deliberately NOT
|
|
389
|
+
// ["knowledge"] wholesale and NOT ["knowledge","sources"]: the corpus
|
|
390
|
+
// (an allowlist of what the agent may read) is human-owned, mirroring the
|
|
391
|
+
// learning.sources / model-roster exclusions.
|
|
392
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
393
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
394
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
395
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
226
396
|
]),
|
|
227
397
|
workflow: Object.freeze([
|
|
398
|
+
// Whole-step replacement — this ALSO reaches item 9's (G37) per-step
|
|
399
|
+
// model routing (model_pool policy/routing/learning, tiers, fallbacks):
|
|
400
|
+
// no narrower entry is possible (the roster/step index is positional) and
|
|
401
|
+
// whole-step replacement already spans model/instructions, so the policy
|
|
402
|
+
// knobs ride here rather than as standalone paths.
|
|
228
403
|
Object.freeze(["steps"]),
|
|
229
404
|
Object.freeze(["failure_taxonomy"]),
|
|
230
405
|
Object.freeze(["chains"]),
|
|
231
406
|
Object.freeze(["transaction_policy"]),
|
|
407
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
408
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
232
409
|
]) /* whole-step replacement allowed */,
|
|
233
410
|
channel: Object.freeze([
|
|
234
411
|
Object.freeze(["agent", "instructions"]),
|
|
412
|
+
// Loop contract 0.4 (Batch A) — see the cli entries.
|
|
413
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
414
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
235
415
|
Object.freeze(["failure_taxonomy"]),
|
|
236
416
|
Object.freeze(["chains"]),
|
|
237
417
|
Object.freeze(["transaction_policy"]),
|
|
@@ -246,15 +426,28 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
246
426
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
247
427
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
248
428
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
429
|
+
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
430
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
431
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
432
|
+
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
433
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
434
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
435
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
436
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
249
437
|
]),
|
|
250
438
|
graph: Object.freeze([
|
|
251
439
|
Object.freeze(["nodes"]),
|
|
252
440
|
Object.freeze(["failure_taxonomy"]),
|
|
253
441
|
Object.freeze(["chains"]),
|
|
254
442
|
Object.freeze(["transaction_policy"]),
|
|
443
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
444
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
255
445
|
]),
|
|
256
446
|
managed: Object.freeze([
|
|
257
447
|
Object.freeze(["agent", "instructions"]),
|
|
448
|
+
// Loop contract 0.4 (Batch A) — see the cli entries.
|
|
449
|
+
Object.freeze(["agent", "thinking", "budget_tokens"]),
|
|
450
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
258
451
|
Object.freeze(["failure_taxonomy"]),
|
|
259
452
|
// Adaptive model routing — pool policy knobs only (see the cli entry).
|
|
260
453
|
Object.freeze(["agent", "model_pool", "policy"]),
|
|
@@ -267,6 +460,14 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
267
460
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
268
461
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
269
462
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
463
|
+
// Loop contract 0.4 (Batch B, G40) — evaluation dials; see the cli entry.
|
|
464
|
+
Object.freeze(["evaluation", "threshold"]),
|
|
465
|
+
Object.freeze(["evaluation", "max_retries"]),
|
|
466
|
+
// Loop contract 0.4 (Batch E) — knowledge/recall dials; see the cli entry.
|
|
467
|
+
Object.freeze(["knowledge", "default_k"]),
|
|
468
|
+
Object.freeze(["knowledge", "chunk", "size"]),
|
|
469
|
+
Object.freeze(["knowledge", "chunk", "overlap"]),
|
|
470
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
270
471
|
]),
|
|
271
472
|
pipeline: Object.freeze([
|
|
272
473
|
Object.freeze(["agent", "instructions"]),
|
|
@@ -276,10 +477,17 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
276
477
|
Object.freeze(["retrieve", "defaultK"]),
|
|
277
478
|
]),
|
|
278
479
|
crew: Object.freeze([
|
|
480
|
+
// Whole-role replacement — this ALSO reaches item 9's (G37) per-role
|
|
481
|
+
// model routing (model_pool policy/routing/learning, tiers, fallbacks):
|
|
482
|
+
// the role name is a dynamic map key, so no static narrower path exists,
|
|
483
|
+
// and whole-role replacement already spans model/instructions, so the
|
|
484
|
+
// policy knobs ride here rather than as standalone paths.
|
|
279
485
|
Object.freeze(["roles"]),
|
|
280
486
|
Object.freeze(["failure_taxonomy"]),
|
|
281
487
|
Object.freeze(["chains"]),
|
|
282
488
|
Object.freeze(["transaction_policy"]),
|
|
489
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
490
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
283
491
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
284
492
|
Object.freeze(["memory", "recallK"]),
|
|
285
493
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -287,13 +495,20 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
287
495
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
288
496
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
289
497
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
498
|
+
// Loop contract 0.4 (Batch E) — per-turn recall cadence; see the cli entry.
|
|
499
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
290
500
|
]) /* whole-role replacement */,
|
|
291
501
|
research: Object.freeze([
|
|
292
502
|
Object.freeze(["agent", "instructions"]),
|
|
293
503
|
Object.freeze(["failure_taxonomy"]),
|
|
294
|
-
|
|
504
|
+
// NOTE ["retrieve","maxDepth"] was REMOVED here (Batch A): the research
|
|
505
|
+
// retrieve block never grew a maxDepth field (strict schema rejects
|
|
506
|
+
// it), and the OPTIMIZABLE_PATHS guard test requires every listed path
|
|
507
|
+
// to round-trip through parseSpec. Re-add it WITH the spec field.
|
|
295
508
|
Object.freeze(["chains"]),
|
|
296
509
|
Object.freeze(["transaction_policy"]),
|
|
510
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
511
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
297
512
|
// 0.3.0 memory/continuity quality knobs — types/bounds at the cli entry.
|
|
298
513
|
Object.freeze(["memory", "recallK"]),
|
|
299
514
|
Object.freeze(["memory", "autoCaptureThreshold"]),
|
|
@@ -301,12 +516,16 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
301
516
|
Object.freeze(["memory", "wiki", "recallK"]),
|
|
302
517
|
Object.freeze(["memory", "dream", "budget_usd"]),
|
|
303
518
|
Object.freeze(["continuity", "focusMaxChars"]),
|
|
519
|
+
// Loop contract 0.4 (Batch E) — per-turn recall cadence; see the cli entry.
|
|
520
|
+
Object.freeze(["memory", "refreshEvery"]),
|
|
304
521
|
]),
|
|
305
522
|
batch: Object.freeze([
|
|
306
523
|
Object.freeze(["agent", "instructions"]),
|
|
307
524
|
Object.freeze(["failure_taxonomy"]),
|
|
308
525
|
Object.freeze(["chains"]),
|
|
309
526
|
Object.freeze(["transaction_policy"]),
|
|
527
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
528
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
310
529
|
]),
|
|
311
530
|
voice: Object.freeze([
|
|
312
531
|
Object.freeze(["agent", "instructions"]),
|
|
@@ -315,6 +534,8 @@ export const OPTIMIZABLE_PATHS = Object.freeze({
|
|
|
315
534
|
browser: Object.freeze([
|
|
316
535
|
Object.freeze(["agent", "instructions"]),
|
|
317
536
|
Object.freeze(["failure_taxonomy"]),
|
|
537
|
+
// Loop contract 0.4 (Batch A) — see the cli entry.
|
|
538
|
+
Object.freeze(["limits", "max_tool_iterations"]),
|
|
318
539
|
]),
|
|
319
540
|
eval: Object.freeze([
|
|
320
541
|
Object.freeze(["agent", "instructions"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/spec-patch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pillar-2 patch infrastructure — apply a SpecPatch to a YAML source preserving comments and key order via the yaml CST. Drives the active eval optimizer's spec-level mutation loop.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/spec": "0.
|
|
18
|
+
"@crewhaus/errors": "0.4.0",
|
|
19
|
+
"@crewhaus/spec": "0.4.0",
|
|
20
20
|
"yaml": "^2.6.0",
|
|
21
21
|
"zod": "^3.23.8"
|
|
22
22
|
},
|