@wairon/cli 5.1.1-dev.42 → 5.1.1-dev.43

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.42";
68
+ WAIRON_VERSION = "5.1.1-dev.43";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -890,6 +890,31 @@ var init_project = __esm({
890
890
  kind: CarriedDebtKindSchema,
891
891
  /** The reason itself, in the author's own words — what is true here, and what paying it would take. */
892
892
  why: import_zod4.z.string().min(1),
893
+ /**
894
+ * This classification is PROVISIONAL, and this is what would settle it.
895
+ *
896
+ * A confident-sounding `why` that is wrong is worse than a missing one: it
897
+ * reads as settled, so nobody looks again, and the register keeps counting
898
+ * the finding under a kind that was never true. Nothing can check a reason
899
+ * for truth — `kind` and `why` are prose, and STALE_CARRIED_FINDING only
900
+ * ever catches "this stopped applying", never "this still applies for a
901
+ * reason that has become false". What a register CAN do is let an author say
902
+ * out loud that they are not sure yet, and then keep saying it: every run
903
+ * counts the groups marked here beside the kind totals, so the uncertainty
904
+ * is as loud as the debt.
905
+ *
906
+ * It is deliberately a SENTENCE and not a flag, and deliberately on the
907
+ * reason GROUP rather than the finding: what is provisional is the reason,
908
+ * and a reader deciding whether to pick this up needs to know what to
909
+ * measure — "is the adapter really realized by the file that consumes it, or
910
+ * is that a modelling error?" — not merely that somebody once hesitated. A
911
+ * finding whose own classification is uncertain while its neighbours' is not
912
+ * is a different reason, and belongs in its own group.
913
+ *
914
+ * There is no counterpart on `lint.allow`, and that is a decision, not an
915
+ * omission: see the note on ConformanceRuleConfig.carried.
916
+ */
917
+ revisit: import_zod4.z.string().min(1).optional(),
893
918
  /** The findings this reason explains. */
894
919
  findings: import_zod4.z.array(CarriedFindingSchema)
895
920
  });
@@ -935,6 +960,17 @@ var init_project = __esm({
935
960
  * • every entry states its kind and its reason, and `wairon validate`
936
961
  * prints the running total, so the debt is loud where a suppression is
937
962
  * silent.
963
+ *
964
+ * A reason group may also declare itself PROVISIONAL (`revisit`), and the
965
+ * total says how many did. There is no such marker on a `lint.allow`, and
966
+ * the asymmetry is the point: an entry here does not silence anything — the
967
+ * finding is counted out loud on every run — so marking one uncertain adds a
968
+ * second dial to something already visible. An allow DOES silence, and a
969
+ * "provisional allow" would buy the silence and defer the decision, which is
970
+ * the one combination that cannot be reviewed: the finding is gone and the
971
+ * doubt is in a field nobody opens. The mechanism for a decision nobody has
972
+ * taken is to not take it — leave the warning firing — or, where the code is
973
+ * carryable, an entry of kind `undecided`, which is exactly that sentence.
938
974
  */
939
975
  carried: import_zod4.z.array(CarriedDebtSchema).optional()
940
976
  });
@@ -1411,8 +1447,28 @@ var init_specs = __esm({
1411
1447
  LintAllowSchema = import_zod7.z.object({
1412
1448
  /** The issue code being allowed (see `wairon rules list`). */
1413
1449
  code: import_zod7.z.string(),
1450
+ /**
1451
+ * The SITE inside this spec the allow covers, named exactly as the finding
1452
+ * names it (a contract method, an import edge "from -> to", a declared edge
1453
+ * "component -> target"). A finding that names a site is covered ONLY by an
1454
+ * allow naming that same site; a finding that names none is covered only by
1455
+ * an allow that names none either.
1456
+ */
1457
+ at: import_zod7.z.string().min(1).optional(),
1458
+ /**
1459
+ * The units of an AGGREGATING finding this allow covers — the steps of one
1460
+ * CALL_STEP_UNREALIZED, the crossings of one UNDECLARED_COLOCATED_CALL —
1461
+ * each named the way the finding's own message names it. The allow silences
1462
+ * the finding only when it lists EVERY unit reported, so a unit nobody
1463
+ * decided on surfaces on the day it appears instead of inheriting a decision
1464
+ * taken about its neighbours. Meaningless without `at`, and refused there.
1465
+ */
1466
+ covers: import_zod7.z.array(import_zod7.z.string()).optional(),
1414
1467
  /** Why this finding is acceptable here (e.g. "dispatcher — fan-out is the point"). */
1415
1468
  reason: import_zod7.z.string().min(1)
1469
+ }).refine((a) => !a.covers?.length || !!a.at, {
1470
+ message: "`covers` names the units of ONE finding, so it needs the `at` that says which finding \u2014 add the site, or drop covers.",
1471
+ path: ["covers"]
1416
1472
  });
1417
1473
  LintConfigSchema = import_zod7.z.object({
1418
1474
  allow: import_zod7.z.array(LintAllowSchema).default([])
@@ -9694,10 +9750,10 @@ var init_lint_allows = __esm({
9694
9750
  "use strict";
9695
9751
  lintAllowsRule = {
9696
9752
  name: "lint-allows",
9697
- 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. Allows silence warnings only; errors always surface.",
9753
+ 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.",
9698
9754
  codes: [
9699
9755
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
9700
- { code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014 remove the stale allow" }
9756
+ { 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" }
9701
9757
  ],
9702
9758
  check(ctx) {
9703
9759
  for (const a of ctx.lintAllows) {
@@ -9710,14 +9766,24 @@ var init_lint_allows = __esm({
9710
9766
  );
9711
9767
  continue;
9712
9768
  }
9713
- if (!a.used) {
9714
- ctx.addIssue(
9715
- "warning",
9716
- "UNUSED_LINT_ALLOW",
9717
- `Spec "${a.specId}" allows "${a.code}" (reason: ${a.reason}) but no such finding fired this run \u2014 remove the stale allow.`,
9718
- a.specId
9719
- );
9769
+ if (a.used) continue;
9770
+ const reported = ctx.sitesReported(a.specId, a.code);
9771
+ const at = a.at ? ` at "${a.at}"` : "";
9772
+ let why;
9773
+ if (a.at && reported.unsited && reported.sites.length === 0) {
9774
+ why = `findings of "${a.code}" on this spec name no site at all, so this allow must not name one \u2014 drop the \`at\``;
9775
+ } else if (reported.sites.length > 0) {
9776
+ const sites = reported.sites.map((s) => `"${s}"`).join("; ");
9777
+ why = a.at ? `that code fired at ${sites} instead \u2014 retarget the allow, or remove it` : `that code fired at ${sites}, and a sited finding is covered only by an allow naming its site \u2014 give this allow an \`at\` (one per site, each with its own reason), or remove it`;
9778
+ } else {
9779
+ why = "no such finding fired this run \u2014 remove the stale allow";
9720
9780
  }
9781
+ ctx.addIssue(
9782
+ "warning",
9783
+ "UNUSED_LINT_ALLOW",
9784
+ `Spec "${a.specId}" allows "${a.code}"${at} (reason: ${a.reason}), but ${why}.`,
9785
+ a.specId
9786
+ );
9721
9787
  }
9722
9788
  }
9723
9789
  };
@@ -13736,7 +13802,13 @@ var init_dependency_conformance = __esm({
13736
13802
  "UNDECLARED_DEPENDENCY",
13737
13803
  `"${fromPath}" (realizing ${fromComponents.map((c) => c.id).join(", ")}) imports "${toPath}" (realizing ${toComponents.map((c) => c.id).join(", ")}) but no declared dependsOn/owns edge justifies it \u2014 declare the collaboration on the component that actually uses it, or route the cross-subsystem hop through the target's published surface.`,
13738
13804
  realization.implementationsAt(fromPath)[0]?.id,
13739
- draftAt(fromPath) || draftAt(toPath)
13805
+ draftAt(fromPath) || draftAt(toPath),
13806
+ void 0,
13807
+ // One import edge, one indivisible fact — but one implementation
13808
+ // maps many files and many edges, so the EDGE is the site, not the
13809
+ // spec. Without it a single allow on the spec covers every crossing
13810
+ // that file ever grows.
13811
+ { at: `${fromPath} -> ${toPath}` }
13740
13812
  );
13741
13813
  }
13742
13814
  }
@@ -13763,7 +13835,12 @@ var init_dependency_conformance = __esm({
13763
13835
  "UNREALIZED_DEPENDENCY",
13764
13836
  `Component "${component.id}" declares ${relation} "${targetId}", but no runtime import connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
13765
13837
  impls[0]?.id ?? component.id,
13766
- draft
13838
+ draft,
13839
+ void 0,
13840
+ // One declared edge, one indivisible fact. A component declares many,
13841
+ // and they all anchor on its one implementation spec, so the EDGE is
13842
+ // the site.
13843
+ { at: `${component.id} -> ${targetId}` }
13767
13844
  );
13768
13845
  }
13769
13846
  }
@@ -14122,7 +14199,7 @@ var init_carried_debt = __esm({
14122
14199
  "use strict";
14123
14200
  carriedDebtRule = {
14124
14201
  name: "carried-debt",
14125
- description: "The conformance debt register (`rules.conformance.carried`) holds the code\u2194spec findings this tree carries as declared, classified debt \u2014 `unclaimed`'s one-way shape, for findings about code a spec does name. It is not a second lint.allow: an allow says a finding is wrong here by design, an entry here says the finding is right and unpaid, and each entry states which of the three kinds it is (drift: spec and code disagree; undecided: the fix waits on a modelling decision nobody has taken; unreadable: the analysis cannot follow the shape the code is written in) and why. Only a code a rule declares CARRYABLE may appear, and naming any other is an error, so the register can never widen into general-purpose suppression. An entry carries a finding only when it lists EVERY unit that finding reports, so an aggregating finding cannot grow behind it; an entry that matched nothing, that duplicates another, that names a spec the tree does not hold, or that lists a unit no longer reported is STALE_CARRIED_FINDING, so the register only shrinks. The audit is silent in a run that could not have fired the findings at all: one that built no code model, the specs outside a scoped run, and a code the project switched off.",
14202
+ description: "The conformance debt register (`rules.conformance.carried`) holds the code\u2194spec findings this tree carries as declared, classified debt \u2014 `unclaimed`'s one-way shape, for findings about code a spec does name. It is not a second lint.allow: an allow says a finding is wrong here by design, an entry here says the finding is right and unpaid, and each entry states which of the three kinds it is (drift: spec and code disagree; undecided: the fix waits on a modelling decision nobody has taken; unreadable: the analysis cannot follow the shape the code is written in) and why. Only a code a rule declares CARRYABLE may appear, and naming any other is an error, so the register can never widen into general-purpose suppression. An entry carries a finding only when it lists EVERY unit that finding reports, so an aggregating finding cannot grow behind it; an entry that matched nothing, that duplicates another, that names a spec the tree does not hold, or that lists a unit no longer reported is STALE_CARRIED_FINDING, so the register only shrinks. The audit is silent in a run that could not have fired the findings at all: one that built no code model, the specs outside a scoped run, and a code the project switched off. Nothing can check a reason for TRUTH \u2014 a stale entry is one that stopped applying, never one that still applies for a reason that has become false \u2014 so a reason group may declare its own classification provisional (`revisit`: what would settle it), and every run counts those findings beside the kind totals.",
14126
14203
  codes: [
14127
14204
  { code: "UNCARRYABLE_FINDING", defaultSeverity: "error", summary: "The conformance debt register names an issue code no rule emits, or one no rule declares carryable \u2014 the register holds measured code\u2194spec debt, never a design rule somebody wants quiet" },
14128
14205
  { code: "STALE_CARRIED_FINDING", defaultSeverity: "warning", summary: "An entry of the conformance debt register carries a finding that no longer fires, or lists a unit that finding no longer reports \u2014 delete it, the register only shrinks" }
@@ -16496,12 +16573,20 @@ function buildRuleContext(opts) {
16496
16573
  const allowLookup = /* @__PURE__ */ new Map();
16497
16574
  const collectAllows = (specId, lint) => {
16498
16575
  for (const a of lint?.allow ?? []) {
16499
- const entry = { specId, code: a.code, reason: a.reason, used: false };
16576
+ const entry = { specId, code: a.code, at: a.at, covers: a.covers, reason: a.reason, used: false };
16500
16577
  lintAllows.push(entry);
16501
16578
  if (!allowLookup.has(specId)) allowLookup.set(specId, /* @__PURE__ */ new Map());
16502
- allowLookup.get(specId).set(a.code, entry);
16579
+ const forSpec = allowLookup.get(specId);
16580
+ const forCode = forSpec.get(a.code) ?? [];
16581
+ forCode.push(entry);
16582
+ forSpec.set(a.code, forCode);
16503
16583
  }
16504
16584
  };
16585
+ const sitesSeen = /* @__PURE__ */ new Map();
16586
+ const sitesReported = (specId, code) => {
16587
+ const seen = sitesSeen.get(`${specId}\0${code}`);
16588
+ return { sites: [...seen?.sites ?? []], unsited: seen?.unsited ?? false };
16589
+ };
16505
16590
  for (const s of subsystems) collectAllows(s.id, s.lint);
16506
16591
  for (const c of components) collectAllows(c.id, c.lint);
16507
16592
  for (const i of interfaces) collectAllows(i.id, i.lint);
@@ -16538,15 +16623,29 @@ function buildRuleContext(opts) {
16538
16623
  }
16539
16624
  const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
16540
16625
  if (severity === "off") return;
16626
+ let text2 = message;
16627
+ if (specId) {
16628
+ const key = `${specId}\0${code}`;
16629
+ let seen = sitesSeen.get(key);
16630
+ if (!seen) sitesSeen.set(key, seen = { sites: /* @__PURE__ */ new Set(), unsited: false });
16631
+ if (parts) seen.sites.add(parts.at);
16632
+ else seen.unsited = true;
16633
+ }
16634
+ let allowClaimed = false;
16541
16635
  if (specId) {
16542
- const allow = allowLookup.get(specId)?.get(code);
16636
+ const allow = (allowLookup.get(specId)?.get(code) ?? []).find((a) => parts ? a.at === parts.at : a.at === void 0);
16543
16637
  if (allow) {
16544
16638
  allow.used = true;
16545
- if (severity === "warning") return;
16639
+ allowClaimed = true;
16640
+ const grew = (parts?.covers ?? []).filter((unit) => !(allow.covers ?? []).includes(unit));
16641
+ if (grew.length === 0) {
16642
+ if (severity === "warning") return;
16643
+ } else {
16644
+ 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.`;
16645
+ }
16546
16646
  }
16547
16647
  }
16548
- let text2 = message;
16549
- if (specId && parts && severity === "warning") {
16648
+ if (specId && parts && severity === "warning" && !allowClaimed) {
16550
16649
  const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
16551
16650
  if (entry) {
16552
16651
  entry.fired = true;
@@ -16639,6 +16738,7 @@ function buildRuleContext(opts) {
16639
16738
  codeModel: opts.codeModel ?? emptyCodeModel(),
16640
16739
  roundTripIssues: opts.roundTripIssues,
16641
16740
  lintAllows,
16741
+ sitesReported,
16642
16742
  knownIssueCodes: opts.knownIssueCodes,
16643
16743
  carriedFindings,
16644
16744
  carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
@@ -24400,9 +24500,11 @@ function identityKeyOf(field, item) {
24400
24500
  return `${String(o.topic)} ${o.event === void 0 ? "" : String(o.event)}`;
24401
24501
  case "trustedLinks":
24402
24502
  return str(o.subsystem);
24503
+ // lint.allow: an allow is one decision about one OCCURRENCE, so several
24504
+ // may share a code on one spec, each naming its own site. Keyed by code
24505
+ // alone a delta for one site would overwrite its neighbours.
24403
24506
  case "allow":
24404
- return str(o.code);
24405
- // lint.allow
24507
+ return `${String(o.code)} ${o.at === void 0 ? "" : String(o.at)}`;
24406
24508
  case "findings":
24407
24509
  return str(o.code);
24408
24510
  // an interface method's findings
@@ -24490,6 +24592,7 @@ function identityFieldsOf(field) {
24490
24592
  case "trustedLinks":
24491
24593
  return ["subsystem"];
24492
24594
  case "allow":
24595
+ return ["code", "at"];
24493
24596
  case "findings":
24494
24597
  return ["code"];
24495
24598
  case "invariants":
@@ -28964,7 +29067,7 @@ function createMcpServer(options = {}) {
28964
29067
  inputSchema: {
28965
29068
  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)"),
28966
29069
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
28967
- 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.`),
29070
+ 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" 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.`),
28968
29071
  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.")
28969
29072
  },
28970
29073
  outputSchema: specChangeReportOutput
@@ -31008,6 +31111,20 @@ init_loader();
31008
31111
  init_errors();
31009
31112
  init_core();
31010
31113
  init_validation();
31114
+ function carriedDebtSummary(carried) {
31115
+ if (!carried || carried.length === 0) return null;
31116
+ const findings = carried.flatMap((g) => g.findings ?? []);
31117
+ const units = findings.reduce((n, f) => n + (f.covers?.length ?? 1), 0);
31118
+ const perKind = (kind) => carried.filter((g) => g.kind === kind).reduce((n, g) => n + (g.findings?.length ?? 0), 0);
31119
+ const provisional = carried.filter((g) => g.revisit);
31120
+ const unsettled = provisional.reduce((n, g) => n + (g.findings?.length ?? 0), 0);
31121
+ return {
31122
+ line: `Conformance debt register: ${findings.length} finding(s) over ${units} unit(s) carried \u2014 ${perKind("drift")} drift, ${perKind("undecided")} undecided, ${perKind("unreadable")} unreadable (\`rules.conformance.carried\`). Drift and undecided are owed; unreadable is what the analysis cannot follow.` + (unsettled > 0 ? ` ${unsettled} finding(s) in ${provisional.length} group(s) are marked for re-evaluation \u2014 the classification is provisional, not the debt.` : ""),
31123
+ revisits: provisional.map(
31124
+ (g) => ` re-evaluate (${g.kind}, ${g.findings?.length ?? 0} finding(s)): ${g.revisit}`
31125
+ )
31126
+ };
31127
+ }
31011
31128
  function isCiDraftWaivable(issue2) {
31012
31129
  if (issue2.severity !== "warning") return false;
31013
31130
  if (issue2.code === "DRAFT_SUBSYSTEM_WARNING") return true;
@@ -31123,15 +31240,11 @@ async function runValidate(options = {}) {
31123
31240
  }
31124
31241
  }
31125
31242
  }
31126
- const carried = projectConfig.rules?.conformance?.carried ?? [];
31127
- if (carried.length > 0) {
31128
- const findings = carried.flatMap((g) => g.findings ?? []);
31129
- const units = findings.reduce((n, f) => n + (f.covers?.length ?? 1), 0);
31130
- const perKind = (kind) => carried.filter((g) => g.kind === kind).reduce((n, g) => n + (g.findings?.length ?? 0), 0);
31243
+ const summary = carriedDebtSummary(projectConfig.rules?.conformance?.carried);
31244
+ if (summary) {
31131
31245
  logger.blank();
31132
- logger.info(import_chalk7.default.yellow(
31133
- `Conformance debt register: ${findings.length} finding(s) over ${units} unit(s) carried \u2014 ${perKind("drift")} drift, ${perKind("undecided")} undecided, ${perKind("unreadable")} unreadable (\`rules.conformance.carried\`). Drift and undecided are owed; unreadable is what the analysis cannot follow.`
31134
- ));
31246
+ logger.info(import_chalk7.default.yellow(summary.line));
31247
+ for (const note of summary.revisits) logger.info(import_chalk7.default.gray(note));
31135
31248
  }
31136
31249
  logger.blank();
31137
31250
  if (options.ci && waivedWarnings > 0) {