@wairon/cli 5.1.1-dev.34 → 5.1.1-dev.36

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.34";
1116
+ WAIRON_VERSION = "5.1.1-dev.36";
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
  );
@@ -18345,6 +18409,17 @@ function identityKeyOf(field, item) {
18345
18409
  return str(o.description);
18346
18410
  case "databases":
18347
18411
  return str(o.id) ?? str(o.name);
18412
+ // A flow step's own arrays, addressed by the thing the author named them
18413
+ // for. Their `step` is a RELOCATABLE number, never an identity — a switch
18414
+ // case is the value it matches and a catch clause is the error it catches,
18415
+ // and both survive the renumbering that moves their target. (A parallel
18416
+ // arm falls to the default: named, it merges by name; unnamed, it carries
18417
+ // no identity at all and the list replaces wholesale, which is the same
18418
+ // rule everywhere else.)
18419
+ case "cases":
18420
+ return str(o.value);
18421
+ case "catches":
18422
+ return str(o.error);
18348
18423
  default:
18349
18424
  return str(o.name) ?? str(o.id);
18350
18425
  }
@@ -19879,9 +19954,39 @@ var init_specs2 = __esm({
19879
19954
  // Granular delta updates (sdd_update_spec)
19880
19955
  // -------------------------------------------------------------------------
19881
19956
  /**
19882
- * Returns non-fatal notices about the merge (e.g. an insert whose position
19883
- * was a jump target, so the relocated jumps now bypass the inserted step) —
19884
- * empty when the merge had nothing to warn about.
19957
+ * Apply a delta to one stored spec, in this ORDER — the order is the contract,
19958
+ * because every stage below can see what the stage above left:
19959
+ *
19960
+ * 1. LOAD the stored spec for the addressed kind and id; a spec that does
19961
+ * not exist refuses, and a mis-selected `system` kind refuses by name.
19962
+ * 2. REFUSE unknown delta keys, whole. A key the level's schema does not
19963
+ * know is dropped on write, so the edit would report a success it never
19964
+ * performed.
19965
+ * 3. QUALIFY the delta's id-bearing references against the spec's namespace,
19966
+ * BEFORE the merge — post-merge a bare id is ambiguous.
19967
+ * 4. MERGE. Identity-keyed arrays upsert element by element and honour their
19968
+ * delete markers, at every depth. Narrative step deltas apply in ASCENDING
19969
+ * stepNumber order, each against the numbering the earlier entries of the
19970
+ * same delta left behind — delete step 3 and step 7 becomes step 6, so a
19971
+ * second delete written as 7 addresses what used to be step 8. That is why
19972
+ * a delete may restate the step's `label` or `description` as a guard, and
19973
+ * why labels are the reliable way to name a jump target. Inserts and
19974
+ * deletes relocate every jump field in the same narrative around them; a
19975
+ * step whose `type` changes is REBUILT for its new type, keeping its
19976
+ * description and label.
19977
+ * 5. UNSET the fields the delta names, at the level each `unset` sits in.
19978
+ * 6. RESOLVE symbolic `*Label` references against the MERGED numbering — after
19979
+ * the merge, so a delta may anchor on a label that only pre-existing steps
19980
+ * carry, and a label in the delta retargets the stored numeric jump it
19981
+ * twins. An unresolved reference aborts the whole update.
19982
+ * 7. COMPARE with what is stored, both read through the level's canonical
19983
+ * schema. Equal means nothing is written and `written` is false.
19984
+ * 8. GATE the merged spec through the caller's hook, then SAVE.
19985
+ *
19986
+ * Returns the change report, whose `notices` carry the non-fatal effects (an
19987
+ * insert whose position was a jump target, so the relocated jumps now bypass
19988
+ * the inserted step; the fields a retype dropped) — empty when the merge had
19989
+ * nothing to say.
19885
19990
  */
19886
19991
  updateSpec(kind, id, delta, hooks) {
19887
19992
  const notices = [];
@@ -19995,14 +20100,113 @@ var init_specs2 = __esm({
19995
20100
  );
19996
20101
  }
19997
20102
  };
19998
- const mergeNarrative = (existingSteps, deltaSteps) => {
20103
+ const NO_NESTED_MERGE = /* @__PURE__ */ new Set(["ext", "narrative", "unset"]);
20104
+ const mergeElement = (existing, deltaItem) => {
20105
+ const out = { ...existing, ...deltaItem };
20106
+ if (!existing || typeof existing !== "object" || !deltaItem || typeof deltaItem !== "object") return out;
20107
+ for (const [key, value] of Object.entries(deltaItem)) {
20108
+ if (NO_NESTED_MERGE.has(key) || !Array.isArray(value) || value.length === 0) continue;
20109
+ const before = existing[key];
20110
+ if (before !== void 0 && !Array.isArray(before)) continue;
20111
+ const stored = Array.isArray(before) ? before : [];
20112
+ if (!isIdentifiedArray(key, stored, value)) continue;
20113
+ out[key] = mergeIdentifiedArray(key, stored, value);
20114
+ }
20115
+ return out;
20116
+ };
20117
+ const retargetByLabel = (merged, deltaStep) => {
20118
+ const out = { ...merged };
20119
+ for (const [stepField, labelField] of Object.entries(STEP_LABEL_TWINS)) {
20120
+ if (deltaStep?.[labelField] !== void 0 && deltaStep?.[stepField] === void 0) delete out[stepField];
20121
+ }
20122
+ for (const key of ["cases", "catches"]) {
20123
+ const deltaEntries = deltaStep?.[key];
20124
+ if (!Array.isArray(deltaEntries) || !Array.isArray(out[key])) continue;
20125
+ const retargeted = new Set(
20126
+ deltaEntries.filter((e) => e?.label !== void 0 && e?.step === void 0).map((e) => identityKeyOf(key, e)).filter((k) => k !== null)
20127
+ );
20128
+ if (!retargeted.size) continue;
20129
+ out[key] = out[key].map((e) => {
20130
+ const k = identityKeyOf(key, e);
20131
+ if (k === null || !retargeted.has(k)) return e;
20132
+ const { step, ...rest } = e;
20133
+ return rest;
20134
+ });
20135
+ }
20136
+ return out;
20137
+ };
20138
+ const rebuildOnRetype = (stored, deltaStep, merged, methodName) => {
20139
+ const newType = merged?.type;
20140
+ if (typeof newType !== "string" || stored?.type === newType) return merged;
20141
+ if (!Object.prototype.hasOwnProperty.call(STEP_TYPE_FIELDS, newType)) return merged;
20142
+ const allowed = stepFieldsFor(newType);
20143
+ const stated = Object.keys(deltaStep).filter(
20144
+ (k) => k !== "type" && deltaStep[k] !== void 0 && !allowed.has(k)
20145
+ );
20146
+ if (stated.length) {
20147
+ throw new Error(
20148
+ `Refusing to retype narrative step ${stored.stepNumber} of "${methodName}" from "${stored.type}" to "${newType}": the same delta sets ${stated.map((f) => `"${f}"`).join(", ")}, which a ${newType} step cannot carry. Drop those keys, or retype to a step type that carries them.`
20149
+ );
20150
+ }
20151
+ const out = {};
20152
+ const dropped = [];
20153
+ for (const [key, value] of Object.entries(merged)) {
20154
+ if (allowed.has(key)) out[key] = value;
20155
+ else dropped.push(key);
20156
+ }
20157
+ if (dropped.length) {
20158
+ notices.push(
20159
+ `Narrative step ${stored.stepNumber} of "${methodName}" was retyped ${stored.type} -> ${newType}: ${dropped.join(", ")} dropped \u2014 a ${newType} step cannot carry ${dropped.length === 1 ? "it" : "them"}. Its description and label are kept.`
20160
+ );
20161
+ }
20162
+ return out;
20163
+ };
20164
+ const mergeNarrative = (methodName, existingSteps, deltaSteps) => {
19999
20165
  let steps = [...existingSteps];
20000
20166
  const sortedDeltas = [...deltaSteps].sort((a, b) => a.stepNumber - b.stepNumber);
20001
20167
  for (const deltaStep of sortedDeltas) {
20002
20168
  const stepNum = deltaStep.stepNumber;
20169
+ if (typeof stepNum !== "number") {
20170
+ throw new Error(
20171
+ `Refusing to update the narrative of "${methodName}": a step delta must carry the "stepNumber" it addresses, got ${JSON.stringify(stepNum ?? null)}.`
20172
+ );
20173
+ }
20174
+ if ("remove" in deltaStep && typeof deltaStep.remove !== "boolean") {
20175
+ throw new Error(
20176
+ `Refusing to update narrative step ${stepNum} of "${methodName}": "remove" must be the boolean true, got ${JSON.stringify(deltaStep.remove)}. A non-boolean is ignored, which would leave the step in place while reporting success.`
20177
+ );
20178
+ }
20179
+ if ("action" in deltaStep && deltaStep.action !== "insert" && deltaStep.action !== "delete") {
20180
+ throw new Error(
20181
+ `Refusing to update narrative step ${stepNum} of "${methodName}": unknown action ${JSON.stringify(deltaStep.action)} \u2014 expected "insert" or "delete", or no action at all to edit the step in place. An unknown verb is ignored, which would report success for an edit never made.`
20182
+ );
20183
+ }
20184
+ if ("captureJumps" in deltaStep && deltaStep.action !== "insert") {
20185
+ throw new Error(
20186
+ `Refusing to update narrative step ${stepNum} of "${methodName}": "captureJumps" only means anything on an inserted step \u2014 it decides whether jumps aimed at that number follow the shifted original or land on the new step. On any other delta it is ignored.`
20187
+ );
20188
+ }
20003
20189
  if (deltaStep.action === "delete" || deltaStep.remove === true) {
20004
20190
  const idx = steps.findIndex((s) => s.stepNumber === stepNum);
20005
20191
  if (idx !== -1) {
20192
+ const victim = steps[idx];
20193
+ for (const field of ["label", "description"]) {
20194
+ if (deltaStep[field] === void 0) continue;
20195
+ if (victim[field] !== deltaStep[field]) {
20196
+ throw new Error(
20197
+ `Refusing to delete narrative step ${stepNum}: the delta states ${field} ${JSON.stringify(deltaStep[field])}, but step ${stepNum} holds ${JSON.stringify(victim[field] ?? null)}. Step deltas apply in ascending order against the numbering earlier entries of the same delta left behind, so a later delete can address a step that has already moved.`
20198
+ );
20199
+ }
20200
+ }
20201
+ if (typeof victim.endStep === "number") {
20202
+ const body = steps.filter((s) => s.stepNumber > stepNum && s.stepNumber <= victim.endStep);
20203
+ if (body.length) {
20204
+ const span = body.length === 1 ? `step ${body[0].stepNumber}` : `steps ${body[0].stepNumber}-${body[body.length - 1].stepNumber}`;
20205
+ throw new Error(
20206
+ `Cannot delete narrative step ${stepNum}: it is a ${victim.type} header and ${span} ${body.length === 1 ? "is" : "are"} still its body. Deleting it would leave them in place with no ${victim.type} around them \u2014 a change of meaning nothing later reports. Dissolve the region first: retype step ${stepNum} to a type that carries no region (the region fields are dropped and reported), then delete it.`
20207
+ );
20208
+ }
20209
+ }
20006
20210
  const referrers = steps.filter((s) => s.stepNumber !== stepNum).map((s) => ({ n: s.stepNumber, refs: jumpRefsTo(s, stepNum) })).filter((r) => r.refs.length > 0);
20007
20211
  if (referrers.length) {
20008
20212
  throw new Error(
@@ -20015,6 +20219,11 @@ var init_specs2 = __esm({
20015
20219
  stepNum,
20016
20220
  -1
20017
20221
  ));
20222
+ } else {
20223
+ const present = steps.length ? ` \u2014 it has steps ${steps[0].stepNumber}-${steps[steps.length - 1].stepNumber}` : " \u2014 it has no steps";
20224
+ throw new Error(
20225
+ `Refusing to delete narrative step ${stepNum}: this narrative has no step ${stepNum}${present}. Step deltas apply in ascending order against the numbering earlier entries of the same delta left behind, so a later delete can address a step that has already moved.`
20226
+ );
20018
20227
  }
20019
20228
  } else if (deltaStep.action === "insert") {
20020
20229
  const capture = deltaStep.captureJumps === true;
@@ -20038,10 +20247,9 @@ var init_specs2 = __esm({
20038
20247
  const idx = steps.findIndex((s) => s.stepNumber === stepNum);
20039
20248
  if (idx !== -1) {
20040
20249
  const { action, remove, ...cleanStep } = deltaStep;
20041
- steps[idx] = withUnset({
20042
- ...steps[idx],
20043
- ...cleanStep
20044
- }, deltaStep);
20250
+ const stored = steps[idx];
20251
+ const merged = withUnset(retargetByLabel(mergeElement(stored, cleanStep), cleanStep), deltaStep);
20252
+ steps[idx] = rebuildOnRetype(stored, cleanStep, merged, methodName);
20045
20253
  } else {
20046
20254
  const { action, remove, ...cleanStep } = deltaStep;
20047
20255
  steps.push(withUnset(cleanStep, deltaStep));
@@ -20072,12 +20280,11 @@ var init_specs2 = __esm({
20072
20280
  const existingMethod = merged[idx];
20073
20281
  let narrative = existingMethod.narrative ? [...existingMethod.narrative] : [];
20074
20282
  if (Array.isArray(deltaMethod.narrative)) {
20075
- narrative = deltaMethod.narrative.length === 0 ? [] : mergeNarrative(narrative, deltaMethod.narrative);
20283
+ narrative = deltaMethod.narrative.length === 0 ? [] : mergeNarrative(String(deltaMethod.name), narrative, deltaMethod.narrative);
20076
20284
  }
20077
20285
  const { narrative: _, ...cleanMethod } = deltaMethod;
20078
20286
  merged[idx] = withUnset({
20079
- ...existingMethod,
20080
- ...cleanMethod,
20287
+ ...mergeElement(existingMethod, cleanMethod),
20081
20288
  ...deltaMethod.ext !== void 0 ? { ext: mergeExt(existingMethod.ext, deltaMethod.ext) } : {},
20082
20289
  narrative
20083
20290
  }, deltaMethod);
@@ -20099,12 +20306,8 @@ var init_specs2 = __esm({
20099
20306
  merged.splice(idx, 1);
20100
20307
  } else {
20101
20308
  merged[idx] = withUnset({
20102
- ...merged[idx],
20103
- ...deltaItem,
20104
- ...deltaItem.ext !== void 0 ? { ext: mergeExt(merged[idx].ext, deltaItem.ext) } : {},
20105
- // An interface method's findings upsert and delete by code, like
20106
- // lint.allow; an empty list still clears them outright.
20107
- ...Array.isArray(deltaItem.findings) && deltaItem.findings.length > 0 ? { findings: mergeIdentifiedArray("findings", merged[idx].findings ?? [], deltaItem.findings) } : {}
20309
+ ...mergeElement(merged[idx], deltaItem),
20310
+ ...deltaItem.ext !== void 0 ? { ext: mergeExt(merged[idx].ext, deltaItem.ext) } : {}
20108
20311
  }, deltaItem);
20109
20312
  }
20110
20313
  } else {
@@ -20123,7 +20326,7 @@ var init_specs2 = __esm({
20123
20326
  if (deltaItem.remove === true || deltaItem.action === "delete") {
20124
20327
  merged.splice(idx, 1);
20125
20328
  } else {
20126
- merged[idx] = withUnset({ ...merged[idx], ...deltaItem }, deltaItem);
20329
+ merged[idx] = withUnset(mergeElement(merged[idx], deltaItem), deltaItem);
20127
20330
  }
20128
20331
  } else {
20129
20332
  assertUnmatchedIsAnAddition(deltaItem, keyOf(deltaItem), merged.map(keyOf), "entry");
@@ -20142,10 +20345,7 @@ var init_specs2 = __esm({
20142
20345
  if (deltaItem.remove === true || deltaItem.action === "delete") {
20143
20346
  merged.splice(idx, 1);
20144
20347
  } else {
20145
- merged[idx] = withUnset({
20146
- ...merged[idx],
20147
- ...deltaItem
20148
- }, deltaItem);
20348
+ merged[idx] = withUnset(mergeElement(merged[idx], deltaItem), deltaItem);
20149
20349
  }
20150
20350
  } else {
20151
20351
  assertUnmatchedIsAnAddition(deltaItem, piKey(deltaItem), merged.map(piKey), "publicInterface");
@@ -20163,7 +20363,7 @@ var init_specs2 = __esm({
20163
20363
  const isDelete = deltaItem?.remove === true || deltaItem?.action === "delete";
20164
20364
  if (idx !== -1) {
20165
20365
  if (isDelete) merged.splice(idx, 1);
20166
- else merged[idx] = withUnset({ ...merged[idx], ...deltaItem }, deltaItem);
20366
+ else merged[idx] = withUnset(mergeElement(merged[idx], deltaItem), deltaItem);
20167
20367
  } else {
20168
20368
  if (key !== null) {
20169
20369
  assertUnmatchedIsAnAddition(
@@ -25697,6 +25897,44 @@ var init_stereotype_migration = __esm({
25697
25897
  }
25698
25898
  });
25699
25899
 
25900
+ // src/core/narrative-repair.ts
25901
+ function repairForeignStepFields(apply) {
25902
+ const implementations = loadImplementationSpecs();
25903
+ const repairs = [];
25904
+ const changed = [];
25905
+ for (const impl of implementations) {
25906
+ let touched = false;
25907
+ for (const method of impl.methods) {
25908
+ for (const step of method.narrative) {
25909
+ const fields = narrativeStepForeignFields(step);
25910
+ if (!fields.length) continue;
25911
+ repairs.push({
25912
+ implementation: impl.id,
25913
+ method: method.name,
25914
+ stepNumber: step.stepNumber,
25915
+ stepType: step.type,
25916
+ fields
25917
+ });
25918
+ for (const field of fields) delete step[field];
25919
+ touched = true;
25920
+ }
25921
+ }
25922
+ if (touched) changed.push(impl);
25923
+ }
25924
+ if (apply && changed.length) {
25925
+ for (const impl of changed) saveImplementationSpec(impl);
25926
+ invalidateSpecCache();
25927
+ }
25928
+ return repairs;
25929
+ }
25930
+ var init_narrative_repair = __esm({
25931
+ "src/core/narrative-repair.ts"() {
25932
+ "use strict";
25933
+ init_specs2();
25934
+ init_models();
25935
+ }
25936
+ });
25937
+
25700
25938
  // src/core/approver.ts
25701
25939
  function gitConfig(key) {
25702
25940
  try {
@@ -25777,6 +26015,7 @@ var init_core = __esm({
25777
26015
  init_treetransfer();
25778
26016
  init_provision();
25779
26017
  init_stereotype_migration();
26018
+ init_narrative_repair();
25780
26019
  init_approval();
25781
26020
  init_approver();
25782
26021
  init_specs2();
@@ -25881,6 +26120,9 @@ __export(src_exports, {
25881
26120
  SCAN_EXCLUDE_DIRS: () => SCAN_EXCLUDE_DIRS,
25882
26121
  SDD_RULES: () => SDD_RULES,
25883
26122
  SEMANTIC_GUARANTEES: () => SEMANTIC_GUARANTEES,
26123
+ STEP_COMMON_FIELDS: () => STEP_COMMON_FIELDS,
26124
+ STEP_LABEL_TWINS: () => STEP_LABEL_TWINS,
26125
+ STEP_TYPE_FIELDS: () => STEP_TYPE_FIELDS,
25884
26126
  SUPPORTED_ALIASES: () => SUPPORTED_ALIASES,
25885
26127
  SURFACE_AUDIENCES: () => SURFACE_AUDIENCES,
25886
26128
  SkillResourceNotFoundError: () => SkillResourceNotFoundError,
@@ -26094,6 +26336,7 @@ __export(src_exports, {
26094
26336
  methodSourceFile: () => methodSourceFile,
26095
26337
  methodTypeRefs: () => methodTypeRefs,
26096
26338
  moveSubsystemProject: () => moveSubsystemProject,
26339
+ narrativeStepForeignFields: () => narrativeStepForeignFields,
26097
26340
  normalizeApprover: () => normalizeApprover,
26098
26341
  normalizeComponentLayout: () => normalizeComponentLayout,
26099
26342
  packDirEntry: () => packDirEntry,
@@ -26138,6 +26381,7 @@ __export(src_exports, {
26138
26381
  renderDomainsDoc: () => renderDomainsDoc,
26139
26382
  renderTemplateInstructions: () => renderTemplateInstructions,
26140
26383
  renderWaironGuide: () => renderWaironGuide,
26384
+ repairForeignStepFields: () => repairForeignStepFields,
26141
26385
  resolveAgentTopology: () => resolveAgentTopology,
26142
26386
  resolveChainingParent: () => resolveChainingParent,
26143
26387
  resolveDomains: () => resolveDomains,
@@ -26176,6 +26420,7 @@ __export(src_exports, {
26176
26420
  stateIdEquals: () => stateIdEquals,
26177
26421
  stateIdString: () => stateIdString,
26178
26422
  stepConfigVerdict: () => stepConfigVerdict,
26423
+ stepFieldsFor: () => stepFieldsFor,
26179
26424
  stepGraph: () => stepGraph,
26180
26425
  syncContextFiles: () => syncContextFiles,
26181
26426
  toMarkdown: () => toMarkdown,
@@ -26602,6 +26847,9 @@ function loadProjectConfig4() {
26602
26847
  SCAN_EXCLUDE_DIRS,
26603
26848
  SDD_RULES,
26604
26849
  SEMANTIC_GUARANTEES,
26850
+ STEP_COMMON_FIELDS,
26851
+ STEP_LABEL_TWINS,
26852
+ STEP_TYPE_FIELDS,
26605
26853
  SUPPORTED_ALIASES,
26606
26854
  SURFACE_AUDIENCES,
26607
26855
  SkillResourceNotFoundError,
@@ -26815,6 +27063,7 @@ function loadProjectConfig4() {
26815
27063
  methodSourceFile,
26816
27064
  methodTypeRefs,
26817
27065
  moveSubsystemProject,
27066
+ narrativeStepForeignFields,
26818
27067
  normalizeApprover,
26819
27068
  normalizeComponentLayout,
26820
27069
  packDirEntry,
@@ -26859,6 +27108,7 @@ function loadProjectConfig4() {
26859
27108
  renderDomainsDoc,
26860
27109
  renderTemplateInstructions,
26861
27110
  renderWaironGuide,
27111
+ repairForeignStepFields,
26862
27112
  resolveAgentTopology,
26863
27113
  resolveChainingParent,
26864
27114
  resolveDomains,
@@ -26897,6 +27147,7 @@ function loadProjectConfig4() {
26897
27147
  stateIdEquals,
26898
27148
  stateIdString,
26899
27149
  stepConfigVerdict,
27150
+ stepFieldsFor,
26900
27151
  stepGraph,
26901
27152
  syncContextFiles,
26902
27153
  toMarkdown,