@wairon/cli 5.1.1-dev.35 → 5.1.1-dev.37

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.js CHANGED
@@ -1113,7 +1113,7 @@ var init_defaults = __esm({
1113
1113
  copilot: ".github/prompts",
1114
1114
  codex: ".codex/agents"
1115
1115
  };
1116
- WAIRON_VERSION = "5.1.1-dev.35";
1116
+ WAIRON_VERSION = "5.1.1-dev.37";
1117
1117
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
1118
1118
  ARCHITECT_AGENT_ID = "agent-architect";
1119
1119
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -2530,13 +2530,34 @@ var init_step_graph = __esm({
2530
2530
  });
2531
2531
 
2532
2532
  // src/models/step-config.ts
2533
+ function stepFieldsFor(type) {
2534
+ const own = STEP_TYPE_FIELDS[type] ?? [];
2535
+ const allowed = /* @__PURE__ */ new Set([...STEP_COMMON_FIELDS, ...own]);
2536
+ for (const field of own) {
2537
+ const twin = STEP_LABEL_TWINS[field];
2538
+ if (twin) allowed.add(twin);
2539
+ }
2540
+ return allowed;
2541
+ }
2542
+ function isSet(step, field) {
2543
+ const v = step[field];
2544
+ if (v === void 0 || v === null) return false;
2545
+ if (Array.isArray(v) && v.length === 0) return false;
2546
+ return true;
2547
+ }
2533
2548
  function flowConfigOn(step) {
2534
- return FLOW_FIELDS.filter((f) => {
2535
- const v = step[f];
2536
- if (v === void 0) return false;
2537
- if (Array.isArray(v) && v.length === 0) return false;
2538
- return true;
2539
- });
2549
+ return FLOW_FIELDS.filter((f) => isSet(step, f));
2550
+ }
2551
+ function narrativeStepForeignFields(step) {
2552
+ const type = step.type;
2553
+ if (!Object.prototype.hasOwnProperty.call(STEP_TYPE_FIELDS, type)) return [];
2554
+ const allowed = stepFieldsFor(type);
2555
+ return Object.keys(step).filter((f) => !allowed.has(f) && isSet(step, f));
2556
+ }
2557
+ function listFields(fields) {
2558
+ const quoted = fields.map((f) => `"${f}"`);
2559
+ if (quoted.length === 1) return quoted[0];
2560
+ return `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}`;
2540
2561
  }
2541
2562
  function stepConfigVerdict(method) {
2542
2563
  const steps = method.narrative ?? [];
@@ -2573,8 +2594,10 @@ function stepConfigVerdict(method) {
2573
2594
  return i !== void 0 && i + 1 < nums.length ? nums[i + 1] : void 0;
2574
2595
  };
2575
2596
  for (const s of steps) {
2597
+ const alreadyNamed = /* @__PURE__ */ new Set();
2576
2598
  if (s.detach && s.type !== "call" && s.type !== "dispatch") {
2577
2599
  malformed(s.stepNumber, `step ${s.stepNumber} (${s.type}) carries "detach" \u2014 fire-and-forget applies to call/dispatch steps only.`);
2600
+ alreadyNamed.add("detach");
2578
2601
  }
2579
2602
  switch (s.type) {
2580
2603
  case "local":
@@ -2582,7 +2605,10 @@ function stepConfigVerdict(method) {
2582
2605
  case "register":
2583
2606
  case "dispatch": {
2584
2607
  const extra = flowConfigOn(s);
2585
- if (extra.length) malformed(s.stepNumber, `step ${s.stepNumber} (${s.type}) carries flow config (${extra.join(", ")}) \u2014 use a flow step type instead.`);
2608
+ if (extra.length) {
2609
+ malformed(s.stepNumber, `step ${s.stepNumber} (${s.type}) carries flow config (${extra.join(", ")}) \u2014 use a flow step type instead.`);
2610
+ for (const f of extra) alreadyNamed.add(f);
2611
+ }
2586
2612
  break;
2587
2613
  }
2588
2614
  case "branch":
@@ -2625,6 +2651,14 @@ function stepConfigVerdict(method) {
2625
2651
  if (s.toStep === void 0) malformed(s.stepNumber, `jump step ${s.stepNumber} requires "toStep".`);
2626
2652
  break;
2627
2653
  }
2654
+ const foreign = narrativeStepForeignFields(s).filter((f) => !alreadyNamed.has(f));
2655
+ if (foreign.length) {
2656
+ problems.push({
2657
+ stepNumber: s.stepNumber,
2658
+ code: "FOREIGN_STEP_FIELD",
2659
+ detail: `step ${s.stepNumber} (${s.type}) carries ${listFields(foreign)}, which a ${s.type} step cannot \u2014 left over from an edit that changed the step's type, configuring nothing. \`wairon doctor --fix\` drops them; give the step the type that carries them instead if that is what it does.`
2660
+ });
2661
+ }
2628
2662
  for (const [field, target] of jumpFieldsOf(s)) {
2629
2663
  if (byNum.has(target)) continue;
2630
2664
  sound = false;
@@ -2650,24 +2684,43 @@ function jumpFieldsOf(s) {
2650
2684
  (s.branches ?? []).forEach((b, i) => fields.push([`branches[${i}].step`, b.step]));
2651
2685
  return fields;
2652
2686
  }
2653
- var FLOW_FIELDS;
2687
+ var STEP_TYPE_FIELDS, STEP_COMMON_FIELDS, STEP_LABEL_TWINS, FLOW_STEP_TYPES, FLOW_FIELDS;
2654
2688
  var init_step_config = __esm({
2655
2689
  "src/models/step-config.ts"() {
2656
2690
  "use strict";
2691
+ STEP_TYPE_FIELDS = {
2692
+ local: [],
2693
+ call: ["targetComponent", "targetMethod", "auth", "detach"],
2694
+ register: ["targetComponent", "targetMethod"],
2695
+ dispatch: ["targetComponent", "capability", "auth", "detach"],
2696
+ branch: ["condition", "onTrueStep", "onFalseStep"],
2697
+ switch: ["on", "cases", "defaultStep"],
2698
+ loop: ["loopKind", "over", "condition", "endStep"],
2699
+ try: ["endStep", "catches", "finallyStep"],
2700
+ parallel: ["endStep", "branches"],
2701
+ jump: ["toStep"],
2702
+ return: ["outcome"],
2703
+ throw: ["error"]
2704
+ };
2705
+ STEP_COMMON_FIELDS = [
2706
+ "stepNumber",
2707
+ "label",
2708
+ "description",
2709
+ "type",
2710
+ "assertsGuarantees",
2711
+ "assertsInvariants"
2712
+ ];
2713
+ STEP_LABEL_TWINS = {
2714
+ onTrueStep: "onTrueLabel",
2715
+ onFalseStep: "onFalseLabel",
2716
+ defaultStep: "defaultLabel",
2717
+ endStep: "endLabel",
2718
+ finallyStep: "finallyLabel",
2719
+ toStep: "toLabel"
2720
+ };
2721
+ FLOW_STEP_TYPES = ["branch", "switch", "loop", "try", "parallel", "jump"];
2657
2722
  FLOW_FIELDS = [
2658
- "condition",
2659
- "onTrueStep",
2660
- "onFalseStep",
2661
- "on",
2662
- "cases",
2663
- "defaultStep",
2664
- "loopKind",
2665
- "over",
2666
- "endStep",
2667
- "catches",
2668
- "finallyStep",
2669
- "branches",
2670
- "toStep"
2723
+ ...new Set(FLOW_STEP_TYPES.flatMap((t) => [...STEP_TYPE_FIELDS[t]]))
2671
2724
  ];
2672
2725
  }
2673
2726
  });
@@ -9744,20 +9797,22 @@ var init_narrative_step_config = __esm({
9744
9797
  init_models();
9745
9798
  narrativeStepConfigRule = {
9746
9799
  name: "narrative-step-config",
9747
- description: 'The step configuration of L5 narratives: every flow step (branch, switch, loop, try, parallel, jump) carries the config its type requires and no simple step carries flow config, "detach" (fire-and-forget) is legal on call/dispatch steps only, a parallel body is covered by contiguous, ordered arms (>= 2), no two steps share a step number or a label, and every jump field targets an existing step number in the same narrative.',
9800
+ description: 'The step configuration of L5 narratives: every flow step (branch, switch, loop, try, parallel, jump) carries the config its type requires and no simple step carries flow config, "detach" (fire-and-forget) is legal on call/dispatch steps only, no step carries a field foreign to its type, a parallel body is covered by contiguous, ordered arms (>= 2), no two steps share a step number or a label, and every jump field targets an existing step number in the same narrative.',
9748
9801
  codes: [
9749
9802
  { code: "MALFORMED_FLOW_STEP", defaultSeverity: "error", summary: "Flow step missing required config (or flow config on a local/call step)" },
9750
9803
  { code: "INVALID_STEP_JUMP", defaultSeverity: "error", summary: "Jump field targets a step number that does not exist in the narrative" },
9751
- { code: "DUPLICATE_STEP_LABEL", defaultSeverity: "error", summary: "Two steps in one narrative share a label \u2014 the symbolic anchor later deltas address by" }
9804
+ { code: "DUPLICATE_STEP_LABEL", defaultSeverity: "error", summary: "Two steps in one narrative share a label \u2014 the symbolic anchor later deltas address by" },
9805
+ { code: "FOREIGN_STEP_FIELD", defaultSeverity: "warning", summary: "Step carries a field its own type cannot have \u2014 a leftover of an edit, meaning nothing" }
9752
9806
  ],
9753
9807
  check(ctx) {
9808
+ const severityOf = new Map(narrativeStepConfigRule.codes.map((c) => [c.code, c.defaultSeverity]));
9754
9809
  for (const impl of ctx.implementations) {
9755
9810
  const isDraftCtx = ctx.isImplementationDraft(impl);
9756
9811
  for (const implMethod of impl.methods) {
9757
9812
  if (!implMethod.narrative.length) continue;
9758
9813
  const where = `Method "${implMethod.name}" in implementation "${impl.id}": `;
9759
9814
  for (const problem of stepConfigVerdict(implMethod).problems) {
9760
- ctx.addIssue("error", problem.code, where + problem.detail, impl.id, isDraftCtx);
9815
+ ctx.addIssue(severityOf.get(problem.code) ?? "error", problem.code, where + problem.detail, impl.id, isDraftCtx);
9761
9816
  }
9762
9817
  }
9763
9818
  }
@@ -12348,15 +12403,16 @@ var init_unused_types = __esm({
12348
12403
  init_models();
12349
12404
  unusedTypesRule = {
12350
12405
  name: "unused-types",
12351
- description: "Flags types no field of any type and no interface method signature references. References are matched through the type-reference grammar (generic arguments, collections, qualified ids), and the declaring type's own generic parameters are left out \u2014 a type parameter is not a reference to a type.",
12406
+ description: "Flags types no field of any type, no interface method signature and no other type's method signature references. References are matched through the type-reference grammar (generic arguments, collections, qualified ids), and the declaring type's own generic parameters are left out \u2014 a type parameter is not a reference to a type. A type named only by its OWN methods stays unused, the way a function that only calls itself is.",
12352
12407
  codes: [
12353
- { code: "UNUSED_TYPE", defaultSeverity: "warning", summary: "Type never referenced by fields or signatures" }
12408
+ { code: "UNUSED_TYPE", defaultSeverity: "warning", summary: "Type never referenced by fields, signatures or type methods" }
12354
12409
  ],
12355
12410
  check(ctx) {
12356
12411
  const referencedTypes = /* @__PURE__ */ new Set();
12357
- const markTypeReferenced = (ref) => {
12412
+ const markTypeReferenced = (ref, exceptId) => {
12358
12413
  if (ctx.isBuiltinType(ref)) return;
12359
12414
  for (const spec of ctx.types) {
12415
+ if (spec.id === exceptId) continue;
12360
12416
  if (typeMatchesRef(spec, ref)) {
12361
12417
  referencedTypes.add(spec.id);
12362
12418
  }
@@ -12378,6 +12434,14 @@ var init_unused_types = __esm({
12378
12434
  }
12379
12435
  }
12380
12436
  }
12437
+ for (const t of ctx.types) {
12438
+ for (const m of t.methods) {
12439
+ const refs = methodTypeRefs(m);
12440
+ for (const ref of refs) {
12441
+ markTypeReferenced(ref, t.id);
12442
+ }
12443
+ }
12444
+ }
12381
12445
  for (const t of ctx.types) {
12382
12446
  if (!ctx.isSpecInScope(t.id)) continue;
12383
12447
  if (!referencedTypes.has(t.id)) {
@@ -12386,7 +12450,7 @@ var init_unused_types = __esm({
12386
12450
  ctx.addIssue(
12387
12451
  "warning",
12388
12452
  "UNUSED_TYPE",
12389
- `Type "${t.id}" is defined but never referenced by any type fields or interface methods.`,
12453
+ `Type "${t.id}" is defined but never referenced by any type field, interface method or other type's method.`,
12390
12454
  t.id,
12391
12455
  isDraftCtx
12392
12456
  );
@@ -18360,15 +18424,6 @@ function identityKeyOf(field, item) {
18360
18424
  return str(o.name) ?? str(o.id);
18361
18425
  }
18362
18426
  }
18363
- function stepFieldsFor(type) {
18364
- const own = STEP_TYPE_FIELDS[type] ?? [];
18365
- const allowed = /* @__PURE__ */ new Set([...STEP_COMMON_FIELDS, ...own]);
18366
- for (const field of own) {
18367
- const twin = STEP_LABEL_TWINS[field];
18368
- if (twin) allowed.add(twin);
18369
- }
18370
- return allowed;
18371
- }
18372
18427
  function reportIdentityOf(field, item) {
18373
18428
  if (item !== null && typeof item === "object") {
18374
18429
  const step = item.stepNumber;
@@ -18416,6 +18471,117 @@ function specChanges(before, after, prefix = "") {
18416
18471
  if (JSON.stringify(before) === JSON.stringify(after)) return [];
18417
18472
  return [{ path: prefix, change: "set", before: summarizeValue(before), after: summarizeValue(after) }];
18418
18473
  }
18474
+ function identityFieldsOf(field) {
18475
+ switch (field) {
18476
+ case "dispatch":
18477
+ return ["capability"];
18478
+ case "lifecycle":
18479
+ return ["phase", "component", "method"];
18480
+ case "emits":
18481
+ case "subscribesTo":
18482
+ return ["topic", "event"];
18483
+ case "trustedLinks":
18484
+ return ["subsystem"];
18485
+ case "allow":
18486
+ case "findings":
18487
+ return ["code"];
18488
+ case "invariants":
18489
+ case "patterns":
18490
+ return ["id"];
18491
+ case "boundaries":
18492
+ return ["name"];
18493
+ case "globalRequirements":
18494
+ return ["description"];
18495
+ case "databases":
18496
+ return ["id", "name"];
18497
+ case "cases":
18498
+ return ["value"];
18499
+ case "catches":
18500
+ return ["error"];
18501
+ case "narrative":
18502
+ return ["stepNumber"];
18503
+ default:
18504
+ return ["name", "id"];
18505
+ }
18506
+ }
18507
+ function carriesEditMarker(item) {
18508
+ if (item === null || typeof item !== "object") return false;
18509
+ const o = item;
18510
+ return o.action !== void 0 || o.remove !== void 0;
18511
+ }
18512
+ function ineffectiveDeltaPaths(delta, merged, stored) {
18513
+ const out = [];
18514
+ const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18515
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
18516
+ const at = (prefix, key) => prefix ? `${prefix}.${key}` : key;
18517
+ const say = (path27, why) => {
18518
+ out.push(`${path27} \u2014 ${why}`);
18519
+ };
18520
+ const walkValue = (d, m, s, path27, field) => {
18521
+ if (Array.isArray(d)) {
18522
+ if (!Array.isArray(m)) {
18523
+ say(path27, "the level does not have this field, so the write dropped it");
18524
+ return;
18525
+ }
18526
+ if (field === "narrative" && d.some(carriesEditMarker)) return;
18527
+ for (const element of d) {
18528
+ if (carriesEditMarker(element)) continue;
18529
+ const key = reportIdentityOf(field, element);
18530
+ if (key === null) {
18531
+ if (!same(m, d)) say(path27, "the level does not keep this value, so the write dropped it");
18532
+ else if (same(s, d)) say(path27, "the stored spec already held this value");
18533
+ return;
18534
+ }
18535
+ const mMatch = m.find((i) => reportIdentityOf(field, i) === key);
18536
+ const sMatch = Array.isArray(s) ? s.find((i) => reportIdentityOf(field, i) === key) : void 0;
18537
+ if (mMatch === void 0) {
18538
+ say(at(path27, key), "the write did not store this element");
18539
+ continue;
18540
+ }
18541
+ walk2(element, mMatch, sMatch, at(path27, key), field);
18542
+ }
18543
+ return;
18544
+ }
18545
+ if (isPlain(d)) {
18546
+ if (!isPlain(m)) {
18547
+ say(path27, "the level does not have this field, so the write dropped it");
18548
+ return;
18549
+ }
18550
+ walk2(d, m, s, path27, field);
18551
+ return;
18552
+ }
18553
+ if (!same(m, d)) say(path27, "the write did not store it \u2014 the level's schema does not keep this value");
18554
+ else if (same(s, d)) say(path27, "the stored spec already held this value");
18555
+ };
18556
+ function walk2(d, m, s, path27, container) {
18557
+ for (const [key, value] of Object.entries(d)) {
18558
+ if (DELTA_VERBS.has(key) || JUMP_LABEL_TWINS.has(key)) continue;
18559
+ if (key === "label" && LABEL_TWIN_CONTAINERS.has(container)) continue;
18560
+ if (identityFieldsOf(container).includes(key)) continue;
18561
+ if (key === "unset") {
18562
+ for (const name of Array.isArray(value) ? value : []) {
18563
+ if (typeof name !== "string") continue;
18564
+ const had = isPlain(s) && s[name] !== void 0;
18565
+ const still = isPlain(m) && m[name] !== void 0;
18566
+ if (!had) say(at(path27, name), "unset named a field the stored spec did not have");
18567
+ else if (still) say(at(path27, name), "unset did not remove it");
18568
+ }
18569
+ continue;
18570
+ }
18571
+ if (value === void 0 || value === null) continue;
18572
+ const here = at(path27, key);
18573
+ const mValue = isPlain(m) ? m[key] : void 0;
18574
+ const sValue = isPlain(s) ? s[key] : void 0;
18575
+ if (mValue === void 0) {
18576
+ say(here, "the level does not have this field, so the write dropped it");
18577
+ continue;
18578
+ }
18579
+ walkValue(value, mValue, sValue, here, key);
18580
+ }
18581
+ }
18582
+ walk2(delta, merged, stored, "", "");
18583
+ return out;
18584
+ }
18419
18585
  function workspaceFor(rootDir) {
18420
18586
  const key = path16.resolve(rootDir);
18421
18587
  let ws = workspaces.get(key);
@@ -18641,10 +18807,10 @@ function restoreSpecFiles(snapshot) {
18641
18807
  function findLegacySpecFiles() {
18642
18808
  return current().findLegacySpecFiles();
18643
18809
  }
18644
- function updateSpec(kind, id, delta, hooks) {
18645
- return current().updateSpec(kind, id, delta, hooks);
18810
+ function updateSpec(kind, id, delta, hooks, dryRun) {
18811
+ return current().updateSpec(kind, id, delta, hooks, dryRun);
18646
18812
  }
18647
- var fs11, path16, NO_ROOT_SUBSYSTEMS, SIGNATURE_TTL_MS, STEP_TYPE_FIELDS, STEP_COMMON_FIELDS, STEP_LABEL_TWINS, SpecWorkspace, workspaces;
18813
+ var fs11, path16, NO_ROOT_SUBSYSTEMS, SIGNATURE_TTL_MS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, workspaces;
18648
18814
  var init_specs2 = __esm({
18649
18815
  "src/core/specs.ts"() {
18650
18816
  "use strict";
@@ -18662,36 +18828,16 @@ var init_specs2 = __esm({
18662
18828
  init_diagram();
18663
18829
  NO_ROOT_SUBSYSTEMS = /* @__PURE__ */ new Set();
18664
18830
  SIGNATURE_TTL_MS = 2e3;
18665
- STEP_TYPE_FIELDS = {
18666
- local: [],
18667
- call: ["targetComponent", "targetMethod", "auth", "detach"],
18668
- register: ["targetComponent", "targetMethod"],
18669
- dispatch: ["targetComponent", "capability", "auth", "detach"],
18670
- branch: ["condition", "onTrueStep", "onFalseStep"],
18671
- switch: ["on", "cases", "defaultStep"],
18672
- loop: ["loopKind", "over", "condition", "endStep"],
18673
- try: ["endStep", "catches", "finallyStep"],
18674
- parallel: ["endStep", "branches"],
18675
- jump: ["toStep"],
18676
- return: ["outcome"],
18677
- throw: ["error"]
18678
- };
18679
- STEP_COMMON_FIELDS = [
18680
- "stepNumber",
18681
- "label",
18682
- "description",
18683
- "type",
18684
- "assertsGuarantees",
18685
- "assertsInvariants"
18686
- ];
18687
- STEP_LABEL_TWINS = {
18688
- onTrueStep: "onTrueLabel",
18689
- onFalseStep: "onFalseLabel",
18690
- defaultStep: "defaultLabel",
18691
- endStep: "endLabel",
18692
- finallyStep: "finallyLabel",
18693
- toStep: "toLabel"
18694
- };
18831
+ DELTA_VERBS = /* @__PURE__ */ new Set(["action", "remove", "captureJumps"]);
18832
+ JUMP_LABEL_TWINS = /* @__PURE__ */ new Set([
18833
+ "onTrueLabel",
18834
+ "onFalseLabel",
18835
+ "defaultLabel",
18836
+ "endLabel",
18837
+ "finallyLabel",
18838
+ "toLabel"
18839
+ ]);
18840
+ LABEL_TWIN_CONTAINERS = /* @__PURE__ */ new Set(["cases", "catches", "branches"]);
18695
18841
  SpecWorkspace = class {
18696
18842
  constructor(rootDir) {
18697
18843
  this.cachedIndex = null;
@@ -19407,6 +19553,9 @@ var init_specs2 = __esm({
19407
19553
  const existing = this.loadSubsystemSpec(spec.id);
19408
19554
  if (existing) {
19409
19555
  specToWrite.createdAt = existing.createdAt;
19556
+ if (!opts?.allowStatusDemotion && existing.status && (!spec.status || spec.status === "draft")) {
19557
+ specToWrite.status = existing.status;
19558
+ }
19410
19559
  }
19411
19560
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
19412
19561
  writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
@@ -19955,15 +20104,22 @@ var init_specs2 = __esm({
19955
20104
  * carry, and a label in the delta retargets the stored numeric jump it
19956
20105
  * twins. An unresolved reference aborts the whole update.
19957
20106
  * 7. COMPARE with what is stored, both read through the level's canonical
19958
- * schema. Equal means nothing is written and `written` is false.
19959
- * 8. GATE the merged spec through the caller's hook, then SAVE.
20107
+ * schema. Equal means nothing is written and `written` is false. The same
20108
+ * comparison names every delta path the write did NOT act on — one the
20109
+ * schema drops at a depth the permissive delta cannot refuse, one the
20110
+ * stored spec already held, an `unset` that removed nothing.
20111
+ * 8. GATE the merged spec through the caller's hook. A DRY RUN stops here
20112
+ * and answers with the report it would have produced — after the gate, so
20113
+ * it accounts for a write that would really be accepted, and before the
20114
+ * stamp, so not one byte of the stored file moves.
20115
+ * 9. STAMP updatedAt and SAVE.
19960
20116
  *
19961
20117
  * Returns the change report, whose `notices` carry the non-fatal effects (an
19962
20118
  * insert whose position was a jump target, so the relocated jumps now bypass
19963
20119
  * the inserted step; the fields a retype dropped) — empty when the merge had
19964
20120
  * nothing to say.
19965
20121
  */
19966
- updateSpec(kind, id, delta, hooks) {
20122
+ updateSpec(kind, id, delta, hooks, dryRun = false) {
19967
20123
  const notices = [];
19968
20124
  const result = (() => {
19969
20125
  switch (kind) {
@@ -20442,7 +20598,8 @@ var init_specs2 = __esm({
20442
20598
  const unsetFields = Array.isArray(delta.unset) ? delta.unset.filter((f) => typeof f === "string") : [];
20443
20599
  const { unset: _unset, ...mergeableDelta } = delta;
20444
20600
  const storedBefore = JSON.parse(JSON.stringify(result));
20445
- const mergedResult = mergeDelta(result, qualifyDeltaRefs(mergeableDelta));
20601
+ const qualifiedDelta = qualifyDeltaRefs(mergeableDelta);
20602
+ const mergedResult = mergeDelta(result, qualifiedDelta);
20446
20603
  for (const field of unsetFields) delete mergedResult[field];
20447
20604
  if (kind === "implementation" && Array.isArray(mergedResult.methods)) {
20448
20605
  const labelErrors = [];
@@ -20460,20 +20617,41 @@ var init_specs2 = __esm({
20460
20617
  delete value.updatedAt;
20461
20618
  return value;
20462
20619
  };
20463
- const changes = specChanges(canonical(storedBefore), canonical(mergedResult));
20620
+ const canonicalStored = canonical(storedBefore);
20621
+ const canonicalMerged = canonical(mergedResult);
20622
+ const changes = specChanges(canonicalStored, canonicalMerged);
20623
+ const ineffective = ineffectiveDeltaPaths(
20624
+ { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
20625
+ canonicalMerged,
20626
+ canonicalStored
20627
+ );
20464
20628
  if (changes.length === 0) {
20465
20629
  return {
20466
20630
  kind,
20467
20631
  id,
20468
20632
  written: false,
20633
+ dryRun,
20469
20634
  changes,
20635
+ ineffective,
20470
20636
  notices,
20471
- summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing was written.`
20637
+ summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
20472
20638
  };
20473
20639
  }
20474
- mergedResult.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
20475
20640
  const gateNotices = hooks?.gate?.(kind, mergedResult);
20476
20641
  if (gateNotices?.length) notices.push(...gateNotices);
20642
+ if (dryRun) {
20643
+ return {
20644
+ kind,
20645
+ id,
20646
+ written: false,
20647
+ dryRun: true,
20648
+ changes,
20649
+ ineffective,
20650
+ notices,
20651
+ summary: `Dry run on ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"} would be made. Nothing was written.`
20652
+ };
20653
+ }
20654
+ mergedResult.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
20477
20655
  const opts = {
20478
20656
  allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
20479
20657
  };
@@ -20482,7 +20660,7 @@ var init_specs2 = __esm({
20482
20660
  this.saveSystemSpec(mergedResult);
20483
20661
  break;
20484
20662
  case "subsystem":
20485
- this.saveSubsystemSpec(mergedResult);
20663
+ this.saveSubsystemSpec(mergedResult, opts);
20486
20664
  break;
20487
20665
  case "component":
20488
20666
  notices.push(...this.saveComponentSpec(mergedResult, opts));
@@ -20501,7 +20679,9 @@ var init_specs2 = __esm({
20501
20679
  kind,
20502
20680
  id,
20503
20681
  written: true,
20682
+ dryRun: false,
20504
20683
  changes,
20684
+ ineffective,
20505
20685
  notices,
20506
20686
  summary: `Updated ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
20507
20687
  };
@@ -25872,6 +26052,44 @@ var init_stereotype_migration = __esm({
25872
26052
  }
25873
26053
  });
25874
26054
 
26055
+ // src/core/narrative-repair.ts
26056
+ function repairForeignStepFields(apply) {
26057
+ const implementations = loadImplementationSpecs();
26058
+ const repairs = [];
26059
+ const changed = [];
26060
+ for (const impl of implementations) {
26061
+ let touched = false;
26062
+ for (const method of impl.methods) {
26063
+ for (const step of method.narrative) {
26064
+ const fields = narrativeStepForeignFields(step);
26065
+ if (!fields.length) continue;
26066
+ repairs.push({
26067
+ implementation: impl.id,
26068
+ method: method.name,
26069
+ stepNumber: step.stepNumber,
26070
+ stepType: step.type,
26071
+ fields
26072
+ });
26073
+ for (const field of fields) delete step[field];
26074
+ touched = true;
26075
+ }
26076
+ }
26077
+ if (touched) changed.push(impl);
26078
+ }
26079
+ if (apply && changed.length) {
26080
+ for (const impl of changed) saveImplementationSpec(impl);
26081
+ invalidateSpecCache();
26082
+ }
26083
+ return repairs;
26084
+ }
26085
+ var init_narrative_repair = __esm({
26086
+ "src/core/narrative-repair.ts"() {
26087
+ "use strict";
26088
+ init_specs2();
26089
+ init_models();
26090
+ }
26091
+ });
26092
+
25875
26093
  // src/core/approver.ts
25876
26094
  function gitConfig(key) {
25877
26095
  try {
@@ -25952,6 +26170,7 @@ var init_core = __esm({
25952
26170
  init_treetransfer();
25953
26171
  init_provision();
25954
26172
  init_stereotype_migration();
26173
+ init_narrative_repair();
25955
26174
  init_approval();
25956
26175
  init_approver();
25957
26176
  init_specs2();
@@ -26056,6 +26275,9 @@ __export(src_exports, {
26056
26275
  SCAN_EXCLUDE_DIRS: () => SCAN_EXCLUDE_DIRS,
26057
26276
  SDD_RULES: () => SDD_RULES,
26058
26277
  SEMANTIC_GUARANTEES: () => SEMANTIC_GUARANTEES,
26278
+ STEP_COMMON_FIELDS: () => STEP_COMMON_FIELDS,
26279
+ STEP_LABEL_TWINS: () => STEP_LABEL_TWINS,
26280
+ STEP_TYPE_FIELDS: () => STEP_TYPE_FIELDS,
26059
26281
  SUPPORTED_ALIASES: () => SUPPORTED_ALIASES,
26060
26282
  SURFACE_AUDIENCES: () => SURFACE_AUDIENCES,
26061
26283
  SkillResourceNotFoundError: () => SkillResourceNotFoundError,
@@ -26207,6 +26429,7 @@ __export(src_exports, {
26207
26429
  implementationSourceFiles: () => implementationSourceFiles,
26208
26430
  importSpecTree: () => importSpecTree,
26209
26431
  importSurface: () => importSurface,
26432
+ ineffectiveDeltaPaths: () => ineffectiveDeltaPaths,
26210
26433
  inspectChainedRoots: () => inspectChainedRoots,
26211
26434
  inspectTreeArchive: () => inspectTreeArchive2,
26212
26435
  installPackFromDirectory: () => installPackFromDirectory,
@@ -26269,6 +26492,7 @@ __export(src_exports, {
26269
26492
  methodSourceFile: () => methodSourceFile,
26270
26493
  methodTypeRefs: () => methodTypeRefs,
26271
26494
  moveSubsystemProject: () => moveSubsystemProject,
26495
+ narrativeStepForeignFields: () => narrativeStepForeignFields,
26272
26496
  normalizeApprover: () => normalizeApprover,
26273
26497
  normalizeComponentLayout: () => normalizeComponentLayout,
26274
26498
  packDirEntry: () => packDirEntry,
@@ -26313,6 +26537,7 @@ __export(src_exports, {
26313
26537
  renderDomainsDoc: () => renderDomainsDoc,
26314
26538
  renderTemplateInstructions: () => renderTemplateInstructions,
26315
26539
  renderWaironGuide: () => renderWaironGuide,
26540
+ repairForeignStepFields: () => repairForeignStepFields,
26316
26541
  resolveAgentTopology: () => resolveAgentTopology,
26317
26542
  resolveChainingParent: () => resolveChainingParent,
26318
26543
  resolveDomains: () => resolveDomains,
@@ -26351,6 +26576,7 @@ __export(src_exports, {
26351
26576
  stateIdEquals: () => stateIdEquals,
26352
26577
  stateIdString: () => stateIdString,
26353
26578
  stepConfigVerdict: () => stepConfigVerdict,
26579
+ stepFieldsFor: () => stepFieldsFor,
26354
26580
  stepGraph: () => stepGraph,
26355
26581
  syncContextFiles: () => syncContextFiles,
26356
26582
  toMarkdown: () => toMarkdown,
@@ -26777,6 +27003,9 @@ function loadProjectConfig4() {
26777
27003
  SCAN_EXCLUDE_DIRS,
26778
27004
  SDD_RULES,
26779
27005
  SEMANTIC_GUARANTEES,
27006
+ STEP_COMMON_FIELDS,
27007
+ STEP_LABEL_TWINS,
27008
+ STEP_TYPE_FIELDS,
26780
27009
  SUPPORTED_ALIASES,
26781
27010
  SURFACE_AUDIENCES,
26782
27011
  SkillResourceNotFoundError,
@@ -26928,6 +27157,7 @@ function loadProjectConfig4() {
26928
27157
  implementationSourceFiles,
26929
27158
  importSpecTree,
26930
27159
  importSurface,
27160
+ ineffectiveDeltaPaths,
26931
27161
  inspectChainedRoots,
26932
27162
  inspectTreeArchive,
26933
27163
  installPackFromDirectory,
@@ -26990,6 +27220,7 @@ function loadProjectConfig4() {
26990
27220
  methodSourceFile,
26991
27221
  methodTypeRefs,
26992
27222
  moveSubsystemProject,
27223
+ narrativeStepForeignFields,
26993
27224
  normalizeApprover,
26994
27225
  normalizeComponentLayout,
26995
27226
  packDirEntry,
@@ -27034,6 +27265,7 @@ function loadProjectConfig4() {
27034
27265
  renderDomainsDoc,
27035
27266
  renderTemplateInstructions,
27036
27267
  renderWaironGuide,
27268
+ repairForeignStepFields,
27037
27269
  resolveAgentTopology,
27038
27270
  resolveChainingParent,
27039
27271
  resolveDomains,
@@ -27072,6 +27304,7 @@ function loadProjectConfig4() {
27072
27304
  stateIdEquals,
27073
27305
  stateIdString,
27074
27306
  stepConfigVerdict,
27307
+ stepFieldsFor,
27075
27308
  stepGraph,
27076
27309
  syncContextFiles,
27077
27310
  toMarkdown,