@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/README.md +1 -0
- package/dist/cli/index.js +640 -123
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +604 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
13248
|
-
|
|
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
|
|
13254
|
-
const
|
|
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
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
13289
|
-
|
|
13290
|
-
|
|
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
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
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
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
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
|
|
14982
|
-
return ruleSet.
|
|
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:
|
|
15264
|
-
//
|
|
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
|
|
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))
|
|
15438
|
-
else if (ts.isPropertyAccessExpression(callee))
|
|
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
|
|
15451
|
-
for (const
|
|
15452
|
-
calls.set(fnName,
|
|
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
|
-
|
|
15480
|
-
if (
|
|
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))
|
|
15483
|
-
else for (const el of clause.namedBindings.elements)
|
|
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
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
|
|
15545
|
-
|
|
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
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
24151
|
+
for (const file of listSpecFiles(specsDir)) {
|
|
23688
24152
|
let raw;
|
|
23689
24153
|
try {
|
|
23690
|
-
raw =
|
|
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
|
|
24186
|
+
for (const file of listSpecFiles(specsDir)) {
|
|
23723
24187
|
let raw;
|
|
23724
24188
|
try {
|
|
23725
|
-
raw =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
});
|