@wairon/cli 5.1.1-dev.70 → 5.1.1-dev.71

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/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.70";
68
+ WAIRON_VERSION = "5.1.1-dev.71";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -19633,6 +19633,7 @@ __export(specs_exports, {
19633
19633
  deleteGroupSpec: () => deleteGroupSpec,
19634
19634
  deleteImplementationSpec: () => deleteImplementationSpec,
19635
19635
  deleteInterfaceSpec: () => deleteInterfaceSpec,
19636
+ deleteSpec: () => deleteSpec,
19636
19637
  deleteSubsystemSpec: () => deleteSubsystemSpec,
19637
19638
  deleteTypeSpec: () => deleteTypeSpec,
19638
19639
  deregisterPackRef: () => deregisterPackRef,
@@ -19659,6 +19660,7 @@ __export(specs_exports, {
19659
19660
  loadImplementationSpecs: () => loadImplementationSpecs,
19660
19661
  loadInterfaceSpec: () => loadInterfaceSpec,
19661
19662
  loadInterfaceSpecs: () => loadInterfaceSpecs,
19663
+ loadSpec: () => loadSpec,
19662
19664
  loadSubsystemSpec: () => loadSubsystemSpec,
19663
19665
  loadSubsystemSpecs: () => loadSubsystemSpecs,
19664
19666
  loadSystemSpec: () => loadSystemSpec,
@@ -19680,6 +19682,7 @@ __export(specs_exports, {
19680
19682
  saveGroupSpec: () => saveGroupSpec,
19681
19683
  saveImplementationSpec: () => saveImplementationSpec,
19682
19684
  saveInterfaceSpec: () => saveInterfaceSpec,
19685
+ saveSpec: () => saveSpec,
19683
19686
  saveSubsystemSpec: () => saveSubsystemSpec,
19684
19687
  saveSystemSpec: () => saveSystemSpec,
19685
19688
  saveTypeSpec: () => saveTypeSpec,
@@ -20169,6 +20172,8 @@ function identityKeyOf(field, item) {
20169
20172
  const o = item;
20170
20173
  const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
20171
20174
  switch (field) {
20175
+ case "publicInterfaces":
20176
+ return publicInterfaceKey(o);
20172
20177
  case "dispatch":
20173
20178
  return str(o.capability);
20174
20179
  // A listener mounts each portal once, so the portal IS the mount's identity.
@@ -20214,6 +20219,13 @@ function identityKeyOf(field, item) {
20214
20219
  return str(o.name) ?? str(o.id);
20215
20220
  }
20216
20221
  }
20222
+ function publicInterfaceKey(o) {
20223
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
20224
+ const component = str(o.component);
20225
+ if (component) return str(o.interface) ? `${component}.${String(o.interface)}` : component;
20226
+ if (typeof o.type === "string" && typeof o.details === "string") return `${o.type} ${o.details}`;
20227
+ return null;
20228
+ }
20217
20229
  function reportIdentityOf(field, item) {
20218
20230
  if (item !== null && typeof item === "object") {
20219
20231
  const step = item.stepNumber;
@@ -20265,6 +20277,9 @@ function identityFieldsOf(field) {
20265
20277
  switch (field) {
20266
20278
  case "dispatch":
20267
20279
  return ["capability"];
20280
+ // Bound: component + interface; not yet bound: type + details.
20281
+ case "publicInterfaces":
20282
+ return ["component", "interface", "type", "details"];
20268
20283
  case "lifecycle":
20269
20284
  return ["phase", "component", "method"];
20270
20285
  case "emits":
@@ -20677,6 +20692,18 @@ function restoreSpecFiles(snapshot) {
20677
20692
  function findLegacySpecFiles() {
20678
20693
  return current().findLegacySpecFiles();
20679
20694
  }
20695
+ function loadSpec(kind, id) {
20696
+ const workspace = current();
20697
+ return workspace.load(kind, id);
20698
+ }
20699
+ function saveSpec(kind, spec) {
20700
+ const workspace = current();
20701
+ return workspace.save(kind, spec);
20702
+ }
20703
+ function deleteSpec(kind, id) {
20704
+ const workspace = current();
20705
+ return workspace.delete(kind, id);
20706
+ }
20680
20707
  function updateSpec(kind, id, delta, hooks, dryRun) {
20681
20708
  return current().updateSpec(kind, id, delta, hooks, dryRun);
20682
20709
  }
@@ -21770,6 +21797,76 @@ var init_specs2 = __esm({
21770
21797
  return true;
21771
21798
  }
21772
21799
  // -------------------------------------------------------------------------
21800
+ // Kind-generic access (spec_index.load, spec_registry.save / delete)
21801
+ //
21802
+ // The one place a kind held as DATA becomes the typed loader, writer or
21803
+ // delete of that kind. Every caller that holds the kind as a value — the
21804
+ // gated whole-spec write, a delete by kind, the delta update, the method
21805
+ // move — used to repeat this switch, and each copy could drift from the
21806
+ // others (one of them forgot that a type's writer takes no status).
21807
+ // -------------------------------------------------------------------------
21808
+ /** One stored spec of the named kind, or null. The L0 is a singleton: its id is informational. */
21809
+ load(kind, id) {
21810
+ switch (kind) {
21811
+ case "system":
21812
+ return this.loadSystemSpec();
21813
+ case "subsystem":
21814
+ return this.loadSubsystemSpec(id);
21815
+ case "component":
21816
+ return this.loadComponentSpec(id);
21817
+ case "interface":
21818
+ return this.loadInterfaceSpec(id);
21819
+ case "implementation":
21820
+ return this.loadImplementationSpec(id);
21821
+ case "type":
21822
+ return this.loadTypeSpec(id);
21823
+ }
21824
+ }
21825
+ /**
21826
+ * Persist one spec through the typed writer of its kind, answering with that
21827
+ * writer's placement notices (none for the L0 and a subsystem). A draft or
21828
+ * status-less save keeps the stored status, exactly as the typed writers do.
21829
+ */
21830
+ save(kind, spec) {
21831
+ return this.saveOfKind(kind, spec);
21832
+ }
21833
+ /** The typed writer of a kind, with the caller's save options — the delta update and the move state a demotion. */
21834
+ saveOfKind(kind, spec, opts) {
21835
+ switch (kind) {
21836
+ case "system":
21837
+ this.saveSystemSpec(spec);
21838
+ return [];
21839
+ case "subsystem":
21840
+ this.saveSubsystemSpec(spec, opts);
21841
+ return [];
21842
+ case "component":
21843
+ return this.saveComponentSpec(spec, opts);
21844
+ case "interface":
21845
+ return this.saveInterfaceSpec(spec, opts);
21846
+ case "implementation":
21847
+ return this.saveImplementationSpec(spec, opts);
21848
+ case "type":
21849
+ return this.saveTypeSpec(spec, opts);
21850
+ }
21851
+ }
21852
+ /** Delete one spec document of the named kind; false when none was stored. The L0 cannot be deleted. */
21853
+ delete(kind, id) {
21854
+ switch (kind) {
21855
+ case "system":
21856
+ throw new Error("The L0 system spec cannot be deleted: it is the root every other spec hangs from.");
21857
+ case "subsystem":
21858
+ return this.deleteSubsystemSpec(id);
21859
+ case "component":
21860
+ return this.deleteComponentSpec(id);
21861
+ case "interface":
21862
+ return this.deleteInterfaceSpec(id);
21863
+ case "implementation":
21864
+ return this.deleteImplementationSpec(id);
21865
+ case "type":
21866
+ return this.deleteTypeSpec(id);
21867
+ }
21868
+ }
21869
+ // -------------------------------------------------------------------------
21773
21870
  // Spec status promotion (draft/design → complete)
21774
21871
  // -------------------------------------------------------------------------
21775
21872
  /** Every spec whose status is not yet 'complete', with its current status captured. */
@@ -21990,23 +22087,7 @@ var init_specs2 = __esm({
21990
22087
  */
21991
22088
  updateSpec(kind, id, delta, hooks, dryRun = false) {
21992
22089
  const notices = [];
21993
- const result = (() => {
21994
- switch (kind) {
21995
- case "system":
21996
- return this.loadSystemSpec();
21997
- // singleton — id is informational
21998
- case "subsystem":
21999
- return this.loadSubsystemSpec(id);
22000
- case "component":
22001
- return this.loadComponentSpec(id);
22002
- case "interface":
22003
- return this.loadInterfaceSpec(id);
22004
- case "implementation":
22005
- return this.loadImplementationSpec(id);
22006
- case "type":
22007
- return this.loadTypeSpec(id);
22008
- }
22009
- })();
22090
+ const result = this.load(kind, id);
22010
22091
  if (!result) {
22011
22092
  throw new Error(`Spec of kind "${kind}" with ID "${id}" does not exist. Define it first.`);
22012
22093
  }
@@ -22338,9 +22419,10 @@ var init_specs2 = __esm({
22338
22419
  const mergePublicInterfaces = (existing, delta2) => {
22339
22420
  const merged = [...existing];
22340
22421
  for (const deltaItem of delta2) {
22341
- const piKey = (i) => `${i?.component}.${i?.interface}`;
22422
+ const piKey = (i) => String(identityKeyOf("publicInterfaces", i));
22342
22423
  assertDeltaMarkers(deltaItem, `publicInterface "${piKey(deltaItem)}"`);
22343
- const idx = merged.findIndex((item) => item.component === deltaItem.component && item.interface === deltaItem.interface);
22424
+ const key = identityKeyOf("publicInterfaces", deltaItem);
22425
+ const idx = key === null ? -1 : merged.findIndex((item) => identityKeyOf("publicInterfaces", item) === key);
22344
22426
  if (idx !== -1) {
22345
22427
  if (deltaItem.remove === true || deltaItem.action === "delete") {
22346
22428
  merged.splice(idx, 1);
@@ -22483,14 +22565,14 @@ var init_specs2 = __esm({
22483
22565
  - ${labelErrors.join("\n- ")}`);
22484
22566
  }
22485
22567
  }
22486
- const canonical = (spec) => {
22568
+ const canonical2 = (spec) => {
22487
22569
  const parsed = deltaSchema.safeParse(spec);
22488
22570
  const value = { ...parsed.success ? parsed.data : spec };
22489
22571
  delete value.updatedAt;
22490
22572
  return value;
22491
22573
  };
22492
- const canonicalStored = canonical(storedBefore);
22493
- const canonicalMerged = canonical(mergedResult);
22574
+ const canonicalStored = canonical2(storedBefore);
22575
+ const canonicalMerged = canonical2(mergedResult);
22494
22576
  const changes = specChanges(canonicalStored, canonicalMerged);
22495
22577
  const ineffective = ineffectiveDeltaPaths(
22496
22578
  { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
@@ -22529,26 +22611,7 @@ var init_specs2 = __esm({
22529
22611
  const opts = {
22530
22612
  allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
22531
22613
  };
22532
- switch (kind) {
22533
- case "system":
22534
- this.saveSystemSpec(mergedResult);
22535
- break;
22536
- case "subsystem":
22537
- this.saveSubsystemSpec(mergedResult, opts);
22538
- break;
22539
- case "component":
22540
- notices.push(...this.saveComponentSpec(mergedResult, opts));
22541
- break;
22542
- case "interface":
22543
- notices.push(...this.saveInterfaceSpec(mergedResult, opts));
22544
- break;
22545
- case "implementation":
22546
- notices.push(...this.saveImplementationSpec(mergedResult, opts));
22547
- break;
22548
- case "type":
22549
- notices.push(...this.saveTypeSpec(mergedResult));
22550
- break;
22551
- }
22614
+ notices.push(...this.saveOfKind(kind, mergedResult, opts));
22552
22615
  return {
22553
22616
  kind,
22554
22617
  id,
@@ -22909,23 +22972,7 @@ var init_specs2 = __esm({
22909
22972
  }
22910
22973
  /** Persist one moved spec through the store's own writer, and answer with its placement notices. */
22911
22974
  saveMovedSpec(kind, spec) {
22912
- const opts = { allowStatusDemotion: true };
22913
- switch (kind) {
22914
- case "system":
22915
- this.saveSystemSpec(spec);
22916
- return [];
22917
- case "subsystem":
22918
- this.saveSubsystemSpec(spec, opts);
22919
- return [];
22920
- case "component":
22921
- return this.saveComponentSpec(spec, opts);
22922
- case "interface":
22923
- return this.saveInterfaceSpec(spec, opts);
22924
- case "implementation":
22925
- return this.saveImplementationSpec(spec, opts);
22926
- case "type":
22927
- return this.saveTypeSpec(spec);
22928
- }
22975
+ return this.saveOfKind(kind, spec, { allowStatusDemotion: true });
22929
22976
  }
22930
22977
  };
22931
22978
  DEFAULT_DEPENDENCY_LIMIT = 8;
@@ -29427,10 +29474,216 @@ function componentCandidateGate(options = candidateOptions()) {
29427
29474
  }
29428
29475
  };
29429
29476
  }
29430
- function addComponent(candidate) {
29431
- const verdict = validateComponentCandidate(candidate, candidateOptions());
29477
+ function restatementParent(restatement) {
29478
+ const spec = restatement.spec;
29479
+ switch (restatement.kind) {
29480
+ case "subsystem":
29481
+ return { kind: "system", id: "system" };
29482
+ case "component":
29483
+ return { kind: "subsystem", id: spec.subsystem };
29484
+ case "interface":
29485
+ return { kind: "component", id: spec.component };
29486
+ case "implementation":
29487
+ return { kind: "interface", id: spec.contract };
29488
+ default:
29489
+ return null;
29490
+ }
29491
+ }
29492
+ function applyRestatement(restatement, existing, parent) {
29493
+ const replacedExisting = existing !== null;
29494
+ const parentRef = restatementParent(restatement);
29495
+ if (parentRef && !parent) return refused(restatement, replacedExisting, MISSING_PARENT[restatement.kind](parentRef.id));
29496
+ const status2 = statusForCreate(restatement, existing);
29497
+ if ("refusal" in status2) return refused(restatement, replacedExisting, status2.refusal);
29498
+ const candidate = JSON.parse(JSON.stringify(restatement.spec));
29499
+ if (restatement.kind === "subsystem") candidate.parentSystem = parent.name;
29500
+ const carried = carryInto(restatement, existing, candidate);
29501
+ const cleared = clearedByOmission(existing, candidate, restatement.fields);
29502
+ stampLifecycle(candidate, existing, status2.status);
29503
+ const labelErrors = resolveLabelsOf(restatement.kind, candidate);
29504
+ if (labelErrors.length) {
29505
+ return refused(restatement, replacedExisting, `Unresolved narrative label references \u2014 nothing was saved:
29506
+ - ${labelErrors.join("\n- ")}`);
29507
+ }
29508
+ return {
29509
+ spec: candidate,
29510
+ ...status2.status ? { status: status2.status } : {},
29511
+ replacedExisting,
29512
+ notices: existing ? rewriteNotices(restatement.kind, existing, candidate, carried, cleared) : [],
29513
+ changedMethods: changedMethodsOf(restatement.kind, existing, candidate)
29514
+ };
29515
+ }
29516
+ function refused(restatement, replacedExisting, refusal2) {
29517
+ return { spec: restatement.spec, refusal: refusal2, replacedExisting, notices: [], changedMethods: [] };
29518
+ }
29519
+ function statusForCreate(restatement, existing) {
29520
+ if (!STATUSED_KINDS.has(restatement.kind)) return {};
29521
+ const stated = restatement.status;
29522
+ const stored = existing?.status;
29523
+ const held = stored !== void 0 && STATUS_ORDER.includes(stored) ? stored : void 0;
29524
+ if (stated === void 0) return { status: held ?? "draft" };
29525
+ if (held === void 0 || STATUS_ORDER.indexOf(stated) >= STATUS_ORDER.indexOf(held)) return { status: stated };
29526
+ const label = `${restatement.kind} "${restatement.spec.id}"`;
29527
+ return {
29528
+ refusal: `Refusing to re-author ${label} at status "${stated}": it is stored at "${held}", and a create tool never lowers a spec's status \u2014 a restatement that reopened a frozen spec would undo a lock without saying so. Nothing was written. To reopen it deliberately, use sdd_update_spec with {"status": "${stated}"}; to keep the level it has, leave status out.`
29529
+ };
29530
+ }
29531
+ function carryInto(restatement, existing, candidate) {
29532
+ if (!existing) return [];
29533
+ const carried = ["createdAt"];
29534
+ if (restatement.memberFields && Array.isArray(candidate.methods)) {
29535
+ const stored = existing.methods ?? [];
29536
+ for (const method2 of candidate.methods) {
29537
+ const prev = stored.find((p) => p.name === method2.name);
29538
+ for (const field of carryUnexpressed(prev, method2, restatement.memberFields)) carried.push(`${field} (${String(method2.name)})`);
29539
+ }
29540
+ }
29541
+ carried.push(...carryUnexpressed(existing, candidate, [...restatement.fields, "createdAt"]));
29542
+ return carried;
29543
+ }
29544
+ function carryUnexpressed(existing, next, expressed) {
29545
+ if (!existing) return [];
29546
+ const carried = [];
29547
+ for (const [field, value] of Object.entries(existing)) {
29548
+ if (value === void 0) continue;
29549
+ if (STORE_MANAGED_FIELDS.has(field)) continue;
29550
+ if (expressed.includes(field) && !ALWAYS_CARRIED_FIELDS.has(field)) continue;
29551
+ if (next[field] !== void 0) continue;
29552
+ next[field] = value;
29553
+ carried.push(field);
29554
+ }
29555
+ return carried;
29556
+ }
29557
+ function isEmptyValue(value) {
29558
+ if (value === void 0 || value === null) return true;
29559
+ if (Array.isArray(value)) return value.length === 0;
29560
+ if (typeof value === "string") return value.trim() === "";
29561
+ if (typeof value === "object") return Object.keys(value).length === 0;
29562
+ return false;
29563
+ }
29564
+ function clearedByOmission(existing, next, expressed) {
29565
+ if (!existing) return [];
29566
+ const stored = existing;
29567
+ const cleared = [];
29568
+ for (const field of expressed) {
29569
+ const before = stored[field];
29570
+ if (STORE_MANAGED_FIELDS.has(field) || ALWAYS_CARRIED_FIELDS.has(field)) continue;
29571
+ if (isEmptyValue(before) || !isEmptyValue(next[field])) continue;
29572
+ cleared.push(Array.isArray(before) ? `${field} (had ${before.length})` : field);
29573
+ }
29574
+ return cleared;
29575
+ }
29576
+ function stampLifecycle(candidate, existing, status2) {
29577
+ const now = (/* @__PURE__ */ new Date()).toISOString();
29578
+ candidate.createdAt = existing?.createdAt ?? now;
29579
+ candidate.updatedAt = now;
29580
+ if (status2) candidate.status = status2;
29581
+ else delete candidate.status;
29582
+ }
29583
+ function resolveLabelsOf(kind, candidate) {
29584
+ if (kind !== "implementation") return [];
29585
+ const methods = candidate.methods ?? [];
29586
+ return methods.flatMap((m) => resolveNarrativeLabels(m.name, m.narrative ?? []));
29587
+ }
29588
+ function removedByName(before, after) {
29589
+ if (!before?.length) return [];
29590
+ const kept = new Set((after ?? []).map((m) => m.name));
29591
+ return before.map((m) => m.name).filter((n) => !kept.has(n));
29592
+ }
29593
+ function rewriteNotices(kind, existing, candidate, carried, cleared) {
29594
+ const named = kind === "system" ? existing.name : String(candidate.id);
29595
+ const notices = [`${REWRITE_LABEL[kind]} "${named}" already existed \u2014 re-authored in place; this input REPLACES what it expresses.`];
29596
+ if (carried.length) notices.push(`Carried forward (not expressible through this tool): ${carried.join(", ")}.`);
29597
+ const removal = removalNotice(kind, existing, candidate);
29598
+ if (removal) notices.push(removal);
29599
+ if (cleared.length) {
29600
+ notices.push(
29601
+ `CLEARED by omission: ${cleared.join(", ")} \u2014 the argument was not repeated, so the previous value is gone. Use sdd_update_spec if you meant to leave it untouched.`
29602
+ );
29603
+ }
29604
+ return notices;
29605
+ }
29606
+ function removalNotice(kind, existing, candidate) {
29607
+ const before = existing;
29608
+ let removed = [];
29609
+ let noun = "method";
29610
+ let suffix = "";
29611
+ if (kind === "interface" || kind === "implementation") {
29612
+ removed = removedByName(before.methods, candidate.methods);
29613
+ suffix = kind === "interface" ? " (endpoint bindings included)" : " (L5 narratives included)";
29614
+ } else if (kind === "type") {
29615
+ removed = [
29616
+ ...removedByName(before.fields, candidate.fields).map((n) => `field ${n}`),
29617
+ ...removedByName(before.methods, candidate.methods).map((n) => `method ${n}`)
29618
+ ];
29619
+ noun = "member";
29620
+ }
29621
+ if (!removed.length) return void 0;
29622
+ return `REMOVED by this restatement: ${removed.length === 1 ? noun : `${noun}s`} ${removed.map((n) => `"${n}"`).join(", ")}${suffix} \u2014 absent from the input, so no longer in the spec. Restate them to keep them, or use sdd_update_spec to edit one member at a time.`;
29623
+ }
29624
+ function changedMethodsOf(kind, existing, candidate) {
29625
+ if (!existing || !(kind in METHOD_SCHEMA)) return [];
29626
+ const schema = METHOD_SCHEMA[kind];
29627
+ const stored = existing.methods ?? [];
29628
+ const next = candidate.methods ?? [];
29629
+ return stored.filter((method2) => {
29630
+ const restated = next.find((m) => m.name === method2.name);
29631
+ if (!restated) return true;
29632
+ const parsed = schema.safeParse(restated);
29633
+ return !parsed.success || canonical(parsed.data) !== canonical(method2);
29634
+ }).map((method2) => method2.name);
29635
+ }
29636
+ function canonical(value) {
29637
+ return JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0).sort(([a], [b]) => a.localeCompare(b))) : v);
29638
+ }
29639
+ function writeSpec(restatement) {
29640
+ const bound2 = candidateOptions();
29641
+ const parentRef = restatementParent(restatement);
29642
+ const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
29643
+ const id = restatement.kind === "system" ? "system" : restatement.spec.id;
29644
+ const existing = loadSpec(restatement.kind, id);
29645
+ const application = applyRestatement(restatement, existing, parent);
29646
+ if (application.refusal !== void 0) throw new Error(application.refusal);
29647
+ const gateNotices = restatement.kind === "component" ? judgeComponent(application.spec, bound2) : [];
29648
+ const persisted = persistCandidate(restatement.kind, application.spec);
29649
+ const testsToRevisit = testsInvalidatedBy(application.changedMethods, [existing, application.spec], bound2);
29650
+ return {
29651
+ kind: restatement.kind,
29652
+ id,
29653
+ name: application.spec.name,
29654
+ replacedExisting: application.replacedExisting,
29655
+ ...application.status ? { status: application.status } : {},
29656
+ notices: [...gateNotices, ...persisted.notices, ...application.notices],
29657
+ ...persisted.scaffoldedProjectPath ? { scaffoldedProjectPath: persisted.scaffoldedProjectPath } : {},
29658
+ testsToRevisit
29659
+ };
29660
+ }
29661
+ function judgeComponent(candidate, options) {
29662
+ const verdict = validateComponentCandidate(candidate, options);
29432
29663
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
29433
- return [...noticesFrom(verdict), ...saveComponentSpec(candidate)];
29664
+ return noticesFrom(verdict);
29665
+ }
29666
+ function persistCandidate(kind, spec) {
29667
+ const projectPath = kind === "subsystem" ? spec.projectPath : void 0;
29668
+ if (projectPath && projectPath.trim() !== "") {
29669
+ createChainedSubsystem(spec, spec.name);
29670
+ return { notices: [], scaffoldedProjectPath: projectPath };
29671
+ }
29672
+ return { notices: saveSpec(kind, spec) };
29673
+ }
29674
+ function testsInvalidatedBy(methods, specs, options) {
29675
+ const testRoots = options.rules?.conformance?.testRoots ?? [];
29676
+ if (methods.length === 0 || testRoots.length === 0) return [];
29677
+ const symbolOf = (name) => specs.map((s) => (s?.methods ?? []).find((m) => m.name === name)?.symbol).find((symbol) => symbol !== void 0);
29678
+ return findTestsReferencing2(methods.map((name) => ({ name, symbol: symbolOf(name) })), getProjectRoot(), testRoots);
29679
+ }
29680
+ function deleteSpec2(kind, id) {
29681
+ const bound2 = candidateOptions();
29682
+ const stored = loadSpec(kind, id);
29683
+ const deleted = deleteSpec(kind, id);
29684
+ const methods = deleted ? (stored?.methods ?? []).map((m) => m.name) : [];
29685
+ const testsToRevisit = testsInvalidatedBy(methods, [stored], bound2);
29686
+ return { kind, id, deleted, testsToRevisit };
29434
29687
  }
29435
29688
  function updateSpecGated(kind, id, delta, dryRun) {
29436
29689
  const bound2 = candidateOptions();
@@ -29481,14 +29734,39 @@ function judgeMoveHome(component, source, options) {
29481
29734
  function moveMethods2(from, to, methods, dryRun) {
29482
29735
  return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
29483
29736
  }
29737
+ var STORE_MANAGED_FIELDS, ALWAYS_CARRIED_FIELDS, STATUS_ORDER, STATUSED_KINDS, MISSING_PARENT, REWRITE_LABEL, METHOD_SCHEMA;
29484
29738
  var init_authoring = __esm({
29485
29739
  "src/core/authoring.ts"() {
29486
29740
  "use strict";
29487
- init_specs2();
29741
+ init_specs();
29742
+ init_core();
29488
29743
  init_validation();
29489
29744
  init_candidate();
29490
- init_core();
29745
+ init_narrative_labels();
29491
29746
  init_fs();
29747
+ STORE_MANAGED_FIELDS = /* @__PURE__ */ new Set(["status", "updatedAt"]);
29748
+ ALWAYS_CARRIED_FIELDS = /* @__PURE__ */ new Set(["ext"]);
29749
+ STATUS_ORDER = ["draft", "design", "complete"];
29750
+ STATUSED_KINDS = /* @__PURE__ */ new Set(["subsystem", "component", "interface", "implementation"]);
29751
+ MISSING_PARENT = {
29752
+ subsystem: () => "System spec must be initialized (sdd_initialize_system) first.",
29753
+ component: (id) => `Parent subsystem "${id}" does not exist.`,
29754
+ interface: (id) => `Component "${id}" does not exist.`,
29755
+ implementation: (id) => `Interface contract "${id}" does not exist.`
29756
+ };
29757
+ REWRITE_LABEL = {
29758
+ system: "System spec",
29759
+ subsystem: "Subsystem",
29760
+ component: "Component",
29761
+ interface: "Interface",
29762
+ implementation: "Implementation",
29763
+ type: "Type"
29764
+ };
29765
+ METHOD_SCHEMA = {
29766
+ interface: MethodSignatureSchema,
29767
+ implementation: MethodImplementationSchema,
29768
+ type: TypeMethodSchema
29769
+ };
29492
29770
  }
29493
29771
  });
29494
29772
 
@@ -29567,6 +29845,25 @@ function qualifiedComponentId(id) {
29567
29845
  const chained = ids.filter((known) => known.endsWith(`::${bare}`));
29568
29846
  return chained.length === 1 ? chained[0] : id;
29569
29847
  }
29848
+ function publicInterfacesReplacement(subsystem, stored, list2) {
29849
+ const identity = (pi) => pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
29850
+ const restated = /* @__PURE__ */ new Map();
29851
+ for (const pi of list2) restated.set(identity(pi), (restated.get(identity(pi)) ?? 0) + 1);
29852
+ const twice = [...restated].filter(([, n]) => n > 1).map(([key]) => `"${key}"`);
29853
+ if (twice.length) {
29854
+ return `Refusing to set the public interfaces of subsystem "${subsystem}": the list names ${twice.join(", ")} more than once. An entry is identified by the component and interface it is bound to \u2014 or, not yet bound, by its type and details \u2014 so two entries sharing one are the same entry stated twice. Drop the duplicate. Nothing was written.`;
29855
+ }
29856
+ const seen = /* @__PURE__ */ new Map();
29857
+ const deletes = [];
29858
+ for (const pi of stored) {
29859
+ const key = identity(pi);
29860
+ seen.set(key, (seen.get(key) ?? 0) + 1);
29861
+ if (seen.get(key) > (restated.get(key) ?? 0)) {
29862
+ deletes.push(pi.component ? { component: pi.component, interface: pi.interface, action: "delete" } : { type: pi.type, details: pi.details, action: "delete" });
29863
+ }
29864
+ }
29865
+ return { publicInterfaces: [...deletes, ...list2] };
29866
+ }
29570
29867
  function text(content) {
29571
29868
  return { content: [{ type: "text", text: content }] };
29572
29869
  }
@@ -29576,6 +29873,9 @@ function json(value) {
29576
29873
  function errText(message) {
29577
29874
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
29578
29875
  }
29876
+ function errMessage(e) {
29877
+ return errText(e instanceof Error ? e.message : String(e));
29878
+ }
29579
29879
  function structured(content, data) {
29580
29880
  return { content: [{ type: "text", text: content }], structuredContent: data };
29581
29881
  }
@@ -29593,25 +29893,30 @@ function renderChangeReport(report2) {
29593
29893
  lines.push("", "NO EFFECT:", ...report2.ineffective.map((i) => `- ${i}`));
29594
29894
  }
29595
29895
  if (report2.notices.length) lines.push("", "NOTICE:", ...report2.notices.map((n) => `- ${n}`));
29596
- if (report2.testsToRevisit.length) {
29597
- lines.push("", "TESTS TO REVISIT:");
29598
- for (const entry of report2.testsToRevisit) {
29599
- lines.push(`- ${entry.method} (searched as "${entry.symbol}")`);
29600
- if (entry.imported.length) lines.push(` imports it: ${entry.imported.join(", ")}`);
29601
- if (entry.mentioned.length) lines.push(` names it: ${entry.mentioned.join(", ")}`);
29602
- if (entry.indiscriminate) {
29603
- lines.push(" names it: withheld \u2014 the bare name matches more than a tenth of the suite, so the list carries no signal.");
29604
- }
29896
+ lines.push(...renderTestsToRevisit(report2.testsToRevisit));
29897
+ return lines.join("\n");
29898
+ }
29899
+ function renderTestsToRevisit(entries) {
29900
+ if (!entries.length) return [];
29901
+ const lines = ["", "TESTS TO REVISIT:"];
29902
+ for (const entry of entries) {
29903
+ lines.push(`- ${entry.method} (searched as "${entry.symbol}")`);
29904
+ if (entry.imported.length) lines.push(` imports it: ${entry.imported.join(", ")}`);
29905
+ if (entry.mentioned.length) lines.push(` names it: ${entry.mentioned.join(", ")}`);
29906
+ if (entry.indiscriminate) {
29907
+ lines.push(" names it: withheld \u2014 the bare name matches more than a tenth of the suite, so the list carries no signal.");
29605
29908
  }
29606
29909
  }
29607
- return lines.join("\n");
29910
+ return lines;
29608
29911
  }
29609
29912
  function writeReceipt(sentence, receipt) {
29610
29913
  const noticeBlock = receipt.notices.length ? `
29611
29914
 
29612
29915
  NOTICE:
29613
29916
  - ${receipt.notices.join("\n- ")}` : "";
29614
- return structured(`${sentence}${noticeBlock}`, receipt);
29917
+ const testsBlock = renderTestsToRevisit(receipt.testsToRevisit).join("\n");
29918
+ return structured(`${sentence}${noticeBlock}${testsBlock ? `
29919
+ ${testsBlock}` : ""}`, receipt);
29615
29920
  }
29616
29921
  function captureBuildStamp(entryPath) {
29617
29922
  try {
@@ -29653,74 +29958,6 @@ function reg(server, name, config, cb) {
29653
29958
  const strictConfig = config.inputSchema ? { ...config, inputSchema: import_zod11.z.object(config.inputSchema).strict() } : config;
29654
29959
  server.registerTool(name, strictConfig, guarded);
29655
29960
  }
29656
- function statusForCreate(label, stated, existing) {
29657
- const stored = existing?.status;
29658
- const held = stored !== void 0 && STATUS_ORDER.includes(stored) ? stored : void 0;
29659
- if (stated === void 0) return { status: held ?? "draft" };
29660
- if (held === void 0) return { status: stated };
29661
- const rank = (s) => STATUS_ORDER.indexOf(s);
29662
- if (rank(stated) < rank(held)) {
29663
- return {
29664
- refusal: `Refusing to re-author ${label} at status "${stated}": it is stored at "${held}", and a create tool never lowers a spec's status \u2014 a restatement that reopened a frozen spec would undo a lock without saying so. Nothing was written. To reopen it deliberately, use sdd_update_spec with {"status": "${stated}"}; to keep the level it has, leave status out.`
29665
- };
29666
- }
29667
- return { status: stated };
29668
- }
29669
- function isEmptyValue(value) {
29670
- if (value === void 0 || value === null) return true;
29671
- if (Array.isArray(value)) return value.length === 0;
29672
- if (typeof value === "string") return value.trim() === "";
29673
- if (typeof value === "object") return Object.keys(value).length === 0;
29674
- return false;
29675
- }
29676
- function carryUnexpressed(existing, next, expressed) {
29677
- if (!existing) return [];
29678
- const carried = [];
29679
- for (const [field, value] of Object.entries(existing)) {
29680
- if (value === void 0) continue;
29681
- if (STORE_MANAGED_FIELDS.has(field)) continue;
29682
- if (expressed.includes(field) && !ALWAYS_CARRIED_FIELDS.has(field)) continue;
29683
- if (next[field] !== void 0) continue;
29684
- next[field] = value;
29685
- carried.push(field);
29686
- }
29687
- return carried;
29688
- }
29689
- function clearedByOmission(existing, next, expressed) {
29690
- if (!existing) return [];
29691
- const cleared = [];
29692
- for (const field of expressed) {
29693
- const before = existing[field];
29694
- if (STORE_MANAGED_FIELDS.has(field) || ALWAYS_CARRIED_FIELDS.has(field)) continue;
29695
- if (isEmptyValue(before) || !isEmptyValue(next[field])) continue;
29696
- cleared.push(Array.isArray(before) ? `${field} (had ${before.length})` : field);
29697
- }
29698
- return cleared;
29699
- }
29700
- function removedByName(before, after) {
29701
- if (!before?.length) return [];
29702
- const kept = new Set((after ?? []).map((m) => m.name));
29703
- return before.map((m) => m.name).filter((n) => !kept.has(n));
29704
- }
29705
- function rewriteNotices(opts) {
29706
- const notices = [`${opts.label} already existed \u2014 re-authored in place; this input REPLACES what it expresses.`];
29707
- if (opts.carried.length) {
29708
- notices.push(`Carried forward (not expressible through this tool): ${opts.carried.join(", ")}.`);
29709
- }
29710
- if (opts.removed.length) {
29711
- const noun = opts.removedNoun ?? "method";
29712
- const plural = opts.removed.length === 1 ? noun : `${noun}s`;
29713
- notices.push(
29714
- `REMOVED by this restatement: ${plural} ${opts.removed.map((n) => `"${n}"`).join(", ")}${opts.removedSuffix ?? ""} \u2014 absent from the input, so no longer in the spec. Restate them to keep them, or use sdd_update_spec to edit one member at a time.`
29715
- );
29716
- }
29717
- if (opts.cleared.length) {
29718
- notices.push(
29719
- `CLEARED by omission: ${opts.cleared.join(", ")} \u2014 the argument was not repeated, so the previous value is gone. Use sdd_update_spec if you meant to leave it untouched.`
29720
- );
29721
- }
29722
- return notices;
29723
- }
29724
29961
  function listSkillResources2() {
29725
29962
  return listResources();
29726
29963
  }
@@ -29975,30 +30212,24 @@ function createMcpServer(options = {}) {
29975
30212
  },
29976
30213
  ({ name, vision, boundaries, globalRequirements, targetLanguage }) => {
29977
30214
  try {
29978
- const { loadSystemSpec: loadSystemSpec3, saveSystemSpec: saveSystemSpec2 } = requireSpecs();
29979
- const now = (/* @__PURE__ */ new Date()).toISOString();
29980
- const existing = loadSystemSpec3();
29981
- const next = {
29982
- name,
29983
- vision,
29984
- boundaries: boundaries ?? [],
29985
- globalRequirements: globalRequirements ?? [],
29986
- ...targetLanguage ? { targetLanguage } : {}
30215
+ const restatement = {
30216
+ kind: "system",
30217
+ spec: {
30218
+ name,
30219
+ vision,
30220
+ boundaries: boundaries ?? [],
30221
+ globalRequirements: globalRequirements ?? [],
30222
+ ...targetLanguage ? { targetLanguage } : {}
30223
+ },
30224
+ fields: systemInputFields
29987
30225
  };
29988
- const carried = carryUnexpressed(existing, next, systemInputFields);
29989
- const cleared = clearedByOmission(existing, next, systemInputFields);
29990
- next.schemaVersion ??= "1.0.0";
29991
- next.databases ??= [];
29992
- next.createdAt ??= now;
29993
- next.updatedAt = now;
29994
- saveSystemSpec2(next);
29995
- const notices = existing ? rewriteNotices({ label: `System spec "${existing.name}"`, carried, cleared, removed: [] }) : [];
30226
+ const receipt = writeSpec(restatement);
29996
30227
  return writeReceipt(
29997
- `Successfully ${existing ? "re-authored" : "initialized"} L0 System Spec for "${name}".`,
29998
- { kind: "system", id: "system", name, replacedExisting: Boolean(existing), notices }
30228
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "initialized"} L0 System Spec for "${name}".`,
30229
+ receipt
29999
30230
  );
30000
30231
  } catch (e) {
30001
- return errText(String(e));
30232
+ return errMessage(e);
30002
30233
  }
30003
30234
  }
30004
30235
  );
@@ -30039,53 +30270,33 @@ function createMcpServer(options = {}) {
30039
30270
  },
30040
30271
  ({ id, name, description, publicInterfaces, projectPath, targetLanguage, profile, designDepth, trustedLinks, lifecycle, status: status2 }) => {
30041
30272
  try {
30042
- const { loadSystemSpec: loadSystemSpec3, loadSubsystemSpec: loadSubsystemSpec2, saveSubsystemSpec: saveSubsystemSpec2 } = requireSpecs();
30043
- const system = loadSystemSpec3();
30044
- if (!system) return errText("System spec must be initialized (sdd_initialize_system) first.");
30045
- const now = (/* @__PURE__ */ new Date()).toISOString();
30046
- const existing = loadSubsystemSpec2(id);
30047
- const resolved = statusForCreate(`subsystem "${id}"`, status2, existing);
30048
- if ("refusal" in resolved) return errText(resolved.refusal);
30049
- const spec = {
30050
- id,
30051
- name,
30052
- description,
30053
- parentSystem: system.name,
30054
- publicInterfaces: publicInterfaces ?? [],
30055
- projectPath,
30056
- ...targetLanguage ? { targetLanguage } : {},
30057
- ...profile ? { profile } : {},
30058
- ...designDepth ? { designDepth } : {},
30059
- ...lifecycle ? { lifecycle } : {},
30060
- trustedLinks: trustedLinks ?? [],
30061
- status: resolved.status,
30062
- createdAt: now,
30063
- updatedAt: now
30064
- };
30065
- const carried = carryUnexpressed(existing, spec, subsystemInputFields);
30066
- const cleared = clearedByOmission(existing, spec, subsystemInputFields);
30067
- if (existing) spec.createdAt = existing.createdAt;
30068
- const notices = existing ? rewriteNotices({ label: `Subsystem "${id}"`, carried: ["createdAt", ...carried], cleared, removed: [] }) : [];
30069
- const receipt = {
30273
+ const restatement = {
30070
30274
  kind: "subsystem",
30071
- id,
30072
- name,
30073
- replacedExisting: Boolean(existing),
30074
- status: resolved.status,
30075
- notices
30275
+ spec: {
30276
+ id,
30277
+ name,
30278
+ description,
30279
+ publicInterfaces: publicInterfaces ?? [],
30280
+ projectPath,
30281
+ ...targetLanguage ? { targetLanguage } : {},
30282
+ ...profile ? { profile } : {},
30283
+ ...designDepth ? { designDepth } : {},
30284
+ ...lifecycle ? { lifecycle } : {},
30285
+ trustedLinks: trustedLinks ?? []
30286
+ },
30287
+ fields: subsystemInputFields,
30288
+ status: status2
30076
30289
  };
30077
- if (projectPath && projectPath.trim() !== "") {
30078
- const { createChainedSubsystem: createChainedSubsystem3 } = requireProvision();
30079
- createChainedSubsystem3(spec, name);
30290
+ const receipt = writeSpec(restatement);
30291
+ if (receipt.scaffoldedProjectPath) {
30080
30292
  return writeReceipt(
30081
- `Successfully added external subsystem "${name}" (${id}) and scaffolded its child project at ${projectPath}.`,
30082
- { ...receipt, scaffoldedProjectPath: projectPath }
30293
+ `Successfully added external subsystem "${name}" (${id}) and scaffolded its child project at ${receipt.scaffoldedProjectPath}.`,
30294
+ receipt
30083
30295
  );
30084
30296
  }
30085
- saveSubsystemSpec2(spec);
30086
30297
  return writeReceipt(`Successfully added L1 Subsystem Spec "${name}" (${id}).`, receipt);
30087
30298
  } catch (e) {
30088
- return errText(String(e));
30299
+ return errMessage(e);
30089
30300
  }
30090
30301
  }
30091
30302
  );
@@ -30093,7 +30304,7 @@ function createMcpServer(options = {}) {
30093
30304
  server,
30094
30305
  "sdd_set_public_interfaces",
30095
30306
  {
30096
- description: "Set (replace) an existing subsystem's publicInterfaces, binding each to the component (and optional interface) that realizes it. Use this to backfill bindings once the subsystem's components exist \u2014 cross-subsystem dependencies may only target a published public component.",
30307
+ description: "Set (replace) an existing subsystem's publicInterfaces, binding each to the component (and optional interface) that realizes it. Use this to backfill bindings once the subsystem's components exist \u2014 cross-subsystem dependencies may only target a published public component. A replacement, not a merge: an entry left out is removed. An entry's identity is its component and interface, so a list naming one identity twice is refused. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every entry added, changed or removed, or that nothing changed \u2014 as structured content beside the sentence.",
30097
30308
  inputSchema: {
30098
30309
  subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
30099
30310
  publicInterfaces: import_zod11.z.array(import_zod11.z.object({
@@ -30102,21 +30313,24 @@ function createMcpServer(options = {}) {
30102
30313
  component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface"),
30103
30314
  interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component")
30104
30315
  }).strict()).describe("The full replacement list of public interfaces for this subsystem")
30105
- }
30316
+ },
30317
+ outputSchema: specChangeReportOutput
30106
30318
  },
30107
30319
  ({ subsystem, publicInterfaces }) => {
30108
30320
  try {
30109
- const { loadSubsystemSpec: loadSubsystemSpec2, saveSubsystemSpec: saveSubsystemSpec2 } = requireSpecs();
30321
+ const { loadSubsystemSpec: loadSubsystemSpec2 } = requireSpecs();
30110
30322
  const sub = loadSubsystemSpec2(subsystem);
30111
30323
  if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
30112
- saveSubsystemSpec2({
30113
- ...sub,
30114
- publicInterfaces,
30115
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30116
- });
30117
- return text(`Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).`);
30324
+ const delta = publicInterfacesReplacement(subsystem, sub.publicInterfaces ?? [], publicInterfaces);
30325
+ if (typeof delta === "string") return errText(delta);
30326
+ const report2 = updateSpecGated("subsystem", subsystem, delta);
30327
+ return structured(
30328
+ `Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).
30329
+ ${renderChangeReport(report2)}`,
30330
+ report2
30331
+ );
30118
30332
  } catch (e) {
30119
- return errText(String(e));
30333
+ return errMessage(e);
30120
30334
  }
30121
30335
  }
30122
30336
  );
@@ -30124,25 +30338,24 @@ function createMcpServer(options = {}) {
30124
30338
  server,
30125
30339
  "sdd_set_subsystem_project_path",
30126
30340
  {
30127
- description: "Set (or clear) the projectPath of an L1 Subsystem for subsystem chaining. projectPath should be a relative path to the external project root directory.",
30341
+ description: "Set (or clear) the projectPath of an L1 Subsystem for subsystem chaining. projectPath should be a relative path to the external project root directory. It rewrites the link only (sdd_move_subsystem_project relocates the directory with it). Written through the authoring seam as a gated delta, and answered with the change report as structured content beside the sentence.",
30128
30342
  inputSchema: {
30129
30343
  subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
30130
30344
  projectPath: import_zod11.z.string().optional().describe("Relative path to external project root (or omit to clear)")
30131
- }
30345
+ },
30346
+ outputSchema: specChangeReportOutput
30132
30347
  },
30133
30348
  ({ subsystem, projectPath }) => {
30134
30349
  try {
30135
- const { loadSubsystemSpec: loadSubsystemSpec2, saveSubsystemSpec: saveSubsystemSpec2 } = requireSpecs();
30136
- const sub = loadSubsystemSpec2(subsystem);
30137
- if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
30138
- saveSubsystemSpec2({
30139
- ...sub,
30140
- projectPath,
30141
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30142
- });
30143
- return text(`Updated projectPath for subsystem "${subsystem}" to: ${projectPath || "none (cleared)"}`);
30350
+ const delta = projectPath ? { projectPath } : { unset: ["projectPath"] };
30351
+ const report2 = updateSpecGated("subsystem", subsystem, delta);
30352
+ return structured(
30353
+ `Updated projectPath for subsystem "${subsystem}" to: ${projectPath || "none (cleared)"}
30354
+ ${renderChangeReport(report2)}`,
30355
+ report2
30356
+ );
30144
30357
  } catch (e) {
30145
- return errText(String(e));
30358
+ return errMessage(e);
30146
30359
  }
30147
30360
  }
30148
30361
  );
@@ -30315,53 +30528,37 @@ function createMcpServer(options = {}) {
30315
30528
  },
30316
30529
  ({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, mounts, durability, dependencyClass, emits, subscribesTo, ext, status: status2 }) => {
30317
30530
  try {
30318
- const { loadSubsystemSpec: loadSubsystemSpec2, loadComponentSpec: loadComponentSpec2 } = requireSpecs();
30319
- const sub = loadSubsystemSpec2(subsystem);
30320
- if (!sub) return errText(`Parent subsystem "${subsystem}" does not exist.`);
30321
- const now = (/* @__PURE__ */ new Date()).toISOString();
30322
- const existing = loadComponentSpec2(id);
30323
- const resolved = statusForCreate(`component "${id}"`, status2, existing);
30324
- if ("refusal" in resolved) return errText(resolved.refusal);
30325
- const candidate = {
30326
- id,
30327
- name,
30328
- description,
30329
- subsystem,
30330
- componentType,
30331
- owns: owns ?? [],
30332
- dependsOn: dependsOn ?? [],
30333
- ...portalType ? { portalType } : {},
30334
- ...basePath ? { basePath } : {},
30335
- ...dispatch ? { dispatch } : {},
30336
- // An EMPTY list is kept: declaring the field is what marks a listener.
30337
- ...mounts ? { mounts } : {},
30338
- ...durability ? { durability } : {},
30339
- ...dependencyClass ? { dependencyClass } : {},
30340
- ...emits ? { emits } : {},
30341
- ...subscribesTo ? { subscribesTo } : {},
30342
- ...ext ? { ext } : {},
30343
- status: resolved.status,
30344
- createdAt: now,
30345
- updatedAt: now
30531
+ const restatement = {
30532
+ kind: "component",
30533
+ spec: {
30534
+ id,
30535
+ name,
30536
+ description,
30537
+ subsystem,
30538
+ componentType,
30539
+ owns: owns ?? [],
30540
+ dependsOn: dependsOn ?? [],
30541
+ ...portalType ? { portalType } : {},
30542
+ ...basePath ? { basePath } : {},
30543
+ ...dispatch ? { dispatch } : {},
30544
+ // An EMPTY list is kept: declaring the field is what marks a listener.
30545
+ ...mounts ? { mounts } : {},
30546
+ ...durability ? { durability } : {},
30547
+ ...dependencyClass ? { dependencyClass } : {},
30548
+ ...emits ? { emits } : {},
30549
+ ...subscribesTo ? { subscribesTo } : {},
30550
+ ...ext ? { ext } : {}
30551
+ },
30552
+ fields: componentInputFields,
30553
+ status: status2
30346
30554
  };
30347
- const carried = carryUnexpressed(existing, candidate, componentInputFields);
30348
- const cleared = clearedByOmission(existing, candidate, componentInputFields);
30349
- if (existing) candidate.createdAt = existing.createdAt;
30350
- const notices = addComponent(candidate);
30351
- if (existing) {
30352
- notices.push(...rewriteNotices({
30353
- label: `Component "${id}"`,
30354
- carried: ["createdAt", ...carried],
30355
- cleared,
30356
- removed: []
30357
- }));
30358
- }
30555
+ const receipt = writeSpec(restatement);
30359
30556
  return writeReceipt(
30360
- `Successfully ${existing ? "re-authored" : "added"} L2 Component Spec "${name}" (${id}, ${componentType}).`,
30361
- { kind: "component", id, name, replacedExisting: Boolean(existing), status: resolved.status, notices }
30557
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "added"} L2 Component Spec "${name}" (${id}, ${componentType}).`,
30558
+ receipt
30362
30559
  );
30363
30560
  } catch (e) {
30364
- return errText(String(e));
30561
+ return errMessage(e);
30365
30562
  }
30366
30563
  }
30367
30564
  );
@@ -30411,51 +30608,20 @@ function createMcpServer(options = {}) {
30411
30608
  },
30412
30609
  ({ id, name, description, component, methods, status: status2 }) => {
30413
30610
  try {
30414
- const { loadComponentSpec: loadComponentSpec2, loadInterfaceSpec: loadInterfaceSpec2, saveInterfaceSpec: saveInterfaceSpec2 } = requireSpecs();
30415
- const comp = loadComponentSpec2(component);
30416
- if (!comp) return errText(`Component "${component}" does not exist.`);
30417
- const now = (/* @__PURE__ */ new Date()).toISOString();
30418
- const existing = loadInterfaceSpec2(id);
30419
- const resolved = statusForCreate(`interface "${id}"`, status2, existing);
30420
- if ("refusal" in resolved) return errText(resolved.refusal);
30421
- const carried = [];
30422
- const specMethods = (methods ?? []).map((m) => {
30423
- const prev = existing?.methods.find((p) => p.name === m.name);
30424
- const next = { ...m };
30425
- for (const field of carryUnexpressed(prev, next, interfaceMethodInputFields)) {
30426
- carried.push(`${field} (${m.name})`);
30427
- }
30428
- return next;
30429
- });
30430
- const spec = {
30431
- id,
30432
- name,
30433
- description,
30434
- component,
30435
- methods: specMethods,
30436
- status: resolved.status,
30437
- createdAt: now,
30438
- updatedAt: now
30611
+ const restatement = {
30612
+ kind: "interface",
30613
+ spec: { id, name, description, component, methods: methods ?? [] },
30614
+ fields: interfaceInputFields,
30615
+ memberFields: interfaceMethodInputFields,
30616
+ status: status2
30439
30617
  };
30440
- carried.push(...carryUnexpressed(existing, spec, interfaceInputFields));
30441
- const cleared = clearedByOmission(existing, spec, interfaceInputFields);
30442
- if (existing) spec.createdAt = existing.createdAt;
30443
- const notices = saveInterfaceSpec2(spec);
30444
- if (existing) {
30445
- notices.push(...rewriteNotices({
30446
- label: `Interface "${id}"`,
30447
- carried: ["createdAt", ...carried],
30448
- cleared,
30449
- removed: removedByName(existing.methods, specMethods),
30450
- removedSuffix: " (endpoint bindings included)"
30451
- }));
30452
- }
30618
+ const receipt = writeSpec(restatement);
30453
30619
  return writeReceipt(
30454
- `Successfully ${existing ? "re-authored" : "defined"} L3 Interface Contract "${name}" (${id}).`,
30455
- { kind: "interface", id, name, replacedExisting: Boolean(existing), status: resolved.status, notices }
30620
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "defined"} L3 Interface Contract "${name}" (${id}).`,
30621
+ receipt
30456
30622
  );
30457
30623
  } catch (e) {
30458
- return errText(String(e));
30624
+ return errMessage(e);
30459
30625
  }
30460
30626
  }
30461
30627
  );
@@ -30463,7 +30629,7 @@ function createMcpServer(options = {}) {
30463
30629
  server,
30464
30630
  "sdd_set_endpoints",
30465
30631
  {
30466
- description: "Bind concrete wire endpoints to existing L3 interface methods (required for every Portal). Pick `transport` and fill that transport's address fields. Run after sdd_define_interface.",
30632
+ description: "Bind concrete wire endpoints to existing L3 interface methods (required for every Portal). Pick `transport` and fill that transport's address fields. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
30467
30633
  inputSchema: {
30468
30634
  interface: import_zod11.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
30469
30635
  endpoints: import_zod11.z.array(import_zod11.z.object({
@@ -30484,11 +30650,12 @@ function createMcpServer(options = {}) {
30484
30650
  command: import_zod11.z.string().optional().describe("CLI: command/subcommand"),
30485
30651
  address: import_zod11.z.string().optional().describe("Custom: free-form address")
30486
30652
  }).strict()).describe("One binding per method")
30487
- }
30653
+ },
30654
+ outputSchema: specChangeReportOutput
30488
30655
  },
30489
30656
  ({ interface: interfaceId, endpoints }) => {
30490
30657
  try {
30491
- const { loadInterfaceSpec: loadInterfaceSpec2, saveInterfaceSpec: saveInterfaceSpec2 } = requireSpecs();
30658
+ const { loadInterfaceSpec: loadInterfaceSpec2 } = requireSpecs();
30492
30659
  const intf = loadInterfaceSpec2(interfaceId);
30493
30660
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
30494
30661
  const buildRaw = (e) => {
@@ -30512,6 +30679,7 @@ function createMcpServer(options = {}) {
30512
30679
  }
30513
30680
  };
30514
30681
  const bound2 = [];
30682
+ const methods = [];
30515
30683
  for (const e of endpoints) {
30516
30684
  const m = intf.methods.find((x) => x.name === e.method);
30517
30685
  if (!m) return errText(`Method "${e.method}" not found on interface "${interfaceId}". Define it via sdd_define_interface first.`);
@@ -30520,14 +30688,17 @@ function createMcpServer(options = {}) {
30520
30688
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
30521
30689
  return errText(`Invalid ${e.transport} endpoint for method "${e.method}": ${detail}`);
30522
30690
  }
30523
- m.endpoint = parsed.data;
30691
+ methods.push({ name: e.method, endpoint: parsed.data });
30524
30692
  bound2.push(`${e.method}\u2192${e.transport}`);
30525
30693
  }
30526
- intf.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
30527
- saveInterfaceSpec2(intf);
30528
- return text(`Bound ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.`);
30694
+ const report2 = updateSpecGated("interface", interfaceId, { methods });
30695
+ return structured(
30696
+ `Bound ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
30697
+ ${renderChangeReport(report2)}`,
30698
+ report2
30699
+ );
30529
30700
  } catch (e) {
30530
- return errText(String(e));
30701
+ return errMessage(e);
30531
30702
  }
30532
30703
  }
30533
30704
  );
@@ -30607,65 +30778,35 @@ function createMcpServer(options = {}) {
30607
30778
  },
30608
30779
  ({ id, name, description, contract, sourcePath, simPath, technologies, injectedParams, detail, conformance, methods, status: status2 }) => {
30609
30780
  try {
30610
- const { loadInterfaceSpec: loadInterfaceSpec2, loadImplementationSpec: loadImplementationSpec2, saveImplementationSpec: saveImplementationSpec2 } = requireSpecs();
30611
- const intf = loadInterfaceSpec2(contract);
30612
- if (!intf) return errText(`Interface contract "${contract}" does not exist.`);
30613
- const existing = loadImplementationSpec2(id);
30614
- const resolved = statusForCreate(`implementation "${id}"`, status2, existing);
30615
- if ("refusal" in resolved) return errText(resolved.refusal);
30616
- const carried = [];
30617
- const resolvedMethods = (methods ?? []).map((m) => {
30618
- const prev = existing?.methods.find((p) => p.name === m.name);
30619
- const next = {
30620
- ...m,
30621
- narrative: (m.narrative ?? []).map((s, i) => ({ ...s, stepNumber: s.stepNumber ?? i + 1 }))
30622
- };
30623
- for (const field of carryUnexpressed(prev, next, implMethodInputFields)) {
30624
- carried.push(`${field} (${m.name})`);
30625
- }
30626
- return next;
30627
- });
30628
- const labelErrors = resolvedMethods.flatMap((m) => resolveNarrativeLabels(m.name, m.narrative));
30629
- if (labelErrors.length) return errText(`Unresolved narrative label references \u2014 nothing was saved:
30630
- - ${labelErrors.join("\n- ")}`);
30631
- const now = (/* @__PURE__ */ new Date()).toISOString();
30632
- const spec = {
30633
- id,
30634
- name,
30635
- description,
30636
- contract,
30637
- sourcePath,
30638
- simPath,
30639
- technologies,
30640
- injectedParams,
30641
- detail,
30642
- conformance,
30643
- // Post-resolution every cases/catches entry has its numeric step —
30644
- // the *Label twins exist only in the input type, not the spec's.
30645
- methods: resolvedMethods,
30646
- status: resolved.status,
30647
- createdAt: now,
30648
- updatedAt: now
30781
+ const restatement = {
30782
+ kind: "implementation",
30783
+ spec: {
30784
+ id,
30785
+ name,
30786
+ description,
30787
+ contract,
30788
+ sourcePath,
30789
+ simPath,
30790
+ technologies,
30791
+ injectedParams,
30792
+ detail,
30793
+ conformance,
30794
+ methods: (methods ?? []).map((m) => ({
30795
+ ...m,
30796
+ narrative: (m.narrative ?? []).map((s, i) => ({ ...s, stepNumber: s.stepNumber ?? i + 1 }))
30797
+ }))
30798
+ },
30799
+ fields: implInputFields,
30800
+ memberFields: implMethodInputFields,
30801
+ status: status2
30649
30802
  };
30650
- carried.push(...carryUnexpressed(existing, spec, implInputFields));
30651
- const cleared = clearedByOmission(existing, spec, implInputFields);
30652
- if (existing) spec.createdAt = existing.createdAt;
30653
- const notices = saveImplementationSpec2(spec);
30654
- if (existing) {
30655
- notices.push(...rewriteNotices({
30656
- label: `Implementation "${id}"`,
30657
- carried: ["createdAt", ...carried],
30658
- cleared,
30659
- removed: removedByName(existing.methods, resolvedMethods),
30660
- removedSuffix: " (L5 narratives included)"
30661
- }));
30662
- }
30803
+ const receipt = writeSpec(restatement);
30663
30804
  return writeReceipt(
30664
30805
  `Successfully saved L4 Implementation Spec "${name}" (${id}) with method narratives.`,
30665
- { kind: "implementation", id, name, replacedExisting: Boolean(existing), status: resolved.status, notices }
30806
+ receipt
30666
30807
  );
30667
30808
  } catch (e) {
30668
- return errText(String(e));
30809
+ return errMessage(e);
30669
30810
  }
30670
30811
  }
30671
30812
  );
@@ -30714,9 +30855,6 @@ function createMcpServer(options = {}) {
30714
30855
  },
30715
30856
  ({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol }) => {
30716
30857
  try {
30717
- const { loadTypeSpec: loadTypeSpec2, saveTypeSpec: saveTypeSpec2 } = requireSpecs();
30718
- const now = (/* @__PURE__ */ new Date()).toISOString();
30719
- const existing = loadTypeSpec2(id);
30720
30858
  const spec = {
30721
30859
  kind,
30722
30860
  id,
@@ -30739,32 +30877,15 @@ function createMcpServer(options = {}) {
30739
30877
  ...table ? { table } : {},
30740
30878
  ...linkedEntity ? { linkedEntity } : {},
30741
30879
  ...sourcePath ? { sourcePath } : {},
30742
- ...symbol ? { symbol } : {},
30743
- createdAt: now,
30744
- updatedAt: now
30880
+ ...symbol ? { symbol } : {}
30745
30881
  };
30746
- const carried = carryUnexpressed(existing, spec, typeInputFields);
30747
- const cleared = clearedByOmission(existing, spec, typeInputFields);
30748
- if (existing) spec.createdAt = existing.createdAt;
30749
- const notices = saveTypeSpec2(spec);
30750
- if (existing) {
30751
- notices.push(...rewriteNotices({
30752
- label: `Type "${id}"`,
30753
- carried: ["createdAt", ...carried],
30754
- cleared,
30755
- removed: [
30756
- ...removedByName(existing.fields, spec.fields).map((n) => `field ${n}`),
30757
- ...removedByName(existing.methods, spec.methods).map((n) => `method ${n}`)
30758
- ],
30759
- removedNoun: "member"
30760
- }));
30761
- }
30882
+ const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields });
30762
30883
  return writeReceipt(
30763
- `Successfully ${existing ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
30764
- { kind: "type", id, name, replacedExisting: Boolean(existing), notices }
30884
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
30885
+ receipt
30765
30886
  );
30766
30887
  } catch (e) {
30767
- return errText(String(e));
30888
+ return errMessage(e);
30768
30889
  }
30769
30890
  }
30770
30891
  );
@@ -30883,35 +31004,20 @@ function createMcpServer(options = {}) {
30883
31004
  server,
30884
31005
  "sdd_delete_spec",
30885
31006
  {
30886
- description: "Delete a specification file from the spec tree and clean up any empty parent directories.",
31007
+ description: "Delete a specification file from the spec tree and clean up any empty parent directories. Answers with a structured deletion beside the sentence: whether a spec was removed, and the tests that encode a method the deletion took away (when the project declares test roots).",
30887
31008
  inputSchema: {
30888
31009
  kind: import_zod11.z.enum(["subsystem", "component", "interface", "implementation", "type"]).describe("The kind of spec to delete"),
30889
31010
  id: import_zod11.z.string().describe("The ID of the spec to delete")
30890
- }
31011
+ },
31012
+ outputSchema: specDeletionOutput
30891
31013
  },
30892
31014
  ({ kind, id }) => {
30893
31015
  try {
30894
- const specs = requireSpecs();
30895
- let deleted = false;
30896
- switch (kind) {
30897
- case "subsystem":
30898
- deleted = specs.deleteSubsystemSpec(id);
30899
- break;
30900
- case "component":
30901
- deleted = specs.deleteComponentSpec(id);
30902
- break;
30903
- case "interface":
30904
- deleted = specs.deleteInterfaceSpec(id);
30905
- break;
30906
- case "implementation":
30907
- deleted = specs.deleteImplementationSpec(id);
30908
- break;
30909
- case "type":
30910
- deleted = specs.deleteTypeSpec(id);
30911
- break;
30912
- }
30913
- if (!deleted) return errText(`Spec of kind "${kind}" with ID "${id}" could not be deleted (file may not exist).`);
30914
- return text(`Successfully deleted ${kind} spec "${id}".`);
31016
+ const deletion = deleteSpec2(kind, id);
31017
+ if (!deletion.deleted) return errText(`Spec of kind "${kind}" with ID "${id}" could not be deleted (file may not exist).`);
31018
+ const testsBlock = renderTestsToRevisit(deletion.testsToRevisit).join("\n");
31019
+ return structured(`Successfully deleted ${kind} spec "${id}".${testsBlock ? `
31020
+ ${testsBlock}` : ""}`, deletion);
30915
31021
  } catch (e) {
30916
31022
  return errText(String(e));
30917
31023
  }
@@ -30925,7 +31031,7 @@ function createMcpServer(options = {}) {
30925
31031
  inputSchema: {
30926
31032
  kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project gateway surface, each entry {id, name, subsystem, component, type, details, audience: project|department|instance|partner|external}; id is informational)"),
30927
31033
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
30928
- delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
31034
+ delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
30929
31035
  dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
30930
31036
  },
30931
31037
  outputSchema: specChangeReportOutput
@@ -31142,7 +31248,7 @@ async function startMcpServer() {
31142
31248
  } catch {
31143
31249
  }
31144
31250
  }
31145
- var import_mcp, import_stdio, import_zod11, import_types3, fs21, path32, import_url, TYPE_REF_GRAMMAR, staleServerOutput, SPEC_KINDS, STATUS_ORDER, specWriteReceiptOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, validateTreeOutput, getSpecOutput, SERVER_BUILD_STAMP, STALE_SERVER_WARNING, SPEC_WRITE_TOOLS, listChangedEmitters, STORE_MANAGED_FIELDS, statusInput, ALWAYS_CARRIED_FIELDS, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
31251
+ var import_mcp, import_stdio, import_zod11, import_types3, fs21, path32, import_url, TYPE_REF_GRAMMAR, staleServerOutput, SPEC_KINDS, STATUS_VALUES, testsToRevisitOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, validateTreeOutput, getSpecOutput, SERVER_BUILD_STAMP, STALE_SERVER_WARNING, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
31146
31252
  var init_server = __esm({
31147
31253
  "src/mcp/server.ts"() {
31148
31254
  "use strict";
@@ -31159,7 +31265,6 @@ var init_server = __esm({
31159
31265
  init_core();
31160
31266
  init_core();
31161
31267
  init_defaults();
31162
- init_narrative_labels();
31163
31268
  init_specs();
31164
31269
  init_skills();
31165
31270
  init_specs2();
@@ -31181,7 +31286,16 @@ var init_server = __esm({
31181
31286
  )
31182
31287
  };
31183
31288
  SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
31184
- STATUS_ORDER = ["draft", "design", "complete"];
31289
+ STATUS_VALUES = ["draft", "design", "complete"];
31290
+ testsToRevisitOutput = import_zod11.z.array(import_zod11.z.object({
31291
+ method: import_zod11.z.string().describe("The contract method this write changed or deleted."),
31292
+ symbol: import_zod11.z.string().describe("The code-level name searched for: the method's `symbol` when it declares one, else its name."),
31293
+ imported: import_zod11.z.array(import_zod11.z.string()).describe("Test files that IMPORT that symbol \u2014 the high-confidence list, because an import is a binding and not a coincidence."),
31294
+ mentioned: import_zod11.z.array(import_zod11.z.string()).describe("Test files that name the symbol without importing it \u2014 usually a test driving it through a portal. Empty when `indiscriminate` is true."),
31295
+ indiscriminate: import_zod11.z.boolean().describe("True when the bare name matched more than a tenth of the test suite, so the mention list was withheld as noise.")
31296
+ })).describe(
31297
+ "The tests that encode a method this write changed or deleted, one entry per such method. Empty when the write touched no method, when the project declares no `rules.conformance.testRoots`, or when nothing references them. Read it before promising that existing tests still pass."
31298
+ );
31185
31299
  specWriteReceiptOutput = {
31186
31300
  kind: import_zod11.z.enum(SPEC_KINDS).describe("The spec kind written."),
31187
31301
  id: import_zod11.z.string().describe('The id the spec is stored under; "system" for the L0 singleton.'),
@@ -31189,8 +31303,8 @@ var init_server = __esm({
31189
31303
  replacedExisting: import_zod11.z.boolean().describe(
31190
31304
  "True when a spec already held this id and the call re-authored it in place rather than adding one. The notices then say what was carried forward, removed or cleared by the restatement."
31191
31305
  ),
31192
- status: import_zod11.z.enum(STATUS_ORDER).optional().describe(
31193
- "The lifecycle status written. Absent for a type, which carries none."
31306
+ status: import_zod11.z.enum(STATUS_VALUES).optional().describe(
31307
+ "The lifecycle status written. Absent for the L0 and a type, which carry none."
31194
31308
  ),
31195
31309
  notices: import_zod11.z.array(import_zod11.z.string()).describe(
31196
31310
  "The notice lines the text answer lists, one per entry: what was carried forward, what a restatement removed, what an omission cleared, and every gate warning the write raised. Empty for a clean create."
@@ -31198,6 +31312,14 @@ var init_server = __esm({
31198
31312
  scaffoldedProjectPath: import_zod11.z.string().optional().describe(
31199
31313
  "The child project directory a chained subsystem's create scaffolded; absent for every other write."
31200
31314
  ),
31315
+ testsToRevisit: testsToRevisitOutput,
31316
+ ...staleServerOutput
31317
+ };
31318
+ specDeletionOutput = {
31319
+ kind: import_zod11.z.enum(SPEC_KINDS).describe("The spec kind deleted."),
31320
+ id: import_zod11.z.string().describe("The id the spec was stored under."),
31321
+ deleted: import_zod11.z.boolean().describe("True when a spec document was removed."),
31322
+ testsToRevisit: testsToRevisitOutput,
31201
31323
  ...staleServerOutput
31202
31324
  };
31203
31325
  specChangeOutput = {
@@ -31221,15 +31343,7 @@ var init_server = __esm({
31221
31343
  ),
31222
31344
  notices: import_zod11.z.array(import_zod11.z.string()).describe("Store placement notices, gate warnings and delta notices."),
31223
31345
  summary: import_zod11.z.string().describe("One line for people."),
31224
- testsToRevisit: import_zod11.z.array(import_zod11.z.object({
31225
- method: import_zod11.z.string().describe("The contract method this write changed or deleted."),
31226
- symbol: import_zod11.z.string().describe("The code-level name searched for: the method's `symbol` when it declares one, else its name."),
31227
- imported: import_zod11.z.array(import_zod11.z.string()).describe("Test files that IMPORT that symbol \u2014 the high-confidence list, because an import is a binding and not a coincidence."),
31228
- mentioned: import_zod11.z.array(import_zod11.z.string()).describe("Test files that name the symbol without importing it \u2014 usually a test driving it through a portal. Empty when `indiscriminate` is true."),
31229
- indiscriminate: import_zod11.z.boolean().describe("True when the bare name matched more than a tenth of the test suite, so the mention list was withheld as noise.")
31230
- })).describe(
31231
- "The tests that encode a method this write changed or deleted, one entry per such method. Empty when the write touched no method, when the project declares no `rules.conformance.testRoots`, or when nothing references them. Read it before promising that existing tests still pass."
31232
- ),
31346
+ testsToRevisit: testsToRevisitOutput,
31233
31347
  ...staleServerOutput
31234
31348
  };
31235
31349
  validationIssueOutput = {
@@ -31303,11 +31417,9 @@ var init_server = __esm({
31303
31417
  "sdd_update_spec"
31304
31418
  ]);
31305
31419
  listChangedEmitters = /* @__PURE__ */ new WeakMap();
31306
- STORE_MANAGED_FIELDS = /* @__PURE__ */ new Set(["status", "updatedAt"]);
31307
- statusInput = import_zod11.z.enum(STATUS_ORDER).optional().describe(
31420
+ statusInput = import_zod11.z.enum(STATUS_VALUES).optional().describe(
31308
31421
  "The spec's lifecycle status. Omitted means draft for a NEW spec and the status already stored for a re-authoring, so a restatement never reopens a frozen spec. State it to author straight at design or complete instead of promoting afterwards. A status that would LOWER the stored one is refused \u2014 reopening a spec for revision is sdd_update_spec's job, which sets the demotion deliberately."
31309
31422
  );
31310
- ALWAYS_CARRIED_FIELDS = /* @__PURE__ */ new Set(["ext"]);
31311
31423
  SKILL_RESOURCE_MIME = "text/markdown";
31312
31424
  AGENT_BRIEF_SCHEME = "wairon-agent://";
31313
31425
  }
@@ -39217,8 +39329,8 @@ function buildEvaluation(input) {
39217
39329
  let unresolvedPacks;
39218
39330
  if (requiredDefaultResolution) {
39219
39331
  unresolvedPacks = requiredDefaultResolution.unresolved;
39220
- const canonical = [...new Set(requiredDefaultResolution.resolved.map((r) => r.name))];
39221
- missingPackNames = canonical.filter((n) => !present.has(n));
39332
+ const canonical2 = [...new Set(requiredDefaultResolution.resolved.map((r) => r.name))];
39333
+ missingPackNames = canonical2.filter((n) => !present.has(n));
39222
39334
  } else {
39223
39335
  missingPackNames = requiredDefaultNames(policy).filter((n) => !present.has(n));
39224
39336
  unresolvedPacks = [];