@ecoma-io/archkeep 0.24.0 → 0.24.1
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 +1 -1
- package/src/analysis/csharp.mjs +3 -1
- package/src/analysis/dotnet/csproj.mjs +5 -1
- package/src/analysis/dotnet/namespaces.mjs +1 -0
- package/src/analysis/go.mjs +6 -0
- package/src/analysis/java.mjs +2 -0
- package/src/analysis/jvm/gradle.mjs +3 -1
- package/src/analysis/jvm/maven.mjs +6 -1
- package/src/analysis/jvm/packages.mjs +1 -0
- package/src/analysis/jvm/resolve.mjs +4 -2
- package/src/analysis/kotlin.mjs +2 -0
- package/src/analysis/markdown.mjs +1 -0
- package/src/analysis/python.mjs +8 -0
- package/src/analysis/rust.mjs +5 -1
- package/src/analysis/source-util.mjs +1 -1
- package/src/analysis/typescript.mjs +2 -0
- package/src/architecture-intent/model.mjs +11 -7
- package/src/architecture-intent/selectors.mjs +2 -1
- package/src/commands/change-intent.mjs +10 -9
- package/src/commands/change.mjs +17 -1
- package/src/commands/check.mjs +11 -9
- package/src/commands/completeness.mjs +7 -6
- package/src/commands/coverage-acceptance.mjs +46 -0
- package/src/commands/custom-rules.mjs +1 -0
- package/src/commands/delta-classify.mjs +3 -0
- package/src/commands/delta-snapshot.mjs +2 -1
- package/src/commands/delta.mjs +35 -21
- package/src/commands/drift.mjs +1 -1
- package/src/commands/evaluation-primitives.mjs +4 -4
- package/src/commands/evolution.mjs +2 -0
- package/src/commands/explain.mjs +2 -0
- package/src/commands/graph.mjs +39 -17
- package/src/commands/history.mjs +2 -0
- package/src/commands/plan-context-command.mjs +4 -1
- package/src/commands/policy.mjs +5 -2
- package/src/commands/scenario-evaluation.mjs +1 -1
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +1 -1
- package/src/custom-rules/host.mjs +3 -3
- package/src/eslint-config.mjs +1 -0
- package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
- package/src/go-work.mjs +1 -1
- package/src/governance/adr-registry.mjs +4 -1
- package/src/governance/debt-ledger.mjs +1 -1
- package/src/governance/decision-fitness.mjs +2 -0
- package/src/governance/decision-graph.mjs +1 -0
- package/src/governance/discovery-proposal.mjs +8 -2
- package/src/governance/evolution-event.mjs +42 -0
- package/src/governance/fitness-registry.mjs +2 -0
- package/src/governance/preset-fingerprints.json +14 -14
- package/src/governance/profile-registry.mjs +22 -3
- package/src/governance/provenance-record.mjs +4 -1
- package/src/governance/reconcile-score.mjs +4 -0
- package/src/governance/row-schema.mjs +1 -0
- package/src/governance/verdict.mjs +1 -0
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +3 -3
- package/src/intent/mask-non-code.mjs +1 -0
- package/src/lsp/diagnostics.mjs +3 -2
- package/src/lsp/protocol.mjs +2 -1
- package/src/lsp/server.mjs +3 -0
- package/src/lsp/workspace-index.mjs +3 -1
- package/src/providers/native/differential.fixtures.mjs +29 -11
- package/src/providers/native/index.mjs +2 -1
- package/src/providers/native/model.mjs +4 -0
- package/src/report/envelope-shape.mjs +2 -0
- package/src/report/sarif.mjs +21 -8
- package/src/report/snapshot-text.mjs +3 -3
- package/src/report/text.mjs +10 -2
- package/src/rules/match.mjs +7 -5
- package/src/rules/specifiers.mjs +2 -0
- package/src/rules/tags.mjs +3 -2
- package/src/rules/topology.mjs +6 -1
- package/src/workspace.mjs +1 -0
package/src/commands/check.mjs
CHANGED
|
@@ -29,7 +29,10 @@ import {
|
|
|
29
29
|
unownedGapWithoutRunConfiguration,
|
|
30
30
|
untrackedOwnedFiles,
|
|
31
31
|
} from "./context.mjs";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
partitionUnownedCoverage,
|
|
34
|
+
withdrawAcceptedUnclaimedFailures,
|
|
35
|
+
} from "./coverage-acceptance.mjs";
|
|
33
36
|
import { readAdrContext } from "./adr.mjs";
|
|
34
37
|
import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/adr-registry.mjs";
|
|
35
38
|
import { declaredEdgeViolationsForCheck, judgeEdge } from "../rules/edge-constraints.mjs";
|
|
@@ -223,7 +226,7 @@ export async function check(
|
|
|
223
226
|
);
|
|
224
227
|
const { root, graph, workspace, tracked } = commandContext;
|
|
225
228
|
const { imports, exemptedFiles, unsupportedLanguageFiles } = commandContext.analysis;
|
|
226
|
-
|
|
229
|
+
let failures = [...commandContext.analysis.failures];
|
|
227
230
|
const analyzed = commandContext.analysis.analyzed;
|
|
228
231
|
|
|
229
232
|
// The config's location is a separate fact from the workspace root, which is
|
|
@@ -308,17 +311,16 @@ export async function check(
|
|
|
308
311
|
// acceptance now — stated below as the `"accepted-unowned-files"` coverage
|
|
309
312
|
// gap, never silently — rather than the exit-3 refusal an unanswered
|
|
310
313
|
// orphan earns. Uncovered unclaimed files keep their failures, and with
|
|
311
|
-
// them the exit code, byte-identical to before the channel existed.
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
314
|
+
// them the exit code, byte-identical to before the channel existed. The
|
|
315
|
+
// one-failure-per-file assumption the withdrawal rests on is enforced where
|
|
316
|
+
// it is spent (`./coverage-acceptance.mjs`), not stated here: an accepted
|
|
317
|
+
// file carrying a second failure refuses the run instead of losing both
|
|
318
|
+
// rows to a by-file splice.
|
|
315
319
|
const acceptedUnclaimed = new Set(
|
|
316
320
|
commandContext.unclaimedGap.files.filter((file) => unownedCoverage.acceptedFiles.has(file)),
|
|
317
321
|
);
|
|
318
322
|
if (acceptedUnclaimed.size > 0) {
|
|
319
|
-
|
|
320
|
-
if (acceptedUnclaimed.has(failures[at].sourceFile)) failures.splice(at, 1);
|
|
321
|
-
}
|
|
323
|
+
failures = withdrawAcceptedUnclaimedFailures(failures, acceptedUnclaimed);
|
|
322
324
|
}
|
|
323
325
|
|
|
324
326
|
// The go.work drift check, keyed off the manifest's presence the way every
|
|
@@ -40,10 +40,10 @@ export const EVALUATION_STATUS = Object.freeze({
|
|
|
40
40
|
REFUSED: "refused",
|
|
41
41
|
});
|
|
42
42
|
export const EVALUATED = EVALUATION_STATUS.EVALUATED;
|
|
43
|
-
export const PARTIAL = EVALUATION_STATUS.PARTIAL;
|
|
43
|
+
export const PARTIAL = EVALUATION_STATUS.PARTIAL; // used by its own test
|
|
44
44
|
export const NOT_EVALUATED = EVALUATION_STATUS.NOT_EVALUATED;
|
|
45
|
-
export const UNSUPPORTED = EVALUATION_STATUS.UNSUPPORTED;
|
|
46
|
-
export const REFUSED = EVALUATION_STATUS.REFUSED;
|
|
45
|
+
export const UNSUPPORTED = EVALUATION_STATUS.UNSUPPORTED; // used by its own test
|
|
46
|
+
export const REFUSED = EVALUATION_STATUS.REFUSED; // used by its own test
|
|
47
47
|
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
// Evaluation contract types — which gates are required per evaluation type
|
|
@@ -71,7 +71,7 @@ export const EVALUATION_CONTRACT_TYPES = Object.freeze({
|
|
|
71
71
|
*
|
|
72
72
|
* @type {Readonly<Object<string, ReadonlySet<string>>>}
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
const REQUIRED_GATES_FOR_CONTRACT = Object.freeze({
|
|
75
75
|
[EVALUATION_CONTRACT_TYPES.CANONICAL]: Object.freeze(
|
|
76
76
|
new Set([
|
|
77
77
|
"domainCoverage",
|
|
@@ -107,7 +107,7 @@ export const REQUIRED_GATES_FOR_CONTRACT = Object.freeze({
|
|
|
107
107
|
* Defaults to SCENARIO (most restrictive).
|
|
108
108
|
* @returns {boolean}
|
|
109
109
|
*/
|
|
110
|
-
|
|
110
|
+
function isGateRequired(gateKey, contractType = EVALUATION_CONTRACT_TYPES.SCENARIO) {
|
|
111
111
|
const required = REQUIRED_GATES_FOR_CONTRACT[contractType];
|
|
112
112
|
return required ? required.has(gateKey) : true;
|
|
113
113
|
}
|
|
@@ -121,6 +121,7 @@ export function isGateRequired(gateKey, contractType = EVALUATION_CONTRACT_TYPES
|
|
|
121
121
|
* @type {Readonly<{key: string, label: string, type: string}[]>}
|
|
122
122
|
*/
|
|
123
123
|
export const EVIDENCE_COMPLETE_GATES = Object.freeze([
|
|
124
|
+
// used by its own test
|
|
124
125
|
{ key: "domainCoverage", label: "Domain coverage", type: "ratio" },
|
|
125
126
|
{ key: "claimEvidenceCoverage", label: "Claim evidence coverage", type: "ratio" },
|
|
126
127
|
{ key: "causalCoverage", label: "Causal coverage", type: "ratio" },
|
|
@@ -265,7 +266,7 @@ export const REQUIRED_DOMAINS = Object.freeze([
|
|
|
265
266
|
* @param {string} status EVALUATION_STATUS value
|
|
266
267
|
* @returns {boolean}
|
|
267
268
|
*/
|
|
268
|
-
|
|
269
|
+
function isDomainEvaluated(status) {
|
|
269
270
|
return status === EVALUATION_STATUS.EVALUATED;
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -111,3 +111,49 @@ export function partitionUnownedCoverage({ rows, unownedGap, unclaimedFiles, tra
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Withdraws each accepted unclaimed file's failure from the run's failure
|
|
117
|
+
* list — the acceptance channel's effect on it, kept beside the matching so
|
|
118
|
+
* the channel's contract (accept the file ⇒ withdraw its refusal) is one
|
|
119
|
+
* module's. `./check.mjs` holds the list and narrows `acceptedFiles` to the
|
|
120
|
+
* unclaimed half; `./waivers.mjs` has no failure list and only reports rows.
|
|
121
|
+
*
|
|
122
|
+
* An accepted unclaimed file carries exactly one failure — its unowned-file
|
|
123
|
+
* refusal, the only row any producer appends for a file no analyzer ever
|
|
124
|
+
* read. The withdrawal does not TRUST that assumption, it ENFORCES it: an
|
|
125
|
+
* accepted file carrying more than one failure means some producer appended a
|
|
126
|
+
* second row for it, and a splice by `sourceFile` would take the second one
|
|
127
|
+
* down with the first — the failure count falls, `coverage.notAnalyzed` loses
|
|
128
|
+
* a row, and a could-not-look run tips toward a clean one, the silent
|
|
129
|
+
* direction (`../../../../AGENTS.md`). The run refuses instead, naming the
|
|
130
|
+
* file and the count it found.
|
|
131
|
+
*
|
|
132
|
+
* @param {{sourceFile: string}[]} failures The run's failure list, in order.
|
|
133
|
+
* @param {Set<string>} acceptedFiles The accepted subset of the unclaimed
|
|
134
|
+
* list — `partitionUnownedCoverage`'s `acceptedFiles` narrowed to that list
|
|
135
|
+
* by the caller.
|
|
136
|
+
* @returns {{sourceFile: string}[]} A new array without the accepted files'
|
|
137
|
+
* single failures; the input is not mutated.
|
|
138
|
+
* @throws {Error} when an accepted file carries more than one failure.
|
|
139
|
+
*/
|
|
140
|
+
export function withdrawAcceptedUnclaimedFailures(failures, acceptedFiles) {
|
|
141
|
+
const carried = new Map();
|
|
142
|
+
for (const failure of failures) {
|
|
143
|
+
if (!acceptedFiles.has(failure.sourceFile)) continue;
|
|
144
|
+
carried.set(failure.sourceFile, (carried.get(failure.sourceFile) ?? 0) + 1);
|
|
145
|
+
}
|
|
146
|
+
const overloaded = [...carried.entries()].filter(([, count]) => count > 1);
|
|
147
|
+
if (overloaded.length > 0) {
|
|
148
|
+
const named = overloaded
|
|
149
|
+
.map(([file, count]) => `'${file}' (carries ${count} failures)`)
|
|
150
|
+
.join(", ");
|
|
151
|
+
throw new Error(
|
|
152
|
+
`archkeep: the coverage.unowned acceptance channel cannot withdraw ${named} — ` +
|
|
153
|
+
`acceptance withdraws exactly one failure per file (the unowned-file refusal), and ` +
|
|
154
|
+
`withdrawing by file would drop the second one with it, understating what this run ` +
|
|
155
|
+
`could not read`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return failures.filter((failure) => !acceptedFiles.has(failure.sourceFile));
|
|
159
|
+
}
|
|
@@ -140,6 +140,7 @@ export function declaresCustomRules(config) {
|
|
|
140
140
|
* @returns {(artifact: string) => Uint8Array|null}
|
|
141
141
|
*/
|
|
142
142
|
export function readArtifactBytes(root) {
|
|
143
|
+
// used by its own test
|
|
143
144
|
return (artifact) => {
|
|
144
145
|
const abs = join(root, artifact);
|
|
145
146
|
if (containmentViolation(root, abs) !== null) return null;
|
|
@@ -79,6 +79,7 @@ import { namespacedId } from "./custom-rules.mjs";
|
|
|
79
79
|
* constraint: object|null}}|{ok: false, reason: string}}
|
|
80
80
|
*/
|
|
81
81
|
export function violationIdentity(violation) {
|
|
82
|
+
// used by its own test
|
|
82
83
|
if (violation === null || typeof violation !== "object" || Array.isArray(violation)) {
|
|
83
84
|
return { ok: false, reason: `violation is ${describe(violation)}, not an object` };
|
|
84
85
|
}
|
|
@@ -148,6 +149,7 @@ export function violationIdentity(violation) {
|
|
|
148
149
|
* original violation plus the `reason` its identity could not be stated.
|
|
149
150
|
*/
|
|
150
151
|
export function classifyViolations({ base, head, suppressions = [], now = referenceTime() }) {
|
|
152
|
+
// used by its own test
|
|
151
153
|
const baseIdentified = base.map(identityOf);
|
|
152
154
|
const headIdentified = head.map(identityOf);
|
|
153
155
|
|
|
@@ -237,6 +239,7 @@ export function classifyViolations({ base, head, suppressions = [], now = refere
|
|
|
237
239
|
* unknown: object[]}}
|
|
238
240
|
*/
|
|
239
241
|
export function classifyUnresolvableRecords({ base, head, sourceProjectOf }) {
|
|
242
|
+
// used by its own test
|
|
240
243
|
const attribute = sourceProjectOf ?? (() => null);
|
|
241
244
|
const baseIdentified = base
|
|
242
245
|
.filter(isUnresolvable)
|
|
@@ -52,7 +52,7 @@ import { isPlainObject } from "../values.mjs";
|
|
|
52
52
|
import { buildDependencies, buildProjects } from "./graph.mjs";
|
|
53
53
|
|
|
54
54
|
/** The only snapshot schemaVersion this module writes and reads. */
|
|
55
|
-
export const EVIDENCE_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
55
|
+
export const EVIDENCE_SNAPSHOT_SCHEMA_VERSION = 1; // used by its own test
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Builds the snapshot object from already-captured evidence.
|
|
@@ -338,6 +338,7 @@ export function readEvidenceSnapshot(path, io = {}) {
|
|
|
338
338
|
* @throws {Error} on every condition above.
|
|
339
339
|
*/
|
|
340
340
|
export function parseEvidenceSnapshot(text, path) {
|
|
341
|
+
// used by its own test
|
|
341
342
|
let parsed;
|
|
342
343
|
try {
|
|
343
344
|
parsed = JSON.parse(text);
|
package/src/commands/delta.mjs
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
import { stripTrailingSlashes } from "../path-util.mjs";
|
|
75
75
|
import { referenceTime } from "../governance/clock.mjs";
|
|
76
76
|
import {
|
|
77
|
+
assertReproducibleEventIdentity,
|
|
77
78
|
eventDedupeKey,
|
|
78
79
|
eventId,
|
|
79
80
|
EVOLUTION_EVENT_SCHEMA_VERSION,
|
|
@@ -161,6 +162,7 @@ export function refusePluginGapHead(commandContext, activity) {
|
|
|
161
162
|
* @throws {Error} on either condition.
|
|
162
163
|
*/
|
|
163
164
|
export function refuseUnjudgeableHead(commandContext, activity) {
|
|
165
|
+
// used by its own test
|
|
164
166
|
refusePluginGapHead(commandContext, activity);
|
|
165
167
|
const notAnalyzed = commandContext.analysis.failures.filter(isWholeFileFailure);
|
|
166
168
|
|
|
@@ -271,6 +273,8 @@ export function captureDelta(commandContext, { config }) {
|
|
|
271
273
|
* type: string}[], workspaceLayout?: object, exemptedFiles?: string[]}} storedGraph
|
|
272
274
|
* A validated snapshot's `graph` section (`parseEvidenceSnapshot`).
|
|
273
275
|
* @returns {object} A graph `evaluate()` consumes.
|
|
276
|
+
* @throws {Error} when an `exemptedFiles` entry is not a string — the engine's
|
|
277
|
+
* own filter would drop it in silence (the refusal beside the copy below).
|
|
274
278
|
*/
|
|
275
279
|
export function evidenceGraphToProjectGraph(storedGraph) {
|
|
276
280
|
/** @type {Record<string, object>} */
|
|
@@ -297,7 +301,26 @@ export function evidenceGraphToProjectGraph(storedGraph) {
|
|
|
297
301
|
const graph = { nodes, dependencies };
|
|
298
302
|
if (storedGraph.workspaceLayout !== undefined)
|
|
299
303
|
graph.workspaceLayout = storedGraph.workspaceLayout;
|
|
300
|
-
if (Array.isArray(storedGraph.exemptedFiles))
|
|
304
|
+
if (Array.isArray(storedGraph.exemptedFiles)) {
|
|
305
|
+
// An entry that is not a string is refused here rather than carried:
|
|
306
|
+
// `../rules/index.mjs`'s `createContext` filters `graph.exemptedFiles`
|
|
307
|
+
// with `typeof file === "string"`, so a corrupted snapshot's entry would
|
|
308
|
+
// ride through this conversion and vanish there — the exemption set the
|
|
309
|
+
// snapshot recorded silently shrinking by one file, the under-count
|
|
310
|
+
// disclosed nowhere.
|
|
311
|
+
const malformed = storedGraph.exemptedFiles
|
|
312
|
+
.map((entry, at) => ({ entry, at }))
|
|
313
|
+
.filter(({ entry }) => typeof entry !== "string");
|
|
314
|
+
if (malformed.length > 0) {
|
|
315
|
+
const { entry, at } = malformed[0];
|
|
316
|
+
throw new Error(
|
|
317
|
+
`archkeep: the snapshot's graph.exemptedFiles[${at}] is ${JSON.stringify(entry)}, ` +
|
|
318
|
+
`not a string — the rule engine drops such entries in silence, which would shrink ` +
|
|
319
|
+
`the exemption set the snapshot recorded; re-capture the baseline or correct the file`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
graph.exemptedFiles = storedGraph.exemptedFiles;
|
|
323
|
+
}
|
|
301
324
|
return graph;
|
|
302
325
|
}
|
|
303
326
|
|
|
@@ -373,6 +396,7 @@ export function sourceProjectAttributor(headGraph, baselineProjects) {
|
|
|
373
396
|
* @returns {"accepted"|"rejected"|"no-verdict"}
|
|
374
397
|
*/
|
|
375
398
|
export function deltaDisposition({ status }) {
|
|
399
|
+
// used by its own test
|
|
376
400
|
if (status === "no-verdict") return "no-verdict";
|
|
377
401
|
if (status === "findings") return "rejected";
|
|
378
402
|
return "accepted";
|
|
@@ -810,26 +834,16 @@ export async function deltaCommand(
|
|
|
810
834
|
let eventWrite = null;
|
|
811
835
|
if (eventOut !== null && eventOut !== undefined) {
|
|
812
836
|
// F-delta-event-id: an evolution event is only written from a reproducible
|
|
813
|
-
// identity — a committed, clean head and a clean base
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
"collide on one event id. Commit the head, or capture without --event-out.",
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
if (baseline.provenance?.dirty === true || headProvenance?.dirty === true) {
|
|
827
|
-
throw new Error(
|
|
828
|
-
"archkeep: refusing to write a delta event from a dirty working tree — the event " +
|
|
829
|
-
"would name a commit whose evidence is uncommitted, and distinct uncommitted " +
|
|
830
|
-
"states would collide on one event id. Commit both sides first.",
|
|
831
|
-
);
|
|
832
|
-
}
|
|
837
|
+
// identity — a committed, clean head and a clean base, by the shared law
|
|
838
|
+
// `assertReproducibleEventIdentity` owns. The wording is frozen there; the
|
|
839
|
+
// messages consumers match on are byte-identical to the inline refusals
|
|
840
|
+
// this call replaces.
|
|
841
|
+
assertReproducibleEventIdentity({
|
|
842
|
+
label: "delta",
|
|
843
|
+
headCommit,
|
|
844
|
+
baseDirty: baseline.provenance?.dirty === true,
|
|
845
|
+
headDirty: headProvenance?.dirty === true,
|
|
846
|
+
});
|
|
833
847
|
// The architecture-debt sub-ledger (design §8): judged by re-running the
|
|
834
848
|
// current intent over this run's base and head graphs — a drift finding
|
|
835
849
|
// present at head but not base is introduced; one gone is resolved. Both
|
package/src/commands/drift.mjs
CHANGED
|
@@ -250,7 +250,7 @@ export async function driftForCheck(commandContext, io = {}) {
|
|
|
250
250
|
* @param {object} intent The normalized intent model.
|
|
251
251
|
* @returns {{kind: string, row: object}[]}
|
|
252
252
|
*/
|
|
253
|
-
|
|
253
|
+
function intentDecisionRefRows(intent) {
|
|
254
254
|
return governanceIntentRows(intent).filter(
|
|
255
255
|
({ row }) => typeof row?.decisionRef === "string" && row.decisionRef.trim() !== "",
|
|
256
256
|
);
|
|
@@ -195,7 +195,7 @@ export function buildDecisionImpact(root, constraintImpact, config) {
|
|
|
195
195
|
* not available.
|
|
196
196
|
* @returns {{evaluated: boolean, findings: object[], count: number}}
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
function evaluateFindingsImpact(affectedProjects, availableFindings) {
|
|
199
199
|
if (!availableFindings || availableFindings.length === 0) {
|
|
200
200
|
return { evaluated: false, findings: [], count: 0 };
|
|
201
201
|
}
|
|
@@ -221,7 +221,7 @@ export function evaluateFindingsImpact(affectedProjects, availableFindings) {
|
|
|
221
221
|
* entry's associated project.
|
|
222
222
|
* @returns {{evaluated: boolean, debt: object[], count: number}}
|
|
223
223
|
*/
|
|
224
|
-
|
|
224
|
+
function evaluateDebtImpact(affectedProjects, availableDebt, resolveProject = null) {
|
|
225
225
|
if (!availableDebt || availableDebt.length === 0) {
|
|
226
226
|
return { evaluated: false, debt: [], count: 0 };
|
|
227
227
|
}
|
|
@@ -247,7 +247,7 @@ export function evaluateDebtImpact(affectedProjects, availableDebt, resolveProje
|
|
|
247
247
|
* @param {string} targetProject The target of the impact analysis.
|
|
248
248
|
* @returns {{boundaries: object[], evaluated: boolean}}
|
|
249
249
|
*/
|
|
250
|
-
|
|
250
|
+
function evaluateBoundaryImpact(graph, constraintImpact, targetProject) {
|
|
251
251
|
if (!constraintImpact || constraintImpact.length === 0) {
|
|
252
252
|
return { boundaries: [], evaluated: false };
|
|
253
253
|
}
|
|
@@ -339,7 +339,7 @@ export function decisionProvenanceCoverage(decisions) {
|
|
|
339
339
|
* @param {object} evaluation.evolutionAlignment Evolution alignment.
|
|
340
340
|
* @returns {object} Evidence gate values for buildEvidenceComplete.
|
|
341
341
|
*/
|
|
342
|
-
|
|
342
|
+
function deriveEvidenceGates(evaluation) {
|
|
343
343
|
const { completeness, constraintImpact, decisionImpact } = evaluation;
|
|
344
344
|
|
|
345
345
|
// domainCoverage: ratio of evaluated required domains
|
|
@@ -138,6 +138,7 @@ function shortSha(sha) {
|
|
|
138
138
|
* and a shallow clone whose cut-off sits below the requested revision.
|
|
139
139
|
*/
|
|
140
140
|
export function resolveRevision(root, rev, flag, { run = runProcess } = {}) {
|
|
141
|
+
// used by its own test
|
|
141
142
|
if (typeof rev !== "string" || rev.length === 0) {
|
|
142
143
|
throw new Error(`archkeep: ${flag} needs a revision — a commit, branch, tag, or HEAD~n.`);
|
|
143
144
|
}
|
|
@@ -190,6 +191,7 @@ export function resolveRevision(root, rev, flag, { run = runProcess } = {}) {
|
|
|
190
191
|
* head's ancestry, or a merge commit inside the range.
|
|
191
192
|
*/
|
|
192
193
|
export function selectLinearRange(root, { base, head }, { run = runProcess } = {}) {
|
|
194
|
+
// used by its own test
|
|
193
195
|
const baseSha = resolveRevision(root, base, "--base", { run });
|
|
194
196
|
const headSha = resolveRevision(root, head ?? "HEAD", "--head", { run });
|
|
195
197
|
|
package/src/commands/explain.mjs
CHANGED
|
@@ -111,6 +111,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
111
111
|
* @throws {UsageError} when the site string is malformed.
|
|
112
112
|
*/
|
|
113
113
|
export function parseSite(site) {
|
|
114
|
+
// used by its own test
|
|
114
115
|
const lastColon = site.lastIndexOf(":");
|
|
115
116
|
if (lastColon === -1 || lastColon === 0) {
|
|
116
117
|
throw new UsageError(
|
|
@@ -157,6 +158,7 @@ export function parseSite(site) {
|
|
|
157
158
|
* @returns {object|null} The matching record, or `null`.
|
|
158
159
|
*/
|
|
159
160
|
export function findSite(parsed, imports) {
|
|
161
|
+
// used by its own test
|
|
160
162
|
return (
|
|
161
163
|
imports.find(
|
|
162
164
|
(site) =>
|
package/src/commands/graph.mjs
CHANGED
|
@@ -151,24 +151,26 @@ export function buildDependencies(dependencies) {
|
|
|
151
151
|
* config comparison logic.
|
|
152
152
|
*
|
|
153
153
|
* The fingerprint is SHA-256 of the canonicalized JSON for `depConstraints`,
|
|
154
|
-
* `options`, `suppressions` and — when the policy declares them —
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* `options`, `suppressions` and — when the policy declares them — the
|
|
155
|
+
* `fitness`, `customRules`, `coverage` and `markdown` blocks, plus the
|
|
156
|
+
* selected profile's NAME when the law was selected by name. Those are every
|
|
157
|
+
* field of a loaded policy that states law: the first three decide which
|
|
158
|
+
* violations `evaluate` produces, the fourth decides which fitness functions
|
|
159
|
+
* `check` folds into the same exit code
|
|
160
|
+
* (`../governance/fitness-registry.mjs`), and the fifth names the rule
|
|
159
161
|
* artifacts a workspace declared, each pinned to the bytes its `sha256` claims
|
|
160
162
|
* (`../config.mjs`'s `customRuleRowViolations`) — swap one row's hash or its
|
|
161
|
-
* `params` and the policy says something different.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* together.
|
|
163
|
+
* `params` and the policy says something different. The three after those are
|
|
164
|
+
* argued at their keys below. A field that can fail a build and is not in the
|
|
165
|
+
* hash is a law that can be rewritten while `diff` reports the policy
|
|
166
|
+
* unchanged — the silent direction, and the reason the list here and
|
|
167
|
+
* `policyFrom`'s return shape (`../config.mjs`) are revisited together.
|
|
166
168
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* declares
|
|
170
|
-
*
|
|
171
|
-
*
|
|
169
|
+
* The conditional fields are included only when they are DECLARED, and the
|
|
170
|
+
* absent case contributes no key rather than an empty array. A policy that
|
|
171
|
+
* declares none therefore fingerprints exactly as it did before those fields
|
|
172
|
+
* were covered, so extending the hash did not move every existing snapshot's
|
|
173
|
+
* value — only those whose law it was failing to describe.
|
|
172
174
|
*
|
|
173
175
|
* @param {object} config The loaded boundary config.
|
|
174
176
|
* @returns {string} A hex-encoded SHA-256 fingerprint.
|
|
@@ -180,13 +182,33 @@ export function computePolicyFingerprint(config) {
|
|
|
180
182
|
suppressions: config.suppressions ?? [],
|
|
181
183
|
...(config.fitness === undefined ? {} : { fitness: config.fitness }),
|
|
182
184
|
...(config.customRules === undefined ? {} : { customRules: config.customRules }),
|
|
183
|
-
// The
|
|
185
|
+
// The acceptance channel is law the same way the blocks above are: a
|
|
186
|
+
// `coverage.unowned` row withdraws an unclaimed file's whole-file failure
|
|
187
|
+
// as a recorded acceptance (`../config.mjs`'s `findCoverageViolations`
|
|
188
|
+
// owns the shape, `../commands/coverage-acceptance.mjs` matches it), so a
|
|
189
|
+
// policy that records or removes an acceptance must not share a
|
|
190
|
+
// fingerprint with one that does not — `diff`'s policy-changed warning
|
|
191
|
+
// reads this hash (#709: the one block `policyFrom` sets that the hash
|
|
192
|
+
// skipped). Conditional, like the three above, so a policy recording no
|
|
193
|
+
// acceptance hashes exactly as it did before this key existed.
|
|
194
|
+
...(config.coverage === undefined ? {} : { coverage: config.coverage }),
|
|
195
|
+
// The document track is law the same way the blocks above are: it
|
|
184
196
|
// decides what this run judges, so a policy that adds or edits a
|
|
185
197
|
// `markdown` block must not share a fingerprint with one that does not —
|
|
186
198
|
// `diff`'s policy-changed warning reads this hash. Conditional, like the
|
|
187
|
-
//
|
|
199
|
+
// three above, so a policy declaring no block hashes exactly as it did
|
|
188
200
|
// before this key existed.
|
|
189
201
|
...(config.markdown === undefined ? {} : { markdown: config.markdown }),
|
|
202
|
+
// The named selection is law identity the same way the blocks above are:
|
|
203
|
+
// `profilePolicy` (`../governance/profile-registry.mjs`) sets this key
|
|
204
|
+
// when the workspace selects a profile by name, and two profiles whose
|
|
205
|
+
// resolved blocks converge are still two different named laws. Only
|
|
206
|
+
// `check`'s report names the selection, so a hash blind to it would let a
|
|
207
|
+
// `delta` across a switch report the law unchanged with nothing anywhere
|
|
208
|
+
// saying it moved. Conditional, like the four above, so a policy
|
|
209
|
+
// selected by file or inline — no `profile` key at all — hashes exactly
|
|
210
|
+
// as it did before the selection was covered.
|
|
211
|
+
...(config.profile === undefined ? {} : { profile: config.profile }),
|
|
190
212
|
};
|
|
191
213
|
// Canonicalise: sort object keys at every depth so insertion order does not
|
|
192
214
|
// affect the hash. Semantic equality, not construction order, is the claim —
|
package/src/commands/history.mjs
CHANGED
|
@@ -229,6 +229,7 @@ export function readSnapshots(dir, root) {
|
|
|
229
229
|
* @returns {string} First 8 hex characters.
|
|
230
230
|
*/
|
|
231
231
|
export function shortId(id) {
|
|
232
|
+
// used by its own test
|
|
232
233
|
return id.slice(0, 8);
|
|
233
234
|
}
|
|
234
235
|
|
|
@@ -248,6 +249,7 @@ export function shortId(id) {
|
|
|
248
249
|
* @returns {string} Zero-padded sequence, at least four digits.
|
|
249
250
|
*/
|
|
250
251
|
export function nextSequence(read) {
|
|
252
|
+
// used by its own test
|
|
251
253
|
let max = 0;
|
|
252
254
|
for (const file of read.files) {
|
|
253
255
|
const match = /^(\d+)-/.exec(file.name);
|
|
@@ -87,7 +87,7 @@ import { declaredFitnessNames, unresolvedDecisionRefRows } from "../governance/a
|
|
|
87
87
|
import { formatPlanContextReport } from "../report/plan-context-text.mjs";
|
|
88
88
|
|
|
89
89
|
/** How many dependents are listed before an explicit overflow note. */
|
|
90
|
-
export const DEPENDENT_CAP = 10;
|
|
90
|
+
export const DEPENDENT_CAP = 10; // used by its own test
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* The projects a change touches: the target project, plus every project whose
|
|
@@ -100,6 +100,7 @@ export const DEPENDENT_CAP = 10;
|
|
|
100
100
|
* @returns {string[]} Distinct affected project names, sorted.
|
|
101
101
|
*/
|
|
102
102
|
export function collectAffectedProjects(commandContext, paths) {
|
|
103
|
+
// used by its own test
|
|
103
104
|
const affected = new Set();
|
|
104
105
|
for (const { file, project } of commandContext.owned ?? []) {
|
|
105
106
|
for (const p of paths) {
|
|
@@ -124,6 +125,7 @@ export function collectAffectedProjects(commandContext, paths) {
|
|
|
124
125
|
* @returns {object[]}
|
|
125
126
|
*/
|
|
126
127
|
export function collectImpact(projectName, affected, graph) {
|
|
128
|
+
// used by its own test
|
|
127
129
|
const targets = [...new Set([projectName, ...affected])].sort((a, b) =>
|
|
128
130
|
a < b ? -1 : a > b ? 1 : 0,
|
|
129
131
|
);
|
|
@@ -172,6 +174,7 @@ function scopedFiles(commandContext, affected) {
|
|
|
172
174
|
* @returns {{goWork: object|null, tsconfigPaths: object|null, failures: object[]}}
|
|
173
175
|
*/
|
|
174
176
|
export function collectDrift(commandContext) {
|
|
177
|
+
// used by its own test
|
|
175
178
|
const { root, tracked, workspace } = commandContext;
|
|
176
179
|
const failures = [];
|
|
177
180
|
|
package/src/commands/policy.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
|
26
26
|
* @param {object} options The resolved options from `resolveCommandContext`.
|
|
27
27
|
* @returns {boolean}
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
function hasProfiles(options) {
|
|
30
30
|
return typeof options?.profiles === "string" && options.profiles !== "";
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -86,10 +86,13 @@ 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, markdown?: {include: string[], markers: {pattern: string, edge: string}[]}, notes?: string[]}|null, profile: string|null, source: string|null}>}
|
|
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[], profile?: string}|null, profile: string|null, source: string|null}>}
|
|
90
90
|
* `fitness`, `customRules` and `markdown` are present only when the resolved
|
|
91
91
|
* policy declares them — an absent key is the workspace's decision not to
|
|
92
92
|
* declare that law, never an empty one (`../config.mjs`'s `policyFrom`).
|
|
93
|
+
* `profile` is present only when the law was selected by name, set by the
|
|
94
|
+
* profiles arm (`../governance/profile-registry.mjs`'s `profilePolicy`) so
|
|
95
|
+
* the selection the policy was resolved by travels with it.
|
|
93
96
|
* @throws {Error} when a named profile, a `--config` file, or an inline
|
|
94
97
|
* policy cannot be resolved or is malformed — every arm's existing failure
|
|
95
98
|
* mode, unchanged by the extraction.
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
|
|
57
57
|
/** The supported scenario change types. */
|
|
58
|
-
export const SCENARIO_CHANGE_TYPES = Object.freeze(["dependency_added", "dependency_removed"]);
|
|
58
|
+
export const SCENARIO_CHANGE_TYPES = Object.freeze(["dependency_added", "dependency_removed"]); // used by its own test
|
|
59
59
|
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
// Input schema types
|
|
@@ -107,7 +107,7 @@ const OBSERVATION_BASIS = "graph_snapshots";
|
|
|
107
107
|
* constant in the envelope, so a consumer branches on a documented value
|
|
108
108
|
* rather than on prose.
|
|
109
109
|
*/
|
|
110
|
-
export const INSUFFICIENT_HISTORY = "insufficient_history";
|
|
110
|
+
export const INSUFFICIENT_HISTORY = "insufficient_history"; // used by its own test
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* The derived-number block for one structural axis (projects keyed by name,
|
|
@@ -187,6 +187,7 @@ export const INSUFFICIENT_HISTORY = "insufficient_history";
|
|
|
187
187
|
* trends: TrajectoryTrends|null}}
|
|
188
188
|
*/
|
|
189
189
|
export function computeTrajectory(files) {
|
|
190
|
+
// used by its own test
|
|
190
191
|
const n = files.length;
|
|
191
192
|
const available = n >= 2;
|
|
192
193
|
|
package/src/config.mjs
CHANGED
|
@@ -650,7 +650,7 @@ function findCoverageViolations(value) {
|
|
|
650
650
|
*
|
|
651
651
|
* @type {Readonly<string[]>}
|
|
652
652
|
*/
|
|
653
|
-
|
|
653
|
+
const MARKDOWN_EDGE_KINDS = Object.freeze(["resolvedExportOwner"]);
|
|
654
654
|
|
|
655
655
|
/**
|
|
656
656
|
* One markdown marker row's problems, prefixed with its index.
|
|
@@ -124,17 +124,17 @@ export const CUSTOM_RULE_TIMEOUT_MS = 10_000;
|
|
|
124
124
|
* bundle for a large workspace is smaller — and it is what stops a claimed
|
|
125
125
|
* length from being an allocation instruction.
|
|
126
126
|
*/
|
|
127
|
-
export const CUSTOM_RULE_MAX_VERDICT_BYTES = 8 * 1024 * 1024;
|
|
127
|
+
export const CUSTOM_RULE_MAX_VERDICT_BYTES = 8 * 1024 * 1024; // used by its own test
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* The most linear memory a rule may hold at the two boundaries the worker
|
|
131
131
|
* measures. 256 MiB is far past what reading an evidence bundle needs and far
|
|
132
132
|
* short of what a workspace's CI runner can absorb without noticing.
|
|
133
133
|
*/
|
|
134
|
-
export const CUSTOM_RULE_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
134
|
+
export const CUSTOM_RULE_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024; // used by its own test
|
|
135
135
|
|
|
136
136
|
/** The four ABI symbols a rule module must export, and the kind each must be. */
|
|
137
|
-
|
|
137
|
+
const REQUIRED_EXPORTS = Object.freeze({
|
|
138
138
|
memory: "memory",
|
|
139
139
|
archkeep_alloc: "function",
|
|
140
140
|
archkeep_describe: "function",
|
package/src/eslint-config.mjs
CHANGED
|
@@ -235,6 +235,7 @@ function parseRuleValue(value, index) {
|
|
|
235
235
|
* severity-only fallback has been applied.
|
|
236
236
|
*/
|
|
237
237
|
export function extractBoundaryRule(flatConfig, readerOptions = {}) {
|
|
238
|
+
// used by its own test
|
|
238
239
|
const pathScopedMode = readerOptions.pathScoped ?? "refuse";
|
|
239
240
|
if (pathScopedMode !== "refuse" && pathScopedMode !== "bind-tree-wide") {
|
|
240
241
|
throw new Error(
|