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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.36";
68
+ WAIRON_VERSION = "5.1.1-dev.37";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -23209,6 +23209,7 @@ __export(specs_exports, {
23209
23209
  getSubprojectPrefix: () => getSubprojectPrefix,
23210
23210
  getSubsystemPath: () => getSubsystemPath,
23211
23211
  getTypePath: () => getTypePath,
23212
+ ineffectiveDeltaPaths: () => ineffectiveDeltaPaths,
23212
23213
  inspectChainedRoots: () => inspectChainedRoots,
23213
23214
  invalidateSpecCache: () => invalidateSpecCache,
23214
23215
  listChainedRoots: () => listChainedRoots,
@@ -23727,6 +23728,117 @@ function specChanges(before, after, prefix = "") {
23727
23728
  if (JSON.stringify(before) === JSON.stringify(after)) return [];
23728
23729
  return [{ path: prefix, change: "set", before: summarizeValue(before), after: summarizeValue(after) }];
23729
23730
  }
23731
+ function identityFieldsOf(field) {
23732
+ switch (field) {
23733
+ case "dispatch":
23734
+ return ["capability"];
23735
+ case "lifecycle":
23736
+ return ["phase", "component", "method"];
23737
+ case "emits":
23738
+ case "subscribesTo":
23739
+ return ["topic", "event"];
23740
+ case "trustedLinks":
23741
+ return ["subsystem"];
23742
+ case "allow":
23743
+ case "findings":
23744
+ return ["code"];
23745
+ case "invariants":
23746
+ case "patterns":
23747
+ return ["id"];
23748
+ case "boundaries":
23749
+ return ["name"];
23750
+ case "globalRequirements":
23751
+ return ["description"];
23752
+ case "databases":
23753
+ return ["id", "name"];
23754
+ case "cases":
23755
+ return ["value"];
23756
+ case "catches":
23757
+ return ["error"];
23758
+ case "narrative":
23759
+ return ["stepNumber"];
23760
+ default:
23761
+ return ["name", "id"];
23762
+ }
23763
+ }
23764
+ function carriesEditMarker(item) {
23765
+ if (item === null || typeof item !== "object") return false;
23766
+ const o = item;
23767
+ return o.action !== void 0 || o.remove !== void 0;
23768
+ }
23769
+ function ineffectiveDeltaPaths(delta, merged, stored) {
23770
+ const out = [];
23771
+ const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
23772
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
23773
+ const at = (prefix, key) => prefix ? `${prefix}.${key}` : key;
23774
+ const say = (path70, why) => {
23775
+ out.push(`${path70} \u2014 ${why}`);
23776
+ };
23777
+ const walkValue = (d, m, s, path70, field) => {
23778
+ if (Array.isArray(d)) {
23779
+ if (!Array.isArray(m)) {
23780
+ say(path70, "the level does not have this field, so the write dropped it");
23781
+ return;
23782
+ }
23783
+ if (field === "narrative" && d.some(carriesEditMarker)) return;
23784
+ for (const element of d) {
23785
+ if (carriesEditMarker(element)) continue;
23786
+ const key = reportIdentityOf(field, element);
23787
+ if (key === null) {
23788
+ if (!same(m, d)) say(path70, "the level does not keep this value, so the write dropped it");
23789
+ else if (same(s, d)) say(path70, "the stored spec already held this value");
23790
+ return;
23791
+ }
23792
+ const mMatch = m.find((i) => reportIdentityOf(field, i) === key);
23793
+ const sMatch = Array.isArray(s) ? s.find((i) => reportIdentityOf(field, i) === key) : void 0;
23794
+ if (mMatch === void 0) {
23795
+ say(at(path70, key), "the write did not store this element");
23796
+ continue;
23797
+ }
23798
+ walk2(element, mMatch, sMatch, at(path70, key), field);
23799
+ }
23800
+ return;
23801
+ }
23802
+ if (isPlain(d)) {
23803
+ if (!isPlain(m)) {
23804
+ say(path70, "the level does not have this field, so the write dropped it");
23805
+ return;
23806
+ }
23807
+ walk2(d, m, s, path70, field);
23808
+ return;
23809
+ }
23810
+ if (!same(m, d)) say(path70, "the write did not store it \u2014 the level's schema does not keep this value");
23811
+ else if (same(s, d)) say(path70, "the stored spec already held this value");
23812
+ };
23813
+ function walk2(d, m, s, path70, container) {
23814
+ for (const [key, value] of Object.entries(d)) {
23815
+ if (DELTA_VERBS.has(key) || JUMP_LABEL_TWINS.has(key)) continue;
23816
+ if (key === "label" && LABEL_TWIN_CONTAINERS.has(container)) continue;
23817
+ if (identityFieldsOf(container).includes(key)) continue;
23818
+ if (key === "unset") {
23819
+ for (const name of Array.isArray(value) ? value : []) {
23820
+ if (typeof name !== "string") continue;
23821
+ const had = isPlain(s) && s[name] !== void 0;
23822
+ const still = isPlain(m) && m[name] !== void 0;
23823
+ if (!had) say(at(path70, name), "unset named a field the stored spec did not have");
23824
+ else if (still) say(at(path70, name), "unset did not remove it");
23825
+ }
23826
+ continue;
23827
+ }
23828
+ if (value === void 0 || value === null) continue;
23829
+ const here = at(path70, key);
23830
+ const mValue = isPlain(m) ? m[key] : void 0;
23831
+ const sValue = isPlain(s) ? s[key] : void 0;
23832
+ if (mValue === void 0) {
23833
+ say(here, "the level does not have this field, so the write dropped it");
23834
+ continue;
23835
+ }
23836
+ walkValue(value, mValue, sValue, here, key);
23837
+ }
23838
+ }
23839
+ walk2(delta, merged, stored, "", "");
23840
+ return out;
23841
+ }
23730
23842
  function workspaceFor(rootDir) {
23731
23843
  const key = path23.resolve(rootDir);
23732
23844
  let ws = workspaces.get(key);
@@ -23952,10 +24064,10 @@ function restoreSpecFiles(snapshot) {
23952
24064
  function findLegacySpecFiles() {
23953
24065
  return current().findLegacySpecFiles();
23954
24066
  }
23955
- function updateSpec(kind, id, delta, hooks) {
23956
- return current().updateSpec(kind, id, delta, hooks);
24067
+ function updateSpec(kind, id, delta, hooks, dryRun) {
24068
+ return current().updateSpec(kind, id, delta, hooks, dryRun);
23957
24069
  }
23958
- var fs17, path23, NO_ROOT_SUBSYSTEMS, SIGNATURE_TTL_MS, SpecWorkspace, workspaces;
24070
+ var fs17, path23, NO_ROOT_SUBSYSTEMS, SIGNATURE_TTL_MS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, workspaces;
23959
24071
  var init_specs2 = __esm({
23960
24072
  "src/core/specs.ts"() {
23961
24073
  "use strict";
@@ -23973,6 +24085,16 @@ var init_specs2 = __esm({
23973
24085
  init_diagram();
23974
24086
  NO_ROOT_SUBSYSTEMS = /* @__PURE__ */ new Set();
23975
24087
  SIGNATURE_TTL_MS = 2e3;
24088
+ DELTA_VERBS = /* @__PURE__ */ new Set(["action", "remove", "captureJumps"]);
24089
+ JUMP_LABEL_TWINS = /* @__PURE__ */ new Set([
24090
+ "onTrueLabel",
24091
+ "onFalseLabel",
24092
+ "defaultLabel",
24093
+ "endLabel",
24094
+ "finallyLabel",
24095
+ "toLabel"
24096
+ ]);
24097
+ LABEL_TWIN_CONTAINERS = /* @__PURE__ */ new Set(["cases", "catches", "branches"]);
23976
24098
  SpecWorkspace = class {
23977
24099
  constructor(rootDir) {
23978
24100
  this.cachedIndex = null;
@@ -24688,6 +24810,9 @@ var init_specs2 = __esm({
24688
24810
  const existing = this.loadSubsystemSpec(spec.id);
24689
24811
  if (existing) {
24690
24812
  specToWrite.createdAt = existing.createdAt;
24813
+ if (!opts?.allowStatusDemotion && existing.status && (!spec.status || spec.status === "draft")) {
24814
+ specToWrite.status = existing.status;
24815
+ }
24691
24816
  }
24692
24817
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
24693
24818
  writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
@@ -25236,15 +25361,22 @@ var init_specs2 = __esm({
25236
25361
  * carry, and a label in the delta retargets the stored numeric jump it
25237
25362
  * twins. An unresolved reference aborts the whole update.
25238
25363
  * 7. COMPARE with what is stored, both read through the level's canonical
25239
- * schema. Equal means nothing is written and `written` is false.
25240
- * 8. GATE the merged spec through the caller's hook, then SAVE.
25364
+ * schema. Equal means nothing is written and `written` is false. The same
25365
+ * comparison names every delta path the write did NOT act on — one the
25366
+ * schema drops at a depth the permissive delta cannot refuse, one the
25367
+ * stored spec already held, an `unset` that removed nothing.
25368
+ * 8. GATE the merged spec through the caller's hook. A DRY RUN stops here
25369
+ * and answers with the report it would have produced — after the gate, so
25370
+ * it accounts for a write that would really be accepted, and before the
25371
+ * stamp, so not one byte of the stored file moves.
25372
+ * 9. STAMP updatedAt and SAVE.
25241
25373
  *
25242
25374
  * Returns the change report, whose `notices` carry the non-fatal effects (an
25243
25375
  * insert whose position was a jump target, so the relocated jumps now bypass
25244
25376
  * the inserted step; the fields a retype dropped) — empty when the merge had
25245
25377
  * nothing to say.
25246
25378
  */
25247
- updateSpec(kind, id, delta, hooks) {
25379
+ updateSpec(kind, id, delta, hooks, dryRun = false) {
25248
25380
  const notices = [];
25249
25381
  const result = (() => {
25250
25382
  switch (kind) {
@@ -25723,7 +25855,8 @@ var init_specs2 = __esm({
25723
25855
  const unsetFields = Array.isArray(delta.unset) ? delta.unset.filter((f) => typeof f === "string") : [];
25724
25856
  const { unset: _unset, ...mergeableDelta } = delta;
25725
25857
  const storedBefore = JSON.parse(JSON.stringify(result));
25726
- const mergedResult = mergeDelta(result, qualifyDeltaRefs(mergeableDelta));
25858
+ const qualifiedDelta = qualifyDeltaRefs(mergeableDelta);
25859
+ const mergedResult = mergeDelta(result, qualifiedDelta);
25727
25860
  for (const field of unsetFields) delete mergedResult[field];
25728
25861
  if (kind === "implementation" && Array.isArray(mergedResult.methods)) {
25729
25862
  const labelErrors = [];
@@ -25741,20 +25874,41 @@ var init_specs2 = __esm({
25741
25874
  delete value.updatedAt;
25742
25875
  return value;
25743
25876
  };
25744
- const changes = specChanges(canonical(storedBefore), canonical(mergedResult));
25877
+ const canonicalStored = canonical(storedBefore);
25878
+ const canonicalMerged = canonical(mergedResult);
25879
+ const changes = specChanges(canonicalStored, canonicalMerged);
25880
+ const ineffective = ineffectiveDeltaPaths(
25881
+ { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
25882
+ canonicalMerged,
25883
+ canonicalStored
25884
+ );
25745
25885
  if (changes.length === 0) {
25746
25886
  return {
25747
25887
  kind,
25748
25888
  id,
25749
25889
  written: false,
25890
+ dryRun,
25750
25891
  changes,
25892
+ ineffective,
25751
25893
  notices,
25752
- summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing was written.`
25894
+ summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
25753
25895
  };
25754
25896
  }
25755
- mergedResult.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
25756
25897
  const gateNotices = hooks?.gate?.(kind, mergedResult);
25757
25898
  if (gateNotices?.length) notices.push(...gateNotices);
25899
+ if (dryRun) {
25900
+ return {
25901
+ kind,
25902
+ id,
25903
+ written: false,
25904
+ dryRun: true,
25905
+ changes,
25906
+ ineffective,
25907
+ notices,
25908
+ summary: `Dry run on ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"} would be made. Nothing was written.`
25909
+ };
25910
+ }
25911
+ mergedResult.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
25758
25912
  const opts = {
25759
25913
  allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
25760
25914
  };
@@ -25763,7 +25917,7 @@ var init_specs2 = __esm({
25763
25917
  this.saveSystemSpec(mergedResult);
25764
25918
  break;
25765
25919
  case "subsystem":
25766
- this.saveSubsystemSpec(mergedResult);
25920
+ this.saveSubsystemSpec(mergedResult, opts);
25767
25921
  break;
25768
25922
  case "component":
25769
25923
  notices.push(...this.saveComponentSpec(mergedResult, opts));
@@ -25782,7 +25936,9 @@ var init_specs2 = __esm({
25782
25936
  kind,
25783
25937
  id,
25784
25938
  written: true,
25939
+ dryRun: false,
25785
25940
  changes,
25941
+ ineffective,
25786
25942
  notices,
25787
25943
  summary: `Updated ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
25788
25944
  };
@@ -26652,8 +26808,8 @@ function addComponent(candidate) {
26652
26808
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
26653
26809
  return [...noticesFrom(verdict), ...saveComponentSpec(candidate)];
26654
26810
  }
26655
- function updateSpecGated(kind, id, delta) {
26656
- return updateSpec(kind, id, delta, componentCandidateGate());
26811
+ function updateSpecGated(kind, id, delta, dryRun) {
26812
+ return updateSpec(kind, id, delta, componentCandidateGate(), dryRun);
26657
26813
  }
26658
26814
  var init_authoring = __esm({
26659
26815
  "src/core/authoring.ts"() {
@@ -26752,11 +26908,14 @@ function errText(message) {
26752
26908
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
26753
26909
  }
26754
26910
  function renderChangeReport(report2) {
26755
- const lines = [report2.summary];
26911
+ const lines = [report2.dryRun ? `DRY RUN \u2014 nothing was written. ${report2.summary}` : report2.summary];
26756
26912
  for (const change of report2.changes) {
26757
26913
  const to = change.after === void 0 ? "" : `: ${JSON.stringify(change.after)}`;
26758
26914
  const from = change.before === void 0 ? "" : ` (was ${JSON.stringify(change.before)})`;
26759
- lines.push(`- ${change.path} ${change.change}${to}${from}`);
26915
+ lines.push(`- ${change.path} ${report2.dryRun ? `would be ${change.change}` : change.change}${to}${from}`);
26916
+ }
26917
+ if (report2.ineffective.length) {
26918
+ lines.push("", "NO EFFECT:", ...report2.ineffective.map((i) => `- ${i}`));
26760
26919
  }
26761
26920
  if (report2.notices.length) lines.push("", "NOTICE:", ...report2.notices.map((n) => `- ${n}`));
26762
26921
  return lines.join("\n");
@@ -26798,6 +26957,18 @@ function reg(server, name, config, cb) {
26798
26957
  const strictConfig = config.inputSchema ? { ...config, inputSchema: import_zod11.z.object(config.inputSchema).strict() } : config;
26799
26958
  server.registerTool(name, strictConfig, guarded);
26800
26959
  }
26960
+ function statusForCreate(label, stated, existing) {
26961
+ if (stated === void 0) return { status: "draft" };
26962
+ const held = existing?.status ?? void 0;
26963
+ if (held === void 0) return { status: stated };
26964
+ const rank = (s) => STATUS_ORDER.indexOf(s);
26965
+ if (rank(stated) < rank(held)) {
26966
+ return {
26967
+ 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.`
26968
+ };
26969
+ }
26970
+ return { status: stated };
26971
+ }
26801
26972
  function isEmptyValue(value) {
26802
26973
  if (value === void 0 || value === null) return true;
26803
26974
  if (Array.isArray(value)) return value.length === 0;
@@ -27095,8 +27266,8 @@ function createMcpServer(options = {}) {
27095
27266
  const systemInput = {
27096
27267
  name: import_zod11.z.string().describe("Overarching name of the project/system"),
27097
27268
  vision: import_zod11.z.string().describe("Vision, mission, and core goals of the system"),
27098
- boundaries: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ name: import_zod11.z.string(), description: import_zod11.z.string().optional() })])).optional().describe("System boundary rules or scope statements (strings or name/description objects)"),
27099
- globalRequirements: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ description: import_zod11.z.string() })])).optional().describe("Global functional and non-functional requirements (strings or description objects)"),
27269
+ boundaries: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ name: import_zod11.z.string(), description: import_zod11.z.string().optional() }).strict()])).optional().describe("System boundary rules or scope statements (strings or name/description objects)"),
27270
+ globalRequirements: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ description: import_zod11.z.string() }).strict()])).optional().describe("Global functional and non-functional requirements (strings or description objects)"),
27100
27271
  targetLanguage: import_zod11.z.string().optional().describe('Default implementation language for the system (e.g. "typescript", "rust", "python"). Subsystems may override. Enables language-aware validation.')
27101
27272
  };
27102
27273
  const systemInputFields = Object.keys(systemInput);
@@ -27146,7 +27317,7 @@ NOTICE:
27146
27317
  details: import_zod11.z.string(),
27147
27318
  component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface (this subsystem's published surface)"),
27148
27319
  interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component backing this entry")
27149
- })).optional().describe("Public entrypoints exposed by this subsystem, each bound to a realizing component"),
27320
+ }).strict()).optional().describe("Public entrypoints exposed by this subsystem, each bound to a realizing component"),
27150
27321
  projectPath: import_zod11.z.string().optional().describe("Relative path to external project root for subsystem chaining"),
27151
27322
  targetLanguage: import_zod11.z.string().optional().describe("Override of the system-level targetLanguage for this subsystem"),
27152
27323
  profile: import_zod11.z.string().optional().describe("Architectural profile override for this subsystem (built-ins: backend, frontend-reactive, frontend-controller, lowlevel-os, game-ecs, realtime-embedded, plc-cyclic; extension packs may add more \u2014 unknown names get UNKNOWN_PROFILE)"),
@@ -27154,29 +27325,32 @@ NOTICE:
27154
27325
  trustedLinks: import_zod11.z.array(import_zod11.z.object({
27155
27326
  subsystem: import_zod11.z.string().describe("Peer subsystem id"),
27156
27327
  reason: import_zod11.z.string().describe('Why the coupling is sanctioned (e.g. "dispatch latency fast lane")')
27157
- })).optional().describe("Sanctioned tight couplings with peers \u2014 required to acknowledge a mutual subsystem dependency; the Adapter \u2192 published Portal shape still applies."),
27328
+ }).strict()).optional().describe("Sanctioned tight couplings with peers \u2014 required to acknowledge a mutual subsystem dependency; the Adapter \u2192 published Portal shape still applies."),
27158
27329
  lifecycle: import_zod11.z.array(import_zod11.z.object({
27159
27330
  phase: import_zod11.z.enum(["init", "shutdown", "cyclic", "interrupt", "scheduled"]).describe("Which lifecycle/execution flow this roots (cyclic = every scan/tick, interrupt = hardware/OS interrupt, scheduled = timer/cron)"),
27160
27331
  component: import_zod11.z.string().describe("Component id whose method the runtime invokes at this phase"),
27161
27332
  method: import_zod11.z.string().describe("Method name on that component's interface"),
27162
27333
  description: import_zod11.z.string().optional()
27163
- })).optional().describe("Declared execution-flow roots \u2014 reachability entrypoints alongside Portals/Observers. Only init flows feed the durable-Store hydration check (MISSING_HYDRATION); cyclic/interrupt/scheduled root non-request/response execution models (PLC scan, ISR, cron).")
27334
+ }).strict()).optional().describe("Declared execution-flow roots \u2014 reachability entrypoints alongside Portals/Observers. Only init flows feed the durable-Store hydration check (MISSING_HYDRATION); cyclic/interrupt/scheduled root non-request/response execution models (PLC scan, ISR, cron)."),
27335
+ status: statusInput
27164
27336
  };
27165
27337
  const subsystemInputFields = [...Object.keys(subsystemInput), "parentSystem"];
27166
27338
  reg(
27167
27339
  server,
27168
27340
  "sdd_add_subsystem",
27169
27341
  {
27170
- description: "Add an L1 Subsystem / Service under the system boundary. publicInterfaces should bind each entry to the component that realizes it (the subsystem's published surface); if components do not exist yet, add them later with sdd_set_public_interfaces. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced, lint/ext are carried forward.",
27342
+ description: "Add an L1 Subsystem / Service under the system boundary. publicInterfaces should bind each entry to the component that realizes it (the subsystem's published surface); if components do not exist yet, add them later with sdd_set_public_interfaces. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced, lint/ext are carried forward, and the stored status is kept unless this input states a higher one.",
27171
27343
  inputSchema: subsystemInput
27172
27344
  },
27173
- ({ id, name, description, publicInterfaces, projectPath, targetLanguage, profile, designDepth, trustedLinks, lifecycle }) => {
27345
+ ({ id, name, description, publicInterfaces, projectPath, targetLanguage, profile, designDepth, trustedLinks, lifecycle, status: status2 }) => {
27174
27346
  try {
27175
27347
  const { loadSystemSpec: loadSystemSpec3, loadSubsystemSpec: loadSubsystemSpec2, saveSubsystemSpec: saveSubsystemSpec2 } = requireSpecs();
27176
27348
  const system = loadSystemSpec3();
27177
27349
  if (!system) return errText("System spec must be initialized (sdd_initialize_system) first.");
27178
27350
  const now = (/* @__PURE__ */ new Date()).toISOString();
27179
27351
  const existing = loadSubsystemSpec2(id);
27352
+ const resolved = statusForCreate(`subsystem "${id}"`, status2, existing);
27353
+ if ("refusal" in resolved) return errText(resolved.refusal);
27180
27354
  const spec = {
27181
27355
  id,
27182
27356
  name,
@@ -27189,7 +27363,7 @@ NOTICE:
27189
27363
  ...designDepth ? { designDepth } : {},
27190
27364
  ...lifecycle ? { lifecycle } : {},
27191
27365
  trustedLinks: trustedLinks ?? [],
27192
- status: "draft",
27366
+ status: resolved.status,
27193
27367
  createdAt: now,
27194
27368
  updatedAt: now
27195
27369
  };
@@ -27225,7 +27399,7 @@ NOTICE:
27225
27399
  details: import_zod11.z.string(),
27226
27400
  component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface"),
27227
27401
  interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component")
27228
- })).describe("The full replacement list of public interfaces for this subsystem")
27402
+ }).strict()).describe("The full replacement list of public interfaces for this subsystem")
27229
27403
  }
27230
27404
  },
27231
27405
  ({ subsystem, publicInterfaces }) => {
@@ -27382,35 +27556,39 @@ NOTICE:
27382
27556
  component: import_zod11.z.string().describe("Component id serving this capability (must also appear under dependsOn/owns)"),
27383
27557
  method: import_zod11.z.string().describe("Method name on the serving component's interface"),
27384
27558
  description: import_zod11.z.string().optional()
27385
- })).optional().describe("Portal-only: capability \u2192 component.method dispatch table for generic-handle portals. Gives the reachability walker real edges and is validated against target interfaces (UNSERVED_CAPABILITY)."),
27559
+ }).strict()).optional().describe("Portal-only: capability \u2192 component.method dispatch table for generic-handle portals. Gives the reachability walker real edges and is validated against target interfaces (UNSERVED_CAPABILITY)."),
27386
27560
  durability: import_zod11.z.enum(["ram-projection", "durable", "read-through", "cache"]).optional().describe("Store-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DURABILITY_ON_NON_STORE) and nothing is saved. Every Store should declare one (MISSING_DURABILITY): durable = persisted RAM projection (hydration read-back from a lifecycle init entrypoint required \u2014 MISSING_HYDRATION); read-through = persisted with no RAM copy (every read is the read-back, hydration exempt); ram-projection = rebuilt not restored; cache = evictable loss-safe memo state."),
27387
27561
  dependencyClass: import_zod11.z.enum(["pure", "read"]).optional().describe("Orchestrator-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DEPENDENCY_CLASS_ON_NON_ORCHESTRATOR) and nothing is saved. Declares what logic may depend on, enforced as a Store's durability is (DEPENDENCY_CLASS_VIOLATION): pure = depends only on pure Orchestrators (a computation over the values it is handed, e.g. an arbiter or codec); read = also on read Orchestrators, Repositories, Indexes and Adapters, never calling their write methods; unset = a workflow."),
27388
27562
  emits: import_zod11.z.array(import_zod11.z.object({
27389
27563
  topic: import_zod11.z.string().describe("Topic/channel name exactly as used on the bus"),
27390
27564
  event: import_zod11.z.string().optional().describe("Optional event name within the topic (informational; pairing is by topic)"),
27391
27565
  description: import_zod11.z.string().optional()
27392
- })).optional().describe("Topics this component publishes \u2014 every emitted topic needs a subscriber somewhere in the tree (UNCONSUMED_TOPIC)."),
27566
+ }).strict()).optional().describe("Topics this component publishes \u2014 every emitted topic needs a subscriber somewhere in the tree (UNCONSUMED_TOPIC)."),
27393
27567
  subscribesTo: import_zod11.z.array(import_zod11.z.object({
27394
27568
  topic: import_zod11.z.string().describe("Topic/channel name exactly as used on the bus"),
27395
27569
  event: import_zod11.z.string().optional(),
27396
27570
  description: import_zod11.z.string().optional()
27397
- })).optional().describe("Topics this component consumes (typical on Observers) \u2014 every subscription needs an emitter somewhere in the tree (UNSOURCED_SUBSCRIPTION)."),
27398
- ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe('Opaque pack/tool extension data (namespaced keys, e.g. "mypack:priority") \u2014 preserved verbatim, never validated or interpreted by the core')
27571
+ }).strict()).optional().describe("Topics this component consumes (typical on Observers) \u2014 every subscription needs an emitter somewhere in the tree (UNSOURCED_SUBSCRIPTION)."),
27572
+ ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe('Opaque pack/tool extension data (namespaced keys, e.g. "mypack:priority") \u2014 preserved verbatim, never validated or interpreted by the core'),
27573
+ status: statusInput
27399
27574
  };
27400
27575
  const componentInputFields = Object.keys(componentInput);
27401
27576
  reg(
27402
27577
  server,
27403
27578
  "sdd_add_component",
27404
27579
  {
27405
- description: `Add an L2 Component under a subsystem. componentType is a building block (Portal, Orchestrator, Supervisor, Actor, Store, Index, Query, Registry, Adapter, Observer) or the pattern Repository. Specialist and Gateway are retired (STEREOTYPE_RETIRED) and cannot be authored: logic is an Orchestrator with a dependencyClass (pure | read; unset = a workflow), and a gateway is a Portal with the gateway variant (set variant with sdd_update_spec). Patterns set "owns" (their private member blocks); all components set "dependsOn" (collaborators \u2014 facades or standalone blocks). Held/persisted state (configs, permissions, sessions, caches): model the Repository recipe \u2014 a Store + Registry (write) + Index (read), plus a Query for computed reads over the Store, owned by a Repository facade consumers depend on; a deliberately standalone Store is the sanctioned lightweight form (workflow-layer consumers + lint.allow on UNOWNED_STORE). Never hold state as fields inside an Orchestrator because a Store link was refused. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced (an omitted array is CLEARED), while lint.allow, a Portal's auth, variant, patterns and externalLinks are carried forward \u2014 edit those with sdd_update_spec.`,
27580
+ description: `Add an L2 Component under a subsystem. componentType is a building block (Portal, Orchestrator, Supervisor, Actor, Store, Index, Query, Registry, Adapter, Observer) or the pattern Repository. Specialist and Gateway are retired (STEREOTYPE_RETIRED) and cannot be authored: logic is an Orchestrator with a dependencyClass (pure | read; unset = a workflow), and a gateway is a Portal with the gateway variant (set variant with sdd_update_spec). Patterns set "owns" (their private member blocks); all components set "dependsOn" (collaborators \u2014 facades or standalone blocks). Held/persisted state (configs, permissions, sessions, caches): model the Repository recipe \u2014 a Store + Registry (write) + Index (read), plus a Query for computed reads over the Store, owned by a Repository facade consumers depend on; a deliberately standalone Store is the sanctioned lightweight form (workflow-layer consumers + lint.allow on UNOWNED_STORE). Never hold state as fields inside an Orchestrator because a Store link was refused. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced (an omitted array is CLEARED), while lint.allow, a Portal's auth, variant, patterns and externalLinks are carried forward \u2014 edit those with sdd_update_spec. The stored status is kept unless this input states a higher one.`,
27406
27581
  inputSchema: componentInput
27407
27582
  },
27408
- ({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, durability, dependencyClass, emits, subscribesTo, ext }) => {
27583
+ ({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, durability, dependencyClass, emits, subscribesTo, ext, status: status2 }) => {
27409
27584
  try {
27410
27585
  const { loadSubsystemSpec: loadSubsystemSpec2, loadComponentSpec: loadComponentSpec2 } = requireSpecs();
27411
27586
  const sub = loadSubsystemSpec2(subsystem);
27412
27587
  if (!sub) return errText(`Parent subsystem "${subsystem}" does not exist.`);
27413
27588
  const now = (/* @__PURE__ */ new Date()).toISOString();
27589
+ const existing = loadComponentSpec2(id);
27590
+ const resolved = statusForCreate(`component "${id}"`, status2, existing);
27591
+ if ("refusal" in resolved) return errText(resolved.refusal);
27414
27592
  const candidate = {
27415
27593
  id,
27416
27594
  name,
@@ -27427,11 +27605,10 @@ NOTICE:
27427
27605
  ...emits ? { emits } : {},
27428
27606
  ...subscribesTo ? { subscribesTo } : {},
27429
27607
  ...ext ? { ext } : {},
27430
- status: "draft",
27608
+ status: resolved.status,
27431
27609
  createdAt: now,
27432
27610
  updatedAt: now
27433
27611
  };
27434
- const existing = loadComponentSpec2(id);
27435
27612
  const carried = carryUnexpressed(existing, candidate, componentInputFields);
27436
27613
  const cleared = clearedByOmission(existing, candidate, componentInputFields);
27437
27614
  if (existing) candidate.createdAt = existing.createdAt;
@@ -27464,18 +27641,18 @@ NOTICE:
27464
27641
  type: import_zod11.z.string().describe('A primitive/builtin or a defined type id (e.g. "billing.Invoice")'),
27465
27642
  description: import_zod11.z.string().optional(),
27466
27643
  optional: import_zod11.z.boolean().optional()
27467
- })).optional().describe("Structured parameters \u2014 authoritative for type checking (the prose signature becomes display-only). Strongly preferred."),
27644
+ }).strict()).optional().describe("Structured parameters \u2014 authoritative for type checking (the prose signature becomes display-only). Strongly preferred."),
27468
27645
  guarantees: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Semantic guarantees the method promises (combinable); any guarantee a narrative step asserts must be declared here. Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
27469
27646
  effect: import_zod11.z.enum(["read", "write"]).optional().describe("State-effect direction on the component's held state \u2014 required on a durable Store's contract methods so the durability round-trip rule can pair writes with hydration read-backs"),
27470
27647
  invokedBy: import_zod11.z.object({
27471
27648
  kind: import_zod11.z.enum(["runtime", "external", "sibling-subsystem"]).describe("Who owns the out-of-graph invocation: runtime (timer/signal/shutdown hook), external (a system outside this project), sibling-subsystem (a modeled sibling whose edge is not narrated here)"),
27472
27649
  caller: import_zod11.z.string().optional().describe("WHO invokes it and when, as reviewable prose \u2014 missing or placeholder-thin prose is INVOKED_BY_UNDESCRIBED")
27473
- }).optional().describe("Typed acknowledgment of a real caller OUTSIDE the modeled narrative graph. Unused-detection seeds the method as an entrypoint so reachability propagates through its narrative (unlike lint.allow); a method the internal walk already reaches is flagged stale (INVOKED_BY_REDUNDANT). Prefer a `register` narrative step when the wiring is internal."),
27650
+ }).strict().optional().describe("Typed acknowledgment of a real caller OUTSIDE the modeled narrative graph. Unused-detection seeds the method as an entrypoint so reachability propagates through its narrative (unlike lint.allow); a method the internal walk already reaches is flagged stale (INVOKED_BY_REDUNDANT). Prefer a `register` narrative step when the wiring is internal."),
27474
27651
  findings: import_zod11.z.array(import_zod11.z.object({
27475
27652
  code: import_zod11.z.string().describe("UPPER_SNAKE finding code, unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>)"),
27476
27653
  severity: import_zod11.z.enum(["error", "warning"]).describe("Default severity, before project severity overrides and draft-context downgrades"),
27477
27654
  summary: import_zod11.z.string().describe("One line saying what the finding means")
27478
- })).optional().describe("The finding codes this method can report, each with its default severity and summary. Each declared code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). A code is declared once per method; sdd_update_spec upserts and deletes findings by code."),
27655
+ }).strict()).optional().describe("The finding codes this method can report, each with its default severity and summary. Each declared code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). A code is declared once per method; sdd_update_spec upserts and deletes findings by code."),
27479
27656
  ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim")
27480
27657
  };
27481
27658
  const interfaceMethodInputFields = Object.keys(interfaceMethodShape);
@@ -27484,23 +27661,26 @@ NOTICE:
27484
27661
  name: import_zod11.z.string().describe("Human-readable contract name"),
27485
27662
  description: import_zod11.z.string().describe("Contract description and obligations"),
27486
27663
  component: import_zod11.z.string().describe("The L2 component ID this interface belongs to"),
27487
- methods: import_zod11.z.array(import_zod11.z.object(interfaceMethodShape)).optional().describe("List of method signature contracts")
27664
+ methods: import_zod11.z.array(import_zod11.z.object(interfaceMethodShape).strict()).optional().describe("List of method signature contracts"),
27665
+ status: statusInput
27488
27666
  };
27489
27667
  const interfaceInputFields = Object.keys(interfaceInput);
27490
27668
  reg(
27491
27669
  server,
27492
27670
  "sdd_define_interface",
27493
27671
  {
27494
- description: "Define an L3 Contract / Interface with method signatures for a component. Prefer supplying structured `params` per method \u2014 they are the authoritative source for type checking (the free-form signature string then becomes display-only and is never heuristically parsed). A method declares the finding codes it reports in `findings` ({code, severity, summary}); each code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). Re-defining an existing id REPLACES the method list: a method left out of the input is REMOVED (and reported); spec-level lint/ext and each method's endpoint binding are carried forward.",
27672
+ description: "Define an L3 Contract / Interface with method signatures for a component. Prefer supplying structured `params` per method \u2014 they are the authoritative source for type checking (the free-form signature string then becomes display-only and is never heuristically parsed). A method declares the finding codes it reports in `findings` ({code, severity, summary}); each code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). Re-defining an existing id REPLACES the method list: a method left out of the input is REMOVED (and reported); spec-level lint/ext and each method's endpoint binding are carried forward, and the stored status is kept unless this input states a higher one.",
27495
27673
  inputSchema: interfaceInput
27496
27674
  },
27497
- ({ id, name, description, component, methods }) => {
27675
+ ({ id, name, description, component, methods, status: status2 }) => {
27498
27676
  try {
27499
27677
  const { loadComponentSpec: loadComponentSpec2, loadInterfaceSpec: loadInterfaceSpec2, saveInterfaceSpec: saveInterfaceSpec2 } = requireSpecs();
27500
27678
  const comp = loadComponentSpec2(component);
27501
27679
  if (!comp) return errText(`Component "${component}" does not exist.`);
27502
27680
  const now = (/* @__PURE__ */ new Date()).toISOString();
27503
27681
  const existing = loadInterfaceSpec2(id);
27682
+ const resolved = statusForCreate(`interface "${id}"`, status2, existing);
27683
+ if ("refusal" in resolved) return errText(resolved.refusal);
27504
27684
  const carried = [];
27505
27685
  const specMethods = (methods ?? []).map((m) => {
27506
27686
  const prev = existing?.methods.find((p) => p.name === m.name);
@@ -27516,7 +27696,7 @@ NOTICE:
27516
27696
  description,
27517
27697
  component,
27518
27698
  methods: specMethods,
27519
- status: "draft",
27699
+ status: resolved.status,
27520
27700
  createdAt: now,
27521
27701
  updatedAt: now
27522
27702
  };
@@ -27567,7 +27747,7 @@ NOTICE:
27567
27747
  channel: import_zod11.z.string().optional().describe("IPC: channel name"),
27568
27748
  command: import_zod11.z.string().optional().describe("CLI: command/subcommand"),
27569
27749
  address: import_zod11.z.string().optional().describe("Custom: free-form address")
27570
- })).describe("One binding per method")
27750
+ }).strict()).describe("One binding per method")
27571
27751
  }
27572
27752
  },
27573
27753
  ({ interface: interfaceId, endpoints }) => {
@@ -27624,7 +27804,7 @@ NOTICE:
27624
27804
  type: import_zod11.z.enum(["local", "call", "dispatch", "register", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
27625
27805
  targetComponent: import_zod11.z.string().optional().describe("call/register/dispatch: L2 component id (for dispatch, the Portal routed through)"),
27626
27806
  targetMethod: import_zod11.z.string().optional().describe("call/register: method name on the target. A register step hands the target method to the runtime as a callback (timer, event listener, shutdown hook): reachability follows the edge, but it is never an invocation \u2014 exempt from call-graph conformance, call-cycle detection, and the durability boot walk"),
27627
- auth: import_zod11.z.object({ from: import_zod11.z.string(), note: import_zod11.z.string().optional() }).optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
27807
+ auth: import_zod11.z.object({ from: import_zod11.z.string(), note: import_zod11.z.string().optional() }).strict().optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
27628
27808
  detach: import_zod11.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
27629
27809
  capability: import_zod11.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
27630
27810
  assertsGuarantees: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
@@ -27635,22 +27815,22 @@ NOTICE:
27635
27815
  onFalseStep: stepNo().optional().describe("branch: required (or onFalseLabel)"),
27636
27816
  onFalseLabel: labelRef().optional().describe("branch: symbolic alternative to onFalseStep"),
27637
27817
  on: import_zod11.z.string().optional().describe("switch: the dispatched value"),
27638
- cases: import_zod11.z.array(import_zod11.z.object({ value: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() })).optional().describe("switch: required; each case targets its region by step number or label"),
27818
+ cases: import_zod11.z.array(import_zod11.z.object({ value: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() }).strict()).optional().describe("switch: required; each case targets its region by step number or label"),
27639
27819
  defaultStep: stepNo().optional().describe("switch: default = next step"),
27640
27820
  defaultLabel: labelRef().optional().describe("switch: symbolic alternative to defaultStep"),
27641
27821
  loopKind: import_zod11.z.enum(["forEach", "for", "while", "doWhile"]).optional().describe('loop: default forEach when "over" is set, else while'),
27642
27822
  over: import_zod11.z.string().optional().describe("loop forEach/for: iteration source"),
27643
27823
  endStep: stepNo().optional().describe("loop/try: last step of the body region (required, or endLabel)"),
27644
27824
  endLabel: labelRef().optional().describe("loop/try: symbolic alternative to endStep"),
27645
- catches: import_zod11.z.array(import_zod11.z.object({ error: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() })).optional().describe("try: handler regions, each targeted by step number or label"),
27825
+ catches: import_zod11.z.array(import_zod11.z.object({ error: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() }).strict()).optional().describe("try: handler regions, each targeted by step number or label"),
27646
27826
  finallyStep: stepNo().optional().describe("try: first step of the always-runs region"),
27647
27827
  finallyLabel: labelRef().optional().describe("try: symbolic alternative to finallyStep"),
27648
- branches: import_zod11.z.array(import_zod11.z.object({ step: stepNo().optional(), label: labelRef().optional(), name: import_zod11.z.string().optional() })).optional().describe("parallel: >= 2 arm entries (step number or label), ascending, first = the step after the header; arms are contiguous sub-regions of body next..endStep, joining after endStep once ALL complete"),
27828
+ branches: import_zod11.z.array(import_zod11.z.object({ step: stepNo().optional(), label: labelRef().optional(), name: import_zod11.z.string().optional() }).strict()).optional().describe("parallel: >= 2 arm entries (step number or label), ascending, first = the step after the header; arms are contiguous sub-regions of body next..endStep, joining after endStep once ALL complete"),
27649
27829
  toStep: stepNo().optional().describe("jump: required (break/continue/rejoin), or toLabel"),
27650
27830
  toLabel: labelRef().optional().describe("jump: symbolic alternative to toStep \u2014 resolves to the labeled step at write time"),
27651
27831
  outcome: import_zod11.z.string().optional().describe('return: e.g. "success", "not found"'),
27652
27832
  error: import_zod11.z.string().optional().describe("throw: the raised error")
27653
- });
27833
+ }).strict();
27654
27834
  const detailEnum = import_zod11.z.enum(["full", "calls-only", "intent"]);
27655
27835
  const conformanceEnum = import_zod11.z.enum(["declared", "anchored", "off"]);
27656
27836
  const implMethodShape = {
@@ -27674,22 +27854,25 @@ NOTICE:
27674
27854
  technologies: import_zod11.z.array(import_zod11.z.string()).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"]) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components.`),
27675
27855
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
27676
27856
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
27677
- methods: import_zod11.z.array(import_zod11.z.object(implMethodShape)).optional().describe("Method implementations containing L5 narratives")
27857
+ methods: import_zod11.z.array(import_zod11.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
27858
+ status: statusInput
27678
27859
  };
27679
27860
  const implInputFields = Object.keys(implInput);
27680
27861
  reg(
27681
27862
  server,
27682
27863
  "sdd_write_narrative",
27683
27864
  {
27684
- description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward.",
27865
+ description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one.",
27685
27866
  inputSchema: implInput
27686
27867
  },
27687
- ({ id, name, description, contract, sourcePath, simPath, technologies, detail, conformance, methods }) => {
27868
+ ({ id, name, description, contract, sourcePath, simPath, technologies, detail, conformance, methods, status: status2 }) => {
27688
27869
  try {
27689
27870
  const { loadInterfaceSpec: loadInterfaceSpec2, loadImplementationSpec: loadImplementationSpec2, saveImplementationSpec: saveImplementationSpec2 } = requireSpecs();
27690
27871
  const intf = loadInterfaceSpec2(contract);
27691
27872
  if (!intf) return errText(`Interface contract "${contract}" does not exist.`);
27692
27873
  const existing = loadImplementationSpec2(id);
27874
+ const resolved = statusForCreate(`implementation "${id}"`, status2, existing);
27875
+ if ("refusal" in resolved) return errText(resolved.refusal);
27693
27876
  const carried = [];
27694
27877
  const resolvedMethods = (methods ?? []).map((m) => {
27695
27878
  const prev = existing?.methods.find((p) => p.name === m.name);
@@ -27719,7 +27902,7 @@ NOTICE:
27719
27902
  // Post-resolution every cases/catches entry has its numeric step —
27720
27903
  // the *Label twins exist only in the input type, not the spec's.
27721
27904
  methods: resolvedMethods,
27722
- status: "draft",
27905
+ status: resolved.status,
27723
27906
  createdAt: now,
27724
27907
  updatedAt: now
27725
27908
  };
@@ -27760,13 +27943,13 @@ NOTICE:
27760
27943
  optional: import_zod11.z.boolean().optional(),
27761
27944
  key: import_zod11.z.enum(["primary", "unique", "foreign"]).optional().describe("Identity marker (PK/unique/FK) for ERD and database schema derivation"),
27762
27945
  references: import_zod11.z.string().optional().describe('For foreign keys, the referenced type/table id and optional field, e.g. "invoice.id"')
27763
- })).optional().describe('Data fields (type is a primitive or a qualified type id, e.g. "billing.Invoice")'),
27764
- methods: import_zod11.z.array(import_zod11.z.object({ name: import_zod11.z.string(), signature: import_zod11.z.string(), returns: import_zod11.z.string(), description: import_zod11.z.string().optional() })).optional().describe("Pure intrinsic methods only"),
27946
+ }).strict()).optional().describe('Data fields (type is a primitive or a qualified type id, e.g. "billing.Invoice")'),
27947
+ methods: import_zod11.z.array(import_zod11.z.object({ name: import_zod11.z.string(), signature: import_zod11.z.string(), returns: import_zod11.z.string(), description: import_zod11.z.string().optional() }).strict()).optional().describe("Pure intrinsic methods only"),
27765
27948
  componentClass: import_zod11.z.string().optional().describe("Optional component id that implements or owns this logical entity"),
27766
27949
  invariants: import_zod11.z.array(import_zod11.z.object({
27767
27950
  id: import_zod11.z.string().describe("Stable invariant id, unique within the entity"),
27768
27951
  description: import_zod11.z.string().describe("The property that must hold, stated precisely")
27769
- })).optional().describe("Declared domain invariants (entities): every write-effect method of the componentClass must carry a narrative step asserting each (assertsInvariants) \u2014 declarations checked, enforcement never proven"),
27952
+ }).strict()).optional().describe("Declared domain invariants (entities): every write-effect method of the componentClass must carry a narrative step asserting each (assertsInvariants) \u2014 declarations checked, enforcement never proven"),
27770
27953
  database: import_zod11.z.string().optional().describe("Optional database id for table-schema types"),
27771
27954
  table: import_zod11.z.string().optional().describe("Optional database table name for table-schema types"),
27772
27955
  linkedEntity: import_zod11.z.string().optional().describe("Optional logical entity id represented by this table-schema type")
@@ -27870,13 +28053,14 @@ NOTICE:
27870
28053
  server,
27871
28054
  "sdd_get_spec",
27872
28055
  {
27873
- description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back.`,
28056
+ description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back.`,
27874
28057
  inputSchema: {
27875
28058
  kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
27876
- id: import_zod11.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")')
28059
+ id: import_zod11.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
28060
+ methods: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Return only these methods by name; every other field of the spec comes back unchanged. Only an interface, an implementation or a type declares methods \u2014 asking for one elsewhere is refused, as is a name the spec does not declare (the answer names the ones it does). Omit it for the whole spec.")
27877
28061
  }
27878
28062
  },
27879
- ({ kind, id }) => {
28063
+ ({ kind, id, methods }) => {
27880
28064
  try {
27881
28065
  const specs = requireSpecs();
27882
28066
  let result = null;
@@ -27901,6 +28085,31 @@ NOTICE:
27901
28085
  break;
27902
28086
  }
27903
28087
  if (!result) return errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
28088
+ if (methods !== void 0) {
28089
+ const declared = result.methods;
28090
+ if (!Array.isArray(declared)) {
28091
+ return errText(
28092
+ `A ${kind} spec declares no methods, so "methods" cannot filter it. Drop the argument to read "${id}" whole; methods live on an interface, an implementation and a type.`
28093
+ );
28094
+ }
28095
+ const names = declared.map((m) => String(m?.name));
28096
+ const unknown = methods.filter((n) => !names.includes(n));
28097
+ if (unknown.length > 0) {
28098
+ return errText(
28099
+ `${kind} "${id}" does not declare ${unknown.map((n) => `"${n}"`).join(", ")}. It declares: ${names.join(", ")}. Nothing was returned, so a misspelled name never reads as a method with no content.`
28100
+ );
28101
+ }
28102
+ const kept = declared.filter((m) => methods.includes(String(m?.name)));
28103
+ result = {
28104
+ ...result,
28105
+ methods: kept,
28106
+ partialResult: {
28107
+ shown: kept.map((m) => String(m?.name)),
28108
+ omitted: names.length - kept.length,
28109
+ warning: `PARTIAL: ${names.length - kept.length} of this spec's ${names.length} methods are not in this answer. Never re-author from it \u2014 sdd_define_interface and sdd_write_narrative REPLACE the method list, so every method missing here would be removed from the spec.`
28110
+ }
28111
+ };
28112
+ }
27904
28113
  if (kind === "component") {
27905
28114
  const guidance = resolveComponentVariantGuidance(result);
27906
28115
  if (guidance) return json({ ...result, variantGuidance: guidance });
@@ -27953,16 +28162,17 @@ NOTICE:
27953
28162
  server,
27954
28163
  "sdd_update_spec",
27955
28164
  {
27956
- description: "Update/patch an existing SDD specification (subsystem, component, interface, implementation, or type) using a granular delta. Updates fields, appends/merges array elements, or inserts/deletes narrative steps.",
28165
+ description: "Update/patch an existing SDD specification (subsystem, component, interface, implementation, or type) using a granular delta. Updates fields, appends/merges array elements, or inserts/deletes narrative steps. Answers with exactly what changed, and with every path the delta named that the write did not act on. Pass dryRun to be told what it would do without writing it.",
27957
28166
  inputSchema: {
27958
28167
  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)"),
27959
28168
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
27960
- 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", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code", 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, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged).`)
28169
+ 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", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code", 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, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). 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.`),
28170
+ 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.")
27961
28171
  }
27962
28172
  },
27963
- ({ kind, id, delta }) => {
28173
+ ({ kind, id, delta, dryRun }) => {
27964
28174
  try {
27965
- return text(renderChangeReport(updateSpecGated(kind, id, delta)));
28175
+ return text(renderChangeReport(updateSpecGated(kind, id, delta, dryRun)));
27966
28176
  } catch (e) {
27967
28177
  return errText(String(e));
27968
28178
  }
@@ -28166,7 +28376,7 @@ async function startMcpServer() {
28166
28376
  } catch {
28167
28377
  }
28168
28378
  }
28169
- var import_mcp, import_stdio, import_zod11, import_types3, fs20, path27, import_url, SERVER_BUILD_STAMP, STALE_SERVER_WARNING, SPEC_WRITE_TOOLS, listChangedEmitters, STORE_MANAGED_FIELDS, ALWAYS_CARRIED_FIELDS, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
28379
+ var import_mcp, import_stdio, import_zod11, import_types3, fs20, path27, import_url, SERVER_BUILD_STAMP, STALE_SERVER_WARNING, SPEC_WRITE_TOOLS, listChangedEmitters, STORE_MANAGED_FIELDS, STATUS_ORDER, statusInput, ALWAYS_CARRIED_FIELDS, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
28170
28380
  var init_server = __esm({
28171
28381
  "src/mcp/server.ts"() {
28172
28382
  "use strict";
@@ -28219,6 +28429,10 @@ var init_server = __esm({
28219
28429
  ]);
28220
28430
  listChangedEmitters = /* @__PURE__ */ new WeakMap();
28221
28431
  STORE_MANAGED_FIELDS = /* @__PURE__ */ new Set(["status", "updatedAt"]);
28432
+ STATUS_ORDER = ["draft", "design", "complete"];
28433
+ statusInput = import_zod11.z.enum(STATUS_ORDER).optional().describe(
28434
+ "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."
28435
+ );
28222
28436
  ALWAYS_CARRIED_FIELDS = /* @__PURE__ */ new Set(["ext"]);
28223
28437
  SKILL_RESOURCE_MIME = "text/markdown";
28224
28438
  AGENT_BRIEF_SCHEME = "wairon-agent://";