@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/index.js CHANGED
@@ -556,6 +556,31 @@ var init_project = __esm({
556
556
  kind: CarriedDebtKindSchema,
557
557
  /** The reason itself, in the author's own words — what is true here, and what paying it would take. */
558
558
  why: import_zod3.z.string().min(1),
559
+ /**
560
+ * This classification is PROVISIONAL, and this is what would settle it.
561
+ *
562
+ * A confident-sounding `why` that is wrong is worse than a missing one: it
563
+ * reads as settled, so nobody looks again, and the register keeps counting
564
+ * the finding under a kind that was never true. Nothing can check a reason
565
+ * for truth — `kind` and `why` are prose, and STALE_CARRIED_FINDING only
566
+ * ever catches "this stopped applying", never "this still applies for a
567
+ * reason that has become false". What a register CAN do is let an author say
568
+ * out loud that they are not sure yet, and then keep saying it: every run
569
+ * counts the groups marked here beside the kind totals, so the uncertainty
570
+ * is as loud as the debt.
571
+ *
572
+ * It is deliberately a SENTENCE and not a flag, and deliberately on the
573
+ * reason GROUP rather than the finding: what is provisional is the reason,
574
+ * and a reader deciding whether to pick this up needs to know what to
575
+ * measure — "is the adapter really realized by the file that consumes it, or
576
+ * is that a modelling error?" — not merely that somebody once hesitated. A
577
+ * finding whose own classification is uncertain while its neighbours' is not
578
+ * is a different reason, and belongs in its own group.
579
+ *
580
+ * There is no counterpart on `lint.allow`, and that is a decision, not an
581
+ * omission: see the note on ConformanceRuleConfig.carried.
582
+ */
583
+ revisit: import_zod3.z.string().min(1).optional(),
559
584
  /** The findings this reason explains. */
560
585
  findings: import_zod3.z.array(CarriedFindingSchema)
561
586
  });
@@ -601,6 +626,17 @@ var init_project = __esm({
601
626
  * • every entry states its kind and its reason, and `wairon validate`
602
627
  * prints the running total, so the debt is loud where a suppression is
603
628
  * silent.
629
+ *
630
+ * A reason group may also declare itself PROVISIONAL (`revisit`), and the
631
+ * total says how many did. There is no such marker on a `lint.allow`, and
632
+ * the asymmetry is the point: an entry here does not silence anything — the
633
+ * finding is counted out loud on every run — so marking one uncertain adds a
634
+ * second dial to something already visible. An allow DOES silence, and a
635
+ * "provisional allow" would buy the silence and defer the decision, which is
636
+ * the one combination that cannot be reviewed: the finding is gone and the
637
+ * doubt is in a field nobody opens. The mechanism for a decision nobody has
638
+ * taken is to not take it — leave the warning firing — or, where the code is
639
+ * carryable, an entry of kind `undecided`, which is exactly that sentence.
604
640
  */
605
641
  carried: import_zod3.z.array(CarriedDebtSchema).optional()
606
642
  });
@@ -1186,7 +1222,7 @@ var init_defaults = __esm({
1186
1222
  copilot: ".github/prompts",
1187
1223
  codex: ".codex/agents"
1188
1224
  };
1189
- WAIRON_VERSION = "5.1.1-dev.42";
1225
+ WAIRON_VERSION = "5.1.1-dev.43";
1190
1226
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
1191
1227
  ARCHITECT_AGENT_ID = "agent-architect";
1192
1228
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -1698,8 +1734,28 @@ var init_specs = __esm({
1698
1734
  LintAllowSchema = import_zod8.z.object({
1699
1735
  /** The issue code being allowed (see `wairon rules list`). */
1700
1736
  code: import_zod8.z.string(),
1737
+ /**
1738
+ * The SITE inside this spec the allow covers, named exactly as the finding
1739
+ * names it (a contract method, an import edge "from -> to", a declared edge
1740
+ * "component -> target"). A finding that names a site is covered ONLY by an
1741
+ * allow naming that same site; a finding that names none is covered only by
1742
+ * an allow that names none either.
1743
+ */
1744
+ at: import_zod8.z.string().min(1).optional(),
1745
+ /**
1746
+ * The units of an AGGREGATING finding this allow covers — the steps of one
1747
+ * CALL_STEP_UNREALIZED, the crossings of one UNDECLARED_COLOCATED_CALL —
1748
+ * each named the way the finding's own message names it. The allow silences
1749
+ * the finding only when it lists EVERY unit reported, so a unit nobody
1750
+ * decided on surfaces on the day it appears instead of inheriting a decision
1751
+ * taken about its neighbours. Meaningless without `at`, and refused there.
1752
+ */
1753
+ covers: import_zod8.z.array(import_zod8.z.string()).optional(),
1701
1754
  /** Why this finding is acceptable here (e.g. "dispatcher — fan-out is the point"). */
1702
1755
  reason: import_zod8.z.string().min(1)
1756
+ }).refine((a) => !a.covers?.length || !!a.at, {
1757
+ message: "`covers` names the units of ONE finding, so it needs the `at` that says which finding \u2014 add the site, or drop covers.",
1758
+ path: ["covers"]
1703
1759
  });
1704
1760
  LintConfigSchema = import_zod8.z.object({
1705
1761
  allow: import_zod8.z.array(LintAllowSchema).default([])
@@ -9640,10 +9696,10 @@ var init_lint_allows = __esm({
9640
9696
  "use strict";
9641
9697
  lintAllowsRule = {
9642
9698
  name: "lint-allows",
9643
- 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.",
9699
+ 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.",
9644
9700
  codes: [
9645
9701
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
9646
- { code: "UNUSED_LINT_ALLOW", defaultSeverity: "warning", summary: "lint.allow entry matched no finding this run \u2014 remove the stale allow" }
9702
+ { 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" }
9647
9703
  ],
9648
9704
  check(ctx) {
9649
9705
  for (const a of ctx.lintAllows) {
@@ -9656,14 +9712,24 @@ var init_lint_allows = __esm({
9656
9712
  );
9657
9713
  continue;
9658
9714
  }
9659
- if (!a.used) {
9660
- ctx.addIssue(
9661
- "warning",
9662
- "UNUSED_LINT_ALLOW",
9663
- `Spec "${a.specId}" allows "${a.code}" (reason: ${a.reason}) but no such finding fired this run \u2014 remove the stale allow.`,
9664
- a.specId
9665
- );
9715
+ if (a.used) continue;
9716
+ const reported = ctx.sitesReported(a.specId, a.code);
9717
+ const at = a.at ? ` at "${a.at}"` : "";
9718
+ let why;
9719
+ if (a.at && reported.unsited && reported.sites.length === 0) {
9720
+ why = `findings of "${a.code}" on this spec name no site at all, so this allow must not name one \u2014 drop the \`at\``;
9721
+ } else if (reported.sites.length > 0) {
9722
+ const sites = reported.sites.map((s) => `"${s}"`).join("; ");
9723
+ 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`;
9724
+ } else {
9725
+ why = "no such finding fired this run \u2014 remove the stale allow";
9666
9726
  }
9727
+ ctx.addIssue(
9728
+ "warning",
9729
+ "UNUSED_LINT_ALLOW",
9730
+ `Spec "${a.specId}" allows "${a.code}"${at} (reason: ${a.reason}), but ${why}.`,
9731
+ a.specId
9732
+ );
9667
9733
  }
9668
9734
  }
9669
9735
  };
@@ -13682,7 +13748,13 @@ var init_dependency_conformance = __esm({
13682
13748
  "UNDECLARED_DEPENDENCY",
13683
13749
  `"${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.`,
13684
13750
  realization.implementationsAt(fromPath)[0]?.id,
13685
- draftAt(fromPath) || draftAt(toPath)
13751
+ draftAt(fromPath) || draftAt(toPath),
13752
+ void 0,
13753
+ // One import edge, one indivisible fact — but one implementation
13754
+ // maps many files and many edges, so the EDGE is the site, not the
13755
+ // spec. Without it a single allow on the spec covers every crossing
13756
+ // that file ever grows.
13757
+ { at: `${fromPath} -> ${toPath}` }
13686
13758
  );
13687
13759
  }
13688
13760
  }
@@ -13709,7 +13781,12 @@ var init_dependency_conformance = __esm({
13709
13781
  "UNREALIZED_DEPENDENCY",
13710
13782
  `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.`,
13711
13783
  impls[0]?.id ?? component.id,
13712
- draft
13784
+ draft,
13785
+ void 0,
13786
+ // One declared edge, one indivisible fact. A component declares many,
13787
+ // and they all anchor on its one implementation spec, so the EDGE is
13788
+ // the site.
13789
+ { at: `${component.id} -> ${targetId}` }
13713
13790
  );
13714
13791
  }
13715
13792
  }
@@ -14068,7 +14145,7 @@ var init_carried_debt = __esm({
14068
14145
  "use strict";
14069
14146
  carriedDebtRule = {
14070
14147
  name: "carried-debt",
14071
- 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.",
14148
+ 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.",
14072
14149
  codes: [
14073
14150
  { 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" },
14074
14151
  { 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" }
@@ -16446,12 +16523,20 @@ function buildRuleContext(opts) {
16446
16523
  const allowLookup = /* @__PURE__ */ new Map();
16447
16524
  const collectAllows = (specId, lint) => {
16448
16525
  for (const a of lint?.allow ?? []) {
16449
- const entry = { specId, code: a.code, reason: a.reason, used: false };
16526
+ const entry = { specId, code: a.code, at: a.at, covers: a.covers, reason: a.reason, used: false };
16450
16527
  lintAllows.push(entry);
16451
16528
  if (!allowLookup.has(specId)) allowLookup.set(specId, /* @__PURE__ */ new Map());
16452
- allowLookup.get(specId).set(a.code, entry);
16529
+ const forSpec = allowLookup.get(specId);
16530
+ const forCode = forSpec.get(a.code) ?? [];
16531
+ forCode.push(entry);
16532
+ forSpec.set(a.code, forCode);
16453
16533
  }
16454
16534
  };
16535
+ const sitesSeen = /* @__PURE__ */ new Map();
16536
+ const sitesReported = (specId, code) => {
16537
+ const seen = sitesSeen.get(`${specId}\0${code}`);
16538
+ return { sites: [...seen?.sites ?? []], unsited: seen?.unsited ?? false };
16539
+ };
16455
16540
  for (const s of subsystems) collectAllows(s.id, s.lint);
16456
16541
  for (const c of components) collectAllows(c.id, c.lint);
16457
16542
  for (const i of interfaces) collectAllows(i.id, i.lint);
@@ -16488,15 +16573,29 @@ function buildRuleContext(opts) {
16488
16573
  }
16489
16574
  const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
16490
16575
  if (severity === "off") return;
16576
+ let text = message;
16577
+ if (specId) {
16578
+ const key = `${specId}\0${code}`;
16579
+ let seen = sitesSeen.get(key);
16580
+ if (!seen) sitesSeen.set(key, seen = { sites: /* @__PURE__ */ new Set(), unsited: false });
16581
+ if (parts) seen.sites.add(parts.at);
16582
+ else seen.unsited = true;
16583
+ }
16584
+ let allowClaimed = false;
16491
16585
  if (specId) {
16492
- const allow = allowLookup.get(specId)?.get(code);
16586
+ const allow = (allowLookup.get(specId)?.get(code) ?? []).find((a) => parts ? a.at === parts.at : a.at === void 0);
16493
16587
  if (allow) {
16494
16588
  allow.used = true;
16495
- if (severity === "warning") return;
16589
+ allowClaimed = true;
16590
+ const grew = (parts?.covers ?? []).filter((unit) => !(allow.covers ?? []).includes(unit));
16591
+ if (grew.length === 0) {
16592
+ if (severity === "warning") return;
16593
+ } else {
16594
+ text = `${text} 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.`;
16595
+ }
16496
16596
  }
16497
16597
  }
16498
- let text = message;
16499
- if (specId && parts && severity === "warning") {
16598
+ if (specId && parts && severity === "warning" && !allowClaimed) {
16500
16599
  const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
16501
16600
  if (entry) {
16502
16601
  entry.fired = true;
@@ -16589,6 +16688,7 @@ function buildRuleContext(opts) {
16589
16688
  codeModel: opts.codeModel ?? emptyCodeModel(),
16590
16689
  roundTripIssues: opts.roundTripIssues,
16591
16690
  lintAllows,
16691
+ sitesReported,
16592
16692
  knownIssueCodes: opts.knownIssueCodes,
16593
16693
  carriedFindings,
16594
16694
  carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
@@ -19143,9 +19243,11 @@ function identityKeyOf(field, item) {
19143
19243
  return `${String(o.topic)} ${o.event === void 0 ? "" : String(o.event)}`;
19144
19244
  case "trustedLinks":
19145
19245
  return str(o.subsystem);
19246
+ // lint.allow: an allow is one decision about one OCCURRENCE, so several
19247
+ // may share a code on one spec, each naming its own site. Keyed by code
19248
+ // alone a delta for one site would overwrite its neighbours.
19146
19249
  case "allow":
19147
- return str(o.code);
19148
- // lint.allow
19250
+ return `${String(o.code)} ${o.at === void 0 ? "" : String(o.at)}`;
19149
19251
  case "findings":
19150
19252
  return str(o.code);
19151
19253
  // an interface method's findings
@@ -19233,6 +19335,7 @@ function identityFieldsOf(field) {
19233
19335
  case "trustedLinks":
19234
19336
  return ["subsystem"];
19235
19337
  case "allow":
19338
+ return ["code", "at"];
19236
19339
  case "findings":
19237
19340
  return ["code"];
19238
19341
  case "invariants":