@wairon/cli 5.1.1-dev.77 → 5.1.1-dev.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4397,9 +4397,11 @@ var RulesConfigSchema = import_zod3.z.object({
4397
4397
  materializeAgentFiles: import_zod3.z.boolean().default(false),
4398
4398
  /**
4399
4399
  * Severity overrides for SDD validation rules.
4400
- * Key: rule code (e.g. CIRCULAR_DEPENDENCY), Value: error | warning | off
4400
+ * Key: rule code (e.g. CIRCULAR_DEPENDENCY), Value: error | warning | notice | off.
4401
+ * A notice is still reported, but never makes the tree invalid and never
4402
+ * fails `--ci`; off is not reported at all.
4401
4403
  */
4402
- sddRuleSeverity: import_zod3.z.record(import_zod3.z.enum(["error", "warning", "off"])).default({}),
4404
+ sddRuleSeverity: import_zod3.z.record(import_zod3.z.enum(["error", "warning", "notice", "off"])).default({}),
4403
4405
  /** Dynamic naming conventions and stereotype suffix rules */
4404
4406
  naming: NamingRuleConfigSchema.optional(),
4405
4407
  /** Dynamic metadata documentation constraints */
@@ -7822,6 +7824,8 @@ body:not(.panel-closed) #panelToggle { background:var(--accent); color:#fff; bor
7822
7824
  #panel .flowbtn:hover { background:var(--hover-bg); }
7823
7825
  #panel .issue { border-left:3px solid var(--danger); padding:6px 9px; margin:6px 0; background:var(--card); font-size:12px; border-radius:0 7px 7px 0; }
7824
7826
  #panel .issue.warning { border-left-color:var(--warn); }
7827
+ #panel .issue.notice { border-left-color:var(--accent); }
7828
+ #panel .issue .sev { font-size:10px; color:var(--dim); text-transform:uppercase; letter-spacing:.04em; margin-left:6px; }
7825
7829
  #panel .issue code { font-size:10.5px; color:var(--dim); }
7826
7830
 
7827
7831
  /* Presentation mode = the canvas page, focused: the header chrome and legend
@@ -8032,7 +8036,15 @@ var MODEL = __MODEL_JSON__;
8032
8036
  });
8033
8037
  document.getElementById('issueCount').textContent =
8034
8038
  MODEL.issues.filter(function (i) { return i.severity === 'error'; }).length + 'e/' +
8035
- MODEL.issues.filter(function (i) { return i.severity === 'warning'; }).length + 'w';
8039
+ MODEL.issues.filter(function (i) { return i.severity === 'warning'; }).length + 'w/' +
8040
+ MODEL.issues.filter(function (i) { return i.severity === 'notice'; }).length + 'n';
8041
+ // A node is marked failing only for an error or a warning; one that holds
8042
+ // only notices gets its own quieter mark, never the failing one.
8043
+ function issueMark(id) {
8044
+ var list = state.showIssues && issuesBySpec[id];
8045
+ if (!list) return '';
8046
+ return list.some(function (i) { return i.severity !== 'notice'; }) ? ' hasIssue' : ' hasNotice';
8047
+ }
8036
8048
 
8037
8049
  var PATTERN_TYPES = { Repository:1, FeatureComponent:1, RouterComponent:1 };
8038
8050
  // A retired Specialist or Gateway renders as a plain box marked retired, so a
@@ -8372,6 +8384,9 @@ var MODEL = __MODEL_JSON__;
8372
8384
  { selector: 'edge.stubHover', style: { 'line-color': t.selGlow, 'target-arrow-color': t.selGlow, width: 2.6, opacity: 1, 'z-compound-depth': 'top' } },
8373
8385
  { selector: '.dimmed', style: { opacity: 0.13 } },
8374
8386
  { selector: '.hasIssue', style: { 'border-color': t.issue, 'border-style': 'dashed', 'border-width': 3 } },
8387
+ // Notices only: the node keeps its own border colour and width (no layout
8388
+ // nudge), drawn dotted so it reads as noted rather than failing.
8389
+ { selector: '.hasNotice', style: { 'border-style': 'dotted' } },
8375
8390
  // Overlay only (no border) \u2014 a border changes node geometry, which nudges
8376
8391
  // the compound parent and makes hover flicker; overlay never affects layout.
8377
8392
  { selector: '.sel', style: { 'overlay-color': t.selGlow, 'overlay-opacity': 0.34, 'overlay-padding': 6 } },
@@ -9161,7 +9176,7 @@ var MODEL = __MODEL_JSON__;
9161
9176
  var kindCls = t.kind === 'entity' ? 'typeEntity' : 'typeValue';
9162
9177
  var dim = !typeMatches(t);
9163
9178
  var extra = (dim ? ' dimmed' : '')
9164
- + (state.showIssues && issuesBySpec[t.id] ? ' hasIssue' : '')
9179
+ + issueMark(t.id)
9165
9180
  + (state.selectedKind === 'type' && state.selected === t.id ? ' sel' : '');
9166
9181
  if (sh.plain) {
9167
9182
  eles.push({
@@ -9451,7 +9466,7 @@ var MODEL = __MODEL_JSON__;
9451
9466
  }
9452
9467
  classes += (e.hasKids ? ' drillable' : '') + (isPub ? ' public' : '')
9453
9468
  + (dim ? ' dimmed' : '')
9454
- + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
9469
+ + issueMark(e.id)
9455
9470
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
9456
9471
  if (inner) {
9457
9472
  var boxNode = { data: { id: aid, label: e.kind === 'subsystem' ? nameOf(e) : nameOf(e), w: p.w, h: p.h, tw: p.w - 16 }, classes: classes };
@@ -11396,7 +11411,7 @@ var MODEL = __MODEL_JSON__;
11396
11411
  }
11397
11412
  function issueHtml(list) {
11398
11413
  return list.map(function (i) {
11399
- return '<div class="issue ' + esc(i.severity) + '"><code>' + esc(i.code) + '</code><br>' + esc(i.message) + '</div>';
11414
+ return '<div class="issue ' + esc(i.severity) + '"><code>' + esc(i.code) + '</code><span class="sev">' + esc(i.severity) + '</span><br>' + esc(i.message) + '</div>';
11400
11415
  }).join('');
11401
11416
  }
11402
11417
 
@@ -12083,7 +12098,7 @@ function defaultTargetConfig(type) {
12083
12098
  enabled: true
12084
12099
  };
12085
12100
  }
12086
- var WAIRON_VERSION = "5.1.1-dev.77";
12101
+ var WAIRON_VERSION = "5.1.1-dev.79";
12087
12102
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
12088
12103
  var ARCHITECT_AGENT_ID = "agent-architect";
12089
12104
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -13465,7 +13480,7 @@ var publicSurfaceBoundContractRule = {
13465
13480
  // src/core/rules/integrity/lint-allows.ts
13466
13481
  var lintAllowsRule = {
13467
13482
  name: "lint-allows",
13468
- description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. An allow covers exactly the occurrence it names: a finding that reports a site is silenced only by an allow whose `at` is that site, a finding that reports none only by an allow that names none, and an aggregating finding only by an allow whose `covers` lists every unit it reports \u2014 a unit nobody listed is named back as new instead of inheriting a decision taken about its neighbours. So a coarse allow left on a rule that names sites, and an allow whose site the run no longer reports, are both UNUSED_LINT_ALLOW, and the finding names the sites that did fire. Allows silence warnings only; errors always surface.",
13483
+ description: "Per-spec lint suppressions (lint.allow) must name real issue codes and actually suppress a finding \u2014 unknown codes and stale allows are flagged. An allow covers exactly the occurrence it names: a finding that reports a site is silenced only by an allow whose `at` is that site, a finding that reports none only by an allow that names none, and an aggregating finding only by an allow whose `covers` lists every unit it reports \u2014 a unit nobody listed is named back as new instead of inheriting a decision taken about its neighbours. So a coarse allow left on a rule that names sites, and an allow whose site the run no longer reports, are both UNUSED_LINT_ALLOW, and the finding names the sites that did fire. Allows silence warnings and notices; errors always surface.",
13469
13484
  codes: [
13470
13485
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
13471
13486
  { code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014 the code never fired, or it fired at sites this allow does not name" }
@@ -20569,6 +20584,9 @@ var DEPTH_GATED_CODES = {
20569
20584
  UNUSED_COMPONENT: "narratives",
20570
20585
  UNUSED_METHOD: "narratives"
20571
20586
  };
20587
+ function atMostWarning(severity) {
20588
+ return severity === "error" ? "warning" : severity;
20589
+ }
20572
20590
  var COMPLETENESS_RULES = /* @__PURE__ */ new Set([
20573
20591
  "MISSING_IMPLEMENTATION_METHOD",
20574
20592
  "MISSING_NARRATIVE",
@@ -20840,7 +20858,7 @@ function buildRuleContext(opts) {
20840
20858
  return profileSeverity;
20841
20859
  }
20842
20860
  if (isDraftContext && COMPLETENESS_RULES.has(ruleCode)) {
20843
- return "warning";
20861
+ return atMostWarning(defaultSeverity);
20844
20862
  }
20845
20863
  return defaultSeverity;
20846
20864
  };
@@ -20914,13 +20932,13 @@ function buildRuleContext(opts) {
20914
20932
  allowClaimed = true;
20915
20933
  const grew = (parts?.covers ?? []).filter((unit) => !(allow.covers ?? []).includes(unit));
20916
20934
  if (grew.length === 0) {
20917
- if (severity === "warning") return;
20935
+ if (severity !== "error") return;
20918
20936
  } else {
20919
20937
  text2 = `${text2} A lint.allow covers this site, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Decide on them: add them to the allow's \`covers\` with a reason that is actually true, or fix them.`;
20920
20938
  }
20921
20939
  }
20922
20940
  }
20923
- if (specId && parts && severity === "warning" && !allowClaimed) {
20941
+ if (specId && parts && severity !== "error" && !allowClaimed) {
20924
20942
  const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
20925
20943
  if (entry) {
20926
20944
  entry.fired = true;
@@ -21066,7 +21084,8 @@ function validateComponentCandidate(candidate, opts = {}) {
21066
21084
  const own = issues.filter((i) => !i.specId || i.specId === candidate.id);
21067
21085
  return {
21068
21086
  errors: own.filter((i) => i.severity === "error"),
21069
- warnings: own.filter((i) => i.severity === "warning")
21087
+ warnings: own.filter((i) => i.severity === "warning"),
21088
+ notices: own.filter((i) => i.severity === "notice")
21070
21089
  };
21071
21090
  }
21072
21091
 
@@ -21090,7 +21109,7 @@ var RESOLUTION_FAILURE_CODES = /* @__PURE__ */ new Set([
21090
21109
  "INVALID_TRUSTED_LINK",
21091
21110
  "CROSS_TREE_REF_UNRESOLVED"
21092
21111
  ]);
21093
- var SEVERITY_RANK = { off: 0, warning: 1, error: 2 };
21112
+ var SEVERITY_RANK = { off: 0, notice: 1, warning: 2, error: 3 };
21094
21113
  function issueKey(issue2) {
21095
21114
  return `${issue2.code}|${issue2.specId ?? ""}`;
21096
21115
  }
@@ -28239,6 +28258,15 @@ var logger = {
28239
28258
  console.warn(import_chalk.default.yellow("\u26A0") + " " + import_chalk.default.yellow(message));
28240
28259
  }
28241
28260
  },
28261
+ /**
28262
+ * A finding reported at `notice` severity: listed, never a failure. Kept
28263
+ * visually apart from warn so a reader never mistakes one for the other.
28264
+ */
28265
+ notice(message) {
28266
+ if (shouldLog("info")) {
28267
+ console.log(import_chalk.default.blue("\u25C6") + " " + import_chalk.default.blue("notice ") + message);
28268
+ }
28269
+ },
28242
28270
  error(message) {
28243
28271
  console.error(import_chalk.default.red("\u2716") + " " + import_chalk.default.red(message));
28244
28272
  },
@@ -28519,11 +28547,15 @@ function candidateOptions() {
28519
28547
  return config ? { rules: config.rules, projectType: config.projectType } : {};
28520
28548
  }
28521
28549
  function noticesFrom(verdict) {
28522
- return verdict.warnings.map((w) => `${w.code}: ${w.message}`);
28550
+ return [
28551
+ ...verdict.warnings.map((w) => `${w.code}: ${w.message}`),
28552
+ ...verdict.notices.map((n) => `${n.code} (notice): ${n.message}`)
28553
+ ];
28523
28554
  }
28524
- function componentCandidateGate(options = candidateOptions()) {
28555
+ function componentCandidateGate(options = candidateOptions(), storedOwner) {
28525
28556
  return {
28526
28557
  gate: (kind, merged) => {
28558
+ refuseUnknownOwner(kind, merged, storedOwner);
28527
28559
  if (kind !== "component") return;
28528
28560
  const verdict = validateComponentCandidate(merged, options);
28529
28561
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
@@ -28531,6 +28563,14 @@ function componentCandidateGate(options = candidateOptions()) {
28531
28563
  }
28532
28564
  };
28533
28565
  }
28566
+ function refuseUnknownOwner(kind, merged, storedOwner) {
28567
+ if (kind !== "component" && kind !== "type") return;
28568
+ const owner = merged.subsystem;
28569
+ if (!owner || owner === storedOwner) return;
28570
+ if (loadSpec("subsystem", owner)) return;
28571
+ const sentence = MISSING_PARENT[kind](owner);
28572
+ throw new Error(`${sentence} Nothing was written.`);
28573
+ }
28534
28574
  var STORE_MANAGED_FIELDS = /* @__PURE__ */ new Set(["status", "updatedAt"]);
28535
28575
  var ALWAYS_CARRIED_FIELDS = /* @__PURE__ */ new Set(["ext"]);
28536
28576
  var STATUS_ORDER = ["draft", "design", "complete"];
@@ -28826,9 +28866,9 @@ function deleteSpec2(kind, id) {
28826
28866
  }
28827
28867
  function updateSpecGated(kind, id, delta, dryRun) {
28828
28868
  const bound2 = candidateOptions();
28829
- const gate = componentCandidateGate(bound2);
28830
28869
  const testRoots = bound2.rules?.conformance?.testRoots ?? [];
28831
- const stored = testRoots.length > 0 ? loadSpec(kind, id) : null;
28870
+ const stored = loadSpec(kind, id);
28871
+ const gate = componentCandidateGate(bound2, stored?.subsystem);
28832
28872
  const report = updateSpec(kind, id, delta, gate, dryRun);
28833
28873
  const changed = changedMethods(report);
28834
28874
  if (changed.length > 0 && testRoots.length > 0) {
@@ -29128,7 +29168,7 @@ var specChangeReportOutput = {
29128
29168
  ...staleServerOutput
29129
29169
  };
29130
29170
  var validationIssueOutput = {
29131
- severity: import_zod11.z.enum(["error", "warning"]).describe("The finding's severity after project overrides."),
29171
+ severity: import_zod11.z.enum(["error", "warning", "notice"]).describe("The finding's severity after project overrides."),
29132
29172
  code: import_zod11.z.string().describe("The rule code, UPPER_SNAKE \u2014 the stable handle to filter and suppress by."),
29133
29173
  message: import_zod11.z.string().describe("What is wrong, named."),
29134
29174
  agentId: import_zod11.z.string().optional().describe("The agent the finding concerns, when it concerns one."),
@@ -29141,9 +29181,12 @@ var validationIssueOutput = {
29141
29181
  )
29142
29182
  };
29143
29183
  var validateTreeOutput = {
29144
- valid: import_zod11.z.boolean().describe("False when the tree holds at least one error."),
29184
+ valid: import_zod11.z.boolean().describe("False when the tree holds at least one error; warnings and notices never make it false."),
29145
29185
  errors: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity error."),
29146
29186
  warnings: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity warning."),
29187
+ notices: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe(
29188
+ "Every finding of severity notice: reported, never a failure \u2014 they never make the tree invalid and never fail `wairon validate --ci`."
29189
+ ),
29147
29190
  resolvedThrough: import_zod11.z.object({
29148
29191
  root: import_zod11.z.string().describe("The top root that was validated."),
29149
29192
  scope: import_zod11.z.string().describe("The mount chain the verdict was scoped to.")
@@ -29530,7 +29573,7 @@ function createMcpServer(options = {}) {
29530
29573
  server,
29531
29574
  "validateTopology",
29532
29575
  {
29533
- description: "Validate the project's agent topology. Returns errors and warnings (duplicate ids, overlapping ownership, missing paths, etc.). Supports optional subsystem scoping.",
29576
+ description: "Validate the project's agent topology. Returns errors, warnings and notices (duplicate ids, overlapping ownership, missing paths, etc.). Supports optional subsystem scoping.",
29534
29577
  inputSchema: {
29535
29578
  subsystem: import_zod11.z.string().optional().describe("Only validate topology for agents under the specified subsystem")
29536
29579
  }
@@ -29550,7 +29593,8 @@ function createMcpServer(options = {}) {
29550
29593
  return json({
29551
29594
  valid: result.issues.filter((i) => i.severity === "error").length === 0,
29552
29595
  errors: result.issues.filter((i) => i.severity === "error"),
29553
- warnings: result.issues.filter((i) => i.severity === "warning")
29596
+ warnings: result.issues.filter((i) => i.severity === "warning"),
29597
+ notices: result.issues.filter((i) => i.severity === "notice")
29554
29598
  });
29555
29599
  } catch (e) {
29556
29600
  return errText(String(e));
@@ -30268,7 +30312,7 @@ ${renderChangeReport(report)}`,
30268
30312
  server,
30269
30313
  "sdd_validate_tree",
30270
30314
  {
30271
- description: "Validate the SDD spec tree, checking parent references, contract compatibility, narratives, and component type boundaries. Supports scoping and recursion controls. Findings come back as structured content too, under the schema this tool declares \u2014 errors and warnings already split, each with its code, severity, message and the spec it concerns \u2014 so a caller filters them as objects instead of parsing the JSON text block and hoping its shape holds.",
30315
+ description: "Validate the SDD spec tree, checking parent references, contract compatibility, narratives, and component type boundaries. Supports scoping and recursion controls. Findings come back as structured content too, under the schema this tool declares \u2014 errors, warnings and notices already split into three lists (a notice never makes the tree invalid and never fails --ci), each with its code, severity, message and the spec it concerns \u2014 so a caller filters them as objects instead of parsing the JSON text block and hoping its shape holds.",
30272
30316
  inputSchema: {
30273
30317
  subsystem: import_zod11.z.string().optional().describe("Only validate the specified subsystem (granular)"),
30274
30318
  recursive: import_zod11.z.boolean().optional().describe("Whether to recursively validate subprojects (default: true)")
@@ -30289,6 +30333,7 @@ ${renderChangeReport(report)}`,
30289
30333
  valid: result.valid,
30290
30334
  errors: result.issues.filter((i) => i.severity === "error"),
30291
30335
  warnings: result.issues.filter((i) => i.severity === "warning"),
30336
+ notices: result.issues.filter((i) => i.severity === "notice"),
30292
30337
  ...result.resolvedThrough ? { resolvedThrough: result.resolvedThrough } : {}
30293
30338
  });
30294
30339
  } catch (e) {
@@ -30404,7 +30449,7 @@ ${testsBlock}` : ""}`, deletion);
30404
30449
  inputSchema: {
30405
30450
  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)"),
30406
30451
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
30407
- 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.`),
30452
+ 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 or NOTICE 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.`),
30408
30453
  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.")
30409
30454
  },
30410
30455
  outputSchema: specChangeReportOutput