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