@ecoma-io/archkeep 0.22.2 → 0.24.0
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/package.json +40 -13
- package/src/analysis/jvm/packages.mjs +0 -17
- package/src/analysis/manifest-util.mjs +14 -5
- package/src/analysis/markdown.mjs +340 -0
- package/src/analysis/source-util.mjs +5 -4
- package/src/analysis/typescript.mjs +146 -0
- package/src/architecture-intent/judge.mjs +1 -1
- package/src/architecture-intent/model.mjs +3 -12
- package/src/commands/README.md +16 -7
- package/src/commands/change-intent.mjs +2 -11
- package/src/commands/check.mjs +234 -12
- package/src/commands/completeness.mjs +0 -32
- package/src/commands/context-command.mjs +13 -21
- package/src/commands/context.mjs +46 -47
- package/src/commands/coverage-verdict.mjs +12 -2
- package/src/commands/delta-snapshot.mjs +1 -5
- package/src/commands/diff.mjs +1 -1
- package/src/commands/discover.mjs +7 -3
- package/src/commands/evaluation-primitives.mjs +6 -2
- package/src/commands/explain.mjs +17 -20
- package/src/commands/graph.mjs +7 -0
- package/src/commands/health.mjs +4 -0
- package/src/commands/impact-reachability.mjs +104 -0
- package/src/commands/impact.mjs +9 -71
- package/src/commands/plan-context-command.mjs +5 -1
- package/src/commands/policy.mjs +4 -4
- package/src/commands/provenance.mjs +8 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/config.mjs +171 -17
- package/src/custom-rules/evidence.mjs +1 -1
- package/src/custom-rules/host.mjs +2 -2
- package/src/custom-rules/values.mjs +8 -3
- package/src/errors.mjs +24 -2
- package/src/eslint-config.mjs +2 -5
- package/src/fixtures/evolution-lifecycle/workspace.mjs +0 -5
- package/src/governance/adr-registry.mjs +33 -17
- package/src/governance/decision-graph.mjs +1 -1
- package/src/governance/evolution-store.mjs +36 -18
- package/src/governance/fitness-registry.mjs +1 -14
- package/src/governance/profile-registry.mjs +20 -23
- package/src/governance/provenance-record.mjs +1 -11
- package/src/governance/reconcile-score.mjs +0 -3
- package/src/governance/row-schema.mjs +1 -14
- package/src/governance/verdict.mjs +168 -4
- package/src/intent/intent-manifest.json +16 -16
- package/src/lsp/diagnose.mjs +2 -2
- package/src/lsp/server.mjs +1 -1
- package/src/lsp/workspace-index.mjs +3 -3
- package/src/options.mjs +1 -1
- package/src/providers/model-gate.mjs +59 -0
- package/src/providers/moon.mjs +6 -6
- package/src/providers/native/model.mjs +2 -16
- package/src/report/README.md +13 -7
- package/src/report/evidence.mjs +11 -168
- package/src/report/json.mjs +10 -7
- package/src/report/sarif.mjs +29 -4
- package/src/report/text.mjs +39 -0
- package/src/rules/README.md +18 -9
- package/src/{commands → rules}/edge-constraints.mjs +18 -12
- package/src/rules/index.mjs +30 -0
- package/src/values.mjs +49 -0
- package/src/verdict.mjs +58 -7
- package/src/workspace.mjs +29 -0
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
isWholeFileFailure,
|
|
50
50
|
unresolvableLiteralCount,
|
|
51
51
|
} from "../analysis/source-util.mjs";
|
|
52
|
-
import { EXIT, coverageIncompleteReasons } from "../verdict.mjs";
|
|
52
|
+
import { EXIT, coverageComplete, coverageIncompleteReasons } from "../verdict.mjs";
|
|
53
53
|
import { buildDecision } from "../report/evidence.mjs";
|
|
54
54
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
55
55
|
import { formatCoverageIncomplete } from "../report/text.mjs";
|
|
@@ -96,7 +96,17 @@ export function coverageVerdict(commandContext, { acceptedFiles } = {}) {
|
|
|
96
96
|
.map(({ sourceFile, reason }) => ({ file: sourceFile, reason }));
|
|
97
97
|
const blindSpots = blindSpotRows(analysis.failures);
|
|
98
98
|
const blindSpotCount = unresolvableLiteralCount(analysis.failures);
|
|
99
|
-
|
|
99
|
+
// The conjunction is `../verdict.mjs`'s `coverageComplete` — the same
|
|
100
|
+
// predicate `verdictFor`'s decision face reads, so `check`'s envelope
|
|
101
|
+
// cannot carry a `coverage.complete` and a `decision.coverageComplete`
|
|
102
|
+
// that disagree about one run. The predicate is defined there, not here,
|
|
103
|
+
// because `verdictFor` needs it and this module already imports from that
|
|
104
|
+
// one: the reverse import would be a cycle.
|
|
105
|
+
const complete = coverageComplete({
|
|
106
|
+
unchecked: notAnalyzed.length,
|
|
107
|
+
blindSpotCount,
|
|
108
|
+
analyzed: analysis.analyzed,
|
|
109
|
+
});
|
|
100
110
|
return {
|
|
101
111
|
notAnalyzed,
|
|
102
112
|
blindSpots,
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
import { readFileSync } from "node:fs";
|
|
49
49
|
|
|
50
50
|
import { canonicalJsonReplacer } from "../canonical.mjs";
|
|
51
|
+
import { isPlainObject } from "../values.mjs";
|
|
51
52
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
52
53
|
|
|
53
54
|
/** The only snapshot schemaVersion this module writes and reads. */
|
|
@@ -518,11 +519,6 @@ export function providerMismatch(baselineProvider, currentProvider) {
|
|
|
518
519
|
);
|
|
519
520
|
}
|
|
520
521
|
|
|
521
|
-
/** Non-empty plain-object guard used across validation. */
|
|
522
|
-
function isPlainObject(value) {
|
|
523
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
522
|
/** Describes a value for error messages without dumping it. */
|
|
527
523
|
function describe(value) {
|
|
528
524
|
if (value === null) return "null";
|
package/src/commands/diff.mjs
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
import { readFileSync } from "node:fs";
|
|
42
42
|
|
|
43
43
|
import { buildDependencies, buildProjects, computePolicyFingerprint } from "./graph.mjs";
|
|
44
|
-
import { computeRuleImpact } from "
|
|
44
|
+
import { computeRuleImpact } from "../rules/edge-constraints.mjs";
|
|
45
45
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
46
46
|
import { SCHEMA_VERSION } from "../report/json.mjs";
|
|
47
47
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
* - **`--propose`** — computes the candidate architecture over those same
|
|
15
15
|
* observations (`src/governance/discovery-proposal.mjs`'s
|
|
16
16
|
* `evaluateDiscovery`) and emits it with `proposed: true` and
|
|
17
|
-
* `notAuthoritative: true` on every candidate.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `notAuthoritative: true` on every candidate. This layer performs no
|
|
18
|
+
* write: the evaluator is pure, and the one route from a proposal to
|
|
19
|
+
* `architecture-intent.json` is the CLI's own `--write-intent <file>` flag
|
|
20
|
+
* (`cli.mjs`'s `runDiscover`, serialized through `proposalToIntent`
|
|
21
|
+
* below) — explicit, named by the operator, and refused when a file
|
|
22
|
+
* already stands at the target. The command never hands a candidate the
|
|
23
|
+
* authority of a decision.
|
|
20
24
|
*
|
|
21
25
|
* ## The empty-result invariant
|
|
22
26
|
*
|
|
@@ -25,8 +25,12 @@ import {
|
|
|
25
25
|
computeDomainCoverage,
|
|
26
26
|
REQUIRED_DOMAINS,
|
|
27
27
|
} from "./completeness.mjs";
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// The reachability walk comes from `./impact-reachability.mjs`, not from
|
|
29
|
+
// `./impact.mjs`: that module drives `./impact-statement.mjs`, which drives
|
|
30
|
+
// this one, so importing the walk from there closed the engine's only import
|
|
31
|
+
// cycle (#644).
|
|
32
|
+
import { computeImpact } from "./impact-reachability.mjs";
|
|
33
|
+
import { computeImpactConstraints } from "../rules/edge-constraints.mjs";
|
|
30
34
|
// ---------------------------------------------------------------------------
|
|
31
35
|
// Decision resolution
|
|
32
36
|
// ---------------------------------------------------------------------------
|
package/src/commands/explain.mjs
CHANGED
|
@@ -79,16 +79,13 @@
|
|
|
79
79
|
* was: the field, and its rendered lines, exist only when the comparison was
|
|
80
80
|
* requested.
|
|
81
81
|
*/
|
|
82
|
-
import {
|
|
83
|
-
blindSpotRows,
|
|
84
|
-
isWholeFileFailure,
|
|
85
|
-
unresolvableLiteralCount,
|
|
86
|
-
} from "../analysis/source-util.mjs";
|
|
82
|
+
import { isWholeFileFailure } from "../analysis/source-util.mjs";
|
|
87
83
|
import { UsageError } from "../errors.mjs";
|
|
88
84
|
import { evaluate } from "../rules/index.mjs";
|
|
89
85
|
import { findConstraintsFor } from "../rules/tags.mjs";
|
|
90
86
|
import { findProjectForPath, createProjectRootMappings } from "../rules/specifiers.mjs";
|
|
91
87
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
88
|
+
import { coverageVerdict } from "./coverage-verdict.mjs";
|
|
92
89
|
import { formatExplainReport } from "../report/explain-text.mjs";
|
|
93
90
|
import { resolveProvenance } from "./provenance.mjs";
|
|
94
91
|
import { readAdrContext } from "./adr.mjs";
|
|
@@ -369,18 +366,18 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
369
366
|
// Normalize backslash separators (Windows paths) to forward slashes.
|
|
370
367
|
parsed.sourceFile = sep === "\\" ? normalizedFile.replaceAll("\\", "/") : normalizedFile;
|
|
371
368
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
369
|
+
// The completeness verdict is the shared constructor's, not this file's —
|
|
370
|
+
// the same contract `graph`/`discover` run: an unresolvable site was seen
|
|
371
|
+
// but never judged (#595), the graph is missing whatever edge it would have
|
|
372
|
+
// drawn, and rules that judge the whole graph (circularity, lazy loading)
|
|
373
|
+
// would answer over a gap. The explanation still reports — status
|
|
374
|
+
// no-verdict — naming the site in `coverage.blindSpots`. The restatement
|
|
375
|
+
// this replaces carried two of the constructor's three axes; the third
|
|
376
|
+
// (`analyzed > 0`, #599) moves no byte here, because every lane below that
|
|
377
|
+
// builds an envelope already implies a read file — an import record or a
|
|
378
|
+
// positioned failure exists only for a file the run analyzed — so the axis
|
|
379
|
+
// is carried by composition, not changed by it.
|
|
380
|
+
const { notAnalyzed, blindSpots, complete, status, exitCode } = coverageVerdict(commandContext);
|
|
384
381
|
|
|
385
382
|
// Find the import record at this site.
|
|
386
383
|
const record = findSite(parsed, commandContext.analysis.imports);
|
|
@@ -432,7 +429,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
432
429
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
433
430
|
imports: commandContext.analysis.imports.length,
|
|
434
431
|
notAnalyzed,
|
|
435
|
-
blindSpots
|
|
432
|
+
blindSpots,
|
|
436
433
|
notes: [],
|
|
437
434
|
};
|
|
438
435
|
|
|
@@ -447,7 +444,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
447
444
|
command: "explain",
|
|
448
445
|
context,
|
|
449
446
|
status,
|
|
450
|
-
exitCode
|
|
447
|
+
exitCode,
|
|
451
448
|
coverage,
|
|
452
449
|
result,
|
|
453
450
|
});
|
|
@@ -579,7 +576,7 @@ export function explainCommand(site, commandContext, config, options = {}) {
|
|
|
579
576
|
analyzedFiles: commandContext.analysis.analyzed,
|
|
580
577
|
imports: commandContext.analysis.imports.length,
|
|
581
578
|
notAnalyzed,
|
|
582
|
-
blindSpots
|
|
579
|
+
blindSpots,
|
|
583
580
|
notes: [],
|
|
584
581
|
};
|
|
585
582
|
|
package/src/commands/graph.mjs
CHANGED
|
@@ -180,6 +180,13 @@ export function computePolicyFingerprint(config) {
|
|
|
180
180
|
suppressions: config.suppressions ?? [],
|
|
181
181
|
...(config.fitness === undefined ? {} : { fitness: config.fitness }),
|
|
182
182
|
...(config.customRules === undefined ? {} : { customRules: config.customRules }),
|
|
183
|
+
// The document track is law the same way the two blocks above are: it
|
|
184
|
+
// decides what this run judges, so a policy that adds or edits a
|
|
185
|
+
// `markdown` block must not share a fingerprint with one that does not —
|
|
186
|
+
// `diff`'s policy-changed warning reads this hash. Conditional, like the
|
|
187
|
+
// two above, so a policy declaring no block hashes exactly as it did
|
|
188
|
+
// before this key existed.
|
|
189
|
+
...(config.markdown === undefined ? {} : { markdown: config.markdown }),
|
|
183
190
|
};
|
|
184
191
|
// Canonicalise: sort object keys at every depth so insertion order does not
|
|
185
192
|
// affect the hash. Semantic equality, not construction order, is the claim —
|
package/src/commands/health.mjs
CHANGED
|
@@ -112,10 +112,14 @@ export function healthCommand(commandContext, io = {}) {
|
|
|
112
112
|
const edges = buildDependencies(graph.dependencies);
|
|
113
113
|
|
|
114
114
|
// The run's coverage facts, the same shape every command's envelope carries.
|
|
115
|
+
// A run that analyzed nothing judged nothing (#599, #694): judging nothing
|
|
116
|
+
// is not finding nothing, so it defeats completeness the way a whole-file
|
|
117
|
+
// failure does.
|
|
115
118
|
// An unresolvable site is a fact the run saw but never judged (#595) —
|
|
116
119
|
// metrics measured over it would read precision the run does not have,
|
|
117
120
|
// so it defeats file completeness the way a whole-file failure does.
|
|
118
121
|
const fileComplete =
|
|
122
|
+
analysis.analyzed > 0 &&
|
|
119
123
|
analysis.failures.filter(isWholeFileFailure).length === 0 &&
|
|
120
124
|
unresolvableLiteralCount(analysis.failures) === 0;
|
|
121
125
|
// The graph is complete only when the files are AND the graph actually sees
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse reachability over the project graph — the impact *computation*.
|
|
3
|
+
*
|
|
4
|
+
* Given a project name, `computeImpact` lists every project that transitively
|
|
5
|
+
* depends on it — the set a developer needs to consider before changing that
|
|
6
|
+
* project. It is descriptive: it never exits anything, because a description
|
|
7
|
+
* of what depends on a project is never a finding.
|
|
8
|
+
*
|
|
9
|
+
* The result separates **direct** dependents (projects whose edges point
|
|
10
|
+
* straight at the target) from **transitive** ones (reachable only through
|
|
11
|
+
* another project), and the union of both is `dependents`. An empty
|
|
12
|
+
* `dependents` list is a claim — "nothing depends on this project" — not a
|
|
13
|
+
* shrug, and the reports built on it are worded that way so a reader never
|
|
14
|
+
* mistakes it for silence.
|
|
15
|
+
*
|
|
16
|
+
* This is a plain module rather than part of `./impact.mjs` on purpose: the
|
|
17
|
+
* canonical evaluator (`./evaluation-primitives.mjs`) needs the same
|
|
18
|
+
* reachability walk the `impact` command reports, and `./impact.mjs` drives
|
|
19
|
+
* `./impact-statement.mjs`, which drives that evaluator. Holding the walk in
|
|
20
|
+
* `./impact.mjs` made `evaluation-primitives → impact → impact-statement →
|
|
21
|
+
* evaluation-primitives` the engine's only import cycle (#644) — three
|
|
22
|
+
* modules forced to load together, with no dependency order left to reason
|
|
23
|
+
* about. Both sides import the walk from here instead:
|
|
24
|
+
* `impact → impact-reachability`, `evaluation-primitives →
|
|
25
|
+
* impact-reachability`, and the trio's remaining direction stays one-way.
|
|
26
|
+
* `./impact.mjs` re-exports `computeImpact` so its existing importers keep
|
|
27
|
+
* resolving.
|
|
28
|
+
*
|
|
29
|
+
* Like `./edge-constraints.mjs`, this is a shared computation layer: it holds
|
|
30
|
+
* no argv, prints nothing, and decides no exit code (`./README.md`).
|
|
31
|
+
*/
|
|
32
|
+
import { UsageError } from "../errors.mjs";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Computes the impact set: every project that transitively depends on
|
|
36
|
+
* `projectName`.
|
|
37
|
+
*
|
|
38
|
+
* Builds a reverse adjacency map from the graph's `dependencies`, then walks
|
|
39
|
+
* it breadth-first starting from the target project. The walk does NOT include
|
|
40
|
+
* the target project itself in the dependent set — a project does not depend
|
|
41
|
+
* on itself — but the returned `dependents` array is the union of `direct`
|
|
42
|
+
* and `transitive`, and the report header names the target separately.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} projectName The project whose impact is being queried.
|
|
45
|
+
* @param {object} graph The project graph: `{nodes, dependencies}`.
|
|
46
|
+
* @returns {{project: string, direct: string[], transitive: string[], dependents: string[]}}
|
|
47
|
+
* @throws {UsageError} when `projectName` is not in the graph.
|
|
48
|
+
*/
|
|
49
|
+
export function computeImpact(projectName, graph) {
|
|
50
|
+
const nodes = graph.nodes;
|
|
51
|
+
const deps = graph.dependencies;
|
|
52
|
+
|
|
53
|
+
if (!Object.hasOwn(nodes, projectName)) {
|
|
54
|
+
throw new UsageError(
|
|
55
|
+
`archkeep: no project named '${projectName}' in the graph — ` +
|
|
56
|
+
`available projects: ${Object.keys(nodes)
|
|
57
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
58
|
+
.join(", ")}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build reverse adjacency: target → [sources that depend on it]
|
|
63
|
+
const reverseAdj = Object.create(null);
|
|
64
|
+
for (const source of Object.keys(deps)) {
|
|
65
|
+
if (!Object.hasOwn(deps, source)) continue;
|
|
66
|
+
const targets = deps[source];
|
|
67
|
+
for (const edge of targets) {
|
|
68
|
+
if (!Object.hasOwn(reverseAdj, edge.target)) {
|
|
69
|
+
reverseAdj[edge.target] = [];
|
|
70
|
+
}
|
|
71
|
+
reverseAdj[edge.target].push(source);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Direct dependents: projects whose edges point straight at the target.
|
|
76
|
+
// Deduplicate (multiple edges between same pair are possible) and sort.
|
|
77
|
+
const directSet = new Set(reverseAdj[projectName] ?? []);
|
|
78
|
+
const direct = [...directSet].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
79
|
+
|
|
80
|
+
// BFS through reverse edges to find transitive dependents.
|
|
81
|
+
const visited = new Set(directSet);
|
|
82
|
+
const queue = [...directSet];
|
|
83
|
+
while (queue.length > 0) {
|
|
84
|
+
const current = queue.shift();
|
|
85
|
+
const parents = reverseAdj[current];
|
|
86
|
+
if (parents === undefined) continue;
|
|
87
|
+
for (const parent of parents) {
|
|
88
|
+
if (!visited.has(parent)) {
|
|
89
|
+
visited.add(parent);
|
|
90
|
+
queue.push(parent);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Transitive dependents: reachable through another project, but not direct.
|
|
96
|
+
const transitive = [...visited]
|
|
97
|
+
.filter((name) => !directSet.has(name))
|
|
98
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
99
|
+
|
|
100
|
+
// All dependents: direct + transitive, sorted.
|
|
101
|
+
const dependents = [...visited].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
102
|
+
|
|
103
|
+
return { project: projectName, direct, transitive, dependents };
|
|
104
|
+
}
|
package/src/commands/impact.mjs
CHANGED
|
@@ -35,85 +35,23 @@
|
|
|
35
35
|
* under project roots, `impact` refuses loudly rather than returning a result
|
|
36
36
|
* whose dependents silently under-represent the real architecture.
|
|
37
37
|
*/
|
|
38
|
-
import {
|
|
39
|
-
import { computeImpactConstraints } from "./edge-constraints.mjs";
|
|
38
|
+
import { computeImpactConstraints } from "../rules/edge-constraints.mjs";
|
|
40
39
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
41
40
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
42
41
|
import { formatImpactReport } from "../report/impact-text.mjs";
|
|
43
42
|
import { resolveProvenance } from "./provenance.mjs";
|
|
44
43
|
import { composeImpactStatement } from "./impact-statement.mjs";
|
|
44
|
+
import { computeImpact } from "./impact-reachability.mjs";
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* on itself — but the returned `dependents` array is the union of `direct`
|
|
54
|
-
* and `transitive`, and the report header names the target separately.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} projectName The project whose impact is being queried.
|
|
57
|
-
* @param {object} graph The project graph: `{nodes, dependencies}`.
|
|
58
|
-
* @returns {{project: string, direct: string[], transitive: string[], dependents: string[]}}
|
|
59
|
-
* @throws {UsageError} when `projectName` is not in the graph.
|
|
47
|
+
* The reachability walk this command reports, shared with the canonical
|
|
48
|
+
* evaluator through `./impact-reachability.mjs` — held there, not here,
|
|
49
|
+
* because this module also drives `./impact-statement.mjs`, which drives that
|
|
50
|
+
* evaluator, and keeping the walk here is what closed the engine's only
|
|
51
|
+
* import cycle (#644). Re-exported so every importer of this module keeps
|
|
52
|
+
* resolving `computeImpact` from where it always has.
|
|
60
53
|
*/
|
|
61
|
-
export
|
|
62
|
-
const nodes = graph.nodes;
|
|
63
|
-
const deps = graph.dependencies;
|
|
64
|
-
|
|
65
|
-
if (!Object.hasOwn(nodes, projectName)) {
|
|
66
|
-
throw new UsageError(
|
|
67
|
-
`archkeep: no project named '${projectName}' in the graph — ` +
|
|
68
|
-
`available projects: ${Object.keys(nodes)
|
|
69
|
-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
70
|
-
.join(", ")}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Build reverse adjacency: target → [sources that depend on it]
|
|
75
|
-
const reverseAdj = Object.create(null);
|
|
76
|
-
for (const source of Object.keys(deps)) {
|
|
77
|
-
if (!Object.hasOwn(deps, source)) continue;
|
|
78
|
-
const targets = deps[source];
|
|
79
|
-
for (const edge of targets) {
|
|
80
|
-
if (!Object.hasOwn(reverseAdj, edge.target)) {
|
|
81
|
-
reverseAdj[edge.target] = [];
|
|
82
|
-
}
|
|
83
|
-
reverseAdj[edge.target].push(source);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Direct dependents: projects whose edges point straight at the target.
|
|
88
|
-
// Deduplicate (multiple edges between same pair are possible) and sort.
|
|
89
|
-
const directSet = new Set(reverseAdj[projectName] ?? []);
|
|
90
|
-
const direct = [...directSet].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
91
|
-
|
|
92
|
-
// BFS through reverse edges to find transitive dependents.
|
|
93
|
-
const visited = new Set(directSet);
|
|
94
|
-
const queue = [...directSet];
|
|
95
|
-
while (queue.length > 0) {
|
|
96
|
-
const current = queue.shift();
|
|
97
|
-
const parents = reverseAdj[current];
|
|
98
|
-
if (parents === undefined) continue;
|
|
99
|
-
for (const parent of parents) {
|
|
100
|
-
if (!visited.has(parent)) {
|
|
101
|
-
visited.add(parent);
|
|
102
|
-
queue.push(parent);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Transitive dependents: reachable through another project, but not direct.
|
|
108
|
-
const transitive = [...visited]
|
|
109
|
-
.filter((name) => !directSet.has(name))
|
|
110
|
-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
111
|
-
|
|
112
|
-
// All dependents: direct + transitive, sorted.
|
|
113
|
-
const dependents = [...visited].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
114
|
-
|
|
115
|
-
return { project: projectName, direct, transitive, dependents };
|
|
116
|
-
}
|
|
54
|
+
export { computeImpact };
|
|
117
55
|
|
|
118
56
|
/**
|
|
119
57
|
* Runs the `impact` command: resolves the command context, checks the
|
|
@@ -501,10 +501,14 @@ export async function planContextCommand(
|
|
|
501
501
|
(a.messageId < b.messageId ? -1 : a.messageId > b.messageId ? 1 : 0),
|
|
502
502
|
);
|
|
503
503
|
|
|
504
|
+
// A run that analyzed nothing judged nothing (#599, #694): judging nothing
|
|
505
|
+
// is not finding nothing, so it defeats completeness the way a whole-file
|
|
506
|
+
// failure does.
|
|
504
507
|
// An unresolvable literal site is work the run saw but never judged
|
|
505
508
|
// (#595, narrowed): a plan over it would present edges the run does not
|
|
506
509
|
// hold, so it defeats completeness the way a whole-file failure does.
|
|
507
|
-
const complete =
|
|
510
|
+
const complete =
|
|
511
|
+
wholeTree.analyzed > 0 && notAnalyzed.length === 0 && unresolvableLiteralCount(failures) === 0;
|
|
508
512
|
const status = complete ? "ok" : "no-verdict";
|
|
509
513
|
const exitCode = complete ? 0 : 3;
|
|
510
514
|
|
package/src/commands/policy.mjs
CHANGED
|
@@ -86,10 +86,10 @@ export function hasProfiles(options) {
|
|
|
86
86
|
* @param {string} cwd The process's working directory a relative `--config`
|
|
87
87
|
* resolves against — kept separate from the workspace root for the reason
|
|
88
88
|
* above.
|
|
89
|
-
* @returns {Promise<{config: {depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object, notes?: string[]}|null, profile: string|null, source: string|null}>}
|
|
90
|
-
* `fitness` and `
|
|
91
|
-
* declares them — an absent key is the workspace's decision not to
|
|
92
|
-
* that law, never an empty one (`../config.mjs`'s `policyFrom`).
|
|
89
|
+
* @returns {Promise<{config: {depConstraints: object[], options: object, suppressions: object[], fitness?: object[], customRules?: object[], coverage?: object, markdown?: {include: string[], markers: {pattern: string, edge: string}[]}, notes?: string[]}|null, profile: string|null, source: string|null}>}
|
|
90
|
+
* `fitness`, `customRules` and `markdown` are present only when the resolved
|
|
91
|
+
* policy declares them — an absent key is the workspace's decision not to
|
|
92
|
+
* declare that law, never an empty one (`../config.mjs`'s `policyFrom`).
|
|
93
93
|
* @throws {Error} when a named profile, a `--config` file, or an inline
|
|
94
94
|
* policy cannot be resolved or is malformed — every arm's existing failure
|
|
95
95
|
* mode, unchanged by the extraction.
|
|
@@ -114,8 +114,14 @@ export function resolveProvenance(root) {
|
|
|
114
114
|
|
|
115
115
|
// Dirty: any uncommitted change to tracked files means the working tree
|
|
116
116
|
// does not match the commit. A baseline from a dirty tree is not a
|
|
117
|
-
// reproducible claim about that commit.
|
|
118
|
-
|
|
117
|
+
// reproducible claim about that commit. `--untracked-files=no` is what
|
|
118
|
+
// makes the code agree with that sentence: bare `--porcelain` includes
|
|
119
|
+
// untracked paths, and an untracked file is not an uncommitted change to a
|
|
120
|
+
// tracked file — the analysis reads `git ls-files`-tracked files only, so a
|
|
121
|
+
// tree whose only dirt is an editor swap, a scratch file, or an unignored
|
|
122
|
+
// build output has an unchanged analyzed input and must produce an
|
|
123
|
+
// unchanged envelope (#683).
|
|
124
|
+
const status = runProcess("git", ["status", "--porcelain", "--untracked-files=no"], root).trim();
|
|
119
125
|
const dirty = status.length > 0;
|
|
120
126
|
|
|
121
127
|
return { commit, remote, dirty };
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* @module
|
|
33
33
|
*/
|
|
34
34
|
import { computeImpact } from "./impact.mjs";
|
|
35
|
-
import { computeImpactConstraints } from "
|
|
35
|
+
import { computeImpactConstraints } from "../rules/edge-constraints.mjs";
|
|
36
36
|
import {
|
|
37
37
|
buildDecisionImpact,
|
|
38
38
|
buildEvolutionAlignment,
|