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

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.42";
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,32 @@ 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
+ /** The findings this reason explains. */
894
+ findings: import_zod4.z.array(CarriedFindingSchema)
895
+ });
870
896
  ConformanceRuleConfigSchema = import_zod4.z.object({
871
897
  /**
872
898
  * Project-relative paths holding this project's own source code; a
@@ -889,7 +915,28 @@ var init_project = __esm({
889
915
  * that is now claimed, proven a barrel, or no longer found is
890
916
  * STALE_UNCLAIMED_ENTRY.
891
917
  */
892
- unclaimed: import_zod4.z.array(import_zod4.z.string()).optional()
918
+ unclaimed: import_zod4.z.array(import_zod4.z.string()).optional(),
919
+ /**
920
+ * The conformance findings this tree carries as declared debt, grouped by
921
+ * the reason that explains them — `unclaimed`'s shape, for findings about
922
+ * code a spec DOES claim.
923
+ *
924
+ * It is not a second lint.allow, and the difference is the claim each makes.
925
+ * An allow says "this finding is wrong here, by design", and is meant to
926
+ * live forever; an entry here says "this finding is RIGHT, and it is not
927
+ * paid yet". A mechanism that cannot tell those apart can never be asked how
928
+ * much the tree owes. Four properties keep it a register:
929
+ * • only a code a rule declares CARRYABLE may appear (UNCARRYABLE_FINDING,
930
+ * an error, which no allow and no --ci waiver can reach);
931
+ * • an entry carries a finding only when it lists EVERY unit that finding
932
+ * reports, so an aggregating finding cannot grow behind it;
933
+ * • an entry that stops applying — gone, or listing a unit no longer
934
+ * reported — is STALE_CARRIED_FINDING, so the register only shrinks;
935
+ * • every entry states its kind and its reason, and `wairon validate`
936
+ * prints the running total, so the debt is loud where a suppression is
937
+ * silent.
938
+ */
939
+ carried: import_zod4.z.array(CarriedDebtSchema).optional()
893
940
  });
894
941
  DesignDepthSchema = import_zod4.z.enum(["components", "interfaces", "implementations", "narratives"]);
895
942
  RulesConfigSchema = import_zod4.z.object({
@@ -2518,6 +2565,24 @@ var init_step_config = __esm({
2518
2565
  function pathKey(sourcePath) {
2519
2566
  return sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
2520
2567
  }
2568
+ function ownEntry(record2, key) {
2569
+ return record2 && Object.prototype.hasOwnProperty.call(record2, key) ? record2[key] : void 0;
2570
+ }
2571
+ function importBindingOf(facts, name) {
2572
+ return ownEntry(facts.importBindings, name);
2573
+ }
2574
+ function fieldTypesOf(facts, field) {
2575
+ return ownEntry(facts.fieldTypes, field) ?? NO_FIELD_TYPES;
2576
+ }
2577
+ function typeBindingOf(facts, name) {
2578
+ return ownEntry(facts.typeOnlyBindings, name) ?? ownEntry(facts.importBindings, name)?.from;
2579
+ }
2580
+ function callSitesOf(facts, fn) {
2581
+ return ownEntry(facts.functionCallSites, fn);
2582
+ }
2583
+ function hasFunctionBody(facts, symbol) {
2584
+ return callSitesOf(facts, symbol) !== void 0;
2585
+ }
2521
2586
  function resolveImport(fromFile, specifier, knownPaths) {
2522
2587
  if (!specifier.startsWith(".")) return void 0;
2523
2588
  const joined = pathKey(path6.posix.normalize(path6.posix.join(path6.posix.dirname(fromFile), specifier)));
@@ -2536,11 +2601,12 @@ function resolveImport(fromFile, specifier, knownPaths) {
2536
2601
  }
2537
2602
  return void 0;
2538
2603
  }
2539
- var path6;
2604
+ var path6, NO_FIELD_TYPES;
2540
2605
  var init_code_model = __esm({
2541
2606
  "src/models/code-model.ts"() {
2542
2607
  "use strict";
2543
2608
  path6 = __toESM(require("path"));
2609
+ NO_FIELD_TYPES = [];
2544
2610
  }
2545
2611
  });
2546
2612
 
@@ -3255,6 +3321,26 @@ var init_lockfile = __esm({
3255
3321
  }
3256
3322
  });
3257
3323
 
3324
+ // src/core/spec-files.ts
3325
+ function readSpecFile(filePath) {
3326
+ return readYamlFile(filePath);
3327
+ }
3328
+ function writeSpecFile(filePath, document) {
3329
+ writeYamlFile(filePath, document);
3330
+ }
3331
+ function listSpecFiles(specsDir) {
3332
+ return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
3333
+ }
3334
+ var SPEC_FILE_EXTENSION;
3335
+ var init_spec_files = __esm({
3336
+ "src/core/spec-files.ts"() {
3337
+ "use strict";
3338
+ init_fs();
3339
+ init_yaml();
3340
+ SPEC_FILE_EXTENSION = ".yaml";
3341
+ }
3342
+ });
3343
+
3258
3344
  // src/core/narrative-labels.ts
3259
3345
  function resolveNarrativeLabels(methodName, steps) {
3260
3346
  const errors = [];
@@ -8917,13 +9003,84 @@ function buildCodeIndex(model) {
8917
9003
  cache2.set(path70, built);
8918
9004
  return built;
8919
9005
  };
9006
+ const republished = /* @__PURE__ */ new Map();
9007
+ const republishedFrom = (path70) => {
9008
+ const cached = republished.get(path70);
9009
+ if (cached) return cached;
9010
+ const out = /* @__PURE__ */ new Set([path70]);
9011
+ const queue = [path70];
9012
+ while (queue.length) {
9013
+ const from = queue.pop();
9014
+ const f = facts.get(from);
9015
+ if (f?.status !== "analyzed") continue;
9016
+ for (const specifier of f.reexports) {
9017
+ const to = resolveImport(from, specifier, paths);
9018
+ if (to && !out.has(to)) {
9019
+ out.add(to);
9020
+ queue.push(to);
9021
+ }
9022
+ }
9023
+ }
9024
+ republished.set(path70, out);
9025
+ return out;
9026
+ };
9027
+ const NO_ORIGIN = /* @__PURE__ */ new Set();
9028
+ const landingFrom = (scope, specifier) => {
9029
+ const to = resolveImport(scope, specifier, paths);
9030
+ return to ? republishedFrom(to) : NO_ORIGIN;
9031
+ };
9032
+ const scopeOf2 = (site, from) => {
9033
+ const path70 = pathKey(site.from ?? from);
9034
+ const f = facts.get(path70);
9035
+ return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path70, facts: f } : void 0;
9036
+ };
9037
+ const originOf = (site, from) => {
9038
+ const resolved = scopeOf2(site, from);
9039
+ if (!resolved) return NO_ORIGIN;
9040
+ const { path: scope, facts: f } = resolved;
9041
+ const landing = (specifier) => landingFrom(scope, specifier);
9042
+ if (!site.member) {
9043
+ const binding = importBindingOf(f, site.name);
9044
+ if (binding) return landing(binding.from);
9045
+ return declarationsAt(scope).has(site.name) ? republishedFrom(scope) : NO_ORIGIN;
9046
+ }
9047
+ if (!site.via) return NO_ORIGIN;
9048
+ const receiver = importBindingOf(f, site.via);
9049
+ if (!receiver) return NO_ORIGIN;
9050
+ if (receiver.namespace) return landing(receiver.from);
9051
+ const through = landing(receiver.from);
9052
+ for (const candidate of through) {
9053
+ if (declarationsAt(candidate).has(site.name)) return through;
9054
+ }
9055
+ return NO_ORIGIN;
9056
+ };
9057
+ const possibleOriginsOf = (site, from) => {
9058
+ const proven = originOf(site, from);
9059
+ if (!site.field && !site.constructed) return proven;
9060
+ const resolved = scopeOf2(site, from);
9061
+ if (!resolved) return proven;
9062
+ const { path: scope, facts: f } = resolved;
9063
+ const out = new Set(proven);
9064
+ const widenThrough = (name, specifier) => {
9065
+ const landings = specifier !== void 0 ? landingFrom(scope, specifier) : declarationsAt(scope).has(name) ? republishedFrom(scope) : NO_ORIGIN;
9066
+ for (const candidate of landings) out.add(candidate);
9067
+ };
9068
+ if (site.field) {
9069
+ for (const typeName of fieldTypesOf(f, site.field)) widenThrough(typeName, typeBindingOf(f, typeName));
9070
+ }
9071
+ if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
9072
+ return out;
9073
+ };
9074
+ const declarationsAt = (path70) => namesAt(declarations, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
8920
9075
  return {
8921
9076
  paths,
8922
9077
  exactPaths,
8923
9078
  factsAt: (path70) => facts.get(pathKey(path70)),
8924
- declarationsAt: (path70) => namesAt(declarations, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames])),
9079
+ declarationsAt,
8925
9080
  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))
9081
+ findingAnchorsAt: (path70) => namesAt(findingAnchors, pathKey(path70), (f) => new Set(f.anchoredNames)),
9082
+ originOf,
9083
+ possibleOriginsOf
8927
9084
  };
8928
9085
  }
8929
9086
  function buildRealizationIndex(ctx) {
@@ -13152,6 +13309,21 @@ var init_source_file_linkage = __esm({
13152
13309
  });
13153
13310
 
13154
13311
  // src/core/rules/conformance/method-realization.ts
13312
+ function bodyReachable(code, file, symbol) {
13313
+ const facts = code.factsAt(file);
13314
+ if (!facts) return true;
13315
+ const names = /* @__PURE__ */ new Set([symbol]);
13316
+ const binding = importBindingOf(facts, symbol);
13317
+ if (binding?.imported) names.add(binding.imported);
13318
+ const origins = code.originOf({ name: symbol, member: false }, file);
13319
+ if (origins.size === 0) return true;
13320
+ for (const origin of origins) {
13321
+ const at = code.factsAt(origin);
13322
+ if (!at || at.status !== "analyzed" || at.analysisGrade !== "exact") return true;
13323
+ for (const name of names) if (hasFunctionBody(at, name)) return true;
13324
+ }
13325
+ return false;
13326
+ }
13155
13327
  var methodRealizationRule;
13156
13328
  var init_method_realization = __esm({
13157
13329
  "src/core/rules/conformance/method-realization.ts"() {
@@ -13159,9 +13331,10 @@ var init_method_realization = __esm({
13159
13331
  init_models();
13160
13332
  methodRealizationRule = {
13161
13333
  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.",
13334
+ 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
13335
  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" }
13336
+ { 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" },
13337
+ { 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
13338
  ],
13166
13339
  check(ctx) {
13167
13340
  const code = ctx.codeIndex();
@@ -13185,7 +13358,22 @@ var init_method_realization = __esm({
13185
13358
  const inDeclared = code.declarationsAt(file).has(symbol);
13186
13359
  const inAnchored = inDeclared || code.anchorsAt(file).has(symbol);
13187
13360
  const realized = tier === "declared" ? inDeclared : inAnchored;
13188
- if (realized) continue;
13361
+ if (realized) {
13362
+ if (!inDeclared || facts.analysisGrade !== "exact" || bodyReachable(code, file, symbol)) continue;
13363
+ const bodyLabel = methodImpl?.symbol ? `"${method2.name}" (symbol "${symbol}")` : `"${method2.name}"`;
13364
+ ctx.addIssue(
13365
+ "warning",
13366
+ "METHOD_BODY_NOT_FOUND",
13367
+ `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".`,
13368
+ impl.id,
13369
+ draft,
13370
+ void 0,
13371
+ // One method, one indivisible fact: nothing here aggregates, so
13372
+ // the site alone is the whole identity.
13373
+ { at: method2.name }
13374
+ );
13375
+ continue;
13376
+ }
13189
13377
  const label = methodImpl?.symbol ? `"${method2.name}" (symbol "${symbol}")` : `"${method2.name}"`;
13190
13378
  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
13379
  ctx.addIssue(
@@ -13252,24 +13440,54 @@ var init_finding_realization = __esm({
13252
13440
  });
13253
13441
 
13254
13442
  // 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);
13443
+ function closedCallSites(code, file, fn, stopAt) {
13444
+ const facts = code.factsAt(file);
13445
+ const direct = facts && callSitesOf(facts, fn);
13260
13446
  if (!direct) return void 0;
13261
- const closed = new Set(direct);
13262
- const queue = [...direct];
13447
+ const here = pathKey(file);
13448
+ const out = [];
13449
+ const descended = /* @__PURE__ */ new Set([`${here}|${fn}`]);
13450
+ const queue = direct.map((s) => ({ ...s, from: s.from ?? here }));
13263
13451
  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
- }
13452
+ const site = queue.pop();
13453
+ out.push(site);
13454
+ if (stopAt?.has(site.name)) continue;
13455
+ const key = `${site.from}|${site.name}`;
13456
+ if (descended.has(key)) continue;
13457
+ descended.add(key);
13458
+ const scope = code.factsAt(site.from);
13459
+ const next = scope && callSitesOf(scope, site.name);
13460
+ if (!next) continue;
13461
+ queue.push(...next.map((s) => ({ ...s, from: s.from ?? site.from })));
13271
13462
  }
13272
- return closed;
13463
+ return out;
13464
+ }
13465
+ function resolveCallTarget(ctx, componentId, methodName) {
13466
+ const accepted = /* @__PURE__ */ new Set([methodName]);
13467
+ const files = /* @__PURE__ */ new Set();
13468
+ for (const intf of ctx.interfacesByComponent.get(componentId) ?? []) {
13469
+ for (const impl of ctx.implementationsByContract.get(intf.id) ?? []) {
13470
+ const method2 = impl.methods.find((m) => m.name === methodName);
13471
+ if (!method2 && !intf.methods.some((m) => m.name === methodName)) continue;
13472
+ if (method2?.symbol) accepted.add(method2.symbol);
13473
+ const file = methodSourceFile(method2 ?? {}, impl.sourcePath);
13474
+ if (file) files.add(pathKey(file));
13475
+ }
13476
+ }
13477
+ return { accepted, files };
13478
+ }
13479
+ function describeSite(site) {
13480
+ if (!site.member) return `${site.name}(\u2026)`;
13481
+ if (site.via) return `${site.via}.${site.name}(\u2026)`;
13482
+ if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
13483
+ if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
13484
+ return `<receiver>.${site.name}(\u2026)`;
13485
+ }
13486
+ function describeMiss(m) {
13487
+ const head = `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")}`;
13488
+ if (m.miss.kind === "elsewhere") return `${head}, called but resolved to ${m.miss.landed.map((p) => `"${p}"`).join(", ")})`;
13489
+ if (m.miss.kind === "unresolved") return `${head}, written as ${m.miss.shapes.map((s) => `\`${s}\``).join(" / ")})`;
13490
+ return `${head})`;
13273
13491
  }
13274
13492
  var callConformanceRule;
13275
13493
  var init_call_conformance = __esm({
@@ -13278,13 +13496,24 @@ var init_call_conformance = __esm({
13278
13496
  init_models();
13279
13497
  callConformanceRule = {
13280
13498
  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.",
13499
+ 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
13500
  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)" }
13501
+ { 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 },
13502
+ { 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 },
13503
+ { 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
13504
  ],
13285
13505
  check(ctx) {
13286
13506
  const code = ctx.codeIndex();
13287
- for (const entry of ctx.implementationMethods()) {
13507
+ const methods = ctx.implementationMethods();
13508
+ const modelledAt = /* @__PURE__ */ new Map();
13509
+ for (const entry of methods) {
13510
+ if (!entry.sourceFile) continue;
13511
+ const file = pathKey(entry.sourceFile);
13512
+ const byName = modelledAt.get(file) ?? /* @__PURE__ */ new Map();
13513
+ byName.set(entry.method.symbol ?? entry.method.name, { component: entry.component.id, method: entry.method.name });
13514
+ modelledAt.set(file, byName);
13515
+ }
13516
+ for (const entry of methods) {
13288
13517
  const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
13289
13518
  const tier = implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component);
13290
13519
  if (tier === "off") continue;
@@ -13292,37 +13521,91 @@ var init_call_conformance = __esm({
13292
13521
  if (!file) continue;
13293
13522
  const facts = code.factsAt(file);
13294
13523
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
13524
+ const here = pathKey(file);
13295
13525
  const fnSymbol = implMethod.symbol ?? implMethod.name;
13296
- const callees = closedCallees(facts, fnSymbol);
13297
- if (!callees) continue;
13298
- const missing = [];
13526
+ const colocated = modelledAt.get(here) ?? /* @__PURE__ */ new Map();
13527
+ const sites = closedCallSites(code, file, fnSymbol);
13528
+ if (!sites) continue;
13529
+ const missed = [];
13299
13530
  for (const step of implMethod.narrative) {
13300
13531
  if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
13301
13532
  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);
13533
+ const target = resolveCallTarget(ctx, step.targetComponent, step.targetMethod);
13534
+ if (target.accepted.has(fnSymbol)) continue;
13535
+ const matching = sites.filter((s) => target.accepted.has(s.name));
13536
+ const ref = `${step.targetComponent}.${step.targetMethod}`;
13537
+ if (target.files.size === 0) {
13538
+ if (matching.length === 0) {
13539
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss: { kind: "absent" } });
13307
13540
  }
13541
+ continue;
13308
13542
  }
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
- });
13543
+ const landed = /* @__PURE__ */ new Set();
13544
+ let realized = false;
13545
+ for (const site of matching) {
13546
+ for (const origin of code.originOf(site, file)) landed.add(origin);
13547
+ for (const origin of code.possibleOriginsOf(site, file)) {
13548
+ if (target.files.has(origin)) realized = true;
13549
+ }
13550
+ }
13551
+ if (realized) continue;
13552
+ const miss = matching.length === 0 ? { kind: "absent" } : landed.size === 0 ? { kind: "unresolved", shapes: [...new Set(matching.map(describeSite))].sort() } : { kind: "elsewhere", landed: [...landed].sort() };
13553
+ missed.push({ step: step.stepNumber, target: ref, accepted: [...target.accepted], miss });
13554
+ }
13555
+ const unrealized = missed.filter((m) => m.miss.kind !== "unresolved");
13556
+ const unresolved = missed.filter((m) => m.miss.kind === "unresolved");
13557
+ if (unrealized.length) {
13558
+ ctx.addIssue(
13559
+ "warning",
13560
+ "CALL_STEP_UNREALIZED",
13561
+ `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.`,
13562
+ impl.id,
13563
+ entry.draftContext,
13564
+ void 0,
13565
+ // A step, named by its number and its target: what the register
13566
+ // carries is ONE unresolved step, never "this method's call steps".
13567
+ { at: implMethod.name, covers: unrealized.map((m) => `${m.step}:${m.target}`) }
13568
+ );
13569
+ }
13570
+ if (unresolved.length) {
13571
+ ctx.addIssue(
13572
+ "warning",
13573
+ "CALL_ORIGIN_UNRESOLVED",
13574
+ `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.`,
13575
+ impl.id,
13576
+ entry.draftContext,
13577
+ void 0,
13578
+ { at: implMethod.name, covers: unresolved.map((m) => `${m.step}:${m.target}`) }
13579
+ );
13580
+ }
13581
+ const declared = /* @__PURE__ */ new Set();
13582
+ for (const step of implMethod.narrative) {
13583
+ if (step.targetComponent && step.targetMethod) declared.add(`${step.targetComponent}.${step.targetMethod}`);
13584
+ }
13585
+ const crossings = /* @__PURE__ */ new Map();
13586
+ for (const site of closedCallSites(code, file, fnSymbol, new Set(colocated.keys())) ?? []) {
13587
+ const owner = colocated.get(site.name);
13588
+ if (!owner || owner.component === component.id) continue;
13589
+ const ref = `${owner.component}.${owner.method}`;
13590
+ if (declared.has(ref) || crossings.has(ref)) continue;
13591
+ if (!code.originOf(site, file).has(here)) continue;
13592
+ crossings.set(ref, site.name);
13593
+ }
13594
+ if (crossings.size) {
13595
+ const detail = [...crossings].map(([ref, name]) => `"${name}" (${ref})`).join("; ");
13596
+ ctx.addIssue(
13597
+ "warning",
13598
+ "UNDECLARED_COLOCATED_CALL",
13599
+ `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.`,
13600
+ impl.id,
13601
+ entry.draftContext,
13602
+ void 0,
13603
+ // The crossings themselves. Keyed by method alone, a 24th crossing
13604
+ // would ride into a register entry written for 23 - so each one is
13605
+ // named, and one nobody carried fires on the day it appears.
13606
+ { at: implMethod.name, covers: [...crossings.keys()] }
13607
+ );
13316
13608
  }
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
13609
  }
13327
13610
  }
13328
13611
  };
@@ -13832,6 +14115,69 @@ var init_unclaimed_source = __esm({
13832
14115
  }
13833
14116
  });
13834
14117
 
14118
+ // src/core/rules/conformance/carried-debt.ts
14119
+ var carriedDebtRule;
14120
+ var init_carried_debt = __esm({
14121
+ "src/core/rules/conformance/carried-debt.ts"() {
14122
+ "use strict";
14123
+ carriedDebtRule = {
14124
+ name: "carried-debt",
14125
+ description: "The conformance debt register (`rules.conformance.carried`) holds the code\u2194spec findings this tree carries as declared, classified debt \u2014 `unclaimed`'s one-way shape, for findings about code a spec does name. It is not a second lint.allow: an allow says a finding is wrong here by design, an entry here says the finding is right and unpaid, and each entry states which of the three kinds it is (drift: spec and code disagree; undecided: the fix waits on a modelling decision nobody has taken; unreadable: the analysis cannot follow the shape the code is written in) and why. Only a code a rule declares CARRYABLE may appear, and naming any other is an error, so the register can never widen into general-purpose suppression. An entry carries a finding only when it lists EVERY unit that finding reports, so an aggregating finding cannot grow behind it; an entry that matched nothing, that duplicates another, that names a spec the tree does not hold, or that lists a unit no longer reported is STALE_CARRIED_FINDING, so the register only shrinks. The audit is silent in a run that could not have fired the findings at all: one that built no code model, the specs outside a scoped run, and a code the project switched off.",
14126
+ codes: [
14127
+ { code: "UNCARRYABLE_FINDING", defaultSeverity: "error", summary: "The conformance debt register names an issue code no rule emits, or one no rule declares carryable \u2014 the register holds measured code\u2194spec debt, never a design rule somebody wants quiet" },
14128
+ { 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" }
14129
+ ],
14130
+ check(ctx) {
14131
+ const carried = ctx.carriedFindings;
14132
+ if (carried.length === 0) return;
14133
+ for (const entry of carried) {
14134
+ if (ctx.carryableIssueCodes.has(entry.code)) continue;
14135
+ 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";
14136
+ ctx.addIssue(
14137
+ "error",
14138
+ "UNCARRYABLE_FINDING",
14139
+ `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.`
14140
+ );
14141
+ }
14142
+ if (ctx.codeModel.files.length === 0) return;
14143
+ const known = new Set(ctx.specIds().map((s) => s.id));
14144
+ const addressed = /* @__PURE__ */ new Set();
14145
+ for (const entry of carried) {
14146
+ if (!ctx.carryableIssueCodes.has(entry.code)) continue;
14147
+ if (ctx.rules?.sddRuleSeverity?.[entry.code] === "off") continue;
14148
+ if (!ctx.isSpecInScope(entry.spec)) continue;
14149
+ const key = `${entry.spec}|${entry.code}|${entry.at}`;
14150
+ if (addressed.has(key)) {
14151
+ ctx.addIssue(
14152
+ "warning",
14153
+ "STALE_CARRIED_FINDING",
14154
+ `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.`
14155
+ );
14156
+ continue;
14157
+ }
14158
+ addressed.add(key);
14159
+ if (!entry.fired) {
14160
+ 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}"`;
14161
+ ctx.addIssue(
14162
+ "warning",
14163
+ "STALE_CARRIED_FINDING",
14164
+ `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.`
14165
+ );
14166
+ continue;
14167
+ }
14168
+ const paid = entry.covers.filter((unit) => !entry.seen.has(unit));
14169
+ if (paid.length === 0) continue;
14170
+ ctx.addIssue(
14171
+ "warning",
14172
+ "STALE_CARRIED_FINDING",
14173
+ `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.`
14174
+ );
14175
+ }
14176
+ }
14177
+ };
14178
+ }
14179
+ });
14180
+
13835
14181
  // src/core/rules/heuristic/coupling-health.ts
13836
14182
  var DEFAULT_GOD_COMPONENT_THRESHOLD, couplingRule;
13837
14183
  var init_coupling_health = __esm({
@@ -14986,8 +15332,8 @@ function registerPackRules(packRules) {
14986
15332
  for (const rule of packRules) addRule(rule);
14987
15333
  }
14988
15334
  function ruleSequence() {
14989
- const base = ruleSet.filter((r) => r !== lintAllowsRule);
14990
- return ruleSet.includes(lintAllowsRule) ? [...base, lintAllowsRule] : base;
15335
+ const audits = [carriedDebtRule, lintAllowsRule].filter((r) => ruleSet.includes(r));
15336
+ return [...ruleSet.filter((r) => !audits.includes(r)), ...audits];
14991
15337
  }
14992
15338
  function specScopedRules() {
14993
15339
  return ruleSequence().filter((r) => r.scope === "spec");
@@ -15077,6 +15423,7 @@ var init_repository = __esm({
15077
15423
  init_integration_sim_coverage();
15078
15424
  init_type_realization();
15079
15425
  init_unclaimed_source();
15426
+ init_carried_debt();
15080
15427
  init_coupling_health();
15081
15428
  init_signature_language_builtins();
15082
15429
  init_narrative_language_constructs();
@@ -15268,8 +15615,11 @@ var init_repository = __esm({
15268
15615
  // elsewhere?) rather than spec content.
15269
15616
  packResolutionRule,
15270
15617
  reproducibilityRule,
15271
- // MUST run last: it audits which lint.allow entries the earlier rules
15272
- // actually consumed (stale/unknown allows).
15618
+ // MUST run last, in this order: each audits what the earlier rules did
15619
+ // with a declared exception. The debt register first (which carried
15620
+ // findings the conformance family actually matched), then the allows
15621
+ // (which suppressions any rule actually consumed).
15622
+ carriedDebtRule,
15273
15623
  lintAllowsRule
15274
15624
  ];
15275
15625
  ruleSet = [];
@@ -15392,9 +15742,15 @@ function walkExact(ts, sourceText, fileName) {
15392
15742
  const imports = /* @__PURE__ */ new Set();
15393
15743
  const reexports = /* @__PURE__ */ new Set();
15394
15744
  const starExports = [];
15745
+ const namedReexports = [];
15395
15746
  const complexity = /* @__PURE__ */ new Map();
15396
15747
  const calls = /* @__PURE__ */ new Map();
15748
+ const importBindings = /* @__PURE__ */ new Map();
15749
+ const typeOnlyBindings = /* @__PURE__ */ new Map();
15750
+ const fieldTypes = /* @__PURE__ */ new Map();
15397
15751
  const mutableBindings = /* @__PURE__ */ new Set();
15752
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : "");
15753
+ const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
15398
15754
  const addBindingNames = (name) => {
15399
15755
  if (ts.isIdentifier(name)) declared.add(name.text);
15400
15756
  else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
@@ -15408,6 +15764,17 @@ function walkExact(ts, sourceText, fileName) {
15408
15764
  if (ts.isPrivateIdentifier(name)) return name.text;
15409
15765
  return void 0;
15410
15766
  };
15767
+ const typeReferenceName = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : void 0;
15768
+ 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);
15769
+ const recordFieldType = (name, type) => {
15770
+ const typeName = typeReferenceName(type);
15771
+ if (!typeName) return;
15772
+ const field = propertyNameText(name);
15773
+ if (!field) return;
15774
+ const named = fieldTypes.get(field) ?? /* @__PURE__ */ new Set();
15775
+ named.add(typeName);
15776
+ fieldTypes.set(field, named);
15777
+ };
15411
15778
  const hasExportModifier = (node) => {
15412
15779
  const mods = node.modifiers;
15413
15780
  return !!mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
@@ -15430,7 +15797,10 @@ function walkExact(ts, sourceText, fileName) {
15430
15797
  };
15431
15798
  const collectFunctionFacts = (fn) => {
15432
15799
  let score = 1;
15433
- const callees = /* @__PURE__ */ new Set();
15800
+ const callees = /* @__PURE__ */ new Map();
15801
+ const addSite = (site) => {
15802
+ callees.set(siteKey(site), site);
15803
+ };
15434
15804
  const count = (node) => {
15435
15805
  if (namedFunctionName(node) !== void 0) return;
15436
15806
  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 +15812,17 @@ function walkExact(ts, sourceText, fileName) {
15442
15812
  }
15443
15813
  } else if (ts.isCallExpression(node)) {
15444
15814
  const callee = node.expression;
15445
- if (ts.isIdentifier(callee)) callees.add(callee.text);
15446
- else if (ts.isPropertyAccessExpression(callee)) callees.add(callee.name.text);
15815
+ if (ts.isIdentifier(callee)) addSite({ name: callee.text, member: false });
15816
+ else if (ts.isPropertyAccessExpression(callee)) {
15817
+ const receiver = callee.expression;
15818
+ const name = callee.name.text;
15819
+ if (ts.isIdentifier(receiver)) addSite({ name, member: true, via: receiver.text });
15820
+ else if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
15821
+ addSite({ name, member: true, field: receiver.name.text });
15822
+ } else if (ts.isNewExpression(receiver) && ts.isIdentifier(receiver.expression)) {
15823
+ addSite({ name, member: true, constructed: receiver.expression.text });
15824
+ } else addSite({ name, member: true });
15825
+ }
15447
15826
  }
15448
15827
  ts.forEachChild(node, count);
15449
15828
  };
@@ -15455,9 +15834,13 @@ function walkExact(ts, sourceText, fileName) {
15455
15834
  if (fnName && node.body) {
15456
15835
  const facts = collectFunctionFacts(node);
15457
15836
  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);
15837
+ const sites = calls.get(fnName) ?? /* @__PURE__ */ new Map();
15838
+ for (const [key, site] of facts.callees) sites.set(key, site);
15839
+ calls.set(fnName, sites);
15840
+ }
15841
+ if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
15842
+ else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
15843
+ recordFieldType(node.name, node.type);
15461
15844
  }
15462
15845
  if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
15463
15846
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : void 0;
@@ -15484,11 +15867,24 @@ function walkExact(ts, sourceText, fileName) {
15484
15867
  } else if (ts.isImportDeclaration(node)) {
15485
15868
  const clause = node.importClause;
15486
15869
  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);
15870
+ const spec = ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
15871
+ if (!typeOnly && spec) imports.add(spec);
15872
+ const bind = (name, namespace, imported, elementTypeOnly = false) => {
15873
+ declared.add(name);
15874
+ if (!spec) return;
15875
+ if (typeOnly || elementTypeOnly) {
15876
+ typeOnlyBindings.set(name, spec);
15877
+ return;
15878
+ }
15879
+ const fact = { from: spec };
15880
+ if (namespace) fact.namespace = true;
15881
+ if (imported && imported !== name) fact.imported = imported;
15882
+ importBindings.set(name, fact);
15883
+ };
15884
+ if (clause?.name) bind(clause.name.text, false);
15489
15885
  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);
15886
+ if (ts.isNamespaceImport(clause.namedBindings)) bind(clause.namedBindings.name.text, true);
15887
+ else for (const el of clause.namedBindings.elements) bind(el.name.text, false, el.propertyName?.text, el.isTypeOnly);
15492
15888
  }
15493
15889
  } else if (ts.isExportDeclaration(node)) {
15494
15890
  const spec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
@@ -15497,6 +15893,9 @@ function walkExact(ts, sourceText, fileName) {
15497
15893
  for (const el of node.exportClause.elements) {
15498
15894
  declared.add(el.name.text);
15499
15895
  exported.add(el.name.text);
15896
+ if (spec && !node.isTypeOnly && !el.isTypeOnly) {
15897
+ namedReexports.push({ exported: el.name.text, local: (el.propertyName ?? el.name).text, from: spec });
15898
+ }
15500
15899
  }
15501
15900
  } else if (!node.exportClause && spec) {
15502
15901
  starExports.push(spec);
@@ -15519,7 +15918,7 @@ function walkExact(ts, sourceText, fileName) {
15519
15918
  };
15520
15919
  visit(sf);
15521
15920
  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 };
15921
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, importBindings, typeOnlyBindings, fieldTypes, mutableBindings, reexportOnly };
15523
15922
  }
15524
15923
  function resolveRelativeModule(fromFile, specifier) {
15525
15924
  if (!specifier.startsWith(".")) return null;
@@ -15542,34 +15941,47 @@ function resolveRelativeModule(fromFile, specifier) {
15542
15941
  }
15543
15942
  return null;
15544
15943
  }
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);
15944
+ function reexportTarget(ts, filePath, specifier, projectRoot2, visited, exactCache) {
15945
+ const target = resolveRelativeModule(filePath, specifier);
15946
+ if (!target || path13.relative(projectRoot2, target).startsWith("..")) return null;
15947
+ let targetFacts = exactCache.get(target);
15948
+ if (targetFacts === void 0) {
15949
+ try {
15950
+ targetFacts = walkExact(ts, fs10.readFileSync(target, "utf8"), target);
15951
+ } catch {
15952
+ targetFacts = null;
15559
15953
  }
15560
- if (!targetFacts) continue;
15561
- chaseStarExports(ts, targetFacts, target, projectRoot2, visited, exactCache);
15562
- for (const name of targetFacts.exported) {
15954
+ exactCache.set(target, targetFacts);
15955
+ }
15956
+ if (!targetFacts) return null;
15957
+ if (!visited.has(target)) {
15958
+ visited.add(target);
15959
+ chaseReexports(ts, targetFacts, target, projectRoot2, visited, exactCache);
15960
+ }
15961
+ return { path: target, facts: targetFacts };
15962
+ }
15963
+ function carryBody(facts, target, targetPath, local, exported, projectRoot2) {
15964
+ const score = target.complexity.get(local);
15965
+ if (score !== void 0) facts.complexity.set(exported, Math.max(facts.complexity.get(exported) ?? 0, score));
15966
+ const targetCalls = target.calls.get(local);
15967
+ if (!targetCalls) return;
15968
+ const carriedFrom = pathKey(path13.relative(projectRoot2, targetPath));
15969
+ const sites = facts.calls.get(exported) ?? /* @__PURE__ */ new Map();
15970
+ for (const [key, site] of targetCalls) sites.set(carriedFrom + "|" + key, { ...site, from: site.from ?? carriedFrom });
15971
+ facts.calls.set(exported, sites);
15972
+ }
15973
+ function chaseReexports(ts, facts, filePath, projectRoot2, visited, exactCache) {
15974
+ for (const re of facts.namedReexports) {
15975
+ const target = reexportTarget(ts, filePath, re.from, projectRoot2, visited, exactCache);
15976
+ if (target) carryBody(facts, target.facts, target.path, re.local, re.exported, projectRoot2);
15977
+ }
15978
+ for (const spec of facts.starExports) {
15979
+ const target = reexportTarget(ts, filePath, spec, projectRoot2, visited, exactCache);
15980
+ if (!target) continue;
15981
+ for (const name of target.facts.exported) {
15563
15982
  facts.exported.add(name);
15564
15983
  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
- }
15984
+ carryBody(facts, target.facts, target.path, name, name, projectRoot2);
15573
15985
  }
15574
15986
  }
15575
15987
  }
@@ -15674,7 +16086,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15674
16086
  if (ts) {
15675
16087
  try {
15676
16088
  const facts = walkExact(ts, text2, absolute);
15677
- chaseStarExports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
16089
+ chaseReexports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
15678
16090
  analyzed = {
15679
16091
  analysisGrade: "exact",
15680
16092
  declaredNames: [...facts.declared],
@@ -15683,7 +16095,10 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15683
16095
  imports: [...facts.imports],
15684
16096
  reexports: [...facts.reexports],
15685
16097
  functionComplexity: Object.fromEntries(facts.complexity),
15686
- functionCalls: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v]])),
16098
+ functionCallSites: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v.values()]])),
16099
+ importBindings: Object.fromEntries(facts.importBindings),
16100
+ typeOnlyBindings: Object.fromEntries(facts.typeOnlyBindings),
16101
+ fieldTypes: Object.fromEntries([...facts.fieldTypes].map(([k, v]) => [k, [...v]])),
15687
16102
  topLevelMutableBindings: [...facts.mutableBindings],
15688
16103
  reexportOnly: facts.reexportOnly
15689
16104
  };
@@ -16092,7 +16507,27 @@ function buildRuleContext(opts) {
16092
16507
  for (const i of interfaces) collectAllows(i.id, i.lint);
16093
16508
  for (const im of implementations) collectAllows(im.id, im.lint);
16094
16509
  for (const t of types) collectAllows(t.id, t.lint);
16095
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
16510
+ const carriedFindings = [];
16511
+ const carriedLookup = /* @__PURE__ */ new Map();
16512
+ const carriedKey = (spec, code, at) => `${spec}\0${code}\0${at}`;
16513
+ for (const group of rules?.conformance?.carried ?? []) {
16514
+ for (const f of group.findings ?? []) {
16515
+ const entry = {
16516
+ kind: group.kind,
16517
+ why: group.why,
16518
+ code: f.code,
16519
+ spec: f.spec,
16520
+ at: f.at,
16521
+ covers: f.covers ?? [],
16522
+ fired: false,
16523
+ seen: /* @__PURE__ */ new Set()
16524
+ };
16525
+ carriedFindings.push(entry);
16526
+ const key = carriedKey(f.spec, f.code, f.at);
16527
+ if (!carriedLookup.has(key)) carriedLookup.set(key, entry);
16528
+ }
16529
+ }
16530
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved, parts) => {
16096
16531
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
16097
16532
  return;
16098
16533
  }
@@ -16110,10 +16545,25 @@ function buildRuleContext(opts) {
16110
16545
  if (severity === "warning") return;
16111
16546
  }
16112
16547
  }
16548
+ let text2 = message;
16549
+ if (specId && parts && severity === "warning") {
16550
+ const entry = carriedLookup.get(carriedKey(specId, code, parts.at));
16551
+ if (entry) {
16552
+ entry.fired = true;
16553
+ const listed = new Set(entry.covers);
16554
+ const grew = [];
16555
+ for (const unit of parts.covers ?? []) {
16556
+ if (listed.has(unit)) entry.seen.add(unit);
16557
+ else grew.push(unit);
16558
+ }
16559
+ if (grew.length === 0) return;
16560
+ 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.`;
16561
+ }
16562
+ }
16113
16563
  issues.push({
16114
16564
  severity,
16115
16565
  code,
16116
- message,
16566
+ message: text2,
16117
16567
  specId,
16118
16568
  ...isDraftContext ? { draftContext: true } : {},
16119
16569
  ...surfaceResolved ? { surfaceResolved: true } : {}
@@ -16190,6 +16640,8 @@ function buildRuleContext(opts) {
16190
16640
  roundTripIssues: opts.roundTripIssues,
16191
16641
  lintAllows,
16192
16642
  knownIssueCodes: opts.knownIssueCodes,
16643
+ carriedFindings,
16644
+ carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
16193
16645
  addIssue
16194
16646
  };
16195
16647
  return ctx;
@@ -22740,6 +23192,9 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22740
23192
  ...knownIssueCodes().map((rc) => rc.code),
22741
23193
  ...extensions.assertions.map((a) => a.fullCode)
22742
23194
  ]);
23195
+ const carryableCodes = new Set(
23196
+ knownIssueCodes().filter((rc) => rc.carryable).map((rc) => rc.code)
23197
+ );
22743
23198
  const ctx = buildRuleContext({
22744
23199
  system,
22745
23200
  subsystems,
@@ -22759,6 +23214,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22759
23214
  codeModel,
22760
23215
  roundTripIssues,
22761
23216
  knownIssueCodes: knownCodes,
23217
+ carryableIssueCodes: carryableCodes,
22762
23218
  issues
22763
23219
  });
22764
23220
  for (const rule of sequence) {
@@ -23692,10 +24148,10 @@ function findChainingParent(childRoot, ceiling) {
23692
24148
  if (bound2 && !isWithin(bound2, dir)) break;
23693
24149
  const specsDir = aiPathsAt(dir).specsDir();
23694
24150
  if (pathExists(specsDir)) {
23695
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24151
+ for (const file of listSpecFiles(specsDir)) {
23696
24152
  let raw;
23697
24153
  try {
23698
- raw = readYamlFile(file);
24154
+ raw = readSpecFile(file);
23699
24155
  } catch {
23700
24156
  continue;
23701
24157
  }
@@ -23727,10 +24183,10 @@ function inspectChainedRoots(rootDir = getProjectRoot()) {
23727
24183
  const walk2 = (projectDir, prefix, ancestors, depth) => {
23728
24184
  const specsDir = aiPathsAt(projectDir).specsDir();
23729
24185
  if (!pathExists(specsDir)) return;
23730
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24186
+ for (const file of listSpecFiles(specsDir)) {
23731
24187
  let raw;
23732
24188
  try {
23733
- raw = readYamlFile(file);
24189
+ raw = readSpecFile(file);
23734
24190
  } catch {
23735
24191
  continue;
23736
24192
  }
@@ -23914,7 +24370,7 @@ function computeSpecTreeSignature(dirs) {
23914
24370
  parts.push(`${dir}:missing`);
23915
24371
  continue;
23916
24372
  }
23917
- for (const f of listFilesRecursive(dir, ".yaml")) {
24373
+ for (const f of listSpecFiles(dir)) {
23918
24374
  try {
23919
24375
  const st = fs17.statSync(f);
23920
24376
  parts.push(`${f}:${st.mtimeMs}:${st.size}`);
@@ -24374,6 +24830,7 @@ var init_specs2 = __esm({
24374
24830
  init_canonical_json();
24375
24831
  init_lockfile();
24376
24832
  init_yaml();
24833
+ init_spec_files();
24377
24834
  init_models();
24378
24835
  init_narrative_labels();
24379
24836
  init_diagram();
@@ -24452,7 +24909,7 @@ var init_specs2 = __esm({
24452
24909
  const specsDir = projectPaths.specsDir();
24453
24910
  this.scanVisitedSpecDirs.push(specsDir);
24454
24911
  if (!pathExists(specsDir)) return index;
24455
- const files = listFilesRecursive(specsDir, ".yaml");
24912
+ const files = listSpecFiles(specsDir);
24456
24913
  const systemYaml = path23.normalize(projectPaths.specsSystem());
24457
24914
  const localSubprojects = [];
24458
24915
  for (const file of files) {
@@ -24461,7 +24918,7 @@ var init_specs2 = __esm({
24461
24918
  let detectedType = "spec";
24462
24919
  let rawId;
24463
24920
  try {
24464
- const raw = readYamlFile(file);
24921
+ const raw = readSpecFile(file);
24465
24922
  if (raw === null || typeof raw !== "object") {
24466
24923
  this.loaderIssues.push({
24467
24924
  severity: "error",
@@ -24944,7 +25401,7 @@ var init_specs2 = __esm({
24944
25401
  const p = this.paths.specsSystem();
24945
25402
  if (!pathExists(p)) return null;
24946
25403
  try {
24947
- const raw = readYamlFile(p);
25404
+ const raw = readSpecFile(p);
24948
25405
  return SystemSpecSchema.parse(raw);
24949
25406
  } catch (e) {
24950
25407
  this.loaderIssues.push({
@@ -24959,7 +25416,7 @@ var init_specs2 = __esm({
24959
25416
  saveSystemSpec(spec) {
24960
25417
  const p = this.paths.specsSystem();
24961
25418
  ensureDir(path23.dirname(p));
24962
- writeYamlFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
25419
+ writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
24963
25420
  invalidateSpecCache();
24964
25421
  }
24965
25422
  // -------------------------------------------------------------------------
@@ -24975,7 +25432,7 @@ var init_specs2 = __esm({
24975
25432
  const p = this.getSubsystemPath(id);
24976
25433
  if (!pathExists(p)) return null;
24977
25434
  try {
24978
- const raw = readYamlFile(p);
25435
+ const raw = readSpecFile(p);
24979
25436
  return SubsystemSpecSchema.parse(raw);
24980
25437
  } catch (e) {
24981
25438
  this.loaderIssues.push({
@@ -25109,7 +25566,7 @@ var init_specs2 = __esm({
25109
25566
  }
25110
25567
  }
25111
25568
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25112
- writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25569
+ writeSpecFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25113
25570
  invalidateSpecCache();
25114
25571
  }
25115
25572
  deleteSubsystemSpec(id) {
@@ -25133,7 +25590,7 @@ var init_specs2 = __esm({
25133
25590
  const p = this.getComponentPath(id);
25134
25591
  if (!pathExists(p)) return null;
25135
25592
  try {
25136
- const raw = readYamlFile(p);
25593
+ const raw = readSpecFile(p);
25137
25594
  return ComponentSpecSchema.parse(raw);
25138
25595
  } catch (e) {
25139
25596
  this.loaderIssues.push({
@@ -25170,7 +25627,7 @@ var init_specs2 = __esm({
25170
25627
  );
25171
25628
  }
25172
25629
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25173
- writeYamlFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25630
+ writeSpecFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25174
25631
  invalidateSpecCache();
25175
25632
  this.normalizeComponentLayout();
25176
25633
  return notices;
@@ -25231,7 +25688,7 @@ var init_specs2 = __esm({
25231
25688
  const p = this.getInterfacePath(id);
25232
25689
  if (!pathExists(p)) return null;
25233
25690
  try {
25234
- const raw = readYamlFile(p);
25691
+ const raw = readSpecFile(p);
25235
25692
  return InterfaceSpecSchema.parse(raw);
25236
25693
  } catch (e) {
25237
25694
  this.loaderIssues.push({
@@ -25262,7 +25719,7 @@ var init_specs2 = __esm({
25262
25719
  if (!pathExists(p)) return;
25263
25720
  let occupantId;
25264
25721
  try {
25265
- occupantId = readYamlFile(p)?.id;
25722
+ occupantId = readSpecFile(p)?.id;
25266
25723
  } catch {
25267
25724
  return;
25268
25725
  }
@@ -25299,7 +25756,7 @@ var init_specs2 = __esm({
25299
25756
  }
25300
25757
  }
25301
25758
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25302
- writeYamlFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25759
+ writeSpecFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25303
25760
  invalidateSpecCache();
25304
25761
  return notices;
25305
25762
  }
@@ -25324,7 +25781,7 @@ var init_specs2 = __esm({
25324
25781
  const p = this.getImplementationPath(id);
25325
25782
  if (!pathExists(p)) return null;
25326
25783
  try {
25327
- const raw = readYamlFile(p);
25784
+ const raw = readSpecFile(p);
25328
25785
  return ImplementationSpecSchema.parse(raw);
25329
25786
  } catch (e) {
25330
25787
  this.loaderIssues.push({
@@ -25356,7 +25813,7 @@ var init_specs2 = __esm({
25356
25813
  }
25357
25814
  }
25358
25815
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25359
- writeYamlFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25816
+ writeSpecFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25360
25817
  invalidateSpecCache();
25361
25818
  return notices;
25362
25819
  }
@@ -25409,7 +25866,7 @@ var init_specs2 = __esm({
25409
25866
  }
25410
25867
  }
25411
25868
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25412
- writeYamlFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25869
+ writeSpecFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25413
25870
  invalidateSpecCache();
25414
25871
  return notices;
25415
25872
  }
@@ -25440,7 +25897,7 @@ var init_specs2 = __esm({
25440
25897
  specToWrite.createdAt = existing.createdAt;
25441
25898
  }
25442
25899
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25443
- writeYamlFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
25900
+ writeSpecFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
25444
25901
  invalidateSpecCache();
25445
25902
  }
25446
25903
  deleteGroupSpec(id) {
@@ -25604,7 +26061,7 @@ var init_specs2 = __esm({
25604
26061
  findLegacySpecFiles() {
25605
26062
  const specsDir = this.paths.specsDir();
25606
26063
  if (!pathExists(specsDir)) return [];
25607
- const files = listFilesRecursive(specsDir, ".yaml");
26064
+ const files = listSpecFiles(specsDir);
25608
26065
  const legacy = [];
25609
26066
  for (const f of files) {
25610
26067
  const base = path23.basename(f);
@@ -30666,6 +31123,16 @@ async function runValidate(options = {}) {
30666
31123
  }
30667
31124
  }
30668
31125
  }
31126
+ const carried = projectConfig.rules?.conformance?.carried ?? [];
31127
+ if (carried.length > 0) {
31128
+ const findings = carried.flatMap((g) => g.findings ?? []);
31129
+ const units = findings.reduce((n, f) => n + (f.covers?.length ?? 1), 0);
31130
+ const perKind = (kind) => carried.filter((g) => g.kind === kind).reduce((n, g) => n + (g.findings?.length ?? 0), 0);
31131
+ logger.blank();
31132
+ logger.info(import_chalk7.default.yellow(
31133
+ `Conformance debt register: ${findings.length} finding(s) over ${units} unit(s) carried \u2014 ${perKind("drift")} drift, ${perKind("undecided")} undecided, ${perKind("unreadable")} unreadable (\`rules.conformance.carried\`). Drift and undecided are owed; unreadable is what the analysis cannot follow.`
31134
+ ));
31135
+ }
30669
31136
  logger.blank();
30670
31137
  if (options.ci && waivedWarnings > 0) {
30671
31138
  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.`));