@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/index.js CHANGED
@@ -436,7 +436,7 @@ function activeTargetTypes(config) {
436
436
  const targets = config.targets;
437
437
  return targets.filter((t) => typeof t === "string" || t.enabled !== false).map((t) => typeof t === "string" ? t : t.type);
438
438
  }
439
- var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
439
+ var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
440
440
  var init_project = __esm({
441
441
  "src/models/project.ts"() {
442
442
  "use strict";
@@ -533,6 +533,57 @@ var init_project = __esm({
533
533
  */
534
534
  maxCognitiveLevel: import_zod3.z.string().optional()
535
535
  });
536
+ CarriedDebtKindSchema = import_zod3.z.enum(["drift", "undecided", "unreadable"]);
537
+ CarriedFindingSchema = import_zod3.z.object({
538
+ /** The issue code, which must be one a rule declares CARRYABLE — anything else is UNCARRYABLE_FINDING, an error. */
539
+ code: import_zod3.z.string(),
540
+ /** The spec the finding is anchored to. */
541
+ spec: import_zod3.z.string(),
542
+ /** The site inside that spec the finding names — for the code-vs-spec call checks, the contract method. */
543
+ at: import_zod3.z.string(),
544
+ /**
545
+ * The units an aggregating finding covers: the crossings of one
546
+ * UNDECLARED_COLOCATED_CALL, the steps of one CALL_STEP_UNREALIZED. Keyed by
547
+ * code + spec + site ALONE, a 24th crossing added to a finding that already
548
+ * lists 23 would be carried by an entry nobody wrote for it — so a live
549
+ * finding is carried only when every unit it reports is listed here, and a
550
+ * unit that appears is reported as new.
551
+ */
552
+ covers: import_zod3.z.array(import_zod3.z.string()).optional()
553
+ });
554
+ CarriedDebtSchema = import_zod3.z.object({
555
+ /** Which of the three kinds this reason is (see CarriedDebtKindSchema). */
556
+ kind: CarriedDebtKindSchema,
557
+ /** The reason itself, in the author's own words — what is true here, and what paying it would take. */
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(),
584
+ /** The findings this reason explains. */
585
+ findings: import_zod3.z.array(CarriedFindingSchema)
586
+ });
536
587
  ConformanceRuleConfigSchema = import_zod3.z.object({
537
588
  /**
538
589
  * Project-relative paths holding this project's own source code; a
@@ -555,7 +606,39 @@ var init_project = __esm({
555
606
  * that is now claimed, proven a barrel, or no longer found is
556
607
  * STALE_UNCLAIMED_ENTRY.
557
608
  */
558
- unclaimed: import_zod3.z.array(import_zod3.z.string()).optional()
609
+ unclaimed: import_zod3.z.array(import_zod3.z.string()).optional(),
610
+ /**
611
+ * The conformance findings this tree carries as declared debt, grouped by
612
+ * the reason that explains them — `unclaimed`'s shape, for findings about
613
+ * code a spec DOES claim.
614
+ *
615
+ * It is not a second lint.allow, and the difference is the claim each makes.
616
+ * An allow says "this finding is wrong here, by design", and is meant to
617
+ * live forever; an entry here says "this finding is RIGHT, and it is not
618
+ * paid yet". A mechanism that cannot tell those apart can never be asked how
619
+ * much the tree owes. Four properties keep it a register:
620
+ * • only a code a rule declares CARRYABLE may appear (UNCARRYABLE_FINDING,
621
+ * an error, which no allow and no --ci waiver can reach);
622
+ * • an entry carries a finding only when it lists EVERY unit that finding
623
+ * reports, so an aggregating finding cannot grow behind it;
624
+ * • an entry that stops applying — gone, or listing a unit no longer
625
+ * reported — is STALE_CARRIED_FINDING, so the register only shrinks;
626
+ * • every entry states its kind and its reason, and `wairon validate`
627
+ * prints the running total, so the debt is loud where a suppression is
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.
640
+ */
641
+ carried: import_zod3.z.array(CarriedDebtSchema).optional()
559
642
  });
560
643
  DesignDepthSchema = import_zod3.z.enum(["components", "interfaces", "implementations", "narratives"]);
561
644
  RulesConfigSchema = import_zod3.z.object({
@@ -1139,7 +1222,7 @@ var init_defaults = __esm({
1139
1222
  copilot: ".github/prompts",
1140
1223
  codex: ".codex/agents"
1141
1224
  };
1142
- WAIRON_VERSION = "5.1.1-dev.41";
1225
+ WAIRON_VERSION = "5.1.1-dev.43";
1143
1226
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
1144
1227
  ARCHITECT_AGENT_ID = "agent-architect";
1145
1228
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -1651,8 +1734,28 @@ var init_specs = __esm({
1651
1734
  LintAllowSchema = import_zod8.z.object({
1652
1735
  /** The issue code being allowed (see `wairon rules list`). */
1653
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(),
1654
1754
  /** Why this finding is acceptable here (e.g. "dispatcher — fan-out is the point"). */
1655
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"]
1656
1759
  });
1657
1760
  LintConfigSchema = import_zod8.z.object({
1658
1761
  allow: import_zod8.z.array(LintAllowSchema).default([])
@@ -2812,6 +2915,24 @@ function factsFor(model, sourcePath) {
2812
2915
  }
2813
2916
  return void 0;
2814
2917
  }
2918
+ function ownEntry(record, key) {
2919
+ return record && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
2920
+ }
2921
+ function importBindingOf(facts, name) {
2922
+ return ownEntry(facts.importBindings, name);
2923
+ }
2924
+ function fieldTypesOf(facts, field) {
2925
+ return ownEntry(facts.fieldTypes, field) ?? NO_FIELD_TYPES;
2926
+ }
2927
+ function typeBindingOf(facts, name) {
2928
+ return ownEntry(facts.typeOnlyBindings, name) ?? ownEntry(facts.importBindings, name)?.from;
2929
+ }
2930
+ function callSitesOf(facts, fn) {
2931
+ return ownEntry(facts.functionCallSites, fn);
2932
+ }
2933
+ function hasFunctionBody(facts, symbol) {
2934
+ return callSitesOf(facts, symbol) !== void 0;
2935
+ }
2815
2936
  function resolveImport(fromFile, specifier, knownPaths) {
2816
2937
  if (!specifier.startsWith(".")) return void 0;
2817
2938
  const joined = pathKey(path5.posix.normalize(path5.posix.join(path5.posix.dirname(fromFile), specifier)));
@@ -2830,11 +2951,12 @@ function resolveImport(fromFile, specifier, knownPaths) {
2830
2951
  }
2831
2952
  return void 0;
2832
2953
  }
2833
- var path5;
2954
+ var path5, NO_FIELD_TYPES;
2834
2955
  var init_code_model = __esm({
2835
2956
  "src/models/code-model.ts"() {
2836
2957
  "use strict";
2837
2958
  path5 = __toESM(require("path"));
2959
+ NO_FIELD_TYPES = [];
2838
2960
  }
2839
2961
  });
2840
2962
 
@@ -3198,6 +3320,26 @@ var init_lockfile = __esm({
3198
3320
  }
3199
3321
  });
3200
3322
 
3323
+ // src/core/spec-files.ts
3324
+ function readSpecFile(filePath) {
3325
+ return readYamlFile(filePath);
3326
+ }
3327
+ function writeSpecFile(filePath, document) {
3328
+ writeYamlFile(filePath, document);
3329
+ }
3330
+ function listSpecFiles(specsDir) {
3331
+ return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
3332
+ }
3333
+ var SPEC_FILE_EXTENSION;
3334
+ var init_spec_files = __esm({
3335
+ "src/core/spec-files.ts"() {
3336
+ "use strict";
3337
+ init_fs();
3338
+ init_yaml();
3339
+ SPEC_FILE_EXTENSION = ".yaml";
3340
+ }
3341
+ });
3342
+
3201
3343
  // src/core/narrative-labels.ts
3202
3344
  function resolveNarrativeLabels(methodName, steps) {
3203
3345
  const errors = [];
@@ -8863,13 +9005,84 @@ function buildCodeIndex(model) {
8863
9005
  cache.set(path27, built);
8864
9006
  return built;
8865
9007
  };
9008
+ const republished = /* @__PURE__ */ new Map();
9009
+ const republishedFrom = (path27) => {
9010
+ const cached = republished.get(path27);
9011
+ if (cached) return cached;
9012
+ const out = /* @__PURE__ */ new Set([path27]);
9013
+ const queue = [path27];
9014
+ while (queue.length) {
9015
+ const from = queue.pop();
9016
+ const f = facts.get(from);
9017
+ if (f?.status !== "analyzed") continue;
9018
+ for (const specifier of f.reexports) {
9019
+ const to = resolveImport(from, specifier, paths);
9020
+ if (to && !out.has(to)) {
9021
+ out.add(to);
9022
+ queue.push(to);
9023
+ }
9024
+ }
9025
+ }
9026
+ republished.set(path27, out);
9027
+ return out;
9028
+ };
9029
+ const NO_ORIGIN = /* @__PURE__ */ new Set();
9030
+ const landingFrom = (scope, specifier) => {
9031
+ const to = resolveImport(scope, specifier, paths);
9032
+ return to ? republishedFrom(to) : NO_ORIGIN;
9033
+ };
9034
+ const scopeOf = (site, from) => {
9035
+ const path27 = pathKey(site.from ?? from);
9036
+ const f = facts.get(path27);
9037
+ return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path27, facts: f } : void 0;
9038
+ };
9039
+ const originOf = (site, from) => {
9040
+ const resolved = scopeOf(site, from);
9041
+ if (!resolved) return NO_ORIGIN;
9042
+ const { path: scope, facts: f } = resolved;
9043
+ const landing = (specifier) => landingFrom(scope, specifier);
9044
+ if (!site.member) {
9045
+ const binding = importBindingOf(f, site.name);
9046
+ if (binding) return landing(binding.from);
9047
+ return declarationsAt(scope).has(site.name) ? republishedFrom(scope) : NO_ORIGIN;
9048
+ }
9049
+ if (!site.via) return NO_ORIGIN;
9050
+ const receiver = importBindingOf(f, site.via);
9051
+ if (!receiver) return NO_ORIGIN;
9052
+ if (receiver.namespace) return landing(receiver.from);
9053
+ const through = landing(receiver.from);
9054
+ for (const candidate of through) {
9055
+ if (declarationsAt(candidate).has(site.name)) return through;
9056
+ }
9057
+ return NO_ORIGIN;
9058
+ };
9059
+ const possibleOriginsOf = (site, from) => {
9060
+ const proven = originOf(site, from);
9061
+ if (!site.field && !site.constructed) return proven;
9062
+ const resolved = scopeOf(site, from);
9063
+ if (!resolved) return proven;
9064
+ const { path: scope, facts: f } = resolved;
9065
+ const out = new Set(proven);
9066
+ const widenThrough = (name, specifier) => {
9067
+ const landings = specifier !== void 0 ? landingFrom(scope, specifier) : declarationsAt(scope).has(name) ? republishedFrom(scope) : NO_ORIGIN;
9068
+ for (const candidate of landings) out.add(candidate);
9069
+ };
9070
+ if (site.field) {
9071
+ for (const typeName of fieldTypesOf(f, site.field)) widenThrough(typeName, typeBindingOf(f, typeName));
9072
+ }
9073
+ if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
9074
+ return out;
9075
+ };
9076
+ const declarationsAt = (path27) => namesAt(declarations, pathKey(path27), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
8866
9077
  return {
8867
9078
  paths,
8868
9079
  exactPaths,
8869
9080
  factsAt: (path27) => facts.get(pathKey(path27)),
8870
- declarationsAt: (path27) => namesAt(declarations, pathKey(path27), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames])),
9081
+ declarationsAt,
8871
9082
  anchorsAt: (path27) => namesAt(anchors, pathKey(path27), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
8872
- findingAnchorsAt: (path27) => namesAt(findingAnchors, pathKey(path27), (f) => new Set(f.anchoredNames))
9083
+ findingAnchorsAt: (path27) => namesAt(findingAnchors, pathKey(path27), (f) => new Set(f.anchoredNames)),
9084
+ originOf,
9085
+ possibleOriginsOf
8873
9086
  };
8874
9087
  }
8875
9088
  function buildRealizationIndex(ctx) {
@@ -9483,10 +9696,10 @@ var init_lint_allows = __esm({
9483
9696
  "use strict";
9484
9697
  lintAllowsRule = {
9485
9698
  name: "lint-allows",
9486
- 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.",
9487
9700
  codes: [
9488
9701
  { code: "UNKNOWN_LINT_ALLOW_CODE", defaultSeverity: "warning", summary: "lint.allow names an issue code no registered rule emits" },
9489
- { 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" }
9490
9703
  ],
9491
9704
  check(ctx) {
9492
9705
  for (const a of ctx.lintAllows) {
@@ -9499,14 +9712,24 @@ var init_lint_allows = __esm({
9499
9712
  );
9500
9713
  continue;
9501
9714
  }
9502
- if (!a.used) {
9503
- ctx.addIssue(
9504
- "warning",
9505
- "UNUSED_LINT_ALLOW",
9506
- `Spec "${a.specId}" allows "${a.code}" (reason: ${a.reason}) but no such finding fired this run \u2014 remove the stale allow.`,
9507
- a.specId
9508
- );
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";
9509
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
+ );
9510
9733
  }
9511
9734
  }
9512
9735
  };
@@ -13098,6 +13321,21 @@ var init_source_file_linkage = __esm({
13098
13321
  });
13099
13322
 
13100
13323
  // src/core/rules/conformance/method-realization.ts
13324
+ function bodyReachable(code, file, symbol) {
13325
+ const facts = code.factsAt(file);
13326
+ if (!facts) return true;
13327
+ const names = /* @__PURE__ */ new Set([symbol]);
13328
+ const binding = importBindingOf(facts, symbol);
13329
+ if (binding?.imported) names.add(binding.imported);
13330
+ const origins = code.originOf({ name: symbol, member: false }, file);
13331
+ if (origins.size === 0) return true;
13332
+ for (const origin of origins) {
13333
+ const at = code.factsAt(origin);
13334
+ if (!at || at.status !== "analyzed" || at.analysisGrade !== "exact") return true;
13335
+ for (const name of names) if (hasFunctionBody(at, name)) return true;
13336
+ }
13337
+ return false;
13338
+ }
13101
13339
  var methodRealizationRule;
13102
13340
  var init_method_realization = __esm({
13103
13341
  "src/core/rules/conformance/method-realization.ts"() {
@@ -13105,9 +13343,10 @@ var init_method_realization = __esm({
13105
13343
  init_models();
13106
13344
  methodRealizationRule = {
13107
13345
  name: "method-realization",
13108
- 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.",
13346
+ 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.",
13109
13347
  codes: [
13110
- { 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" }
13348
+ { 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" },
13349
+ { 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 }
13111
13350
  ],
13112
13351
  check(ctx) {
13113
13352
  const code = ctx.codeIndex();
@@ -13131,7 +13370,22 @@ var init_method_realization = __esm({
13131
13370
  const inDeclared = code.declarationsAt(file).has(symbol);
13132
13371
  const inAnchored = inDeclared || code.anchorsAt(file).has(symbol);
13133
13372
  const realized = tier === "declared" ? inDeclared : inAnchored;
13134
- if (realized) continue;
13373
+ if (realized) {
13374
+ if (!inDeclared || facts.analysisGrade !== "exact" || bodyReachable(code, file, symbol)) continue;
13375
+ const bodyLabel = methodImpl?.symbol ? `"${method.name}" (symbol "${symbol}")` : `"${method.name}"`;
13376
+ ctx.addIssue(
13377
+ "warning",
13378
+ "METHOD_BODY_NOT_FOUND",
13379
+ `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".`,
13380
+ impl.id,
13381
+ draft,
13382
+ void 0,
13383
+ // One method, one indivisible fact: nothing here aggregates, so
13384
+ // the site alone is the whole identity.
13385
+ { at: method.name }
13386
+ );
13387
+ continue;
13388
+ }
13135
13389
  const label = methodImpl?.symbol ? `"${method.name}" (symbol "${symbol}")` : `"${method.name}"`;
13136
13390
  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`." : "";
13137
13391
  ctx.addIssue(
@@ -13198,24 +13452,54 @@ var init_finding_realization = __esm({
13198
13452
  });
13199
13453
 
13200
13454
  // src/core/rules/conformance/call-conformance.ts
13201
- function ownEntry(record, key) {
13202
- return record && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
13203
- }
13204
- function closedCallees(facts, fn) {
13205
- const direct = ownEntry(facts.functionCalls, fn);
13455
+ function closedCallSites(code, file, fn, stopAt) {
13456
+ const facts = code.factsAt(file);
13457
+ const direct = facts && callSitesOf(facts, fn);
13206
13458
  if (!direct) return void 0;
13207
- const closed = new Set(direct);
13208
- const queue = [...direct];
13459
+ const here = pathKey(file);
13460
+ const out = [];
13461
+ const descended = /* @__PURE__ */ new Set([`${here}|${fn}`]);
13462
+ const queue = direct.map((s) => ({ ...s, from: s.from ?? here }));
13209
13463
  while (queue.length) {
13210
- const name = queue.pop();
13211
- for (const next of ownEntry(facts.functionCalls, name) ?? []) {
13212
- if (!closed.has(next)) {
13213
- closed.add(next);
13214
- queue.push(next);
13215
- }
13216
- }
13464
+ const site = queue.pop();
13465
+ out.push(site);
13466
+ if (stopAt?.has(site.name)) continue;
13467
+ const key = `${site.from}|${site.name}`;
13468
+ if (descended.has(key)) continue;
13469
+ descended.add(key);
13470
+ const scope = code.factsAt(site.from);
13471
+ const next = scope && callSitesOf(scope, site.name);
13472
+ if (!next) continue;
13473
+ queue.push(...next.map((s) => ({ ...s, from: s.from ?? site.from })));
13217
13474
  }
13218
- return closed;
13475
+ return out;
13476
+ }
13477
+ function resolveCallTarget(ctx, componentId, methodName) {
13478
+ const accepted = /* @__PURE__ */ new Set([methodName]);
13479
+ const files = /* @__PURE__ */ new Set();
13480
+ for (const intf of ctx.interfacesByComponent.get(componentId) ?? []) {
13481
+ for (const impl of ctx.implementationsByContract.get(intf.id) ?? []) {
13482
+ const method = impl.methods.find((m) => m.name === methodName);
13483
+ if (!method && !intf.methods.some((m) => m.name === methodName)) continue;
13484
+ if (method?.symbol) accepted.add(method.symbol);
13485
+ const file = methodSourceFile(method ?? {}, impl.sourcePath);
13486
+ if (file) files.add(pathKey(file));
13487
+ }
13488
+ }
13489
+ return { accepted, files };
13490
+ }
13491
+ function describeSite(site) {
13492
+ if (!site.member) return `${site.name}(\u2026)`;
13493
+ if (site.via) return `${site.via}.${site.name}(\u2026)`;
13494
+ if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
13495
+ if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
13496
+ return `<receiver>.${site.name}(\u2026)`;
13497
+ }
13498
+ function describeMiss(m) {
13499
+ const head = `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")}`;
13500
+ if (m.miss.kind === "elsewhere") return `${head}, called but resolved to ${m.miss.landed.map((p) => `"${p}"`).join(", ")})`;
13501
+ if (m.miss.kind === "unresolved") return `${head}, written as ${m.miss.shapes.map((s) => `\`${s}\``).join(" / ")})`;
13502
+ return `${head})`;
13219
13503
  }
13220
13504
  var callConformanceRule;
13221
13505
  var init_call_conformance = __esm({
@@ -13224,13 +13508,24 @@ var init_call_conformance = __esm({
13224
13508
  init_models();
13225
13509
  callConformanceRule = {
13226
13510
  name: "call-conformance",
13227
- 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.",
13511
+ 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.",
13228
13512
  codes: [
13229
- { 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)" }
13513
+ { 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 },
13514
+ { 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 },
13515
+ { 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 }
13230
13516
  ],
13231
13517
  check(ctx) {
13232
13518
  const code = ctx.codeIndex();
13233
- for (const entry of ctx.implementationMethods()) {
13519
+ const methods = ctx.implementationMethods();
13520
+ const modelledAt = /* @__PURE__ */ new Map();
13521
+ for (const entry of methods) {
13522
+ if (!entry.sourceFile) continue;
13523
+ const file = pathKey(entry.sourceFile);
13524
+ const byName = modelledAt.get(file) ?? /* @__PURE__ */ new Map();
13525
+ byName.set(entry.method.symbol ?? entry.method.name, { component: entry.component.id, method: entry.method.name });
13526
+ modelledAt.set(file, byName);
13527
+ }
13528
+ for (const entry of methods) {
13234
13529
  const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
13235
13530
  const tier = implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component);
13236
13531
  if (tier === "off") continue;
@@ -13238,37 +13533,91 @@ var init_call_conformance = __esm({
13238
13533
  if (!file) continue;
13239
13534
  const facts = code.factsAt(file);
13240
13535
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
13536
+ const here = pathKey(file);
13241
13537
  const fnSymbol = implMethod.symbol ?? implMethod.name;
13242
- const callees = closedCallees(facts, fnSymbol);
13243
- if (!callees) continue;
13244
- const missing = [];
13538
+ const colocated = modelledAt.get(here) ?? /* @__PURE__ */ new Map();
13539
+ const sites = closedCallSites(code, file, fnSymbol);
13540
+ if (!sites) continue;
13541
+ const missed = [];
13245
13542
  for (const step of implMethod.narrative) {
13246
13543
  if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
13247
13544
  if (!ctx.componentMap.has(step.targetComponent)) continue;
13248
- const accepted = /* @__PURE__ */ new Set([step.targetMethod]);
13249
- for (const targetIntf of ctx.interfacesByComponent.get(step.targetComponent) ?? []) {
13250
- for (const targetImpl of ctx.implementationsByContract.get(targetIntf.id) ?? []) {
13251
- const targetMethod = targetImpl.methods.find((m) => m.name === step.targetMethod);
13252
- if (targetMethod?.symbol) accepted.add(targetMethod.symbol);
13545
+ const target = resolveCallTarget(ctx, step.targetComponent, step.targetMethod);
13546
+ if (target.accepted.has(fnSymbol)) continue;
13547
+ const matching = sites.filter((s) => target.accepted.has(s.name));
13548
+ const ref = `${step.targetComponent}.${step.targetMethod}`;
13549
+ if (target.files.size === 0) {
13550
+ if (matching.length === 0) {
13551
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss: { kind: "absent" } });
13253
13552
  }
13553
+ continue;
13254
13554
  }
13255
- if (accepted.has(fnSymbol)) continue;
13256
- if ([...accepted].some((name) => callees.has(name))) continue;
13257
- missing.push({
13258
- step: step.stepNumber,
13259
- target: `${step.targetComponent}.${step.targetMethod}`,
13260
- accepted: [...accepted]
13261
- });
13555
+ const landed = /* @__PURE__ */ new Set();
13556
+ let realized = false;
13557
+ for (const site of matching) {
13558
+ for (const origin of code.originOf(site, file)) landed.add(origin);
13559
+ for (const origin of code.possibleOriginsOf(site, file)) {
13560
+ if (target.files.has(origin)) realized = true;
13561
+ }
13562
+ }
13563
+ if (realized) continue;
13564
+ const miss = matching.length === 0 ? { kind: "absent" } : landed.size === 0 ? { kind: "unresolved", shapes: [...new Set(matching.map(describeSite))].sort() } : { kind: "elsewhere", landed: [...landed].sort() };
13565
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss });
13566
+ }
13567
+ const unrealized = missed.filter((m) => m.miss.kind !== "unresolved");
13568
+ const unresolved = missed.filter((m) => m.miss.kind === "unresolved");
13569
+ if (unrealized.length) {
13570
+ ctx.addIssue(
13571
+ "warning",
13572
+ "CALL_STEP_UNREALIZED",
13573
+ `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.`,
13574
+ impl.id,
13575
+ entry.draftContext,
13576
+ void 0,
13577
+ // A step, named by its number and its target: what the register
13578
+ // carries is ONE unresolved step, never "this method's call steps".
13579
+ { at: implMethod.name, covers: unrealized.map((m) => `${m.step}:${m.target}`) }
13580
+ );
13581
+ }
13582
+ if (unresolved.length) {
13583
+ ctx.addIssue(
13584
+ "warning",
13585
+ "CALL_ORIGIN_UNRESOLVED",
13586
+ `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.`,
13587
+ impl.id,
13588
+ entry.draftContext,
13589
+ void 0,
13590
+ { at: implMethod.name, covers: unresolved.map((m) => `${m.step}:${m.target}`) }
13591
+ );
13592
+ }
13593
+ const declared = /* @__PURE__ */ new Set();
13594
+ for (const step of implMethod.narrative) {
13595
+ if (step.targetComponent && step.targetMethod) declared.add(`${step.targetComponent}.${step.targetMethod}`);
13596
+ }
13597
+ const crossings = /* @__PURE__ */ new Map();
13598
+ for (const site of closedCallSites(code, file, fnSymbol, new Set(colocated.keys())) ?? []) {
13599
+ const owner = colocated.get(site.name);
13600
+ if (!owner || owner.component === component.id) continue;
13601
+ const ref = `${owner.component}.${owner.method}`;
13602
+ if (declared.has(ref) || crossings.has(ref)) continue;
13603
+ if (!code.originOf(site, file).has(here)) continue;
13604
+ crossings.set(ref, site.name);
13605
+ }
13606
+ if (crossings.size) {
13607
+ const detail = [...crossings].map(([ref, name]) => `"${name}" (${ref})`).join("; ");
13608
+ ctx.addIssue(
13609
+ "warning",
13610
+ "UNDECLARED_COLOCATED_CALL",
13611
+ `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.`,
13612
+ impl.id,
13613
+ entry.draftContext,
13614
+ void 0,
13615
+ // The crossings themselves. Keyed by method alone, a 24th crossing
13616
+ // would ride into a register entry written for 23 - so each one is
13617
+ // named, and one nobody carried fires on the day it appears.
13618
+ { at: implMethod.name, covers: [...crossings.keys()] }
13619
+ );
13262
13620
  }
13263
- if (missing.length === 0) continue;
13264
- const detail = missing.map((m) => `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")})`).join("; ");
13265
- ctx.addIssue(
13266
- "warning",
13267
- "CALL_STEP_UNREALIZED",
13268
- `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.`,
13269
- impl.id,
13270
- entry.draftContext
13271
- );
13272
13621
  }
13273
13622
  }
13274
13623
  };
@@ -13399,7 +13748,13 @@ var init_dependency_conformance = __esm({
13399
13748
  "UNDECLARED_DEPENDENCY",
13400
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.`,
13401
13750
  realization.implementationsAt(fromPath)[0]?.id,
13402
- 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}` }
13403
13758
  );
13404
13759
  }
13405
13760
  }
@@ -13426,7 +13781,12 @@ var init_dependency_conformance = __esm({
13426
13781
  "UNREALIZED_DEPENDENCY",
13427
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.`,
13428
13783
  impls[0]?.id ?? component.id,
13429
- 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}` }
13430
13790
  );
13431
13791
  }
13432
13792
  }
@@ -13778,6 +14138,69 @@ var init_unclaimed_source = __esm({
13778
14138
  }
13779
14139
  });
13780
14140
 
14141
+ // src/core/rules/conformance/carried-debt.ts
14142
+ var carriedDebtRule;
14143
+ var init_carried_debt = __esm({
14144
+ "src/core/rules/conformance/carried-debt.ts"() {
14145
+ "use strict";
14146
+ carriedDebtRule = {
14147
+ name: "carried-debt",
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.",
14149
+ codes: [
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" },
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" }
14152
+ ],
14153
+ check(ctx) {
14154
+ const carried = ctx.carriedFindings;
14155
+ if (carried.length === 0) return;
14156
+ for (const entry of carried) {
14157
+ if (ctx.carryableIssueCodes.has(entry.code)) continue;
14158
+ 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";
14159
+ ctx.addIssue(
14160
+ "error",
14161
+ "UNCARRYABLE_FINDING",
14162
+ `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.`
14163
+ );
14164
+ }
14165
+ if (ctx.codeModel.files.length === 0) return;
14166
+ const known = new Set(ctx.specIds().map((s) => s.id));
14167
+ const addressed = /* @__PURE__ */ new Set();
14168
+ for (const entry of carried) {
14169
+ if (!ctx.carryableIssueCodes.has(entry.code)) continue;
14170
+ if (ctx.rules?.sddRuleSeverity?.[entry.code] === "off") continue;
14171
+ if (!ctx.isSpecInScope(entry.spec)) continue;
14172
+ const key = `${entry.spec}|${entry.code}|${entry.at}`;
14173
+ if (addressed.has(key)) {
14174
+ ctx.addIssue(
14175
+ "warning",
14176
+ "STALE_CARRIED_FINDING",
14177
+ `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.`
14178
+ );
14179
+ continue;
14180
+ }
14181
+ addressed.add(key);
14182
+ if (!entry.fired) {
14183
+ 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}"`;
14184
+ ctx.addIssue(
14185
+ "warning",
14186
+ "STALE_CARRIED_FINDING",
14187
+ `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.`
14188
+ );
14189
+ continue;
14190
+ }
14191
+ const paid = entry.covers.filter((unit) => !entry.seen.has(unit));
14192
+ if (paid.length === 0) continue;
14193
+ ctx.addIssue(
14194
+ "warning",
14195
+ "STALE_CARRIED_FINDING",
14196
+ `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.`
14197
+ );
14198
+ }
14199
+ }
14200
+ };
14201
+ }
14202
+ });
14203
+
13781
14204
  // src/core/rules/heuristic/coupling-health.ts
13782
14205
  var DEFAULT_GOD_COMPONENT_THRESHOLD, couplingRule;
13783
14206
  var init_coupling_health = __esm({
@@ -14932,8 +15355,8 @@ function registerPackRules(packRules) {
14932
15355
  for (const rule of packRules) addRule(rule);
14933
15356
  }
14934
15357
  function ruleSequence() {
14935
- const base = ruleSet.filter((r) => r !== lintAllowsRule);
14936
- return ruleSet.includes(lintAllowsRule) ? [...base, lintAllowsRule] : base;
15358
+ const audits = [carriedDebtRule, lintAllowsRule].filter((r) => ruleSet.includes(r));
15359
+ return [...ruleSet.filter((r) => !audits.includes(r)), ...audits];
14937
15360
  }
14938
15361
  function specScopedRules() {
14939
15362
  return ruleSequence().filter((r) => r.scope === "spec");
@@ -15023,6 +15446,7 @@ var init_repository = __esm({
15023
15446
  init_integration_sim_coverage();
15024
15447
  init_type_realization();
15025
15448
  init_unclaimed_source();
15449
+ init_carried_debt();
15026
15450
  init_coupling_health();
15027
15451
  init_signature_language_builtins();
15028
15452
  init_narrative_language_constructs();
@@ -15214,8 +15638,11 @@ var init_repository = __esm({
15214
15638
  // elsewhere?) rather than spec content.
15215
15639
  packResolutionRule,
15216
15640
  reproducibilityRule,
15217
- // MUST run last: it audits which lint.allow entries the earlier rules
15218
- // actually consumed (stale/unknown allows).
15641
+ // MUST run last, in this order: each audits what the earlier rules did
15642
+ // with a declared exception. The debt register first (which carried
15643
+ // findings the conformance family actually matched), then the allows
15644
+ // (which suppressions any rule actually consumed).
15645
+ carriedDebtRule,
15219
15646
  lintAllowsRule
15220
15647
  ];
15221
15648
  ruleSet = [];
@@ -15338,9 +15765,15 @@ function walkExact(ts, sourceText, fileName) {
15338
15765
  const imports = /* @__PURE__ */ new Set();
15339
15766
  const reexports = /* @__PURE__ */ new Set();
15340
15767
  const starExports = [];
15768
+ const namedReexports = [];
15341
15769
  const complexity = /* @__PURE__ */ new Map();
15342
15770
  const calls = /* @__PURE__ */ new Map();
15771
+ const importBindings = /* @__PURE__ */ new Map();
15772
+ const typeOnlyBindings = /* @__PURE__ */ new Map();
15773
+ const fieldTypes = /* @__PURE__ */ new Map();
15343
15774
  const mutableBindings = /* @__PURE__ */ new Set();
15775
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : "");
15776
+ const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
15344
15777
  const addBindingNames = (name) => {
15345
15778
  if (ts.isIdentifier(name)) declared.add(name.text);
15346
15779
  else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
@@ -15354,6 +15787,17 @@ function walkExact(ts, sourceText, fileName) {
15354
15787
  if (ts.isPrivateIdentifier(name)) return name.text;
15355
15788
  return void 0;
15356
15789
  };
15790
+ const typeReferenceName = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : void 0;
15791
+ 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);
15792
+ const recordFieldType = (name, type) => {
15793
+ const typeName = typeReferenceName(type);
15794
+ if (!typeName) return;
15795
+ const field = propertyNameText(name);
15796
+ if (!field) return;
15797
+ const named = fieldTypes.get(field) ?? /* @__PURE__ */ new Set();
15798
+ named.add(typeName);
15799
+ fieldTypes.set(field, named);
15800
+ };
15357
15801
  const hasExportModifier = (node) => {
15358
15802
  const mods = node.modifiers;
15359
15803
  return !!mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
@@ -15376,7 +15820,10 @@ function walkExact(ts, sourceText, fileName) {
15376
15820
  };
15377
15821
  const collectFunctionFacts = (fn) => {
15378
15822
  let score = 1;
15379
- const callees = /* @__PURE__ */ new Set();
15823
+ const callees = /* @__PURE__ */ new Map();
15824
+ const addSite = (site) => {
15825
+ callees.set(siteKey(site), site);
15826
+ };
15380
15827
  const count = (node) => {
15381
15828
  if (namedFunctionName(node) !== void 0) return;
15382
15829
  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)) {
@@ -15388,8 +15835,17 @@ function walkExact(ts, sourceText, fileName) {
15388
15835
  }
15389
15836
  } else if (ts.isCallExpression(node)) {
15390
15837
  const callee = node.expression;
15391
- if (ts.isIdentifier(callee)) callees.add(callee.text);
15392
- else if (ts.isPropertyAccessExpression(callee)) callees.add(callee.name.text);
15838
+ if (ts.isIdentifier(callee)) addSite({ name: callee.text, member: false });
15839
+ else if (ts.isPropertyAccessExpression(callee)) {
15840
+ const receiver = callee.expression;
15841
+ const name = callee.name.text;
15842
+ if (ts.isIdentifier(receiver)) addSite({ name, member: true, via: receiver.text });
15843
+ else if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
15844
+ addSite({ name, member: true, field: receiver.name.text });
15845
+ } else if (ts.isNewExpression(receiver) && ts.isIdentifier(receiver.expression)) {
15846
+ addSite({ name, member: true, constructed: receiver.expression.text });
15847
+ } else addSite({ name, member: true });
15848
+ }
15393
15849
  }
15394
15850
  ts.forEachChild(node, count);
15395
15851
  };
@@ -15401,9 +15857,13 @@ function walkExact(ts, sourceText, fileName) {
15401
15857
  if (fnName && node.body) {
15402
15858
  const facts = collectFunctionFacts(node);
15403
15859
  complexity.set(fnName, Math.max(complexity.get(fnName) ?? 0, facts.score));
15404
- const set = calls.get(fnName) ?? /* @__PURE__ */ new Set();
15405
- for (const c of facts.callees) set.add(c);
15406
- calls.set(fnName, set);
15860
+ const sites = calls.get(fnName) ?? /* @__PURE__ */ new Map();
15861
+ for (const [key, site] of facts.callees) sites.set(key, site);
15862
+ calls.set(fnName, sites);
15863
+ }
15864
+ if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
15865
+ else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
15866
+ recordFieldType(node.name, node.type);
15407
15867
  }
15408
15868
  if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
15409
15869
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : void 0;
@@ -15430,11 +15890,24 @@ function walkExact(ts, sourceText, fileName) {
15430
15890
  } else if (ts.isImportDeclaration(node)) {
15431
15891
  const clause = node.importClause;
15432
15892
  const typeOnly = clause?.isTypeOnly ?? false;
15433
- if (!typeOnly && ts.isStringLiteral(node.moduleSpecifier)) imports.add(node.moduleSpecifier.text);
15434
- if (clause?.name) declared.add(clause.name.text);
15893
+ const spec = ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
15894
+ if (!typeOnly && spec) imports.add(spec);
15895
+ const bind = (name, namespace, imported, elementTypeOnly = false) => {
15896
+ declared.add(name);
15897
+ if (!spec) return;
15898
+ if (typeOnly || elementTypeOnly) {
15899
+ typeOnlyBindings.set(name, spec);
15900
+ return;
15901
+ }
15902
+ const fact = { from: spec };
15903
+ if (namespace) fact.namespace = true;
15904
+ if (imported && imported !== name) fact.imported = imported;
15905
+ importBindings.set(name, fact);
15906
+ };
15907
+ if (clause?.name) bind(clause.name.text, false);
15435
15908
  if (clause?.namedBindings) {
15436
- if (ts.isNamespaceImport(clause.namedBindings)) declared.add(clause.namedBindings.name.text);
15437
- else for (const el of clause.namedBindings.elements) declared.add(el.name.text);
15909
+ if (ts.isNamespaceImport(clause.namedBindings)) bind(clause.namedBindings.name.text, true);
15910
+ else for (const el of clause.namedBindings.elements) bind(el.name.text, false, el.propertyName?.text, el.isTypeOnly);
15438
15911
  }
15439
15912
  } else if (ts.isExportDeclaration(node)) {
15440
15913
  const spec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
@@ -15443,6 +15916,9 @@ function walkExact(ts, sourceText, fileName) {
15443
15916
  for (const el of node.exportClause.elements) {
15444
15917
  declared.add(el.name.text);
15445
15918
  exported.add(el.name.text);
15919
+ if (spec && !node.isTypeOnly && !el.isTypeOnly) {
15920
+ namedReexports.push({ exported: el.name.text, local: (el.propertyName ?? el.name).text, from: spec });
15921
+ }
15446
15922
  }
15447
15923
  } else if (!node.exportClause && spec) {
15448
15924
  starExports.push(spec);
@@ -15465,7 +15941,7 @@ function walkExact(ts, sourceText, fileName) {
15465
15941
  };
15466
15942
  visit(sf);
15467
15943
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
15468
- return { declared, anchors, exported, imports, reexports, starExports, complexity, calls, mutableBindings, reexportOnly };
15944
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, importBindings, typeOnlyBindings, fieldTypes, mutableBindings, reexportOnly };
15469
15945
  }
15470
15946
  function resolveRelativeModule(fromFile, specifier) {
15471
15947
  if (!specifier.startsWith(".")) return null;
@@ -15488,34 +15964,47 @@ function resolveRelativeModule(fromFile, specifier) {
15488
15964
  }
15489
15965
  return null;
15490
15966
  }
15491
- function chaseStarExports(ts, facts, filePath, projectRoot, visited, exactCache) {
15492
- for (const spec of facts.starExports) {
15493
- const target = resolveRelativeModule(filePath, spec);
15494
- if (!target || visited.has(target)) continue;
15495
- if (path11.relative(projectRoot, target).startsWith("..")) continue;
15496
- visited.add(target);
15497
- let targetFacts = exactCache.get(target);
15498
- if (targetFacts === void 0) {
15499
- try {
15500
- targetFacts = walkExact(ts, fs8.readFileSync(target, "utf8"), target);
15501
- } catch {
15502
- targetFacts = null;
15503
- }
15504
- exactCache.set(target, targetFacts);
15967
+ function reexportTarget(ts, filePath, specifier, projectRoot, visited, exactCache) {
15968
+ const target = resolveRelativeModule(filePath, specifier);
15969
+ if (!target || path11.relative(projectRoot, target).startsWith("..")) return null;
15970
+ let targetFacts = exactCache.get(target);
15971
+ if (targetFacts === void 0) {
15972
+ try {
15973
+ targetFacts = walkExact(ts, fs8.readFileSync(target, "utf8"), target);
15974
+ } catch {
15975
+ targetFacts = null;
15505
15976
  }
15506
- if (!targetFacts) continue;
15507
- chaseStarExports(ts, targetFacts, target, projectRoot, visited, exactCache);
15508
- for (const name of targetFacts.exported) {
15977
+ exactCache.set(target, targetFacts);
15978
+ }
15979
+ if (!targetFacts) return null;
15980
+ if (!visited.has(target)) {
15981
+ visited.add(target);
15982
+ chaseReexports(ts, targetFacts, target, projectRoot, visited, exactCache);
15983
+ }
15984
+ return { path: target, facts: targetFacts };
15985
+ }
15986
+ function carryBody(facts, target, targetPath, local, exported, projectRoot) {
15987
+ const score = target.complexity.get(local);
15988
+ if (score !== void 0) facts.complexity.set(exported, Math.max(facts.complexity.get(exported) ?? 0, score));
15989
+ const targetCalls = target.calls.get(local);
15990
+ if (!targetCalls) return;
15991
+ const carriedFrom = pathKey(path11.relative(projectRoot, targetPath));
15992
+ const sites = facts.calls.get(exported) ?? /* @__PURE__ */ new Map();
15993
+ for (const [key, site] of targetCalls) sites.set(carriedFrom + "|" + key, { ...site, from: site.from ?? carriedFrom });
15994
+ facts.calls.set(exported, sites);
15995
+ }
15996
+ function chaseReexports(ts, facts, filePath, projectRoot, visited, exactCache) {
15997
+ for (const re of facts.namedReexports) {
15998
+ const target = reexportTarget(ts, filePath, re.from, projectRoot, visited, exactCache);
15999
+ if (target) carryBody(facts, target.facts, target.path, re.local, re.exported, projectRoot);
16000
+ }
16001
+ for (const spec of facts.starExports) {
16002
+ const target = reexportTarget(ts, filePath, spec, projectRoot, visited, exactCache);
16003
+ if (!target) continue;
16004
+ for (const name of target.facts.exported) {
15509
16005
  facts.exported.add(name);
15510
16006
  facts.declared.add(name);
15511
- const c = targetFacts.complexity.get(name);
15512
- if (c !== void 0) facts.complexity.set(name, Math.max(facts.complexity.get(name) ?? 0, c));
15513
- const targetCalls = targetFacts.calls.get(name);
15514
- if (targetCalls) {
15515
- const set = facts.calls.get(name) ?? /* @__PURE__ */ new Set();
15516
- for (const callee of targetCalls) set.add(callee);
15517
- facts.calls.set(name, set);
15518
- }
16007
+ carryBody(facts, target.facts, target.path, name, name, projectRoot);
15519
16008
  }
15520
16009
  }
15521
16010
  }
@@ -15620,7 +16109,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
15620
16109
  if (ts) {
15621
16110
  try {
15622
16111
  const facts = walkExact(ts, text, absolute);
15623
- chaseStarExports(ts, facts, absolute, projectRoot, /* @__PURE__ */ new Set([absolute]), exactCache);
16112
+ chaseReexports(ts, facts, absolute, projectRoot, /* @__PURE__ */ new Set([absolute]), exactCache);
15624
16113
  analyzed = {
15625
16114
  analysisGrade: "exact",
15626
16115
  declaredNames: [...facts.declared],
@@ -15629,7 +16118,10 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
15629
16118
  imports: [...facts.imports],
15630
16119
  reexports: [...facts.reexports],
15631
16120
  functionComplexity: Object.fromEntries(facts.complexity),
15632
- functionCalls: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v]])),
16121
+ functionCallSites: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v.values()]])),
16122
+ importBindings: Object.fromEntries(facts.importBindings),
16123
+ typeOnlyBindings: Object.fromEntries(facts.typeOnlyBindings),
16124
+ fieldTypes: Object.fromEntries([...facts.fieldTypes].map(([k, v]) => [k, [...v]])),
15633
16125
  topLevelMutableBindings: [...facts.mutableBindings],
15634
16126
  reexportOnly: facts.reexportOnly
15635
16127
  };
@@ -16031,18 +16523,46 @@ function buildRuleContext(opts) {
16031
16523
  const allowLookup = /* @__PURE__ */ new Map();
16032
16524
  const collectAllows = (specId, lint) => {
16033
16525
  for (const a of lint?.allow ?? []) {
16034
- 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 };
16035
16527
  lintAllows.push(entry);
16036
16528
  if (!allowLookup.has(specId)) allowLookup.set(specId, /* @__PURE__ */ new Map());
16037
- 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);
16038
16533
  }
16039
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
+ };
16040
16540
  for (const s of subsystems) collectAllows(s.id, s.lint);
16041
16541
  for (const c of components) collectAllows(c.id, c.lint);
16042
16542
  for (const i of interfaces) collectAllows(i.id, i.lint);
16043
16543
  for (const im of implementations) collectAllows(im.id, im.lint);
16044
16544
  for (const t of types) collectAllows(t.id, t.lint);
16045
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
16545
+ const carriedFindings = [];
16546
+ const carriedLookup = /* @__PURE__ */ new Map();
16547
+ const carriedKey = (spec, code, at) => `${spec}\0${code}\0${at}`;
16548
+ for (const group of rules?.conformance?.carried ?? []) {
16549
+ for (const f of group.findings ?? []) {
16550
+ const entry = {
16551
+ kind: group.kind,
16552
+ why: group.why,
16553
+ code: f.code,
16554
+ spec: f.spec,
16555
+ at: f.at,
16556
+ covers: f.covers ?? [],
16557
+ fired: false,
16558
+ seen: /* @__PURE__ */ new Set()
16559
+ };
16560
+ carriedFindings.push(entry);
16561
+ const key = carriedKey(f.spec, f.code, f.at);
16562
+ if (!carriedLookup.has(key)) carriedLookup.set(key, entry);
16563
+ }
16564
+ }
16565
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved, parts) => {
16046
16566
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
16047
16567
  return;
16048
16568
  }
@@ -16053,17 +16573,46 @@ function buildRuleContext(opts) {
16053
16573
  }
16054
16574
  const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
16055
16575
  if (severity === "off") return;
16576
+ let text = message;
16056
16577
  if (specId) {
16057
- const allow = allowLookup.get(specId)?.get(code);
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;
16585
+ if (specId) {
16586
+ const allow = (allowLookup.get(specId)?.get(code) ?? []).find((a) => parts ? a.at === parts.at : a.at === void 0);
16058
16587
  if (allow) {
16059
16588
  allow.used = true;
16060
- 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
+ }
16596
+ }
16597
+ }
16598
+ if (specId && parts && severity === "warning" && !allowClaimed) {
16599
+ const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
16600
+ if (entry) {
16601
+ entry.fired = true;
16602
+ const listed = new Set(entry.covers);
16603
+ const grew = [];
16604
+ for (const unit of parts.covers ?? []) {
16605
+ if (listed.has(unit)) entry.seen.add(unit);
16606
+ else grew.push(unit);
16607
+ }
16608
+ if (grew.length === 0) return;
16609
+ text = `${text} 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.`;
16061
16610
  }
16062
16611
  }
16063
16612
  issues.push({
16064
16613
  severity,
16065
16614
  code,
16066
- message,
16615
+ message: text,
16067
16616
  specId,
16068
16617
  ...isDraftContext ? { draftContext: true } : {},
16069
16618
  ...surfaceResolved ? { surfaceResolved: true } : {}
@@ -16139,7 +16688,10 @@ function buildRuleContext(opts) {
16139
16688
  codeModel: opts.codeModel ?? emptyCodeModel(),
16140
16689
  roundTripIssues: opts.roundTripIssues,
16141
16690
  lintAllows,
16691
+ sitesReported,
16142
16692
  knownIssueCodes: opts.knownIssueCodes,
16693
+ carriedFindings,
16694
+ carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
16143
16695
  addIssue
16144
16696
  };
16145
16697
  return ctx;
@@ -17556,6 +18108,9 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
17556
18108
  ...knownIssueCodes().map((rc) => rc.code),
17557
18109
  ...extensions.assertions.map((a) => a.fullCode)
17558
18110
  ]);
18111
+ const carryableCodes = new Set(
18112
+ knownIssueCodes().filter((rc) => rc.carryable).map((rc) => rc.code)
18113
+ );
17559
18114
  const ctx = buildRuleContext({
17560
18115
  system,
17561
18116
  subsystems,
@@ -17575,6 +18130,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
17575
18130
  codeModel,
17576
18131
  roundTripIssues,
17577
18132
  knownIssueCodes: knownCodes,
18133
+ carryableIssueCodes: carryableCodes,
17578
18134
  issues
17579
18135
  });
17580
18136
  for (const rule of sequence) {
@@ -18435,10 +18991,10 @@ function findChainingParent(childRoot, ceiling) {
18435
18991
  if (bound2 && !isWithin(bound2, dir)) break;
18436
18992
  const specsDir = aiPathsAt(dir).specsDir();
18437
18993
  if (pathExists(specsDir)) {
18438
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
18994
+ for (const file of listSpecFiles(specsDir)) {
18439
18995
  let raw;
18440
18996
  try {
18441
- raw = readYamlFile(file);
18997
+ raw = readSpecFile(file);
18442
18998
  } catch {
18443
18999
  continue;
18444
19000
  }
@@ -18470,10 +19026,10 @@ function inspectChainedRoots(rootDir = getProjectRoot()) {
18470
19026
  const walk2 = (projectDir, prefix, ancestors, depth) => {
18471
19027
  const specsDir = aiPathsAt(projectDir).specsDir();
18472
19028
  if (!pathExists(specsDir)) return;
18473
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
19029
+ for (const file of listSpecFiles(specsDir)) {
18474
19030
  let raw;
18475
19031
  try {
18476
- raw = readYamlFile(file);
19032
+ raw = readSpecFile(file);
18477
19033
  } catch {
18478
19034
  continue;
18479
19035
  }
@@ -18657,7 +19213,7 @@ function computeSpecTreeSignature(dirs) {
18657
19213
  parts.push(`${dir}:missing`);
18658
19214
  continue;
18659
19215
  }
18660
- for (const f of listFilesRecursive(dir, ".yaml")) {
19216
+ for (const f of listSpecFiles(dir)) {
18661
19217
  try {
18662
19218
  const st = fs11.statSync(f);
18663
19219
  parts.push(`${f}:${st.mtimeMs}:${st.size}`);
@@ -18687,9 +19243,11 @@ function identityKeyOf(field, item) {
18687
19243
  return `${String(o.topic)} ${o.event === void 0 ? "" : String(o.event)}`;
18688
19244
  case "trustedLinks":
18689
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.
18690
19249
  case "allow":
18691
- return str(o.code);
18692
- // lint.allow
19250
+ return `${String(o.code)} ${o.at === void 0 ? "" : String(o.at)}`;
18693
19251
  case "findings":
18694
19252
  return str(o.code);
18695
19253
  // an interface method's findings
@@ -18777,6 +19335,7 @@ function identityFieldsOf(field) {
18777
19335
  case "trustedLinks":
18778
19336
  return ["subsystem"];
18779
19337
  case "allow":
19338
+ return ["code", "at"];
18780
19339
  case "findings":
18781
19340
  return ["code"];
18782
19341
  case "invariants":
@@ -19117,6 +19676,7 @@ var init_specs2 = __esm({
19117
19676
  init_canonical_json();
19118
19677
  init_lockfile();
19119
19678
  init_yaml();
19679
+ init_spec_files();
19120
19680
  init_models();
19121
19681
  init_narrative_labels();
19122
19682
  init_diagram();
@@ -19195,7 +19755,7 @@ var init_specs2 = __esm({
19195
19755
  const specsDir = projectPaths.specsDir();
19196
19756
  this.scanVisitedSpecDirs.push(specsDir);
19197
19757
  if (!pathExists(specsDir)) return index;
19198
- const files = listFilesRecursive(specsDir, ".yaml");
19758
+ const files = listSpecFiles(specsDir);
19199
19759
  const systemYaml = path16.normalize(projectPaths.specsSystem());
19200
19760
  const localSubprojects = [];
19201
19761
  for (const file of files) {
@@ -19204,7 +19764,7 @@ var init_specs2 = __esm({
19204
19764
  let detectedType = "spec";
19205
19765
  let rawId;
19206
19766
  try {
19207
- const raw = readYamlFile(file);
19767
+ const raw = readSpecFile(file);
19208
19768
  if (raw === null || typeof raw !== "object") {
19209
19769
  this.loaderIssues.push({
19210
19770
  severity: "error",
@@ -19687,7 +20247,7 @@ var init_specs2 = __esm({
19687
20247
  const p = this.paths.specsSystem();
19688
20248
  if (!pathExists(p)) return null;
19689
20249
  try {
19690
- const raw = readYamlFile(p);
20250
+ const raw = readSpecFile(p);
19691
20251
  return SystemSpecSchema.parse(raw);
19692
20252
  } catch (e) {
19693
20253
  this.loaderIssues.push({
@@ -19702,7 +20262,7 @@ var init_specs2 = __esm({
19702
20262
  saveSystemSpec(spec) {
19703
20263
  const p = this.paths.specsSystem();
19704
20264
  ensureDir(path16.dirname(p));
19705
- writeYamlFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
20265
+ writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
19706
20266
  invalidateSpecCache();
19707
20267
  }
19708
20268
  // -------------------------------------------------------------------------
@@ -19718,7 +20278,7 @@ var init_specs2 = __esm({
19718
20278
  const p = this.getSubsystemPath(id);
19719
20279
  if (!pathExists(p)) return null;
19720
20280
  try {
19721
- const raw = readYamlFile(p);
20281
+ const raw = readSpecFile(p);
19722
20282
  return SubsystemSpecSchema.parse(raw);
19723
20283
  } catch (e) {
19724
20284
  this.loaderIssues.push({
@@ -19852,7 +20412,7 @@ var init_specs2 = __esm({
19852
20412
  }
19853
20413
  }
19854
20414
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
19855
- writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
20415
+ writeSpecFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
19856
20416
  invalidateSpecCache();
19857
20417
  }
19858
20418
  deleteSubsystemSpec(id) {
@@ -19876,7 +20436,7 @@ var init_specs2 = __esm({
19876
20436
  const p = this.getComponentPath(id);
19877
20437
  if (!pathExists(p)) return null;
19878
20438
  try {
19879
- const raw = readYamlFile(p);
20439
+ const raw = readSpecFile(p);
19880
20440
  return ComponentSpecSchema.parse(raw);
19881
20441
  } catch (e) {
19882
20442
  this.loaderIssues.push({
@@ -19913,7 +20473,7 @@ var init_specs2 = __esm({
19913
20473
  );
19914
20474
  }
19915
20475
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
19916
- writeYamlFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
20476
+ writeSpecFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
19917
20477
  invalidateSpecCache();
19918
20478
  this.normalizeComponentLayout();
19919
20479
  return notices;
@@ -19974,7 +20534,7 @@ var init_specs2 = __esm({
19974
20534
  const p = this.getInterfacePath(id);
19975
20535
  if (!pathExists(p)) return null;
19976
20536
  try {
19977
- const raw = readYamlFile(p);
20537
+ const raw = readSpecFile(p);
19978
20538
  return InterfaceSpecSchema.parse(raw);
19979
20539
  } catch (e) {
19980
20540
  this.loaderIssues.push({
@@ -20005,7 +20565,7 @@ var init_specs2 = __esm({
20005
20565
  if (!pathExists(p)) return;
20006
20566
  let occupantId;
20007
20567
  try {
20008
- occupantId = readYamlFile(p)?.id;
20568
+ occupantId = readSpecFile(p)?.id;
20009
20569
  } catch {
20010
20570
  return;
20011
20571
  }
@@ -20042,7 +20602,7 @@ var init_specs2 = __esm({
20042
20602
  }
20043
20603
  }
20044
20604
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
20045
- writeYamlFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
20605
+ writeSpecFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
20046
20606
  invalidateSpecCache();
20047
20607
  return notices;
20048
20608
  }
@@ -20067,7 +20627,7 @@ var init_specs2 = __esm({
20067
20627
  const p = this.getImplementationPath(id);
20068
20628
  if (!pathExists(p)) return null;
20069
20629
  try {
20070
- const raw = readYamlFile(p);
20630
+ const raw = readSpecFile(p);
20071
20631
  return ImplementationSpecSchema.parse(raw);
20072
20632
  } catch (e) {
20073
20633
  this.loaderIssues.push({
@@ -20099,7 +20659,7 @@ var init_specs2 = __esm({
20099
20659
  }
20100
20660
  }
20101
20661
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
20102
- writeYamlFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
20662
+ writeSpecFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
20103
20663
  invalidateSpecCache();
20104
20664
  return notices;
20105
20665
  }
@@ -20152,7 +20712,7 @@ var init_specs2 = __esm({
20152
20712
  }
20153
20713
  }
20154
20714
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
20155
- writeYamlFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
20715
+ writeSpecFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
20156
20716
  invalidateSpecCache();
20157
20717
  return notices;
20158
20718
  }
@@ -20183,7 +20743,7 @@ var init_specs2 = __esm({
20183
20743
  specToWrite.createdAt = existing.createdAt;
20184
20744
  }
20185
20745
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
20186
- writeYamlFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
20746
+ writeSpecFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
20187
20747
  invalidateSpecCache();
20188
20748
  }
20189
20749
  deleteGroupSpec(id) {
@@ -20347,7 +20907,7 @@ var init_specs2 = __esm({
20347
20907
  findLegacySpecFiles() {
20348
20908
  const specsDir = this.paths.specsDir();
20349
20909
  if (!pathExists(specsDir)) return [];
20350
- const files = listFilesRecursive(specsDir, ".yaml");
20910
+ const files = listSpecFiles(specsDir);
20351
20911
  const legacy = [];
20352
20912
  for (const f of files) {
20353
20913
  const base = path16.basename(f);
@@ -26491,6 +27051,9 @@ __export(src_exports, {
26491
27051
  BuiltinTargetSchema: () => BuiltinTargetSchema,
26492
27052
  BundleNotFoundError: () => BundleNotFoundError,
26493
27053
  CONTEXT_PATHS: () => CONTEXT_PATHS,
27054
+ CarriedDebtKindSchema: () => CarriedDebtKindSchema,
27055
+ CarriedDebtSchema: () => CarriedDebtSchema,
27056
+ CarriedFindingSchema: () => CarriedFindingSchema,
26494
27057
  CatchClauseSchema: () => CatchClauseSchema,
26495
27058
  ClaudeExporter: () => ClaudeExporter,
26496
27059
  ComplexityRuleConfigSchema: () => ComplexityRuleConfigSchema,
@@ -26625,6 +27188,7 @@ __export(src_exports, {
26625
27188
  buildRuleContext: () => buildRuleContext,
26626
27189
  buildServerInstructions: () => buildServerInstructions2,
26627
27190
  buildTreeArchive: () => buildTreeArchive2,
27191
+ callSitesOf: () => callSitesOf,
26628
27192
  captureApprovedSpecs: () => captureApprovedSpecs,
26629
27193
  checkSkillFreshness: () => checkSkillFreshness,
26630
27194
  clearLoaderIssues: () => clearLoaderIssues,
@@ -26684,6 +27248,7 @@ __export(src_exports, {
26684
27248
  extractTypeIdentifiers: () => extractTypeIdentifiers,
26685
27249
  factsFor: () => factsFor,
26686
27250
  fieldTypeRefs: () => fieldTypeRefs,
27251
+ fieldTypesOf: () => fieldTypesOf,
26687
27252
  findChainingParent: () => findChainingParent,
26688
27253
  findChainingSubprojectsMissingConfig: () => findChainingSubprojectsMissingConfig,
26689
27254
  findDomain: () => findDomain,
@@ -26718,10 +27283,12 @@ __export(src_exports, {
26718
27283
  hasArchitectureContext: () => hasArchitectureContext,
26719
27284
  hasAuthoredSpecs: () => hasAuthoredSpecs,
26720
27285
  hasContext: () => hasContext,
27286
+ hasFunctionBody: () => hasFunctionBody,
26721
27287
  hasSpecTree: () => hasSpecTree,
26722
27288
  headNoun: () => headNoun,
26723
27289
  holdsState: () => holdsState,
26724
27290
  implementationSourceFiles: () => implementationSourceFiles,
27291
+ importBindingOf: () => importBindingOf,
26725
27292
  importSpecTree: () => importSpecTree,
26726
27293
  importSurface: () => importSurface,
26727
27294
  ineffectiveDeltaPaths: () => ineffectiveDeltaPaths,
@@ -26877,6 +27444,7 @@ __export(src_exports, {
26877
27444
  toMarkdown: () => toMarkdown,
26878
27445
  toOpenApi: () => toOpenApi,
26879
27446
  toOpenApiSet: () => toOpenApiSet,
27447
+ typeBindingOf: () => typeBindingOf,
26880
27448
  typeGenericParameters: () => typeGenericParameters,
26881
27449
  typeMatchesRef: () => typeMatchesRef,
26882
27450
  typeSourceFiles: () => typeSourceFiles,
@@ -27221,6 +27789,9 @@ function loadProjectConfig4() {
27221
27789
  BuiltinTargetSchema,
27222
27790
  BundleNotFoundError,
27223
27791
  CONTEXT_PATHS,
27792
+ CarriedDebtKindSchema,
27793
+ CarriedDebtSchema,
27794
+ CarriedFindingSchema,
27224
27795
  CatchClauseSchema,
27225
27796
  ClaudeExporter,
27226
27797
  ComplexityRuleConfigSchema,
@@ -27355,6 +27926,7 @@ function loadProjectConfig4() {
27355
27926
  buildRuleContext,
27356
27927
  buildServerInstructions,
27357
27928
  buildTreeArchive,
27929
+ callSitesOf,
27358
27930
  captureApprovedSpecs,
27359
27931
  checkSkillFreshness,
27360
27932
  clearLoaderIssues,
@@ -27414,6 +27986,7 @@ function loadProjectConfig4() {
27414
27986
  extractTypeIdentifiers,
27415
27987
  factsFor,
27416
27988
  fieldTypeRefs,
27989
+ fieldTypesOf,
27417
27990
  findChainingParent,
27418
27991
  findChainingSubprojectsMissingConfig,
27419
27992
  findDomain,
@@ -27448,10 +28021,12 @@ function loadProjectConfig4() {
27448
28021
  hasArchitectureContext,
27449
28022
  hasAuthoredSpecs,
27450
28023
  hasContext,
28024
+ hasFunctionBody,
27451
28025
  hasSpecTree,
27452
28026
  headNoun,
27453
28027
  holdsState,
27454
28028
  implementationSourceFiles,
28029
+ importBindingOf,
27455
28030
  importSpecTree,
27456
28031
  importSurface,
27457
28032
  ineffectiveDeltaPaths,
@@ -27607,6 +28182,7 @@ function loadProjectConfig4() {
27607
28182
  toMarkdown,
27608
28183
  toOpenApi,
27609
28184
  toOpenApiSet,
28185
+ typeBindingOf,
27610
28186
  typeGenericParameters,
27611
28187
  typeMatchesRef,
27612
28188
  typeSourceFiles,