@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/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.77";
68
+ WAIRON_VERSION = "5.1.1-dev.79";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -117,6 +117,15 @@ var init_logger = __esm({
117
117
  console.warn(import_chalk.default.yellow("\u26A0") + " " + import_chalk.default.yellow(message));
118
118
  }
119
119
  },
120
+ /**
121
+ * A finding reported at `notice` severity: listed, never a failure. Kept
122
+ * visually apart from warn so a reader never mistakes one for the other.
123
+ */
124
+ notice(message) {
125
+ if (shouldLog("info")) {
126
+ console.log(import_chalk.default.blue("\u25C6") + " " + import_chalk.default.blue("notice ") + message);
127
+ }
128
+ },
120
129
  error(message) {
121
130
  console.error(import_chalk.default.red("\u2716") + " " + import_chalk.default.red(message));
122
131
  },
@@ -864,9 +873,11 @@ var init_project = __esm({
864
873
  materializeAgentFiles: import_zod3.z.boolean().default(false),
865
874
  /**
866
875
  * Severity overrides for SDD validation rules.
867
- * Key: rule code (e.g. CIRCULAR_DEPENDENCY), Value: error | warning | off
876
+ * Key: rule code (e.g. CIRCULAR_DEPENDENCY), Value: error | warning | notice | off.
877
+ * A notice is still reported, but never makes the tree invalid and never
878
+ * fails `--ci`; off is not reported at all.
868
879
  */
869
- sddRuleSeverity: import_zod3.z.record(import_zod3.z.enum(["error", "warning", "off"])).default({}),
880
+ sddRuleSeverity: import_zod3.z.record(import_zod3.z.enum(["error", "warning", "notice", "off"])).default({}),
870
881
  /** Dynamic naming conventions and stereotype suffix rules */
871
882
  naming: NamingRuleConfigSchema.optional(),
872
883
  /** Dynamic metadata documentation constraints */
@@ -4412,6 +4423,8 @@ body:not(.panel-closed) #panelToggle { background:var(--accent); color:#fff; bor
4412
4423
  #panel .flowbtn:hover { background:var(--hover-bg); }
4413
4424
  #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; }
4414
4425
  #panel .issue.warning { border-left-color:var(--warn); }
4426
+ #panel .issue.notice { border-left-color:var(--accent); }
4427
+ #panel .issue .sev { font-size:10px; color:var(--dim); text-transform:uppercase; letter-spacing:.04em; margin-left:6px; }
4415
4428
  #panel .issue code { font-size:10.5px; color:var(--dim); }
4416
4429
 
4417
4430
  /* Presentation mode = the canvas page, focused: the header chrome and legend
@@ -4622,7 +4635,15 @@ var MODEL = __MODEL_JSON__;
4622
4635
  });
4623
4636
  document.getElementById('issueCount').textContent =
4624
4637
  MODEL.issues.filter(function (i) { return i.severity === 'error'; }).length + 'e/' +
4625
- MODEL.issues.filter(function (i) { return i.severity === 'warning'; }).length + 'w';
4638
+ MODEL.issues.filter(function (i) { return i.severity === 'warning'; }).length + 'w/' +
4639
+ MODEL.issues.filter(function (i) { return i.severity === 'notice'; }).length + 'n';
4640
+ // A node is marked failing only for an error or a warning; one that holds
4641
+ // only notices gets its own quieter mark, never the failing one.
4642
+ function issueMark(id) {
4643
+ var list = state.showIssues && issuesBySpec[id];
4644
+ if (!list) return '';
4645
+ return list.some(function (i) { return i.severity !== 'notice'; }) ? ' hasIssue' : ' hasNotice';
4646
+ }
4626
4647
 
4627
4648
  var PATTERN_TYPES = { Repository:1, FeatureComponent:1, RouterComponent:1 };
4628
4649
  // A retired Specialist or Gateway renders as a plain box marked retired, so a
@@ -4962,6 +4983,9 @@ var MODEL = __MODEL_JSON__;
4962
4983
  { selector: 'edge.stubHover', style: { 'line-color': t.selGlow, 'target-arrow-color': t.selGlow, width: 2.6, opacity: 1, 'z-compound-depth': 'top' } },
4963
4984
  { selector: '.dimmed', style: { opacity: 0.13 } },
4964
4985
  { selector: '.hasIssue', style: { 'border-color': t.issue, 'border-style': 'dashed', 'border-width': 3 } },
4986
+ // Notices only: the node keeps its own border colour and width (no layout
4987
+ // nudge), drawn dotted so it reads as noted rather than failing.
4988
+ { selector: '.hasNotice', style: { 'border-style': 'dotted' } },
4965
4989
  // Overlay only (no border) \u2014 a border changes node geometry, which nudges
4966
4990
  // the compound parent and makes hover flicker; overlay never affects layout.
4967
4991
  { selector: '.sel', style: { 'overlay-color': t.selGlow, 'overlay-opacity': 0.34, 'overlay-padding': 6 } },
@@ -5751,7 +5775,7 @@ var MODEL = __MODEL_JSON__;
5751
5775
  var kindCls = t.kind === 'entity' ? 'typeEntity' : 'typeValue';
5752
5776
  var dim = !typeMatches(t);
5753
5777
  var extra = (dim ? ' dimmed' : '')
5754
- + (state.showIssues && issuesBySpec[t.id] ? ' hasIssue' : '')
5778
+ + issueMark(t.id)
5755
5779
  + (state.selectedKind === 'type' && state.selected === t.id ? ' sel' : '');
5756
5780
  if (sh.plain) {
5757
5781
  eles.push({
@@ -6041,7 +6065,7 @@ var MODEL = __MODEL_JSON__;
6041
6065
  }
6042
6066
  classes += (e.hasKids ? ' drillable' : '') + (isPub ? ' public' : '')
6043
6067
  + (dim ? ' dimmed' : '')
6044
- + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
6068
+ + issueMark(e.id)
6045
6069
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
6046
6070
  if (inner) {
6047
6071
  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 };
@@ -7986,7 +8010,7 @@ var MODEL = __MODEL_JSON__;
7986
8010
  }
7987
8011
  function issueHtml(list) {
7988
8012
  return list.map(function (i) {
7989
- return '<div class="issue ' + esc(i.severity) + '"><code>' + esc(i.code) + '</code><br>' + esc(i.message) + '</div>';
8013
+ return '<div class="issue ' + esc(i.severity) + '"><code>' + esc(i.code) + '</code><span class="sev">' + esc(i.severity) + '</span><br>' + esc(i.message) + '</div>';
7990
8014
  }).join('');
7991
8015
  }
7992
8016
 
@@ -10613,7 +10637,7 @@ var init_lint_allows = __esm({
10613
10637
  "use strict";
10614
10638
  lintAllowsRule = {
10615
10639
  name: "lint-allows",
10616
- 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.",
10640
+ 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.",
10617
10641
  codes: [
10618
10642
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
10619
10643
  { 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" }
@@ -18379,6 +18403,9 @@ var init_source_analysis = __esm({
18379
18403
  });
18380
18404
 
18381
18405
  // src/core/rules/index.ts
18406
+ function atMostWarning(severity) {
18407
+ return severity === "error" ? "warning" : severity;
18408
+ }
18382
18409
  function makeScopeFilter(opts) {
18383
18410
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
18384
18411
  return (specId) => {
@@ -18614,7 +18641,7 @@ function buildRuleContext(opts) {
18614
18641
  return profileSeverity;
18615
18642
  }
18616
18643
  if (isDraftContext && COMPLETENESS_RULES.has(ruleCode)) {
18617
- return "warning";
18644
+ return atMostWarning(defaultSeverity);
18618
18645
  }
18619
18646
  return defaultSeverity;
18620
18647
  };
@@ -18688,13 +18715,13 @@ function buildRuleContext(opts) {
18688
18715
  allowClaimed = true;
18689
18716
  const grew = (parts?.covers ?? []).filter((unit) => !(allow.covers ?? []).includes(unit));
18690
18717
  if (grew.length === 0) {
18691
- if (severity === "warning") return;
18718
+ if (severity !== "error") return;
18692
18719
  } else {
18693
18720
  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.`;
18694
18721
  }
18695
18722
  }
18696
18723
  }
18697
- if (specId && parts && severity === "warning" && !allowClaimed) {
18724
+ if (specId && parts && severity !== "error" && !allowClaimed) {
18698
18725
  const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
18699
18726
  if (entry) {
18700
18727
  entry.fired = true;
@@ -18927,7 +18954,8 @@ function validateComponentCandidate(candidate, opts = {}) {
18927
18954
  const own = issues.filter((i) => !i.specId || i.specId === candidate.id);
18928
18955
  return {
18929
18956
  errors: own.filter((i) => i.severity === "error"),
18930
- warnings: own.filter((i) => i.severity === "warning")
18957
+ warnings: own.filter((i) => i.severity === "warning"),
18958
+ notices: own.filter((i) => i.severity === "notice")
18931
18959
  };
18932
18960
  }
18933
18961
  var init_candidate = __esm({
@@ -19414,7 +19442,7 @@ var init_validation = __esm({
19414
19442
  "INVALID_TRUSTED_LINK",
19415
19443
  "CROSS_TREE_REF_UNRESOLVED"
19416
19444
  ]);
19417
- SEVERITY_RANK = { off: 0, warning: 1, error: 2 };
19445
+ SEVERITY_RANK = { off: 0, notice: 1, warning: 2, error: 3 };
19418
19446
  }
19419
19447
  });
19420
19448
 
@@ -30222,11 +30250,15 @@ function candidateOptions() {
30222
30250
  return config ? { rules: config.rules, projectType: config.projectType } : {};
30223
30251
  }
30224
30252
  function noticesFrom(verdict) {
30225
- return verdict.warnings.map((w) => `${w.code}: ${w.message}`);
30253
+ return [
30254
+ ...verdict.warnings.map((w) => `${w.code}: ${w.message}`),
30255
+ ...verdict.notices.map((n) => `${n.code} (notice): ${n.message}`)
30256
+ ];
30226
30257
  }
30227
- function componentCandidateGate(options = candidateOptions()) {
30258
+ function componentCandidateGate(options = candidateOptions(), storedOwner) {
30228
30259
  return {
30229
30260
  gate: (kind, merged) => {
30261
+ refuseUnknownOwner(kind, merged, storedOwner);
30230
30262
  if (kind !== "component") return;
30231
30263
  const verdict = validateComponentCandidate(merged, options);
30232
30264
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
@@ -30234,6 +30266,14 @@ function componentCandidateGate(options = candidateOptions()) {
30234
30266
  }
30235
30267
  };
30236
30268
  }
30269
+ function refuseUnknownOwner(kind, merged, storedOwner) {
30270
+ if (kind !== "component" && kind !== "type") return;
30271
+ const owner = merged.subsystem;
30272
+ if (!owner || owner === storedOwner) return;
30273
+ if (loadSpec("subsystem", owner)) return;
30274
+ const sentence = MISSING_PARENT[kind](owner);
30275
+ throw new Error(`${sentence} Nothing was written.`);
30276
+ }
30237
30277
  function restatementParent(restatement) {
30238
30278
  const spec = restatement.spec;
30239
30279
  switch (restatement.kind) {
@@ -30497,9 +30537,9 @@ function deleteSpec2(kind, id) {
30497
30537
  }
30498
30538
  function updateSpecGated(kind, id, delta, dryRun) {
30499
30539
  const bound2 = candidateOptions();
30500
- const gate = componentCandidateGate(bound2);
30501
30540
  const testRoots = bound2.rules?.conformance?.testRoots ?? [];
30502
- const stored = testRoots.length > 0 ? loadSpec(kind, id) : null;
30541
+ const stored = loadSpec(kind, id);
30542
+ const gate = componentCandidateGate(bound2, stored?.subsystem);
30503
30543
  const report2 = updateSpec(kind, id, delta, gate, dryRun);
30504
30544
  const changed = changedMethods(report2);
30505
30545
  if (changed.length > 0 && testRoots.length > 0) {
@@ -31143,7 +31183,7 @@ function createMcpServer(options = {}) {
31143
31183
  server,
31144
31184
  "validateTopology",
31145
31185
  {
31146
- description: "Validate the project's agent topology. Returns errors and warnings (duplicate ids, overlapping ownership, missing paths, etc.). Supports optional subsystem scoping.",
31186
+ description: "Validate the project's agent topology. Returns errors, warnings and notices (duplicate ids, overlapping ownership, missing paths, etc.). Supports optional subsystem scoping.",
31147
31187
  inputSchema: {
31148
31188
  subsystem: import_zod11.z.string().optional().describe("Only validate topology for agents under the specified subsystem")
31149
31189
  }
@@ -31163,7 +31203,8 @@ function createMcpServer(options = {}) {
31163
31203
  return json({
31164
31204
  valid: result.issues.filter((i) => i.severity === "error").length === 0,
31165
31205
  errors: result.issues.filter((i) => i.severity === "error"),
31166
- warnings: result.issues.filter((i) => i.severity === "warning")
31206
+ warnings: result.issues.filter((i) => i.severity === "warning"),
31207
+ notices: result.issues.filter((i) => i.severity === "notice")
31167
31208
  });
31168
31209
  } catch (e) {
31169
31210
  return errText(String(e));
@@ -31881,7 +31922,7 @@ ${renderChangeReport(report2)}`,
31881
31922
  server,
31882
31923
  "sdd_validate_tree",
31883
31924
  {
31884
- 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.",
31925
+ 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.",
31885
31926
  inputSchema: {
31886
31927
  subsystem: import_zod11.z.string().optional().describe("Only validate the specified subsystem (granular)"),
31887
31928
  recursive: import_zod11.z.boolean().optional().describe("Whether to recursively validate subprojects (default: true)")
@@ -31902,6 +31943,7 @@ ${renderChangeReport(report2)}`,
31902
31943
  valid: result.valid,
31903
31944
  errors: result.issues.filter((i) => i.severity === "error"),
31904
31945
  warnings: result.issues.filter((i) => i.severity === "warning"),
31946
+ notices: result.issues.filter((i) => i.severity === "notice"),
31905
31947
  ...result.resolvedThrough ? { resolvedThrough: result.resolvedThrough } : {}
31906
31948
  });
31907
31949
  } catch (e) {
@@ -32017,7 +32059,7 @@ ${testsBlock}` : ""}`, deletion);
32017
32059
  inputSchema: {
32018
32060
  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)"),
32019
32061
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
32020
- 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.`),
32062
+ 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.`),
32021
32063
  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.")
32022
32064
  },
32023
32065
  outputSchema: specChangeReportOutput
@@ -32333,7 +32375,7 @@ var init_server = __esm({
32333
32375
  ...staleServerOutput
32334
32376
  };
32335
32377
  validationIssueOutput = {
32336
- severity: import_zod11.z.enum(["error", "warning"]).describe("The finding's severity after project overrides."),
32378
+ severity: import_zod11.z.enum(["error", "warning", "notice"]).describe("The finding's severity after project overrides."),
32337
32379
  code: import_zod11.z.string().describe("The rule code, UPPER_SNAKE \u2014 the stable handle to filter and suppress by."),
32338
32380
  message: import_zod11.z.string().describe("What is wrong, named."),
32339
32381
  agentId: import_zod11.z.string().optional().describe("The agent the finding concerns, when it concerns one."),
@@ -32346,9 +32388,12 @@ var init_server = __esm({
32346
32388
  )
32347
32389
  };
32348
32390
  validateTreeOutput = {
32349
- valid: import_zod11.z.boolean().describe("False when the tree holds at least one error."),
32391
+ valid: import_zod11.z.boolean().describe("False when the tree holds at least one error; warnings and notices never make it false."),
32350
32392
  errors: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity error."),
32351
32393
  warnings: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity warning."),
32394
+ notices: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe(
32395
+ "Every finding of severity notice: reported, never a failure \u2014 they never make the tree invalid and never fail `wairon validate --ci`."
32396
+ ),
32352
32397
  resolvedThrough: import_zod11.z.object({
32353
32398
  root: import_zod11.z.string().describe("The top root that was validated."),
32354
32399
  scope: import_zod11.z.string().describe("The mount chain the verdict was scoped to.")
@@ -33679,7 +33724,8 @@ async function runLock(options = {}, gate) {
33679
33724
  validationResult: {
33680
33725
  valid: true,
33681
33726
  errors: 0,
33682
- warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0
33727
+ warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0,
33728
+ notices: gate ? gate.issues.filter((i) => i.severity === "notice").length : 0
33683
33729
  },
33684
33730
  status: "ready",
33685
33731
  specs: captureApprovedSpecs(root, scope),
@@ -33731,6 +33777,7 @@ async function runValidate(options = {}) {
33731
33777
  let hasErrors = false;
33732
33778
  let hasFatalWarnings = false;
33733
33779
  let waivedWarnings = 0;
33780
+ let noticeTotal = 0;
33734
33781
  const legacySpecs = findLegacySpecFiles();
33735
33782
  if (legacySpecs.length > 0) {
33736
33783
  logger.warn(`Warning: ${legacySpecs.length} legacy spec filename(s) detected (e.g., component.yaml). These are deprecated. Please run \`wairon doctor --fix\` to migrate them to the new unified .index.yaml schema.`);
@@ -33745,6 +33792,9 @@ async function runValidate(options = {}) {
33745
33792
  if (issue2.severity === "error") {
33746
33793
  logger.error(`[${issue2.code}] ${issue2.message}`);
33747
33794
  hasErrors = true;
33795
+ } else if (issue2.severity === "notice") {
33796
+ logger.notice(`[${issue2.code}] ${issue2.message}`);
33797
+ noticeTotal++;
33748
33798
  } else {
33749
33799
  logger.warn(`[${issue2.code}] ${issue2.message}`);
33750
33800
  hasFatalWarnings = true;
@@ -33762,6 +33812,9 @@ async function runValidate(options = {}) {
33762
33812
  if (issue2.severity === "error") {
33763
33813
  logger.error(`${prefix}[${issue2.code}] ${issue2.message}`);
33764
33814
  hasErrors = true;
33815
+ } else if (issue2.severity === "notice") {
33816
+ logger.notice(`${prefix}[${issue2.code}] ${issue2.message}`);
33817
+ noticeTotal++;
33765
33818
  } else {
33766
33819
  logger.warn(`${prefix}[${issue2.code}] ${issue2.message}`);
33767
33820
  hasFatalWarnings = true;
@@ -33788,9 +33841,11 @@ async function runValidate(options = {}) {
33788
33841
  } else {
33789
33842
  let errorCount = 0;
33790
33843
  let warningCount = 0;
33844
+ let noticeCount = 0;
33791
33845
  const MAX_PRINT = 100;
33792
33846
  let skippedErrors = 0;
33793
33847
  let skippedWarnings = 0;
33848
+ let skippedNotices = 0;
33794
33849
  for (const issue2 of sddResult.issues) {
33795
33850
  const prefix = issue2.specId ? import_chalk5.default.gray(`[${issue2.specId}] `) : "";
33796
33851
  if (issue2.severity === "error") {
@@ -33801,6 +33856,14 @@ async function runValidate(options = {}) {
33801
33856
  } else {
33802
33857
  skippedErrors++;
33803
33858
  }
33859
+ } else if (issue2.severity === "notice") {
33860
+ noticeTotal++;
33861
+ if (noticeCount < MAX_PRINT) {
33862
+ logger.notice(`${prefix}[${issue2.code}] ${issue2.message}`);
33863
+ noticeCount++;
33864
+ } else {
33865
+ skippedNotices++;
33866
+ }
33804
33867
  } else {
33805
33868
  if (isCiDraftWaivable(issue2)) {
33806
33869
  waivedWarnings++;
@@ -33821,6 +33884,9 @@ async function runValidate(options = {}) {
33821
33884
  if (skippedWarnings > 0) {
33822
33885
  logger.warn(`... and ${skippedWarnings} more warning(s) omitted. Use '--subsystem <id>' to validate a specific subsystem.`);
33823
33886
  }
33887
+ if (skippedNotices > 0) {
33888
+ logger.notice(`... and ${skippedNotices} more notice(s) omitted. Use '--subsystem <id>' to validate a specific subsystem.`);
33889
+ }
33824
33890
  }
33825
33891
  }
33826
33892
  const summary = carriedDebtSummary(projectConfig.rules?.conformance?.carried);
@@ -33830,6 +33896,9 @@ async function runValidate(options = {}) {
33830
33896
  for (const note of summary.revisits) logger.info(import_chalk5.default.gray(note));
33831
33897
  }
33832
33898
  logger.blank();
33899
+ if (noticeTotal > 0) {
33900
+ logger.info(import_chalk5.default.blue(`${noticeTotal} notice(s) reported \u2014 never part of the failure decision${options.ci ? " (--ci does not fail on notices)" : ""}.`));
33901
+ }
33833
33902
  if (options.ci && waivedWarnings > 0) {
33834
33903
  logger.info(import_chalk5.default.gray(`${waivedWarnings} draft-related warning(s) (non-fatal in --ci): excluded from the failure decision because the referenced specs are in draft/design status.`));
33835
33904
  }
@@ -33844,7 +33913,7 @@ async function runValidate(options = {}) {
33844
33913
  process.exit(1);
33845
33914
  } else {
33846
33915
  if (options.ci) {
33847
- logger.success("All checks passed (CI mode \u2014 warnings treated as errors, draft-related warnings excepted).");
33916
+ logger.success("All checks passed (CI mode \u2014 warnings treated as errors, draft-related warnings excepted; notices never fail).");
33848
33917
  } else {
33849
33918
  logger.success("All checks passed.");
33850
33919
  }
@@ -34871,8 +34940,11 @@ async function runDoctor(options = {}) {
34871
34940
  const result = validateSddTree(cfg.rules, cfg.projectType);
34872
34941
  const errs = result.issues.filter((i) => i.severity === "error").length;
34873
34942
  const warns = result.issues.filter((i) => i.severity === "warning").length;
34874
- if (errs > 0) line(tally, "error", `Conformance: ${errs} error(s), ${warns} warning(s) \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
34875
- else if (warns > 0) line(tally, "warn", `Conformance: ${warns} warning(s) \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
34943
+ const notes = result.issues.filter((i) => i.severity === "notice").length;
34944
+ const noted = notes > 0 ? `, ${notes} notice(s)` : "";
34945
+ if (errs > 0) line(tally, "error", `Conformance: ${errs} error(s), ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
34946
+ else if (warns > 0) line(tally, "warn", `Conformance: ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
34947
+ else if (notes > 0) line(tally, "ok", `Conformance: 0 errors, 0 warnings, ${notes} notice(s) \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
34876
34948
  else line(tally, "ok", "Conformance: 0 errors, 0 warnings");
34877
34949
  } catch (e) {
34878
34950
  line(tally, "warn", `Could not run conformance check: ${e instanceof Error ? e.message : String(e)}`);
@@ -35362,6 +35434,7 @@ async function listRules3() {
35362
35434
  const sevLabel = (sev) => {
35363
35435
  if (sev === "error") return import_chalk13.default.red("error ");
35364
35436
  if (sev === "warning") return import_chalk13.default.yellow("warning");
35437
+ if (sev === "notice") return import_chalk13.default.blue("notice ");
35365
35438
  return import_chalk13.default.gray("off ");
35366
35439
  };
35367
35440
  const ext = loadProjectExtensions();
@@ -35394,7 +35467,7 @@ SDD conformance rules (${active.length})
35394
35467
  for (const err of ext.errors) {
35395
35468
  logger.error(err);
35396
35469
  }
35397
- logger.info("Override severities per project via rules.sddRuleSeverity in .wai/project.yaml (error | warning | off).");
35470
+ logger.info("Override severities per project via rules.sddRuleSeverity in .wai/project.yaml (error | warning | notice | off). A notice is reported but never fails the gate, --ci included.");
35398
35471
  if (ext.packNames.length) {
35399
35472
  logger.info(`Extension packs loaded: ${ext.packNames.join(", ")} (.wai/project.yaml \u2192 extensions.packs).`);
35400
35473
  }
@@ -38445,7 +38518,8 @@ function executeApprovedLock(cfg, projectId, approver, subproject) {
38445
38518
  validationResult: {
38446
38519
  valid: true,
38447
38520
  errors: 0,
38448
- warnings: result.issues.filter((i) => i.severity === "warning").length
38521
+ warnings: result.issues.filter((i) => i.severity === "warning").length,
38522
+ notices: result.issues.filter((i) => i.severity === "notice").length
38449
38523
  },
38450
38524
  status: "ready",
38451
38525
  specs,
@@ -44709,13 +44783,22 @@ function reshapeLandscapeGraph(model, level) {
44709
44783
  }
44710
44784
  function overlayProjectIssues(graph, issues, projectId, level) {
44711
44785
  const counts = /* @__PURE__ */ new Map();
44786
+ const noticeCounts = /* @__PURE__ */ new Map();
44712
44787
  for (const issue2 of issues) {
44713
44788
  const id = issue2.specId;
44714
- if (id) counts.set(id, (counts.get(id) ?? 0) + 1);
44789
+ if (!id) continue;
44790
+ const into = issue2.severity === "notice" ? noticeCounts : counts;
44791
+ into.set(id, (into.get(id) ?? 0) + 1);
44715
44792
  }
44716
44793
  const nodes = graph.nodes.map((n) => {
44717
44794
  const count = counts.get(n.id);
44718
- return count !== void 0 ? { ...n, issueCount: count } : n;
44795
+ const notices = noticeCounts.get(n.id);
44796
+ if (count === void 0 && notices === void 0) return n;
44797
+ return {
44798
+ ...n,
44799
+ ...count !== void 0 ? { issueCount: count } : {},
44800
+ ...notices !== void 0 ? { noticeCount: notices } : {}
44801
+ };
44719
44802
  });
44720
44803
  return {
44721
44804
  ...graph,
@@ -45419,7 +45502,8 @@ details.adv summary { cursor:pointer; color:var(--dim); font-size:12px; margin-b
45419
45502
  nodes.forEach(function (n) {
45420
45503
  var li = document.createElement('li');
45421
45504
  li.innerHTML = '<span class="kind">' + esc(n.kind) + '</span><span>' + esc(n.label || n.id) + '</span>'
45422
- + (n.issueCount ? ' <span class="pill bad" title="validation issues">' + n.issueCount + '</span>' : '');
45505
+ + (n.issueCount ? ' <span class="pill bad" title="validation errors and warnings">' + n.issueCount + '</span>' : '')
45506
+ + (n.noticeCount ? ' <span class="pill" title="validation notices">' + n.noticeCount + '</span>' : '');
45423
45507
  li.addEventListener('click', function () {
45424
45508
  Array.prototype.forEach.call(list.children, function (x) { x.classList.remove('sel'); });
45425
45509
  li.classList.add('sel');
@@ -45462,14 +45546,18 @@ details.adv summary { cursor:pointer; color:var(--dim); font-size:12px; margin-b
45462
45546
  $('btnValidate').addEventListener('click', function () {
45463
45547
  var insp = $('insp'); insp.innerHTML = '<div class="hint">Validating\u2026</div>';
45464
45548
  mcp('sdd_validate_tree', {}).then(function (res) {
45465
- var issues = (res && res.issues) || [];
45549
+ // sdd_validate_tree answers with three lists \u2014 errors, warnings, notices;
45550
+ // this view used to read an "issues" list the tool never sends, so it
45551
+ // reported every tree clean.
45552
+ var issues = (res && res.issues) || [].concat((res && res.errors) || [], (res && res.warnings) || [], (res && res.notices) || []);
45466
45553
  var errs = issues.filter(function (i) { return i.severity === 'error'; }).length;
45467
- var warns = issues.length - errs;
45468
- var html = '<h2>Validation</h2><p class="meta">' + (issues.length ? (errs + ' error(s), ' + warns + ' warning(s)') : 'clean \u2014 no findings') + '</p>';
45554
+ var notes = issues.filter(function (i) { return i.severity === 'notice'; }).length;
45555
+ var warns = issues.length - errs - notes;
45556
+ var html = '<h2>Validation</h2><p class="meta">' + (issues.length ? (errs + ' error(s), ' + warns + ' warning(s), ' + notes + ' notice(s)') : 'clean \u2014 no findings') + '</p>';
45469
45557
  if (issues.length) {
45470
45558
  html += '<table class="grid"><thead><tr><th>Severity</th><th>Code</th><th>Spec</th><th>Message</th></tr></thead><tbody>';
45471
45559
  issues.slice(0, 200).forEach(function (i) {
45472
- var cls = i.severity === 'error' ? 'bad' : 'warn';
45560
+ var cls = i.severity === 'error' ? 'bad' : i.severity === 'notice' ? '' : 'warn';
45473
45561
  html += '<tr><td><span class="pill ' + cls + '">' + esc(i.severity) + '</span></td><td>' + esc(i.code) + '</td><td>' + esc(i.specId || '') + '</td><td>' + esc(i.message) + '</td></tr>';
45474
45562
  });
45475
45563
  html += '</tbody></table>';
@@ -50446,9 +50534,11 @@ async function validateCommand(opts) {
50446
50534
  const report2 = await validateAttached(target, opts.subsystem);
50447
50535
  const errors = report2.errors ?? [];
50448
50536
  const warnings = report2.warnings ?? [];
50449
- logger.info(`Validated "${target.projectId}" on ${target.url} \u2014 ${errors.length} error(s), ${warnings.length} warning(s).`);
50537
+ const notices = report2.notices ?? [];
50538
+ logger.info(`Validated "${target.projectId}" on ${target.url} \u2014 ${errors.length} error(s), ${warnings.length} warning(s), ${notices.length} notice(s).`);
50450
50539
  for (const e of errors) logger.error(` [${e.code}] ${e.specId ? `${e.specId}: ` : ""}${e.message}`);
50451
50540
  for (const w of warnings) logger.warn(` [${w.code}] ${w.specId ? `${w.specId}: ` : ""}${w.message}`);
50541
+ for (const n of notices) logger.notice(` [${n.code}] ${n.specId ? `${n.specId}: ` : ""}${n.message}`);
50452
50542
  if (errors.length || opts.ci && warnings.length) process.exit(1);
50453
50543
  return;
50454
50544
  }
@@ -50478,7 +50568,7 @@ program.command("lock-check").description("Merge gate: is the design in this wor
50478
50568
  program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
50479
50569
  await lockCommand(opts);
50480
50570
  });
50481
- program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
50571
+ program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines (notices are printed and counted, never fatal)").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
50482
50572
  await validateCommand(opts);
50483
50573
  });
50484
50574
  program.command("status").description("Show a hierarchical completeness graph of the SDD Spec Tree").option("--subsystem <id>", "only show status for the specified subsystem").option("--no-recursive", "do not recursively show status for subprojects").action(async (opts) => {