@wairon/cli 5.1.1-dev.40 → 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.40";
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
 
@@ -3136,6 +3202,10 @@ var init_budget_policy = __esm({
3136
3202
  function canonicalize(value) {
3137
3203
  return JSON.stringify(sortKeys(value));
3138
3204
  }
3205
+ function compareOrdinal(a, b) {
3206
+ if (a < b) return -1;
3207
+ return a > b ? 1 : 0;
3208
+ }
3139
3209
  function sortKeys(v) {
3140
3210
  if (Array.isArray(v)) return v.map(sortKeys);
3141
3211
  if (v && typeof v === "object") {
@@ -3156,14 +3226,17 @@ var init_canonical_json = __esm({
3156
3226
  });
3157
3227
 
3158
3228
  // src/core/statehash.ts
3229
+ function inIdentityOrder(specs) {
3230
+ return specs.map((spec) => ({ spec, content: canonicalize(spec) })).sort((a, b) => compareOrdinal(a.spec.id, b.spec.id) || compareOrdinal(a.content, b.content)).map((entry) => entry.spec);
3231
+ }
3159
3232
  function computeStateId() {
3160
3233
  const tree = {
3161
3234
  system: loadSystemSpec(),
3162
- subsystems: loadSubsystemSpecs(),
3163
- components: loadComponentSpecs(),
3164
- interfaces: loadInterfaceSpecs(),
3165
- implementations: loadImplementationSpecs(),
3166
- types: loadTypeSpecs()
3235
+ subsystems: inIdentityOrder(loadSubsystemSpecs()),
3236
+ components: inIdentityOrder(loadComponentSpecs()),
3237
+ interfaces: inIdentityOrder(loadInterfaceSpecs()),
3238
+ implementations: inIdentityOrder(loadImplementationSpecs()),
3239
+ types: inIdentityOrder(loadTypeSpecs())
3167
3240
  };
3168
3241
  const digest2 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
3169
3242
  return { algorithm: CONTENT_ALGORITHM, digest: digest2 };
@@ -3248,6 +3321,26 @@ var init_lockfile = __esm({
3248
3321
  }
3249
3322
  });
3250
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
+
3251
3344
  // src/core/narrative-labels.ts
3252
3345
  function resolveNarrativeLabels(methodName, steps) {
3253
3346
  const errors = [];
@@ -8426,7 +8519,7 @@ function discoverPacks(dir) {
8426
8519
  return [];
8427
8520
  }
8428
8521
  const refs = [];
8429
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
8522
+ for (const e of entries.sort((a, b) => compareOrdinal(a.name, b.name))) {
8430
8523
  const full = path12.join(dir, e.name);
8431
8524
  if (e.isDirectory()) {
8432
8525
  if (packDirEntry(full)) refs.push(full);
@@ -8716,6 +8809,7 @@ var init_extensions = __esm({
8716
8809
  init_fs();
8717
8810
  init_project_config();
8718
8811
  init_version();
8812
+ init_canonical_json();
8719
8813
  init_project();
8720
8814
  init_packstore();
8721
8815
  ProfileDefSchema = import_zod9.z.object({
@@ -8909,13 +9003,84 @@ function buildCodeIndex(model) {
8909
9003
  cache2.set(path70, built);
8910
9004
  return built;
8911
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]));
8912
9075
  return {
8913
9076
  paths,
8914
9077
  exactPaths,
8915
9078
  factsAt: (path70) => facts.get(pathKey(path70)),
8916
- declarationsAt: (path70) => namesAt(declarations, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames])),
9079
+ declarationsAt,
8917
9080
  anchorsAt: (path70) => namesAt(anchors, pathKey(path70), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
8918
- 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
8919
9084
  };
8920
9085
  }
8921
9086
  function buildRealizationIndex(ctx) {
@@ -13144,6 +13309,21 @@ var init_source_file_linkage = __esm({
13144
13309
  });
13145
13310
 
13146
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
+ }
13147
13327
  var methodRealizationRule;
13148
13328
  var init_method_realization = __esm({
13149
13329
  "src/core/rules/conformance/method-realization.ts"() {
@@ -13151,9 +13331,10 @@ var init_method_realization = __esm({
13151
13331
  init_models();
13152
13332
  methodRealizationRule = {
13153
13333
  name: "method-realization",
13154
- 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.",
13155
13335
  codes: [
13156
- { 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 }
13157
13338
  ],
13158
13339
  check(ctx) {
13159
13340
  const code = ctx.codeIndex();
@@ -13177,7 +13358,22 @@ var init_method_realization = __esm({
13177
13358
  const inDeclared = code.declarationsAt(file).has(symbol);
13178
13359
  const inAnchored = inDeclared || code.anchorsAt(file).has(symbol);
13179
13360
  const realized = tier === "declared" ? inDeclared : inAnchored;
13180
- 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
+ }
13181
13377
  const label = methodImpl?.symbol ? `"${method2.name}" (symbol "${symbol}")` : `"${method2.name}"`;
13182
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`." : "";
13183
13379
  ctx.addIssue(
@@ -13244,24 +13440,54 @@ var init_finding_realization = __esm({
13244
13440
  });
13245
13441
 
13246
13442
  // src/core/rules/conformance/call-conformance.ts
13247
- function ownEntry(record2, key) {
13248
- return record2 && Object.prototype.hasOwnProperty.call(record2, key) ? record2[key] : void 0;
13249
- }
13250
- function closedCallees(facts, fn) {
13251
- 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);
13252
13446
  if (!direct) return void 0;
13253
- const closed = new Set(direct);
13254
- 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 }));
13255
13451
  while (queue.length) {
13256
- const name = queue.pop();
13257
- for (const next of ownEntry(facts.functionCalls, name) ?? []) {
13258
- if (!closed.has(next)) {
13259
- closed.add(next);
13260
- queue.push(next);
13261
- }
13262
- }
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 })));
13263
13462
  }
13264
- 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})`;
13265
13491
  }
13266
13492
  var callConformanceRule;
13267
13493
  var init_call_conformance = __esm({
@@ -13270,13 +13496,24 @@ var init_call_conformance = __esm({
13270
13496
  init_models();
13271
13497
  callConformanceRule = {
13272
13498
  name: "call-conformance",
13273
- 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.",
13274
13500
  codes: [
13275
- { 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 }
13276
13504
  ],
13277
13505
  check(ctx) {
13278
13506
  const code = ctx.codeIndex();
13279
- 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) {
13280
13517
  const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
13281
13518
  const tier = implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component);
13282
13519
  if (tier === "off") continue;
@@ -13284,37 +13521,91 @@ var init_call_conformance = __esm({
13284
13521
  if (!file) continue;
13285
13522
  const facts = code.factsAt(file);
13286
13523
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
13524
+ const here = pathKey(file);
13287
13525
  const fnSymbol = implMethod.symbol ?? implMethod.name;
13288
- const callees = closedCallees(facts, fnSymbol);
13289
- if (!callees) continue;
13290
- 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 = [];
13291
13530
  for (const step of implMethod.narrative) {
13292
13531
  if (step.type !== "call" || !step.targetComponent || !step.targetMethod) continue;
13293
13532
  if (!ctx.componentMap.has(step.targetComponent)) continue;
13294
- const accepted = /* @__PURE__ */ new Set([step.targetMethod]);
13295
- for (const targetIntf of ctx.interfacesByComponent.get(step.targetComponent) ?? []) {
13296
- for (const targetImpl of ctx.implementationsByContract.get(targetIntf.id) ?? []) {
13297
- const targetMethod = targetImpl.methods.find((m) => m.name === step.targetMethod);
13298
- 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" } });
13299
13540
  }
13541
+ continue;
13300
13542
  }
13301
- if (accepted.has(fnSymbol)) continue;
13302
- if ([...accepted].some((name) => callees.has(name))) continue;
13303
- missing.push({
13304
- step: step.stepNumber,
13305
- target: `${step.targetComponent}.${step.targetMethod}`,
13306
- accepted: [...accepted]
13307
- });
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
+ );
13308
13608
  }
13309
- if (missing.length === 0) continue;
13310
- const detail = missing.map((m) => `step ${m.step} \u2192 ${m.target} (looked for ${m.accepted.map((a) => `"${a}"`).join(" / ")})`).join("; ");
13311
- ctx.addIssue(
13312
- "warning",
13313
- "CALL_STEP_UNREALIZED",
13314
- `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.`,
13315
- impl.id,
13316
- entry.draftContext
13317
- );
13318
13609
  }
13319
13610
  }
13320
13611
  };
@@ -13824,6 +14115,69 @@ var init_unclaimed_source = __esm({
13824
14115
  }
13825
14116
  });
13826
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
+
13827
14181
  // src/core/rules/heuristic/coupling-health.ts
13828
14182
  var DEFAULT_GOD_COMPONENT_THRESHOLD, couplingRule;
13829
14183
  var init_coupling_health = __esm({
@@ -14978,8 +15332,8 @@ function registerPackRules(packRules) {
14978
15332
  for (const rule of packRules) addRule(rule);
14979
15333
  }
14980
15334
  function ruleSequence() {
14981
- const base = ruleSet.filter((r) => r !== lintAllowsRule);
14982
- 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];
14983
15337
  }
14984
15338
  function specScopedRules() {
14985
15339
  return ruleSequence().filter((r) => r.scope === "spec");
@@ -15069,6 +15423,7 @@ var init_repository = __esm({
15069
15423
  init_integration_sim_coverage();
15070
15424
  init_type_realization();
15071
15425
  init_unclaimed_source();
15426
+ init_carried_debt();
15072
15427
  init_coupling_health();
15073
15428
  init_signature_language_builtins();
15074
15429
  init_narrative_language_constructs();
@@ -15260,8 +15615,11 @@ var init_repository = __esm({
15260
15615
  // elsewhere?) rather than spec content.
15261
15616
  packResolutionRule,
15262
15617
  reproducibilityRule,
15263
- // MUST run last: it audits which lint.allow entries the earlier rules
15264
- // 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,
15265
15623
  lintAllowsRule
15266
15624
  ];
15267
15625
  ruleSet = [];
@@ -15384,9 +15742,15 @@ function walkExact(ts, sourceText, fileName) {
15384
15742
  const imports = /* @__PURE__ */ new Set();
15385
15743
  const reexports = /* @__PURE__ */ new Set();
15386
15744
  const starExports = [];
15745
+ const namedReexports = [];
15387
15746
  const complexity = /* @__PURE__ */ new Map();
15388
15747
  const calls = /* @__PURE__ */ new Map();
15748
+ const importBindings = /* @__PURE__ */ new Map();
15749
+ const typeOnlyBindings = /* @__PURE__ */ new Map();
15750
+ const fieldTypes = /* @__PURE__ */ new Map();
15389
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}`;
15390
15754
  const addBindingNames = (name) => {
15391
15755
  if (ts.isIdentifier(name)) declared.add(name.text);
15392
15756
  else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
@@ -15400,6 +15764,17 @@ function walkExact(ts, sourceText, fileName) {
15400
15764
  if (ts.isPrivateIdentifier(name)) return name.text;
15401
15765
  return void 0;
15402
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
+ };
15403
15778
  const hasExportModifier = (node) => {
15404
15779
  const mods = node.modifiers;
15405
15780
  return !!mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
@@ -15422,7 +15797,10 @@ function walkExact(ts, sourceText, fileName) {
15422
15797
  };
15423
15798
  const collectFunctionFacts = (fn) => {
15424
15799
  let score = 1;
15425
- const callees = /* @__PURE__ */ new Set();
15800
+ const callees = /* @__PURE__ */ new Map();
15801
+ const addSite = (site) => {
15802
+ callees.set(siteKey(site), site);
15803
+ };
15426
15804
  const count = (node) => {
15427
15805
  if (namedFunctionName(node) !== void 0) return;
15428
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)) {
@@ -15434,8 +15812,17 @@ function walkExact(ts, sourceText, fileName) {
15434
15812
  }
15435
15813
  } else if (ts.isCallExpression(node)) {
15436
15814
  const callee = node.expression;
15437
- if (ts.isIdentifier(callee)) callees.add(callee.text);
15438
- 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
+ }
15439
15826
  }
15440
15827
  ts.forEachChild(node, count);
15441
15828
  };
@@ -15447,9 +15834,13 @@ function walkExact(ts, sourceText, fileName) {
15447
15834
  if (fnName && node.body) {
15448
15835
  const facts = collectFunctionFacts(node);
15449
15836
  complexity.set(fnName, Math.max(complexity.get(fnName) ?? 0, facts.score));
15450
- const set = calls.get(fnName) ?? /* @__PURE__ */ new Set();
15451
- for (const c of facts.callees) set.add(c);
15452
- 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);
15453
15844
  }
15454
15845
  if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
15455
15846
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : void 0;
@@ -15476,11 +15867,24 @@ function walkExact(ts, sourceText, fileName) {
15476
15867
  } else if (ts.isImportDeclaration(node)) {
15477
15868
  const clause = node.importClause;
15478
15869
  const typeOnly = clause?.isTypeOnly ?? false;
15479
- if (!typeOnly && ts.isStringLiteral(node.moduleSpecifier)) imports.add(node.moduleSpecifier.text);
15480
- 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);
15481
15885
  if (clause?.namedBindings) {
15482
- if (ts.isNamespaceImport(clause.namedBindings)) declared.add(clause.namedBindings.name.text);
15483
- 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);
15484
15888
  }
15485
15889
  } else if (ts.isExportDeclaration(node)) {
15486
15890
  const spec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : void 0;
@@ -15489,6 +15893,9 @@ function walkExact(ts, sourceText, fileName) {
15489
15893
  for (const el of node.exportClause.elements) {
15490
15894
  declared.add(el.name.text);
15491
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
+ }
15492
15899
  }
15493
15900
  } else if (!node.exportClause && spec) {
15494
15901
  starExports.push(spec);
@@ -15511,7 +15918,7 @@ function walkExact(ts, sourceText, fileName) {
15511
15918
  };
15512
15919
  visit(sf);
15513
15920
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
15514
- 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 };
15515
15922
  }
15516
15923
  function resolveRelativeModule(fromFile, specifier) {
15517
15924
  if (!specifier.startsWith(".")) return null;
@@ -15534,34 +15941,47 @@ function resolveRelativeModule(fromFile, specifier) {
15534
15941
  }
15535
15942
  return null;
15536
15943
  }
15537
- function chaseStarExports(ts, facts, filePath, projectRoot2, visited, exactCache) {
15538
- for (const spec of facts.starExports) {
15539
- const target = resolveRelativeModule(filePath, spec);
15540
- if (!target || visited.has(target)) continue;
15541
- if (path13.relative(projectRoot2, target).startsWith("..")) continue;
15542
- visited.add(target);
15543
- let targetFacts = exactCache.get(target);
15544
- if (targetFacts === void 0) {
15545
- try {
15546
- targetFacts = walkExact(ts, fs10.readFileSync(target, "utf8"), target);
15547
- } catch {
15548
- targetFacts = null;
15549
- }
15550
- 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;
15551
15953
  }
15552
- if (!targetFacts) continue;
15553
- chaseStarExports(ts, targetFacts, target, projectRoot2, visited, exactCache);
15554
- 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) {
15555
15982
  facts.exported.add(name);
15556
15983
  facts.declared.add(name);
15557
- const c = targetFacts.complexity.get(name);
15558
- if (c !== void 0) facts.complexity.set(name, Math.max(facts.complexity.get(name) ?? 0, c));
15559
- const targetCalls = targetFacts.calls.get(name);
15560
- if (targetCalls) {
15561
- const set = facts.calls.get(name) ?? /* @__PURE__ */ new Set();
15562
- for (const callee of targetCalls) set.add(callee);
15563
- facts.calls.set(name, set);
15564
- }
15984
+ carryBody(facts, target.facts, target.path, name, name, projectRoot2);
15565
15985
  }
15566
15986
  }
15567
15987
  }
@@ -15666,7 +16086,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15666
16086
  if (ts) {
15667
16087
  try {
15668
16088
  const facts = walkExact(ts, text2, absolute);
15669
- chaseStarExports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
16089
+ chaseReexports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
15670
16090
  analyzed = {
15671
16091
  analysisGrade: "exact",
15672
16092
  declaredNames: [...facts.declared],
@@ -15675,7 +16095,10 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
15675
16095
  imports: [...facts.imports],
15676
16096
  reexports: [...facts.reexports],
15677
16097
  functionComplexity: Object.fromEntries(facts.complexity),
15678
- 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]])),
15679
16102
  topLevelMutableBindings: [...facts.mutableBindings],
15680
16103
  reexportOnly: facts.reexportOnly
15681
16104
  };
@@ -16084,7 +16507,27 @@ function buildRuleContext(opts) {
16084
16507
  for (const i of interfaces) collectAllows(i.id, i.lint);
16085
16508
  for (const im of implementations) collectAllows(im.id, im.lint);
16086
16509
  for (const t of types) collectAllows(t.id, t.lint);
16087
- 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) => {
16088
16531
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
16089
16532
  return;
16090
16533
  }
@@ -16102,10 +16545,25 @@ function buildRuleContext(opts) {
16102
16545
  if (severity === "warning") return;
16103
16546
  }
16104
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
+ }
16105
16563
  issues.push({
16106
16564
  severity,
16107
16565
  code,
16108
- message,
16566
+ message: text2,
16109
16567
  specId,
16110
16568
  ...isDraftContext ? { draftContext: true } : {},
16111
16569
  ...surfaceResolved ? { surfaceResolved: true } : {}
@@ -16182,6 +16640,8 @@ function buildRuleContext(opts) {
16182
16640
  roundTripIssues: opts.roundTripIssues,
16183
16641
  lintAllows,
16184
16642
  knownIssueCodes: opts.knownIssueCodes,
16643
+ carriedFindings,
16644
+ carryableIssueCodes: opts.carryableIssueCodes ?? /* @__PURE__ */ new Set(),
16185
16645
  addIssue
16186
16646
  };
16187
16647
  return ctx;
@@ -22436,7 +22896,7 @@ var init_core = __esm({
22436
22896
 
22437
22897
  // src/core/rules/gate-identity.ts
22438
22898
  function doctrineIdentity(doctrine, builtinRules, gate) {
22439
- const byKey = (items, key) => [...items].sort((a, b) => key(a).localeCompare(key(b)));
22899
+ const byKey = (items, key) => [...items].sort((a, b) => compareOrdinal(key(a), key(b)));
22440
22900
  return {
22441
22901
  /**
22442
22902
  * The BUILTIN rule set, by identity rather than by wairon version.
@@ -22732,6 +23192,9 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22732
23192
  ...knownIssueCodes().map((rc) => rc.code),
22733
23193
  ...extensions.assertions.map((a) => a.fullCode)
22734
23194
  ]);
23195
+ const carryableCodes = new Set(
23196
+ knownIssueCodes().filter((rc) => rc.carryable).map((rc) => rc.code)
23197
+ );
22735
23198
  const ctx = buildRuleContext({
22736
23199
  system,
22737
23200
  subsystems,
@@ -22751,6 +23214,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
22751
23214
  codeModel,
22752
23215
  roundTripIssues,
22753
23216
  knownIssueCodes: knownCodes,
23217
+ carryableIssueCodes: carryableCodes,
22754
23218
  issues
22755
23219
  });
22756
23220
  for (const rule of sequence) {
@@ -23684,10 +24148,10 @@ function findChainingParent(childRoot, ceiling) {
23684
24148
  if (bound2 && !isWithin(bound2, dir)) break;
23685
24149
  const specsDir = aiPathsAt(dir).specsDir();
23686
24150
  if (pathExists(specsDir)) {
23687
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24151
+ for (const file of listSpecFiles(specsDir)) {
23688
24152
  let raw;
23689
24153
  try {
23690
- raw = readYamlFile(file);
24154
+ raw = readSpecFile(file);
23691
24155
  } catch {
23692
24156
  continue;
23693
24157
  }
@@ -23719,10 +24183,10 @@ function inspectChainedRoots(rootDir = getProjectRoot()) {
23719
24183
  const walk2 = (projectDir, prefix, ancestors, depth) => {
23720
24184
  const specsDir = aiPathsAt(projectDir).specsDir();
23721
24185
  if (!pathExists(specsDir)) return;
23722
- for (const file of listFilesRecursive(specsDir, ".yaml")) {
24186
+ for (const file of listSpecFiles(specsDir)) {
23723
24187
  let raw;
23724
24188
  try {
23725
- raw = readYamlFile(file);
24189
+ raw = readSpecFile(file);
23726
24190
  } catch {
23727
24191
  continue;
23728
24192
  }
@@ -23906,7 +24370,7 @@ function computeSpecTreeSignature(dirs) {
23906
24370
  parts.push(`${dir}:missing`);
23907
24371
  continue;
23908
24372
  }
23909
- for (const f of listFilesRecursive(dir, ".yaml")) {
24373
+ for (const f of listSpecFiles(dir)) {
23910
24374
  try {
23911
24375
  const st = fs17.statSync(f);
23912
24376
  parts.push(`${f}:${st.mtimeMs}:${st.size}`);
@@ -24366,6 +24830,7 @@ var init_specs2 = __esm({
24366
24830
  init_canonical_json();
24367
24831
  init_lockfile();
24368
24832
  init_yaml();
24833
+ init_spec_files();
24369
24834
  init_models();
24370
24835
  init_narrative_labels();
24371
24836
  init_diagram();
@@ -24444,7 +24909,7 @@ var init_specs2 = __esm({
24444
24909
  const specsDir = projectPaths.specsDir();
24445
24910
  this.scanVisitedSpecDirs.push(specsDir);
24446
24911
  if (!pathExists(specsDir)) return index;
24447
- const files = listFilesRecursive(specsDir, ".yaml");
24912
+ const files = listSpecFiles(specsDir);
24448
24913
  const systemYaml = path23.normalize(projectPaths.specsSystem());
24449
24914
  const localSubprojects = [];
24450
24915
  for (const file of files) {
@@ -24453,7 +24918,7 @@ var init_specs2 = __esm({
24453
24918
  let detectedType = "spec";
24454
24919
  let rawId;
24455
24920
  try {
24456
- const raw = readYamlFile(file);
24921
+ const raw = readSpecFile(file);
24457
24922
  if (raw === null || typeof raw !== "object") {
24458
24923
  this.loaderIssues.push({
24459
24924
  severity: "error",
@@ -24936,7 +25401,7 @@ var init_specs2 = __esm({
24936
25401
  const p = this.paths.specsSystem();
24937
25402
  if (!pathExists(p)) return null;
24938
25403
  try {
24939
- const raw = readYamlFile(p);
25404
+ const raw = readSpecFile(p);
24940
25405
  return SystemSpecSchema.parse(raw);
24941
25406
  } catch (e) {
24942
25407
  this.loaderIssues.push({
@@ -24951,7 +25416,7 @@ var init_specs2 = __esm({
24951
25416
  saveSystemSpec(spec) {
24952
25417
  const p = this.paths.specsSystem();
24953
25418
  ensureDir(path23.dirname(p));
24954
- writeYamlFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
25419
+ writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
24955
25420
  invalidateSpecCache();
24956
25421
  }
24957
25422
  // -------------------------------------------------------------------------
@@ -24967,7 +25432,7 @@ var init_specs2 = __esm({
24967
25432
  const p = this.getSubsystemPath(id);
24968
25433
  if (!pathExists(p)) return null;
24969
25434
  try {
24970
- const raw = readYamlFile(p);
25435
+ const raw = readSpecFile(p);
24971
25436
  return SubsystemSpecSchema.parse(raw);
24972
25437
  } catch (e) {
24973
25438
  this.loaderIssues.push({
@@ -25101,7 +25566,7 @@ var init_specs2 = __esm({
25101
25566
  }
25102
25567
  }
25103
25568
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25104
- writeYamlFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25569
+ writeSpecFile(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
25105
25570
  invalidateSpecCache();
25106
25571
  }
25107
25572
  deleteSubsystemSpec(id) {
@@ -25125,7 +25590,7 @@ var init_specs2 = __esm({
25125
25590
  const p = this.getComponentPath(id);
25126
25591
  if (!pathExists(p)) return null;
25127
25592
  try {
25128
- const raw = readYamlFile(p);
25593
+ const raw = readSpecFile(p);
25129
25594
  return ComponentSpecSchema.parse(raw);
25130
25595
  } catch (e) {
25131
25596
  this.loaderIssues.push({
@@ -25162,7 +25627,7 @@ var init_specs2 = __esm({
25162
25627
  );
25163
25628
  }
25164
25629
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25165
- writeYamlFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25630
+ writeSpecFile(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
25166
25631
  invalidateSpecCache();
25167
25632
  this.normalizeComponentLayout();
25168
25633
  return notices;
@@ -25223,7 +25688,7 @@ var init_specs2 = __esm({
25223
25688
  const p = this.getInterfacePath(id);
25224
25689
  if (!pathExists(p)) return null;
25225
25690
  try {
25226
- const raw = readYamlFile(p);
25691
+ const raw = readSpecFile(p);
25227
25692
  return InterfaceSpecSchema.parse(raw);
25228
25693
  } catch (e) {
25229
25694
  this.loaderIssues.push({
@@ -25254,7 +25719,7 @@ var init_specs2 = __esm({
25254
25719
  if (!pathExists(p)) return;
25255
25720
  let occupantId;
25256
25721
  try {
25257
- occupantId = readYamlFile(p)?.id;
25722
+ occupantId = readSpecFile(p)?.id;
25258
25723
  } catch {
25259
25724
  return;
25260
25725
  }
@@ -25291,7 +25756,7 @@ var init_specs2 = __esm({
25291
25756
  }
25292
25757
  }
25293
25758
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25294
- writeYamlFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25759
+ writeSpecFile(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
25295
25760
  invalidateSpecCache();
25296
25761
  return notices;
25297
25762
  }
@@ -25316,7 +25781,7 @@ var init_specs2 = __esm({
25316
25781
  const p = this.getImplementationPath(id);
25317
25782
  if (!pathExists(p)) return null;
25318
25783
  try {
25319
- const raw = readYamlFile(p);
25784
+ const raw = readSpecFile(p);
25320
25785
  return ImplementationSpecSchema.parse(raw);
25321
25786
  } catch (e) {
25322
25787
  this.loaderIssues.push({
@@ -25348,7 +25813,7 @@ var init_specs2 = __esm({
25348
25813
  }
25349
25814
  }
25350
25815
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25351
- writeYamlFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25816
+ writeSpecFile(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
25352
25817
  invalidateSpecCache();
25353
25818
  return notices;
25354
25819
  }
@@ -25401,7 +25866,7 @@ var init_specs2 = __esm({
25401
25866
  }
25402
25867
  }
25403
25868
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25404
- writeYamlFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25869
+ writeSpecFile(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
25405
25870
  invalidateSpecCache();
25406
25871
  return notices;
25407
25872
  }
@@ -25432,7 +25897,7 @@ var init_specs2 = __esm({
25432
25897
  specToWrite.createdAt = existing.createdAt;
25433
25898
  }
25434
25899
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
25435
- writeYamlFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
25900
+ writeSpecFile(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
25436
25901
  invalidateSpecCache();
25437
25902
  }
25438
25903
  deleteGroupSpec(id) {
@@ -25596,7 +26061,7 @@ var init_specs2 = __esm({
25596
26061
  findLegacySpecFiles() {
25597
26062
  const specsDir = this.paths.specsDir();
25598
26063
  if (!pathExists(specsDir)) return [];
25599
- const files = listFilesRecursive(specsDir, ".yaml");
26064
+ const files = listSpecFiles(specsDir);
25600
26065
  const legacy = [];
25601
26066
  for (const f of files) {
25602
26067
  const base = path23.basename(f);
@@ -30445,6 +30910,37 @@ var path35 = __toESM(require("path"));
30445
30910
  init_core();
30446
30911
  init_fs();
30447
30912
  init_validation();
30913
+ function checkApproval(strict) {
30914
+ if (!loadSystemSpec()) {
30915
+ return {
30916
+ state: "no-tree",
30917
+ approved: !strict,
30918
+ message: strict ? "No SDD spec tree here (.wai/specs) \u2014 and --strict asks for an approved design, so this is a failure. Run `wairon init` to start one, or drop --strict." : "No SDD spec tree here (.wai/specs) \u2014 there is no design to approve, so nothing is gated."
30919
+ };
30920
+ }
30921
+ const lock = readLockState(computeGateStateId());
30922
+ const record2 = lock.record;
30923
+ switch (lock.state) {
30924
+ case "locked":
30925
+ return {
30926
+ state: "locked",
30927
+ approved: true,
30928
+ message: `The design in this tree is the approved design \u2014 approved ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}.`
30929
+ };
30930
+ case "stale":
30931
+ return {
30932
+ state: "stale",
30933
+ approved: false,
30934
+ message: `The design changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
30935
+ };
30936
+ default:
30937
+ return {
30938
+ state: "unlocked",
30939
+ approved: !strict,
30940
+ message: strict ? "No approval on record (.wai/lock.json is absent), and --strict asks for one. Run `wairon lock` and commit the record." : "No approval on record (.wai/lock.json is absent) \u2014 this project has not opted into the approval gate, so nothing is gated. Run `wairon lock` and commit the record to turn it on, or pass --strict to fail here instead."
30941
+ };
30942
+ }
30943
+ }
30448
30944
  async function runLock(options = {}, gate) {
30449
30945
  const diff = diffAgainstApproval();
30450
30946
  if (!diff) {
@@ -30627,6 +31123,16 @@ async function runValidate(options = {}) {
30627
31123
  }
30628
31124
  }
30629
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
+ }
30630
31136
  logger.blank();
30631
31137
  if (options.ci && waivedWarnings > 0) {
30632
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.`));
@@ -47147,6 +47653,17 @@ async function statusCommand(opts) {
47147
47653
  }
47148
47654
  await runStatus({ subsystem: opts.subsystem, recursive: opts.recursive });
47149
47655
  }
47656
+ async function lockCheckCommand(opts) {
47657
+ const verdict = checkApproval(opts.strict === true);
47658
+ if (!verdict.approved) logger.error(verdict.message);
47659
+ else if (verdict.state === "locked") logger.success(verdict.message);
47660
+ else logger.info(verdict.message);
47661
+ if (verdict.approved) return;
47662
+ process.exit(1);
47663
+ }
47664
+ program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against the committed .wai/lock.json. Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
47665
+ await lockCheckCommand(opts);
47666
+ });
47150
47667
  program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
47151
47668
  await lockCommand(opts);
47152
47669
  });