@wairon/cli 5.1.1-dev.41 → 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.41";
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([
@@ -770,7 +770,7 @@ function activeTargetTypes(config) {
770
770
  const targets = config.targets;
771
771
  return targets.filter((t) => typeof t === "string" || t.enabled !== false).map((t) => typeof t === "string" ? t : t.type);
772
772
  }
773
- var import_zod4, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
773
+ var import_zod4, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
774
774
  var init_project = __esm({
775
775
  "src/models/project.ts"() {
776
776
  "use strict";
@@ -867,6 +867,57 @@ var init_project = __esm({
867
867
  */
868
868
  maxCognitiveLevel: import_zod4.z.string().optional()
869
869
  });
870
+ CarriedDebtKindSchema = import_zod4.z.enum(["drift", "undecided", "unreadable"]);
871
+ CarriedFindingSchema = import_zod4.z.object({
872
+ /** The issue code, which must be one a rule declares CARRYABLE — anything else is UNCARRYABLE_FINDING, an error. */
873
+ code: import_zod4.z.string(),
874
+ /** The spec the finding is anchored to. */
875
+ spec: import_zod4.z.string(),
876
+ /** The site inside that spec the finding names — for the code-vs-spec call checks, the contract method. */
877
+ at: import_zod4.z.string(),
878
+ /**
879
+ * The units an aggregating finding covers: the crossings of one
880
+ * UNDECLARED_COLOCATED_CALL, the steps of one CALL_STEP_UNREALIZED. Keyed by
881
+ * code + spec + site ALONE, a 24th crossing added to a finding that already
882
+ * lists 23 would be carried by an entry nobody wrote for it — so a live
883
+ * finding is carried only when every unit it reports is listed here, and a
884
+ * unit that appears is reported as new.
885
+ */
886
+ covers: import_zod4.z.array(import_zod4.z.string()).optional()
887
+ });
888
+ CarriedDebtSchema = import_zod4.z.object({
889
+ /** Which of the three kinds this reason is (see CarriedDebtKindSchema). */
890
+ kind: CarriedDebtKindSchema,
891
+ /** The reason itself, in the author's own words — what is true here, and what paying it would take. */
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(),
918
+ /** The findings this reason explains. */
919
+ findings: import_zod4.z.array(CarriedFindingSchema)
920
+ });
870
921
  ConformanceRuleConfigSchema = import_zod4.z.object({
871
922
  /**
872
923
  * Project-relative paths holding this project's own source code; a
@@ -889,7 +940,39 @@ var init_project = __esm({
889
940
  * that is now claimed, proven a barrel, or no longer found is
890
941
  * STALE_UNCLAIMED_ENTRY.
891
942
  */
892
- unclaimed: import_zod4.z.array(import_zod4.z.string()).optional()
943
+ unclaimed: import_zod4.z.array(import_zod4.z.string()).optional(),
944
+ /**
945
+ * The conformance findings this tree carries as declared debt, grouped by
946
+ * the reason that explains them — `unclaimed`'s shape, for findings about
947
+ * code a spec DOES claim.
948
+ *
949
+ * It is not a second lint.allow, and the difference is the claim each makes.
950
+ * An allow says "this finding is wrong here, by design", and is meant to
951
+ * live forever; an entry here says "this finding is RIGHT, and it is not
952
+ * paid yet". A mechanism that cannot tell those apart can never be asked how
953
+ * much the tree owes. Four properties keep it a register:
954
+ * • only a code a rule declares CARRYABLE may appear (UNCARRYABLE_FINDING,
955
+ * an error, which no allow and no --ci waiver can reach);
956
+ * • an entry carries a finding only when it lists EVERY unit that finding
957
+ * reports, so an aggregating finding cannot grow behind it;
958
+ * • an entry that stops applying — gone, or listing a unit no longer
959
+ * reported — is STALE_CARRIED_FINDING, so the register only shrinks;
960
+ * • every entry states its kind and its reason, and `wairon validate`
961
+ * prints the running total, so the debt is loud where a suppression is
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.
974
+ */
975
+ carried: import_zod4.z.array(CarriedDebtSchema).optional()
893
976
  });
894
977
  DesignDepthSchema = import_zod4.z.enum(["components", "interfaces", "implementations", "narratives"]);
895
978
  RulesConfigSchema = import_zod4.z.object({
@@ -1364,8 +1447,28 @@ var init_specs = __esm({
1364
1447
  LintAllowSchema = import_zod7.z.object({
1365
1448
  /** The issue code being allowed (see `wairon rules list`). */
1366
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(),
1367
1467
  /** Why this finding is acceptable here (e.g. "dispatcher — fan-out is the point"). */
1368
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"]
1369
1472
  });
1370
1473
  LintConfigSchema = import_zod7.z.object({
1371
1474
  allow: import_zod7.z.array(LintAllowSchema).default([])
@@ -2518,6 +2621,24 @@ var init_step_config = __esm({
2518
2621
  function pathKey(sourcePath) {
2519
2622
  return sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
2520
2623
  }
2624
+ function ownEntry(record2, key) {
2625
+ return record2 && Object.prototype.hasOwnProperty.call(record2, key) ? record2[key] : void 0;
2626
+ }
2627
+ function importBindingOf(facts, name) {
2628
+ return ownEntry(facts.importBindings, name);
2629
+ }
2630
+ function fieldTypesOf(facts, field) {
2631
+ return ownEntry(facts.fieldTypes, field) ?? NO_FIELD_TYPES;
2632
+ }
2633
+ function typeBindingOf(facts, name) {
2634
+ return ownEntry(facts.typeOnlyBindings, name) ?? ownEntry(facts.importBindings, name)?.from;
2635
+ }
2636
+ function callSitesOf(facts, fn) {
2637
+ return ownEntry(facts.functionCallSites, fn);
2638
+ }
2639
+ function hasFunctionBody(facts, symbol) {
2640
+ return callSitesOf(facts, symbol) !== void 0;
2641
+ }
2521
2642
  function resolveImport(fromFile, specifier, knownPaths) {
2522
2643
  if (!specifier.startsWith(".")) return void 0;
2523
2644
  const joined = pathKey(path6.posix.normalize(path6.posix.join(path6.posix.dirname(fromFile), specifier)));
@@ -2536,11 +2657,12 @@ function resolveImport(fromFile, specifier, knownPaths) {
2536
2657
  }
2537
2658
  return void 0;
2538
2659
  }
2539
- var path6;
2660
+ var path6, NO_FIELD_TYPES;
2540
2661
  var init_code_model = __esm({
2541
2662
  "src/models/code-model.ts"() {
2542
2663
  "use strict";
2543
2664
  path6 = __toESM(require("path"));
2665
+ NO_FIELD_TYPES = [];
2544
2666
  }
2545
2667
  });
2546
2668
 
@@ -3255,6 +3377,26 @@ var init_lockfile = __esm({
3255
3377
  }
3256
3378
  });
3257
3379
 
3380
+ // src/core/spec-files.ts
3381
+ function readSpecFile(filePath) {
3382
+ return readYamlFile(filePath);
3383
+ }
3384
+ function writeSpecFile(filePath, document) {
3385
+ writeYamlFile(filePath, document);
3386
+ }
3387
+ function listSpecFiles(specsDir) {
3388
+ return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
3389
+ }
3390
+ var SPEC_FILE_EXTENSION;
3391
+ var init_spec_files = __esm({
3392
+ "src/core/spec-files.ts"() {
3393
+ "use strict";
3394
+ init_fs();
3395
+ init_yaml();
3396
+ SPEC_FILE_EXTENSION = ".yaml";
3397
+ }
3398
+ });
3399
+
3258
3400
  // src/core/narrative-labels.ts
3259
3401
  function resolveNarrativeLabels(methodName, steps) {
3260
3402
  const errors = [];
@@ -8917,13 +9059,84 @@ function buildCodeIndex(model) {
8917
9059
  cache2.set(path70, built);
8918
9060
  return built;
8919
9061
  };
9062
+ const republished = /* @__PURE__ */ new Map();
9063
+ const republishedFrom = (path70) => {
9064
+ const cached = republished.get(path70);
9065
+ if (cached) return cached;
9066
+ const out = /* @__PURE__ */ new Set([path70]);
9067
+ const queue = [path70];
9068
+ while (queue.length) {
9069
+ const from = queue.pop();
9070
+ const f = facts.get(from);
9071
+ if (f?.status !== "analyzed") continue;
9072
+ for (const specifier of f.reexports) {
9073
+ const to = resolveImport(from, specifier, paths);
9074
+ if (to && !out.has(to)) {
9075
+ out.add(to);
9076
+ queue.push(to);
9077
+ }
9078
+ }
9079
+ }
9080
+ republished.set(path70, out);
9081
+ return out;
9082
+ };
9083
+ const NO_ORIGIN = /* @__PURE__ */ new Set();
9084
+ const landingFrom = (scope, specifier) => {
9085
+ const to = resolveImport(scope, specifier, paths);
9086
+ return to ? republishedFrom(to) : NO_ORIGIN;
9087
+ };
9088
+ const scopeOf2 = (site, from) => {
9089
+ const path70 = pathKey(site.from ?? from);
9090
+ const f = facts.get(path70);
9091
+ return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path70, facts: f } : void 0;
9092
+ };
9093
+ const originOf = (site, from) => {
9094
+ const resolved = scopeOf2(site, from);
9095
+ if (!resolved) return NO_ORIGIN;
9096
+ const { path: scope, facts: f } = resolved;
9097
+ const landing = (specifier) => landingFrom(scope, specifier);
9098
+ if (!site.member) {
9099
+ const binding = importBindingOf(f, site.name);
9100
+ if (binding) return landing(binding.from);
9101
+ return declarationsAt(scope).has(site.name) ? republishedFrom(scope) : NO_ORIGIN;
9102
+ }
9103
+ if (!site.via) return NO_ORIGIN;
9104
+ const receiver = importBindingOf(f, site.via);
9105
+ if (!receiver) return NO_ORIGIN;
9106
+ if (receiver.namespace) return landing(receiver.from);
9107
+ const through = landing(receiver.from);
9108
+ for (const candidate of through) {
9109
+ if (declarationsAt(candidate).has(site.name)) return through;
9110
+ }
9111
+ return NO_ORIGIN;
9112
+ };
9113
+ const possibleOriginsOf = (site, from) => {
9114
+ const proven = originOf(site, from);
9115
+ if (!site.field && !site.constructed) return proven;
9116
+ const resolved = scopeOf2(site, from);
9117
+ if (!resolved) return proven;
9118
+ const { path: scope, facts: f } = resolved;
9119
+ const out = new Set(proven);
9120
+ const widenThrough = (name, specifier) => {
9121
+ const landings = specifier !== void 0 ? landingFrom(scope, specifier) : declarationsAt(scope).has(name) ? republishedFrom(scope) : NO_ORIGIN;
9122
+ for (const candidate of landings) out.add(candidate);
9123
+ };
9124
+ if (site.field) {
9125
+ for (const typeName of fieldTypesOf(f, site.field)) widenThrough(typeName, typeBindingOf(f, typeName));
9126
+ }
9127
+ if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
9128
+ return out;
9129
+ };
9130
+ const declarationsAt = (path70) => namesAt(declarations, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
8920
9131
  return {
8921
9132
  paths,
8922
9133
  exactPaths,
8923
9134
  factsAt: (path70) => facts.get(pathKey(path70)),
8924
- declarationsAt: (path70) => namesAt(declarations, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames])),
9135
+ declarationsAt,
8925
9136
  anchorsAt: (path70) => namesAt(anchors, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
8926
- findingAnchorsAt: (path70) => namesAt(findingAnchors, pathKey(path70), (f) => new Set(f.anchoredNames))
9137
+ findingAnchorsAt: (path70) => namesAt(findingAnchors, pathKey(path70), (f) => new Set(f.anchoredNames)),
9138
+ originOf,
9139
+ possibleOriginsOf
8927
9140
  };
8928
9141
  }
8929
9142
  function buildRealizationIndex(ctx) {
@@ -9537,10 +9750,10 @@ var init_lint_allows = __esm({
9537
9750
  "use strict";
9538
9751
  lintAllowsRule = {
9539
9752
  name: "lint-allows",
9540
- 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.",
9541
9754
  codes: [
9542
9755
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
9543
- { 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" }
9544
9757
  ],
9545
9758
  check(ctx) {
9546
9759
  for (const a of ctx.lintAllows) {
@@ -9553,14 +9766,24 @@ var init_lint_allows = __esm({
9553
9766
  );
9554
9767
  continue;
9555
9768
  }
9556
- if (!a.used) {
9557
- ctx.addIssue(
9558
- "warning",
9559
- "UNUSED_LINT_ALLOW",
9560
- `Spec "${a.specId}" allows "${a.code}" (reason: ${a.reason}) but no such finding fired this run \u2014 remove the stale allow.`,
9561
- a.specId
9562
- );
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";
9563
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
+ );
9564
9787
  }
9565
9788
  }
9566
9789
  };
@@ -13152,6 +13375,21 @@ var init_source_file_linkage = __esm({
13152
13375
  });
13153
13376
 
13154
13377
  // src/core/rules/conformance/method-realization.ts
13378
+ function bodyReachable(code, file, symbol) {
13379
+ const facts = code.factsAt(file);
13380
+ if (!facts) return true;
13381
+ const names = /* @__PURE__ */ new Set([symbol]);
13382
+ const binding = importBindingOf(facts, symbol);
13383
+ if (binding?.imported) names.add(binding.imported);
13384
+ const origins = code.originOf({ name: symbol, member: false }, file);
13385
+ if (origins.size === 0) return true;
13386
+ for (const origin of origins) {
13387
+ const at = code.factsAt(origin);
13388
+ if (!at || at.status !== "analyzed" || at.analysisGrade !== "exact") return true;
13389
+ for (const name of names) if (hasFunctionBody(at, name)) return true;
13390
+ }
13391
+ return false;
13392
+ }
13155
13393
  var methodRealizationRule;
13156
13394
  var init_method_realization = __esm({
13157
13395
  "src/core/rules/conformance/method-realization.ts"() {
@@ -13159,9 +13397,10 @@ var init_method_realization = __esm({
13159
13397
  init_models();
13160
13398
  methodRealizationRule = {
13161
13399
  name: "method-realization",
13162
- description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
13400
+ description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. A method realized by a DECLARATION owes a function BODY as well: at exact grade one must be reachable under the symbol, here or through the imports and republications this file forwards it by, so a signature, an ambient or interface declaration or a plain value binding stops reading as an implementation (METHOD_BODY_NOT_FOUND). Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
13163
13401
  codes: [
13164
- { code: "UNREALIZED_METHOD", defaultSeverity: "warning", summary: "An L3 contract method has no anchor in its own source file (the method's sourcePath, else the implementation's) at the required conformance tier" }
13402
+ { code: "UNREALIZED_METHOD", defaultSeverity: "warning", summary: "An L3 contract method has no anchor in its own source file (the method's sourcePath, else the implementation's) at the required conformance tier" },
13403
+ { code: "METHOD_BODY_NOT_FOUND", defaultSeverity: "warning", summary: "The method symbol IS declared in its own source file, but the file holds no function-like body under it \u2014 a signature, an ambient or interface declaration, a value binding, an imported or re-exported name", carryable: true }
13165
13404
  ],
13166
13405
  check(ctx) {
13167
13406
  const code = ctx.codeIndex();
@@ -13185,7 +13424,22 @@ var init_method_realization = __esm({
13185
13424
  const inDeclared = code.declarationsAt(file).has(symbol);
13186
13425
  const inAnchored = inDeclared || code.anchorsAt(file).has(symbol);
13187
13426
  const realized = tier === "declared" ? inDeclared : inAnchored;
13188
- if (realized) continue;
13427
+ if (realized) {
13428
+ if (!inDeclared || facts.analysisGrade !== "exact" || bodyReachable(code, file, symbol)) continue;
13429
+ const bodyLabel = methodImpl?.symbol ? `"${method2.name}" (symbol "${symbol}")` : `"${method2.name}"`;
13430
+ ctx.addIssue(
13431
+ "warning",
13432
+ "METHOD_BODY_NOT_FOUND",
13433
+ `Method ${bodyLabel} of contract "${impl.contract}" is declared in "${file}" but has no function body there \u2014 a signature, an ambient or interface declaration, a value binding, or a name this file only imports or re-exports. Every deeper check reads a body (the realized calls of Level 3, the measured complexity of the detail dial), so this method is judged on its name alone. Point the sourcePath at the file that implements it, map the code name via a per-method SYMBOL, or dial this method to "off".`,
13434
+ impl.id,
13435
+ draft,
13436
+ void 0,
13437
+ // One method, one indivisible fact: nothing here aggregates, so
13438
+ // the site alone is the whole identity.
13439
+ { at: method2.name }
13440
+ );
13441
+ continue;
13442
+ }
13189
13443
  const label = methodImpl?.symbol ? `"${method2.name}" (symbol "${symbol}")` : `"${method2.name}"`;
13190
13444
  const weakHint = tier === "declared" && inAnchored ? " Only a weak string/word anchor exists \u2014 declare the symbol, map it via a per-method `symbol`, or dial this method to `anchored`." : "";
13191
13445
  ctx.addIssue(
@@ -13252,24 +13506,54 @@ var init_finding_realization = __esm({
13252
13506
  });
13253
13507
 
13254
13508
  // src/core/rules/conformance/call-conformance.ts
13255
- function ownEntry(record2, key) {
13256
- return record2 && Object.prototype.hasOwnProperty.call(record2, key) ? record2[key] : void 0;
13257
- }
13258
- function closedCallees(facts, fn) {
13259
- const direct = ownEntry(facts.functionCalls, fn);
13509
+ function closedCallSites(code, file, fn, stopAt) {
13510
+ const facts = code.factsAt(file);
13511
+ const direct = facts && callSitesOf(facts, fn);
13260
13512
  if (!direct) return void 0;
13261
- const closed = new Set(direct);
13262
- const queue = [...direct];
13513
+ const here = pathKey(file);
13514
+ const out = [];
13515
+ const descended = /* @__PURE__ */ new Set([`${here}|${fn}`]);
13516
+ const queue = direct.map((s) => ({ ...s, from: s.from ?? here }));
13263
13517
  while (queue.length) {
13264
- const name = queue.pop();
13265
- for (const next of ownEntry(facts.functionCalls, name) ?? []) {
13266
- if (!closed.has(next)) {
13267
- closed.add(next);
13268
- queue.push(next);
13269
- }
13270
- }
13518
+ const site = queue.pop();
13519
+ out.push(site);
13520
+ if (stopAt?.has(site.name)) continue;
13521
+ const key = `${site.from}|${site.name}`;
13522
+ if (descended.has(key)) continue;
13523
+ descended.add(key);
13524
+ const scope = code.factsAt(site.from);
13525
+ const next = scope && callSitesOf(scope, site.name);
13526
+ if (!next) continue;
13527
+ queue.push(...next.map((s) => ({ ...s, from: s.from ?? site.from })));
13271
13528
  }
13272
- return closed;
13529
+ return out;
13530
+ }
13531
+ function resolveCallTarget(ctx, componentId, methodName) {
13532
+ const accepted = /* @__PURE__ */ new Set([methodName]);
13533
+ const files = /* @__PURE__ */ new Set();
13534
+ for (const intf of ctx.interfacesByComponent.get(componentId) ?? []) {
13535
+ for (const impl of ctx.implementationsByContract.get(intf.id) ?? []) {
13536
+ const method2 = impl.methods.find((m) => m.name === methodName);
13537
+ if (!method2 && !intf.methods.some((m) => m.name === methodName)) continue;
13538
+ if (method2?.symbol) accepted.add(method2.symbol);
13539
+ const file = methodSourceFile(method2 ?? {}, impl.sourcePath);
13540
+ if (file) files.add(pathKey(file));
13541
+ }
13542
+ }
13543
+ return { accepted, files };
13544
+ }
13545
+ function describeSite(site) {
13546
+ if (!site.member) return `${site.name}(\u2026)`;
13547
+ if (site.via) return `${site.via}.${site.name}(\u2026)`;
13548
+ if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
13549
+ if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
13550
+ return `<receiver>.${site.name}(\u2026)`;
13551
+ }
13552
+ function describeMiss(m) {
13553
+ const head = `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")}`;
13554
+ if (m.miss.kind === "elsewhere") return `${head}, called but resolved to ${m.miss.landed.map((p) => `"${p}"`).join(", ")})`;
13555
+ if (m.miss.kind === "unresolved") return `${head}, written as ${m.miss.shapes.map((s) => `\`${s}\``).join(" / ")})`;
13556
+ return `${head})`;
13273
13557
  }
13274
13558
  var callConformanceRule;
13275
13559
  var init_call_conformance = __esm({
@@ -13278,13 +13562,24 @@ var init_call_conformance = __esm({
13278
13562
  init_models();
13279
13563
  callConformanceRule = {
13280
13564
  name: "call-conformance",
13281
- description: "Code\u2194spec Level 3 (opener): every narrative `call` step of an exactly-analyzed method must be realized as a call in the realized function, read from the method's own source file (its sourcePath, else the implementation's) \u2014 the target method's contract name or its per-method symbol override must appear among the function's callees, closed transitively over same-file named helpers. Set membership only: order, arguments, and conditions are deliberately unverified, and dispatch steps (runtime-table routed) are skipped. Respects the conformance dial (off skips) and fires only at exact analysis grade \u2014 weaker grades never guess.",
13565
+ description: "Code\u2194spec Level 3: the narrative `call` step \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. Forward: every `call` step must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a step but never accuse one, and a target that names no file of its own falls back to name membership. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the narrative must declare it (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are skipped, the conformance dial (off) skips, and weaker analysis grades never guess.",
13282
13566
  codes: [
13283
- { code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step whose target method name (or symbol) never appears among the realized function's callees in the method's source file (exact grade, set membership)" }
13567
+ { code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step realized by no call that resolves to the target method's own source file \u2014 the call is absent, or it lands in another module", carryable: true },
13568
+ { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the step was not checked, and is neither proven realized nor accused", carryable: true },
13569
+ { code: "UNDECLARED_COLOCATED_CALL", defaultSeverity: "warning", summary: "The realized function calls a modelled method of another component living in the same source file, and no narrative step declares that call", carryable: true }
13284
13570
  ],
13285
13571
  check(ctx) {
13286
13572
  const code = ctx.codeIndex();
13287
- for (const entry of ctx.implementationMethods()) {
13573
+ const methods = ctx.implementationMethods();
13574
+ const modelledAt = /* @__PURE__ */ new Map();
13575
+ for (const entry of methods) {
13576
+ if (!entry.sourceFile) continue;
13577
+ const file = pathKey(entry.sourceFile);
13578
+ const byName = modelledAt.get(file) ?? /* @__PURE__ */ new Map();
13579
+ byName.set(entry.method.symbol ?? entry.method.name, { component: entry.component.id, method: entry.method.name });
13580
+ modelledAt.set(file, byName);
13581
+ }
13582
+ for (const entry of methods) {
13288
13583
  const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
13289
13584
  const tier = implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component);
13290
13585
  if (tier === "off") continue;
@@ -13292,37 +13587,91 @@ var init_call_conformance = __esm({
13292
13587
  if (!file) continue;
13293
13588
  const facts = code.factsAt(file);
13294
13589
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
13590
+ const here = pathKey(file);
13295
13591
  const fnSymbol = implMethod.symbol ?? implMethod.name;
13296
- const callees = closedCallees(facts, fnSymbol);
13297
- if (!callees) continue;
13298
- const missing = [];
13592
+ const colocated = modelledAt.get(here) ?? /* @__PURE__ */ new Map();
13593
+ const sites = closedCallSites(code, file, fnSymbol);
13594
+ if (!sites) continue;
13595
+ const missed = [];
13299
13596
  for (const step of implMethod.narrative) {
13300
13597
  if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
13301
13598
  if (!ctx.componentMap.has(step.targetComponent)) continue;
13302
- const accepted = /* @__PURE__ */ new Set([step.targetMethod]);
13303
- for (const targetIntf of ctx.interfacesByComponent.get(step.targetComponent) ?? []) {
13304
- for (const targetImpl of ctx.implementationsByContract.get(targetIntf.id) ?? []) {
13305
- const targetMethod = targetImpl.methods.find((m) => m.name === step.targetMethod);
13306
- if (targetMethod?.symbol) accepted.add(targetMethod.symbol);
13599
+ const target = resolveCallTarget(ctx, step.targetComponent, step.targetMethod);
13600
+ if (target.accepted.has(fnSymbol)) continue;
13601
+ const matching = sites.filter((s) => target.accepted.has(s.name));
13602
+ const ref = `${step.targetComponent}.${step.targetMethod}`;
13603
+ if (target.files.size === 0) {
13604
+ if (matching.length === 0) {
13605
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss: { kind: "absent" } });
13307
13606
  }
13607
+ continue;
13308
13608
  }
13309
- if (accepted.has(fnSymbol)) continue;
13310
- if ([...accepted].some((name) => callees.has(name))) continue;
13311
- missing.push({
13312
- step: step.stepNumber,
13313
- target: `${step.targetComponent}.${step.targetMethod}`,
13314
- accepted: [...accepted]
13315
- });
13609
+ const landed = /* @__PURE__ */ new Set();
13610
+ let realized = false;
13611
+ for (const site of matching) {
13612
+ for (const origin of code.originOf(site, file)) landed.add(origin);
13613
+ for (const origin of code.possibleOriginsOf(site, file)) {
13614
+ if (target.files.has(origin)) realized = true;
13615
+ }
13616
+ }
13617
+ if (realized) continue;
13618
+ const miss = matching.length === 0 ? { kind: "absent" } : landed.size === 0 ? { kind: "unresolved", shapes: [...new Set(matching.map(describeSite))].sort() } : { kind: "elsewhere", landed: [...landed].sort() };
13619
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss });
13620
+ }
13621
+ const unrealized = missed.filter((m) => m.miss.kind !== "unresolved");
13622
+ const unresolved = missed.filter((m) => m.miss.kind === "unresolved");
13623
+ if (unrealized.length) {
13624
+ ctx.addIssue(
13625
+ "warning",
13626
+ "CALL_STEP_UNREALIZED",
13627
+ `Method "${implMethod.name}" in implementation "${impl.id}": ${unrealized.length} narrative call step(s) are realized by no call of the function "${fnSymbol}" in "${file}" that resolves to the target's own source file \u2014 ${unrealized.map(describeMiss).join("; ")}. Callees are closed over the named helpers the function calls, and each call site is resolved through this file's import bindings (order, arguments and conditions are not checked). Realize the calls, fix the narrative, or map code names via per-method symbols on the targets.`,
13628
+ impl.id,
13629
+ entry.draftContext,
13630
+ void 0,
13631
+ // A step, named by its number and its target: what the register
13632
+ // carries is ONE unresolved step, never "this method's call steps".
13633
+ { at: implMethod.name, covers: unrealized.map((m) => `${m.step}:${m.target}`) }
13634
+ );
13635
+ }
13636
+ if (unresolved.length) {
13637
+ ctx.addIssue(
13638
+ "warning",
13639
+ "CALL_ORIGIN_UNRESOLVED",
13640
+ `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} narrative call step(s) ARE called by name inside the function "${fnSymbol}" in "${file}", but every site calling them is written in a shape this analysis cannot resolve to a file \u2014 ${unresolved.map(describeMiss).join("; ")}. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a \`this.<field>\` receiver whose declared type names one, a \`new Class(\u2026)\` receiver whose class does. What does not: an import from a PACKAGE specifier, a receiver holding a value the module assembled, a receiver this model records no name for. This reports what was not checked, not what is wrong \u2014 the step is neither proven realized nor accused of being missing, and no working call is asked to be rewritten to suit the reader.`,
13641
+ impl.id,
13642
+ entry.draftContext,
13643
+ void 0,
13644
+ { at: implMethod.name, covers: unresolved.map((m) => `${m.step}:${m.target}`) }
13645
+ );
13646
+ }
13647
+ const declared = /* @__PURE__ */ new Set();
13648
+ for (const step of implMethod.narrative) {
13649
+ if (step.targetComponent && step.targetMethod) declared.add(`${step.targetComponent}.${step.targetMethod}`);
13650
+ }
13651
+ const crossings = /* @__PURE__ */ new Map();
13652
+ for (const site of closedCallSites(code, file, fnSymbol, new Set(colocated.keys())) ?? []) {
13653
+ const owner = colocated.get(site.name);
13654
+ if (!owner || owner.component === component.id) continue;
13655
+ const ref = `${owner.component}.${owner.method}`;
13656
+ if (declared.has(ref) || crossings.has(ref)) continue;
13657
+ if (!code.originOf(site, file).has(here)) continue;
13658
+ crossings.set(ref, site.name);
13659
+ }
13660
+ if (crossings.size) {
13661
+ const detail = [...crossings].map(([ref, name]) => `"${name}" (${ref})`).join("; ");
13662
+ ctx.addIssue(
13663
+ "warning",
13664
+ "UNDECLARED_COLOCATED_CALL",
13665
+ `Method "${implMethod.name}" in implementation "${impl.id}": the function "${fnSymbol}" in "${file}" calls ${crossings.size} modelled method(s) of OTHER components living in that same file, and no narrative step declares the call \u2014 ${detail}. Sharing a file does not make the hop internal: it crosses a component boundary nothing imports, so no file-level check can see it. Narrate the call, or move the code so the boundary is real.`,
13666
+ impl.id,
13667
+ entry.draftContext,
13668
+ void 0,
13669
+ // The crossings themselves. Keyed by method alone, a 24th crossing
13670
+ // would ride into a register entry written for 23 - so each one is
13671
+ // named, and one nobody carried fires on the day it appears.
13672
+ { at: implMethod.name, covers: [...crossings.keys()] }
13673
+ );
13316
13674
  }
13317
- if (missing.length === 0) continue;
13318
- const detail = missing.map((m) => `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")})`).join("; ");
13319
- ctx.addIssue(
13320
- "warning",
13321
- "CALL_STEP_UNREALIZED",
13322
- `Method "${implMethod.name}" in implementation "${impl.id}": ${missing.length} narrative call step(s) are not realized as calls of the function "${fnSymbol}" in "${file}" \u2014 ${detail}. Callees are matched by name, closed over same-file helpers (exact grade, set membership \u2014 order and arguments are not checked). Realize the calls, fix the narrative, or map code names via per-method symbols on the targets.`,
13323
- impl.id,
13324
- entry.draftContext
13325
- );
13326
13675
  }
13327
13676
  }
13328
13677
  };
@@ -13453,7 +13802,13 @@ var init_dependency_conformance = __esm({
13453
13802
  "UNDECLARED_DEPENDENCY",
13454
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.`,
13455
13804
  realization.implementationsAt(fromPath)[0]?.id,
13456
- 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}` }
13457
13812
  );
13458
13813
  }
13459
13814
  }
@@ -13480,7 +13835,12 @@ var init_dependency_conformance = __esm({
13480
13835
  "UNREALIZED_DEPENDENCY",
13481
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.`,
13482
13837
  impls[0]?.id ?? component.id,
13483
- 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}` }
13484
13844
  );
13485
13845
  }
13486
13846
  }
@@ -13832,6 +14192,69 @@ var init_unclaimed_source = __esm({
13832
14192
  }
13833
14193
  });
13834
14194
 
14195
+ // src/core/rules/conformance/carried-debt.ts
14196
+ var carriedDebtRule;
14197
+ var init_carried_debt = __esm({
14198
+ "src/core/rules/conformance/carried-debt.ts"() {
14199
+ "use strict";
14200
+ carriedDebtRule = {
14201
+ name: "carried-debt",
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.",
14203
+ codes: [
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" },
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" }
14206
+ ],
14207
+ check(ctx) {
14208
+ const carried = ctx.carriedFindings;
14209
+ if (carried.length === 0) return;
14210
+ for (const entry of carried) {
14211
+ if (ctx.carryableIssueCodes.has(entry.code)) continue;
14212
+ const why = ctx.knownIssueCodes.has(entry.code) ? "that code is not CARRYABLE \u2014 the register holds measured code\u2194spec debt, and a doctrine, soundness or configuration finding says the design is illegal rather than unfinished" : "no registered rule emits that code";
14213
+ ctx.addIssue(
14214
+ "error",
14215
+ "UNCARRYABLE_FINDING",
14216
+ `The conformance debt register carries "${entry.code}" on "${entry.spec}" (at "${entry.at}"), but ${why}. Remove the entry; if the code belongs in the register, declare it carryable on the rule that emits it.`
14217
+ );
14218
+ }
14219
+ if (ctx.codeModel.files.length === 0) return;
14220
+ const known = new Set(ctx.specIds().map((s) => s.id));
14221
+ const addressed = /* @__PURE__ */ new Set();
14222
+ for (const entry of carried) {
14223
+ if (!ctx.carryableIssueCodes.has(entry.code)) continue;
14224
+ if (ctx.rules?.sddRuleSeverity?.[entry.code] === "off") continue;
14225
+ if (!ctx.isSpecInScope(entry.spec)) continue;
14226
+ const key = `${entry.spec}|${entry.code}|${entry.at}`;
14227
+ if (addressed.has(key)) {
14228
+ ctx.addIssue(
14229
+ "warning",
14230
+ "STALE_CARRIED_FINDING",
14231
+ `The conformance debt register carries "${entry.code}" on "${entry.spec}" (at "${entry.at}") more than once. Only the first entry can ever match, so the rest carry nothing \u2014 delete them and merge their units into the one entry.`
14232
+ );
14233
+ continue;
14234
+ }
14235
+ addressed.add(key);
14236
+ if (!entry.fired) {
14237
+ const why = known.has(entry.spec) ? "no such finding fired this run \u2014 the debt is paid, or the spec no longer reaches that site" : `the tree holds no spec "${entry.spec}"`;
14238
+ ctx.addIssue(
14239
+ "warning",
14240
+ "STALE_CARRIED_FINDING",
14241
+ `The conformance debt register carries "${entry.code}" on "${entry.spec}" (at "${entry.at}" \u2014 ${entry.kind}: ${entry.why}), but ${why}. Delete the entry; the register only shrinks.`
14242
+ );
14243
+ continue;
14244
+ }
14245
+ const paid = entry.covers.filter((unit) => !entry.seen.has(unit));
14246
+ if (paid.length === 0) continue;
14247
+ ctx.addIssue(
14248
+ "warning",
14249
+ "STALE_CARRIED_FINDING",
14250
+ `The conformance debt register carries "${entry.code}" on "${entry.spec}" (at "${entry.at}") together with ${paid.length} unit(s) the finding no longer reports \u2014 ${paid.map((u) => `"${u}"`).join("; ")}. That much of the debt is paid: delete those lines from the entry's \`covers\`; the register only shrinks.`
14251
+ );
14252
+ }
14253
+ }
14254
+ };
14255
+ }
14256
+ });
14257
+
13835
14258
  // src/core/rules/heuristic/coupling-health.ts
13836
14259
  var DEFAULT_GOD_COMPONENT_THRESHOLD, couplingRule;
13837
14260
  var init_coupling_health = __esm({
@@ -14986,8 +15409,8 @@ function registerPackRules(packRules) {
14986
15409
  for (const rule of packRules) addRule(rule);
14987
15410
  }
14988
15411
  function ruleSequence() {
14989
- const base = ruleSet.filter((r) => r !== lintAllowsRule);
14990
- return ruleSet.includes(lintAllowsRule) ? [...base, lintAllowsRule] : base;
15412
+ const audits = [carriedDebtRule, lintAllowsRule].filter((r) => ruleSet.includes(r));
15413
+ return [...ruleSet.filter((r) => !audits.includes(r)), ...audits];
14991
15414
  }
14992
15415
  function specScopedRules() {
14993
15416
  return ruleSequence().filter((r) => r.scope === "spec");
@@ -15077,6 +15500,7 @@ var init_repository = __esm({
15077
15500
  init_integration_sim_coverage();
15078
15501
  init_type_realization();
15079
15502
  init_unclaimed_source();
15503
+ init_carried_debt();
15080
15504
  init_coupling_health();
15081
15505
  init_signature_language_builtins();
15082
15506
  init_narrative_language_constructs();
@@ -15268,8 +15692,11 @@ var init_repository = __esm({
15268
15692
  // elsewhere?) rather than spec content.
15269
15693
  packResolutionRule,
15270
15694
  reproducibilityRule,
15271
- // MUST run last: it audits which lint.allow entries the earlier rules
15272
- // actually consumed (stale/unknown allows).
15695
+ // MUST run last, in this order: each audits what the earlier rules did
15696
+ // with a declared exception. The debt register first (which carried
15697
+ // findings the conformance family actually matched), then the allows
15698
+ // (which suppressions any rule actually consumed).
15699
+ carriedDebtRule,
15273
15700
  lintAllowsRule
15274
15701
  ];
15275
15702
  ruleSet = [];
@@ -15392,9 +15819,15 @@ function walkExact(ts, sourceText, fileName) {
15392
15819
  const imports = /* @__PURE__ */ new Set();
15393
15820
  const reexports = /* @__PURE__ */ new Set();
15394
15821
  const starExports = [];
15822
+ const namedReexports = [];
15395
15823
  const complexity = /* @__PURE__ */ new Map();
15396
15824
  const calls = /* @__PURE__ */ new Map();
15825
+ const importBindings = /* @__PURE__ */ new Map();
15826
+ const typeOnlyBindings = /* @__PURE__ */ new Map();
15827
+ const fieldTypes = /* @__PURE__ */ new Map();
15397
15828
  const mutableBindings = /* @__PURE__ */ new Set();
15829
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : "");
15830
+ const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
15398
15831
  const addBindingNames = (name) => {
15399
15832
  if (ts.isIdentifier(name)) declared.add(name.text);
15400
15833
  else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
@@ -15408,6 +15841,17 @@ function walkExact(ts, sourceText, fileName) {
15408
15841
  if (ts.isPrivateIdentifier(name)) return name.text;
15409
15842
  return void 0;
15410
15843
  };
15844
+ const typeReferenceName = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : void 0;
15845
+ const isParameterProperty = (node) => !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword);
15846
+ const recordFieldType = (name, type) => {
15847
+ const typeName = typeReferenceName(type);
15848
+ if (!typeName) return;
15849
+ const field = propertyNameText(name);
15850
+ if (!field) return;
15851
+ const named = fieldTypes.get(field) ?? /* @__PURE__ */ new Set();
15852
+ named.add(typeName);
15853
+ fieldTypes.set(field, named);
15854
+ };
15411
15855
  const hasExportModifier = (node) => {
15412
15856
  const mods = node.modifiers;
15413
15857
  return !!mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
@@ -15430,7 +15874,10 @@ function walkExact(ts, sourceText, fileName) {
15430
15874
  };
15431
15875
  const collectFunctionFacts = (fn) => {
15432
15876
  let score = 1;
15433
- const callees = /* @__PURE__ */ new Set();
15877
+ const callees = /* @__PURE__ */ new Map();
15878
+ const addSite = (site) => {
15879
+ callees.set(siteKey(site), site);
15880
+ };
15434
15881
  const count = (node) => {
15435
15882
  if (namedFunctionName(node) !== void 0) return;
15436
15883
  if (ts.isIfStatement(node) || ts.isConditionalExpression(node) || ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node) || ts.isCaseClause(node) || ts.isCatchClause(node)) {
@@ -15442,8 +15889,17 @@ function walkExact(ts, sourceText, fileName) {
15442
15889
  }
15443
15890
  } else if (ts.isCallExpression(node)) {
15444
15891
  const callee = node.expression;
15445
- if (ts.isIdentifier(callee)) callees.add(callee.text);
15446
- else if (ts.isPropertyAccessExpression(callee)) callees.add(callee.name.text);
15892
+ if (ts.isIdentifier(callee)) addSite({ name: callee.text, member: false });
15893
+ else if (ts.isPropertyAccessExpression(callee)) {
15894
+ const receiver = callee.expression;
15895
+ const name = callee.name.text;
15896
+ if (ts.isIdentifier(receiver)) addSite({ name, member: true, via: receiver.text });
15897
+ else if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
15898
+ addSite({ name, member: true, field: receiver.name.text });
15899
+ } else if (ts.isNewExpression(receiver) && ts.isIdentifier(receiver.expression)) {
15900
+ addSite({ name, member: true, constructed: receiver.expression.text });
15901
+ } else addSite({ name, member: true });
15902
+ }
15447
15903
  }
15448
15904
  ts.forEachChild(node, count);
15449
15905
  };
@@ -15455,9 +15911,13 @@ function walkExact(ts, sourceText, fileName) {
15455
15911
  if (fnName && node.body) {
15456
15912
  const facts = collectFunctionFacts(node);
15457
15913
  complexity.set(fnName, Math.max(complexity.get(fnName) ?? 0, facts.score));
15458
- const set = calls.get(fnName) ?? /* @__PURE__ */ new Set();
15459
- for (const c of facts.callees) set.add(c);
15460
- calls.set(fnName, set);
15914
+ const sites = calls.get(fnName) ?? /* @__PURE__ */ new Map();
15915
+ for (const [key, site] of facts.callees) sites.set(key, site);
15916
+ calls.set(fnName, sites);
15917
+ }
15918
+ if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
15919
+ else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
15920
+ recordFieldType(node.name, node.type);
15461
15921
  }
15462
15922
  if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
15463
15923
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : void 0;
@@ -15484,11 +15944,24 @@ function walkExact(ts, sourceText, fileName) {
15484
15944
  } else if (ts.isImportDeclaration(node)) {
15485
15945
  const clause = node.importClause;
15486
15946
  const typeOnly = clause?.isTypeOnly ?? false;
15487
- if (!typeOnly && ts.isStringLiteral(node.moduleSpecifier)) imports.add(node.moduleSpecifier.text);
15488
- if (clause?.name) declared.add(clause.name.text);
15947
+ const spec = ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
15948
+ if (!typeOnly && spec) imports.add(spec);
15949
+ const bind = (name, namespace, imported, elementTypeOnly = false) => {
15950
+ declared.add(name);
15951
+ if (!spec) return;
15952
+ if (typeOnly || elementTypeOnly) {
15953
+ typeOnlyBindings.set(name, spec);
15954
+ return;
15955
+ }
15956
+ const fact = { from: spec };
15957
+ if (namespace) fact.namespace = true;
15958
+ if (imported && imported !== name) fact.imported = imported;
15959
+ importBindings.set(name, fact);
15960
+ };
15961
+ if (clause?.name) bind(clause.name.text, false);
15489
15962
  if (clause?.namedBindings) {
15490
- if (ts.isNamespaceImport(clause.namedBindings)) declared.add(clause.namedBindings.name.text);
15491
- else for (const el of clause.namedBindings.elements) declared.add(el.name.text);
15963
+ if (ts.isNamespaceImport(clause.namedBindings)) bind(clause.namedBindings.name.text, true);
15964
+ else for (const el of clause.namedBindings.elements) bind(el.name.text, false, el.propertyName?.text, el.isTypeOnly);
15492
15965
  }
15493
15966
  } else if (ts.isExportDeclaration(node)) {
15494
15967
  const spec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
@@ -15497,6 +15970,9 @@ function walkExact(ts, sourceText, fileName) {
15497
15970
  for (const el of node.exportClause.elements) {
15498
15971
  declared.add(el.name.text);
15499
15972
  exported.add(el.name.text);
15973
+ if (spec && !node.isTypeOnly && !el.isTypeOnly) {
15974
+ namedReexports.push({ exported: el.name.text, local: (el.propertyName ?? el.name).text, from: spec });
15975
+ }
15500
15976
  }
15501
15977
  } else if (!node.exportClause && spec) {
15502
15978
  starExports.push(spec);
@@ -15519,7 +15995,7 @@ function walkExact(ts, sourceText, fileName) {
15519
15995
  };
15520
15996
  visit(sf);
15521
15997
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
15522
- return { declared, anchors, exported, imports, reexports, starExports, complexity, calls, mutableBindings, reexportOnly };
15998
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, importBindings, typeOnlyBindings, fieldTypes, mutableBindings, reexportOnly };
15523
15999
  }
15524
16000
  function resolveRelativeModule(fromFile, specifier) {
15525
16001
  if (!specifier.startsWith(".")) return null;
@@ -15542,34 +16018,47 @@ function resolveRelativeModule(fromFile, specifier) {
15542
16018
  }
15543
16019
  return null;
15544
16020
  }
15545
- function chaseStarExports(ts, facts, filePath, projectRoot2, visited, exactCache) {
15546
- for (const spec of facts.starExports) {
15547
- const target = resolveRelativeModule(filePath, spec);
15548
- if (!target || visited.has(target)) continue;
15549
- if (path13.relative(projectRoot2, target).startsWith("..")) continue;
15550
- visited.add(target);
15551
- let targetFacts = exactCache.get(target);
15552
- if (targetFacts === void 0) {
15553
- try {
15554
- targetFacts = walkExact(ts, fs10.readFileSync(target, "utf8"), target);
15555
- } catch {
15556
- targetFacts = null;
15557
- }
15558
- exactCache.set(target, targetFacts);
16021
+ function reexportTarget(ts, filePath, specifier, projectRoot2, visited, exactCache) {
16022
+ const target = resolveRelativeModule(filePath, specifier);
16023
+ if (!target || path13.relative(projectRoot2, target).startsWith("..")) return null;
16024
+ let targetFacts = exactCache.get(target);
16025
+ if (targetFacts === void 0) {
16026
+ try {
16027
+ targetFacts = walkExact(ts, fs10.readFileSync(target, "utf8"), target);
16028
+ } catch {
16029
+ targetFacts = null;
15559
16030
  }
15560
- if (!targetFacts) continue;
15561
- chaseStarExports(ts, targetFacts, target, projectRoot2, visited, exactCache);
15562
- for (const name of targetFacts.exported) {
16031
+ exactCache.set(target, targetFacts);
16032
+ }
16033
+ if (!targetFacts) return null;
16034
+ if (!visited.has(target)) {
16035
+ visited.add(target);
16036
+ chaseReexports(ts, targetFacts, target, projectRoot2, visited, exactCache);
16037
+ }
16038
+ return { path: target, facts: targetFacts };
16039
+ }
16040
+ function carryBody(facts, target, targetPath, local, exported, projectRoot2) {
16041
+ const score = target.complexity.get(local);
16042
+ if (score !== void 0) facts.complexity.set(exported, Math.max(facts.complexity.get(exported) ?? 0, score));
16043
+ const targetCalls = target.calls.get(local);
16044
+ if (!targetCalls) return;
16045
+ const carriedFrom = pathKey(path13.relative(projectRoot2, targetPath));
16046
+ const sites = facts.calls.get(exported) ?? /* @__PURE__ */ new Map();
16047
+ for (const [key, site] of targetCalls) sites.set(carriedFrom + "|" + key, { ...site, from: site.from ?? carriedFrom });
16048
+ facts.calls.set(exported, sites);
16049
+ }
16050
+ function chaseReexports(ts, facts, filePath, projectRoot2, visited, exactCache) {
16051
+ for (const re of facts.namedReexports) {
16052
+ const target = reexportTarget(ts, filePath, re.from, projectRoot2, visited, exactCache);
16053
+ if (target) carryBody(facts, target.facts, target.path, re.local, re.exported, projectRoot2);
16054
+ }
16055
+ for (const spec of facts.starExports) {
16056
+ const target = reexportTarget(ts, filePath, spec, projectRoot2, visited, exactCache);
16057
+ if (!target) continue;
16058
+ for (const name of target.facts.exported) {
15563
16059
  facts.exported.add(name);
15564
16060
  facts.declared.add(name);
15565
- const c = targetFacts.complexity.get(name);
15566
- if (c !== void 0) facts.complexity.set(name, Math.max(facts.complexity.get(name) ?? 0, c));
15567
- const targetCalls = targetFacts.calls.get(name);
15568
- if (targetCalls) {
15569
- const set = facts.calls.get(name) ?? /* @__PURE__ */ new Set();
15570
- for (const callee of targetCalls) set.add(callee);
15571
- facts.calls.set(name, set);
15572
- }
16061
+ carryBody(facts, target.facts, target.path, name, name, projectRoot2);
15573
16062
  }
15574
16063
  }
15575
16064
  }
@@ -15674,7 +16163,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15674
16163
  if (ts) {
15675
16164
  try {
15676
16165
  const facts = walkExact(ts, text2, absolute);
15677
- chaseStarExports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
16166
+ chaseReexports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
15678
16167
  analyzed = {
15679
16168
  analysisGrade: "exact",
15680
16169
  declaredNames: [...facts.declared],
@@ -15683,7 +16172,10 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15683
16172
  imports: [...facts.imports],
15684
16173
  reexports: [...facts.reexports],
15685
16174
  functionComplexity: Object.fromEntries(facts.complexity),
15686
- functionCalls: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v]])),
16175
+ functionCallSites: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v.values()]])),
16176
+ importBindings: Object.fromEntries(facts.importBindings),
16177
+ typeOnlyBindings: Object.fromEntries(facts.typeOnlyBindings),
16178
+ fieldTypes: Object.fromEntries([...facts.fieldTypes].map(([k, v]) => [k, [...v]])),
15687
16179
  topLevelMutableBindings: [...facts.mutableBindings],
15688
16180
  reexportOnly: facts.reexportOnly
15689
16181
  };
@@ -16081,18 +16573,46 @@ function buildRuleContext(opts) {
16081
16573
  const allowLookup = /* @__PURE__ */ new Map();
16082
16574
  const collectAllows = (specId, lint) => {
16083
16575
  for (const a of lint?.allow ?? []) {
16084
- 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 };
16085
16577
  lintAllows.push(entry);
16086
16578
  if (!allowLookup.has(specId)) allowLookup.set(specId, /* @__PURE__ */ new Map());
16087
- 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);
16088
16583
  }
16089
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
+ };
16090
16590
  for (const s of subsystems) collectAllows(s.id, s.lint);
16091
16591
  for (const c of components) collectAllows(c.id, c.lint);
16092
16592
  for (const i of interfaces) collectAllows(i.id, i.lint);
16093
16593
  for (const im of implementations) collectAllows(im.id, im.lint);
16094
16594
  for (const t of types) collectAllows(t.id, t.lint);
16095
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
16595
+ const carriedFindings = [];
16596
+ const carriedLookup = /* @__PURE__ */ new Map();
16597
+ const carriedKey = (spec, code, at) => `${spec}\0${code}\0${at}`;
16598
+ for (const group of rules?.conformance?.carried ?? []) {
16599
+ for (const f of group.findings ?? []) {
16600
+ const entry = {
16601
+ kind: group.kind,
16602
+ why: group.why,
16603
+ code: f.code,
16604
+ spec: f.spec,
16605
+ at: f.at,
16606
+ covers: f.covers ?? [],
16607
+ fired: false,
16608
+ seen: /* @__PURE__ */ new Set()
16609
+ };
16610
+ carriedFindings.push(entry);
16611
+ const key = carriedKey(f.spec, f.code, f.at);
16612
+ if (!carriedLookup.has(key)) carriedLookup.set(key, entry);
16613
+ }
16614
+ }
16615
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved, parts) => {
16096
16616
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
16097
16617
  return;
16098
16618
  }
@@ -16103,17 +16623,46 @@ function buildRuleContext(opts) {
16103
16623
  }
16104
16624
  const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
16105
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;
16106
16635
  if (specId) {
16107
- 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);
16108
16637
  if (allow) {
16109
16638
  allow.used = true;
16110
- 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
+ }
16646
+ }
16647
+ }
16648
+ if (specId && parts && severity === "warning" && !allowClaimed) {
16649
+ const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
16650
+ if (entry) {
16651
+ entry.fired = true;
16652
+ const listed = new Set(entry.covers);
16653
+ const grew = [];
16654
+ for (const unit of parts.covers ?? []) {
16655
+ if (listed.has(unit)) entry.seen.add(unit);
16656
+ else grew.push(unit);
16657
+ }
16658
+ if (grew.length === 0) return;
16659
+ text2 = `${text2} The conformance debt register carries this finding, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Fix them, or add them to the entry's \`covers\` with a reason that is actually true.`;
16111
16660
  }
16112
16661
  }
16113
16662
  issues.push({
16114
16663
  severity,
16115
16664
  code,
16116
- message,
16665
+ message: text2,
16117
16666
  specId,
16118
16667
  ...isDraftContext ? { draftContext: true } : {},
16119
16668
  ...surfaceResolved ? { surfaceResolved: true } : {}
@@ -16189,7 +16738,10 @@ function buildRuleContext(opts) {
16189
16738
  codeModel: opts.codeModel ?? emptyCodeModel(),
16190
16739
  roundTripIssues: opts.roundTripIssues,
16191
16740
  lintAllows,
16741
+ sitesReported,
16192
16742
  knownIssueCodes: opts.knownIssueCodes,
16743
+ carriedFindings,
16744
+ carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
16193
16745
  addIssue
16194
16746
  };
16195
16747
  return ctx;
@@ -22740,6 +23292,9 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22740
23292
  ...knownIssueCodes().map((rc) => rc.code),
22741
23293
  ...extensions.assertions.map((a) => a.fullCode)
22742
23294
  ]);
23295
+ const carryableCodes = new Set(
23296
+ knownIssueCodes().filter((rc) => rc.carryable).map((rc) => rc.code)
23297
+ );
22743
23298
  const ctx = buildRuleContext({
22744
23299
  system,
22745
23300
  subsystems,
@@ -22759,6 +23314,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22759
23314
  codeModel,
22760
23315
  roundTripIssues,
22761
23316
  knownIssueCodes: knownCodes,
23317
+ carryableIssueCodes: carryableCodes,
22762
23318
  issues
22763
23319
  });
22764
23320
  for (const rule of sequence) {
@@ -23692,10 +24248,10 @@ function findChainingParent(childRoot, ceiling) {
23692
24248
  if (bound2 && !isWithin(bound2, dir)) break;
23693
24249
  const specsDir = aiPathsAt(dir).specsDir();
23694
24250
  if (pathExists(specsDir)) {
23695
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24251
+ for (const file of listSpecFiles(specsDir)) {
23696
24252
  let raw;
23697
24253
  try {
23698
- raw = readYamlFile(file);
24254
+ raw = readSpecFile(file);
23699
24255
  } catch {
23700
24256
  continue;
23701
24257
  }
@@ -23727,10 +24283,10 @@ function inspectChainedRoots(rootDir = getProjectRoot()) {
23727
24283
  const walk2 = (projectDir, prefix, ancestors, depth) => {
23728
24284
  const specsDir = aiPathsAt(projectDir).specsDir();
23729
24285
  if (!pathExists(specsDir)) return;
23730
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24286
+ for (const file of listSpecFiles(specsDir)) {
23731
24287
  let raw;
23732
24288
  try {
23733
- raw = readYamlFile(file);
24289
+ raw = readSpecFile(file);
23734
24290
  } catch {
23735
24291
  continue;
23736
24292
  }
@@ -23914,7 +24470,7 @@ function computeSpecTreeSignature(dirs) {
23914
24470
  parts.push(`${dir}:missing`);
23915
24471
  continue;
23916
24472
  }
23917
- for (const f of listFilesRecursive(dir, ".yaml")) {
24473
+ for (const f of listSpecFiles(dir)) {
23918
24474
  try {
23919
24475
  const st = fs17.statSync(f);
23920
24476
  parts.push(`${f}:${st.mtimeMs}:${st.size}`);
@@ -23944,9 +24500,11 @@ function identityKeyOf(field, item) {
23944
24500
  return `${String(o.topic)} ${o.event === void 0 ? "" : String(o.event)}`;
23945
24501
  case "trustedLinks":
23946
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.
23947
24506
  case "allow":
23948
- return str(o.code);
23949
- // lint.allow
24507
+ return `${String(o.code)} ${o.at === void 0 ? "" : String(o.at)}`;
23950
24508
  case "findings":
23951
24509
  return str(o.code);
23952
24510
  // an interface method's findings
@@ -24034,6 +24592,7 @@ function identityFieldsOf(field) {
24034
24592
  case "trustedLinks":
24035
24593
  return ["subsystem"];
24036
24594
  case "allow":
24595
+ return ["code", "at"];
24037
24596
  case "findings":
24038
24597
  return ["code"];
24039
24598
  case "invariants":
@@ -24374,6 +24933,7 @@ var init_specs2 = __esm({
24374
24933
  init_canonical_json();
24375
24934
  init_lockfile();
24376
24935
  init_yaml();
24936
+ init_spec_files();
24377
24937
  init_models();
24378
24938
  init_narrative_labels();
24379
24939
  init_diagram();
@@ -24452,7 +25012,7 @@ var init_specs2 = __esm({
24452
25012
  const specsDir = projectPaths.specsDir();
24453
25013
  this.scanVisitedSpecDirs.push(specsDir);
24454
25014
  if (!pathExists(specsDir)) return index;
24455
- const files = listFilesRecursive(specsDir, ".yaml");
25015
+ const files = listSpecFiles(specsDir);
24456
25016
  const systemYaml = path23.normalize(projectPaths.specsSystem());
24457
25017
  const localSubprojects = [];
24458
25018
  for (const file of files) {
@@ -24461,7 +25021,7 @@ var init_specs2 = __esm({
24461
25021
  let detectedType = "spec";
24462
25022
  let rawId;
24463
25023
  try {
24464
- const raw = readYamlFile(file);
25024
+ const raw = readSpecFile(file);
24465
25025
  if (raw === null || typeof raw !== "object") {
24466
25026
  this.loaderIssues.push({
24467
25027
  severity: "error",
@@ -24944,7 +25504,7 @@ var init_specs2 = __esm({
24944
25504
  const p = this.paths.specsSystem();
24945
25505
  if (!pathExists(p)) return null;
24946
25506
  try {
24947
- const raw = readYamlFile(p);
25507
+ const raw = readSpecFile(p);
24948
25508
  return SystemSpecSchema.parse(raw);
24949
25509
  } catch (e) {
24950
25510
  this.loaderIssues.push({
@@ -24959,7 +25519,7 @@ var init_specs2 = __esm({
24959
25519
  saveSystemSpec(spec) {
24960
25520
  const p = this.paths.specsSystem();
24961
25521
  ensureDir(path23.dirname(p));
24962
- writeYamlFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
25522
+ writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
24963
25523
  invalidateSpecCache();
24964
25524
  }
24965
25525
  // -------------------------------------------------------------------------
@@ -24975,7 +25535,7 @@ var init_specs2 = __esm({
24975
25535
  const p = this.getSubsystemPath(id);
24976
25536
  if (!pathExists(p)) return null;
24977
25537
  try {
24978
- const raw = readYamlFile(p);
25538
+ const raw = readSpecFile(p);
24979
25539
  return SubsystemSpecSchema.parse(raw);
24980
25540
  } catch (e) {
24981
25541
  this.loaderIssues.push({
@@ -25109,7 +25669,7 @@ var init_specs2 = __esm({
25109
25669
  }
25110
25670
  }
25111
25671
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25112
- writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25672
+ writeSpecFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25113
25673
  invalidateSpecCache();
25114
25674
  }
25115
25675
  deleteSubsystemSpec(id) {
@@ -25133,7 +25693,7 @@ var init_specs2 = __esm({
25133
25693
  const p = this.getComponentPath(id);
25134
25694
  if (!pathExists(p)) return null;
25135
25695
  try {
25136
- const raw = readYamlFile(p);
25696
+ const raw = readSpecFile(p);
25137
25697
  return ComponentSpecSchema.parse(raw);
25138
25698
  } catch (e) {
25139
25699
  this.loaderIssues.push({
@@ -25170,7 +25730,7 @@ var init_specs2 = __esm({
25170
25730
  );
25171
25731
  }
25172
25732
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25173
- writeYamlFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25733
+ writeSpecFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25174
25734
  invalidateSpecCache();
25175
25735
  this.normalizeComponentLayout();
25176
25736
  return notices;
@@ -25231,7 +25791,7 @@ var init_specs2 = __esm({
25231
25791
  const p = this.getInterfacePath(id);
25232
25792
  if (!pathExists(p)) return null;
25233
25793
  try {
25234
- const raw = readYamlFile(p);
25794
+ const raw = readSpecFile(p);
25235
25795
  return InterfaceSpecSchema.parse(raw);
25236
25796
  } catch (e) {
25237
25797
  this.loaderIssues.push({
@@ -25262,7 +25822,7 @@ var init_specs2 = __esm({
25262
25822
  if (!pathExists(p)) return;
25263
25823
  let occupantId;
25264
25824
  try {
25265
- occupantId = readYamlFile(p)?.id;
25825
+ occupantId = readSpecFile(p)?.id;
25266
25826
  } catch {
25267
25827
  return;
25268
25828
  }
@@ -25299,7 +25859,7 @@ var init_specs2 = __esm({
25299
25859
  }
25300
25860
  }
25301
25861
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25302
- writeYamlFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25862
+ writeSpecFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25303
25863
  invalidateSpecCache();
25304
25864
  return notices;
25305
25865
  }
@@ -25324,7 +25884,7 @@ var init_specs2 = __esm({
25324
25884
  const p = this.getImplementationPath(id);
25325
25885
  if (!pathExists(p)) return null;
25326
25886
  try {
25327
- const raw = readYamlFile(p);
25887
+ const raw = readSpecFile(p);
25328
25888
  return ImplementationSpecSchema.parse(raw);
25329
25889
  } catch (e) {
25330
25890
  this.loaderIssues.push({
@@ -25356,7 +25916,7 @@ var init_specs2 = __esm({
25356
25916
  }
25357
25917
  }
25358
25918
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25359
- writeYamlFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25919
+ writeSpecFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25360
25920
  invalidateSpecCache();
25361
25921
  return notices;
25362
25922
  }
@@ -25409,7 +25969,7 @@ var init_specs2 = __esm({
25409
25969
  }
25410
25970
  }
25411
25971
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25412
- writeYamlFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25972
+ writeSpecFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25413
25973
  invalidateSpecCache();
25414
25974
  return notices;
25415
25975
  }
@@ -25440,7 +26000,7 @@ var init_specs2 = __esm({
25440
26000
  specToWrite.createdAt = existing.createdAt;
25441
26001
  }
25442
26002
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25443
- writeYamlFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
26003
+ writeSpecFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
25444
26004
  invalidateSpecCache();
25445
26005
  }
25446
26006
  deleteGroupSpec(id) {
@@ -25604,7 +26164,7 @@ var init_specs2 = __esm({
25604
26164
  findLegacySpecFiles() {
25605
26165
  const specsDir = this.paths.specsDir();
25606
26166
  if (!pathExists(specsDir)) return [];
25607
- const files = listFilesRecursive(specsDir, ".yaml");
26167
+ const files = listSpecFiles(specsDir);
25608
26168
  const legacy = [];
25609
26169
  for (const f of files) {
25610
26170
  const base = path23.basename(f);
@@ -28507,7 +29067,7 @@ function createMcpServer(options = {}) {
28507
29067
  inputSchema: {
28508
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)"),
28509
29069
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
28510
- 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.`),
28511
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.")
28512
29072
  },
28513
29073
  outputSchema: specChangeReportOutput
@@ -30551,6 +31111,20 @@ init_loader();
30551
31111
  init_errors();
30552
31112
  init_core();
30553
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
+ }
30554
31128
  function isCiDraftWaivable(issue2) {
30555
31129
  if (issue2.severity !== "warning") return false;
30556
31130
  if (issue2.code === "DRAFT_SUBSYSTEM_WARNING") return true;
@@ -30666,6 +31240,12 @@ async function runValidate(options = {}) {
30666
31240
  }
30667
31241
  }
30668
31242
  }
31243
+ const summary = carriedDebtSummary(projectConfig.rules?.conformance?.carried);
31244
+ if (summary) {
31245
+ logger.blank();
31246
+ logger.info(import_chalk7.default.yellow(summary.line));
31247
+ for (const note of summary.revisits) logger.info(import_chalk7.default.gray(note));
31248
+ }
30669
31249
  logger.blank();
30670
31250
  if (options.ci && waivedWarnings > 0) {
30671
31251
  logger.info(import_chalk7.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.`));