@ecoma-io/archkeep 0.24.0 → 0.25.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 +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 +86 -23
- 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 +27 -5
- package/src/commands/delta.mjs +73 -39
- package/src/commands/diff.mjs +3 -9
- 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 +36 -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/snapshot-meta.mjs +33 -0
- package/src/commands/trajectory.mjs +2 -1
- package/src/config.mjs +1 -1
- package/src/custom-rules/host.mjs +3 -3
- package/src/custom-rules/values.mjs +1 -4
- 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 +16 -1
- 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 +37 -4
- package/src/governance/waiver.mjs +1 -0
- package/src/intent/intent-manifest.json +18 -12
- 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/json.mjs +4 -2
- 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/values.mjs +11 -0
- package/src/workspace.mjs +1 -0
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* The uncertainty marker vocabulary — three values, the bound the "bounded
|
|
62
62
|
* uncertainty markers" test asserts.
|
|
63
63
|
*/
|
|
64
|
-
export const CONFIDENCE = Object.freeze(["high", "medium", "low"]);
|
|
64
|
+
export const CONFIDENCE = Object.freeze(["high", "medium", "low"]); // used by its own test
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* The component model: every project's root's first path segment (`""` at the
|
|
@@ -73,6 +73,7 @@ export const CONFIDENCE = Object.freeze(["high", "medium", "low"]);
|
|
|
73
73
|
* @returns {Map<string, {name: string, root: string, tags: string[]}[]>}
|
|
74
74
|
*/
|
|
75
75
|
export function componentsByDirectory(projects) {
|
|
76
|
+
// used by its own test
|
|
76
77
|
const buckets = new Map();
|
|
77
78
|
for (const project of projects) {
|
|
78
79
|
const component = project.root === "" ? "" : project.root.split("/")[0];
|
|
@@ -101,6 +102,7 @@ export function componentsByDirectory(projects) {
|
|
|
101
102
|
* @returns {{tag: string, component: string, members: string[]}[]} Sorted.
|
|
102
103
|
*/
|
|
103
104
|
export function dominantTags(components) {
|
|
105
|
+
// used by its own test
|
|
104
106
|
const tags = [];
|
|
105
107
|
for (const [component, members] of components) {
|
|
106
108
|
if (members.length < 2) continue;
|
|
@@ -136,6 +138,7 @@ export function dominantTags(components) {
|
|
|
136
138
|
* @returns {{axis: string, values: string[]}[]} Sorted by axis.
|
|
137
139
|
*/
|
|
138
140
|
export function tagAxes(projects) {
|
|
141
|
+
// used by its own test
|
|
139
142
|
const byAxis = new Map();
|
|
140
143
|
for (const project of projects) {
|
|
141
144
|
for (const tag of project.tags) {
|
|
@@ -169,7 +172,7 @@ export function tagAxes(projects) {
|
|
|
169
172
|
* @param {string} target
|
|
170
173
|
* @returns {boolean}
|
|
171
174
|
*/
|
|
172
|
-
|
|
175
|
+
function sameComponent(components, source, target) {
|
|
173
176
|
for (const members of components.values()) {
|
|
174
177
|
if (members.some((m) => m.name === source) && members.some((m) => m.name === target)) {
|
|
175
178
|
return true;
|
|
@@ -198,6 +201,7 @@ export function sameComponent(components, source, target) {
|
|
|
198
201
|
* component?: string, evidence: object[], confidence: string}[]} Sorted.
|
|
199
202
|
*/
|
|
200
203
|
export function boundaryAssertions({ projects, edges }) {
|
|
204
|
+
// used by its own test
|
|
201
205
|
const components = componentsByDirectory(projects);
|
|
202
206
|
const projectNames = new Set(projects.map((p) => p.name));
|
|
203
207
|
/** @type {{kind: "edge"|"component", source: string|undefined, target: string|undefined,
|
|
@@ -256,6 +260,7 @@ export function boundaryAssertions({ projects, edges }) {
|
|
|
256
260
|
* evidence: object[], confidence: string}[]} Sorted.
|
|
257
261
|
*/
|
|
258
262
|
export function tagVocabulary(projects) {
|
|
263
|
+
// used by its own test
|
|
259
264
|
const components = componentsByDirectory(projects);
|
|
260
265
|
/** @type {{kind: "observed"|"suggested", tag: string|undefined, axis: string|undefined,
|
|
261
266
|
* component: string|undefined, members: string[]|undefined, values: string[]|undefined,
|
|
@@ -308,6 +313,7 @@ export function tagVocabulary(projects) {
|
|
|
308
313
|
* component?: string, evidence: object[], confidence: string}[]} Sorted.
|
|
309
314
|
*/
|
|
310
315
|
export function candidateRules(assertions) {
|
|
316
|
+
// used by its own test
|
|
311
317
|
/** @type {{kind: "noDependency"|"boundary", source: string|undefined, target: string|undefined,
|
|
312
318
|
* component: string|undefined, evidence: object[], confidence: "medium"}[]} */
|
|
313
319
|
const rules = [];
|
|
@@ -80,6 +80,48 @@ export function eventId(event) {
|
|
|
80
80
|
return createHash("sha256").update(eventDedupeKey(event)).digest("hex");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The refusal law every command that writes an evolution event holds: the
|
|
85
|
+
* write happens only from a reproducible identity — a committed, clean head
|
|
86
|
+
* and a clean base. A commitless head has no revision to name (the event
|
|
87
|
+
* would serialize its `head` as `{}`, and every run over that workspace
|
|
88
|
+
* would collide on one event id); a dirty tree names a commit its evidence
|
|
89
|
+
* does not back, so two distinct uncommitted states collapse onto one event
|
|
90
|
+
* id — a later transition is silently lost or aliased. That is the silent
|
|
91
|
+
* direction this repository refuses, so the write is refused loudly instead.
|
|
92
|
+
* The same run without `--event-out` stays a byte-identical in-memory run —
|
|
93
|
+
* the gate is the event write, never the verdict.
|
|
94
|
+
*
|
|
95
|
+
* The messages are the `delta` command's original wording, parameterized by
|
|
96
|
+
* the writing command's label. Consumers match on these strings (a refusal
|
|
97
|
+
* is part of a run's observable contract), so the wording is frozen here —
|
|
98
|
+
* one home, one copy, no per-command drift.
|
|
99
|
+
*
|
|
100
|
+
* @param {{label: "delta"|"change", headCommit: string|undefined,
|
|
101
|
+
* baseDirty: boolean, headDirty: boolean}} input `label` names the writing
|
|
102
|
+
* command in the refusal message; `headCommit` is the head revision the
|
|
103
|
+
* event would carry (`undefined` when provenance could not resolve one);
|
|
104
|
+
* `baseDirty`/`headDirty` are the two sides' provenance dirty bits.
|
|
105
|
+
* @returns {void} Throws on every state that cannot produce a reproducible
|
|
106
|
+
* event identity.
|
|
107
|
+
*/
|
|
108
|
+
export function assertReproducibleEventIdentity({ label, headCommit, baseDirty, headDirty }) {
|
|
109
|
+
if (typeof headCommit !== "string") {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`archkeep: refusing to write a ${label} event without a committed head — a commitless ` +
|
|
112
|
+
"head has no reproducible event identity, and every distinct head state would " +
|
|
113
|
+
"collide on one event id. Commit the head, or capture without --event-out.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (baseDirty === true || headDirty === true) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`archkeep: refusing to write a ${label} event from a dirty working tree — the event ` +
|
|
119
|
+
"would name a commit whose evidence is uncommitted, and distinct uncommitted " +
|
|
120
|
+
"states would collide on one event id. Commit both sides first.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
/**
|
|
84
126
|
* The digest of a normalized change-intent's DECLARATIVE parts only:
|
|
85
127
|
* `{version, base, projects, edges, constraints}`. The prose `summary` is
|
|
@@ -52,7 +52,7 @@ import { languageOf } from "../analysis/registry.mjs";
|
|
|
52
52
|
import { canonicalizeJson } from "../canonical.mjs";
|
|
53
53
|
import { describe, isPlainObject } from "../values.mjs";
|
|
54
54
|
import { GOVERNANCE_ROW_KEYS, rowSchemaViolations } from "./row-schema.mjs";
|
|
55
|
-
import { fitnessVerdict, isVerdict } from "./verdict.mjs";
|
|
55
|
+
import { VERDICTS, fitnessVerdict, isVerdict } from "./verdict.mjs";
|
|
56
56
|
import {
|
|
57
57
|
coverageMinimum,
|
|
58
58
|
cycleFree,
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
|
|
66
66
|
/** The condition types the registry can evaluate. */
|
|
67
67
|
export const CONDITION_TYPES = Object.freeze([
|
|
68
|
+
// used by its own test
|
|
68
69
|
"cycle-free",
|
|
69
70
|
"layer-dependency",
|
|
70
71
|
"tag-conformance",
|
|
@@ -327,6 +328,7 @@ function tagAxisIsolationViolations(condition, at) {
|
|
|
327
328
|
* @returns {object} A verdict record from `fitnessVerdict`.
|
|
328
329
|
*/
|
|
329
330
|
export function judgeFitnessRow(row, graph, analysis, intent, suppressions) {
|
|
331
|
+
// used by its own test
|
|
330
332
|
const names = resolveMembers(row.match, graph.nodes);
|
|
331
333
|
if (names.length === 0) {
|
|
332
334
|
return fitnessVerdict({
|
|
@@ -479,6 +481,19 @@ export function evaluateFitness(rows, snapshot) {
|
|
|
479
481
|
* @returns {{verdict: "pass"|"fail"|"unknown"|"not_applicable", decisions: object[]}}
|
|
480
482
|
*/
|
|
481
483
|
export function fitnessVerdictFor(decisions) {
|
|
484
|
+
// The fold is an overall-verdict constructor, so it owns the same vocabulary
|
|
485
|
+
// latch `buildDecision` does rather than trusting every caller to have
|
|
486
|
+
// latched its rows first: a stranger verdict matches none of the checks
|
|
487
|
+
// below and falls through to `pass` — the loudest clean state a governance
|
|
488
|
+
// run can emit, fabricated from a row nobody judged.
|
|
489
|
+
for (const decision of decisions) {
|
|
490
|
+
if (!isVerdict(decision.verdict)) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`archkeep: refusing to fold a fitness row whose verdict is ${describe(decision.verdict)} — ` +
|
|
493
|
+
`expected one of ${VERDICTS.join(", ")}. This is a bug in the rule that judged the row.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
482
497
|
if (decisions.some((d) => d.verdict === "fail")) return { verdict: "fail", decisions };
|
|
483
498
|
if (decisions.some((d) => d.verdict === "unknown")) return { verdict: "unknown", decisions };
|
|
484
499
|
if (decisions.length === 0 || decisions.every((d) => d.verdict === "not_applicable")) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"clean-architecture#clean-architecture": "
|
|
3
|
-
"clean-architecture#clean-architecture-pure-core": "
|
|
4
|
-
"ddd-bounded-contexts#ddd-bounded-contexts": "
|
|
5
|
-
"ddd-bounded-contexts#ddd-bounded-contexts-isolated": "
|
|
6
|
-
"ddd-bounded-contexts#ddd-bounded-contexts-partitioned": "
|
|
7
|
-
"hexagonal#hexagonal": "
|
|
8
|
-
"hexagonal#hexagonal-pure-domain": "
|
|
9
|
-
"layered#layered-relaxed": "
|
|
10
|
-
"layered#layered-strict": "
|
|
11
|
-
"modular-monolith#modular-monolith": "
|
|
12
|
-
"modular-monolith#modular-monolith-sealed-kernel": "
|
|
13
|
-
"modular-monolith#modular-monolith-sealed-modules": "
|
|
14
|
-
"vertical-slice#vertical-slice": "
|
|
15
|
-
"vertical-slice#vertical-slice-sealed-kernel": "
|
|
2
|
+
"clean-architecture#clean-architecture": "5f32ab7e98b5e49ed832bbb4dd97d6757abdf607ca85dbe70a3fc9515493fcb4",
|
|
3
|
+
"clean-architecture#clean-architecture-pure-core": "9728201a3f57fc7db38747e58cd277fdff54277a34d3997b6ae67827dc547404",
|
|
4
|
+
"ddd-bounded-contexts#ddd-bounded-contexts": "6d9f733c915e3ab33932d5150d0651a4603a7869625a7ec55afc7bb3a0328871",
|
|
5
|
+
"ddd-bounded-contexts#ddd-bounded-contexts-isolated": "6dd7e4360b8d8fb0359148af1360bc8986526b2be51c6b7a647f7fb414e5a0a3",
|
|
6
|
+
"ddd-bounded-contexts#ddd-bounded-contexts-partitioned": "93b15523dda77be5aca06806d0235eefa32a504e411d4fcf6962a7c1bec9e719",
|
|
7
|
+
"hexagonal#hexagonal": "86321ee12d3e304567c988a5fb04ef17d040763c45f021a9b1f64c3852ea7c12",
|
|
8
|
+
"hexagonal#hexagonal-pure-domain": "2aa8efe7d71a4c731aee61c46961d6bba8d239af68909a71595488374c5de80a",
|
|
9
|
+
"layered#layered-relaxed": "ffc0f268d37b88c97eb8c783283adbf9779850750b3e21c780c7851d936fb6b4",
|
|
10
|
+
"layered#layered-strict": "4c90afb7cfe1e273fedce2a85e2403a1f354a4a038f1c872ad103f1c2f1d7df1",
|
|
11
|
+
"modular-monolith#modular-monolith": "634ef3e706d06780bfd72a57c1a4a3c0be2d4163b0728fc734f4bd4e5da8cbe1",
|
|
12
|
+
"modular-monolith#modular-monolith-sealed-kernel": "372cabc2d618aea8a0aa8b284e8be93a062dd5ebe22f64af52bc1fa785826a97",
|
|
13
|
+
"modular-monolith#modular-monolith-sealed-modules": "0ff0b39328d50fb3622dc84fb4d7b26fd567a5110437536aaa586e29ad4b8c92",
|
|
14
|
+
"vertical-slice#vertical-slice": "4df057e4ec1f92dae8aada8203d18869c6e283764f66a9f96f1c425304436f34",
|
|
15
|
+
"vertical-slice#vertical-slice-sealed-kernel": "1d1fbaf268a7afc851e6e4a126bd586dd0ca9547cf9d654e44dcf0dd99537f78"
|
|
16
16
|
}
|
|
@@ -74,7 +74,7 @@ import { describe, isPlainObject } from "../values.mjs";
|
|
|
74
74
|
const REGISTRY_KEYS = ["profiles", "version", "$schema"];
|
|
75
75
|
|
|
76
76
|
/** A version a reader that predates it must refuse, per `docs/reference/profiles.md`. */
|
|
77
|
-
|
|
77
|
+
const PROFILE_REGISTRY_SCHEMA_VERSION = 1;
|
|
78
78
|
|
|
79
79
|
/** The registry's schema version: stated, or schema 1 when absent. */
|
|
80
80
|
function registrySchemaVersion(raw) {
|
|
@@ -112,6 +112,7 @@ const NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
|
|
|
112
112
|
|
|
113
113
|
/** A profile's declared block, kept ONLY for this command's own data. */
|
|
114
114
|
export function listNames(registry) {
|
|
115
|
+
// used by its own test
|
|
115
116
|
return registry.profiles.map((profile) => profile.name);
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -126,6 +127,7 @@ export function listNames(registry) {
|
|
|
126
127
|
* @returns {string[]}
|
|
127
128
|
*/
|
|
128
129
|
export function profileRegistryViolations(raw) {
|
|
130
|
+
// used by its own test
|
|
129
131
|
if (!isPlainObject(raw)) {
|
|
130
132
|
return [`profiles: expected a JSON object, got ${describe(raw)}`];
|
|
131
133
|
}
|
|
@@ -201,6 +203,7 @@ export function profileRegistryViolations(raw) {
|
|
|
201
203
|
* @returns {string[]}
|
|
202
204
|
*/
|
|
203
205
|
export function profileReferenceViolations(profiles) {
|
|
206
|
+
// used by its own test
|
|
204
207
|
const violations = [];
|
|
205
208
|
const byName = new Map(profiles.map((profile) => [profile.name, profile]));
|
|
206
209
|
for (const profile of profiles) {
|
|
@@ -250,6 +253,7 @@ export function profileReferenceViolations(profiles) {
|
|
|
250
253
|
* silently resolved as "no profile".
|
|
251
254
|
*/
|
|
252
255
|
export function resolveProfile(profiles, name, seen = new Set()) {
|
|
256
|
+
// used by its own test
|
|
253
257
|
const profile = profiles.find((candidate) => candidate.name === name);
|
|
254
258
|
if (profile === undefined) {
|
|
255
259
|
throw new Error(
|
|
@@ -302,6 +306,7 @@ export function resolveProfile(profiles, name, seen = new Set()) {
|
|
|
302
306
|
* profile-registry or reference-graph defect.
|
|
303
307
|
*/
|
|
304
308
|
export function loadProfileRegistry(path, { readFile = defaultProfileIo.readFile } = {}) {
|
|
309
|
+
// used by its own test
|
|
305
310
|
const text = readFile(path);
|
|
306
311
|
if (text === null) {
|
|
307
312
|
throw new Error(`archkeep: cannot read profiles file ${path}`);
|
|
@@ -336,13 +341,27 @@ export function loadProfileRegistry(path, { readFile = defaultProfileIo.readFile
|
|
|
336
341
|
* @param {string} profileName The profile to resolve.
|
|
337
342
|
* @param {string} sourceLabel What failed, named in the thrown message.
|
|
338
343
|
* @param {{readFile?: (path: string) => string|null}} [io]
|
|
339
|
-
* @returns {{depConstraints: object[], options: object, suppressions: object[], fitness?: object[]
|
|
344
|
+
* @returns {{depConstraints: object[], options: object, suppressions: object[], fitness?: object[],
|
|
345
|
+
* profile: string}} `profile` is the selection the policy was resolved by,
|
|
346
|
+
* carried on the policy itself so the law's identity travels with it —
|
|
347
|
+
* `../commands/graph.mjs`'s `computePolicyFingerprint` reads it, and the
|
|
348
|
+
* fingerprint is the one policy fact that travels between captures
|
|
349
|
+
* (`delta`, `diff`, `history`).
|
|
340
350
|
* @throws {Error} when the registry or the named profile is defective.
|
|
341
351
|
*/
|
|
342
352
|
export function profilePolicy(registryPath, profileName, sourceLabel, io = {}) {
|
|
343
353
|
const registry = loadProfileRegistry(registryPath, io);
|
|
344
354
|
const effective = resolveProfile(registry.profiles, profileName);
|
|
345
|
-
return
|
|
355
|
+
return {
|
|
356
|
+
...policyFrom(effective, `${sourceLabel} (profile "${profileName}")`),
|
|
357
|
+
// The selection rides on the policy it produced: two profiles whose
|
|
358
|
+
// blocks converge resolve to the same fields, so without this key a
|
|
359
|
+
// switch between them resolves to the same fingerprint and a `delta`
|
|
360
|
+
// across the switch classifies as no law change. The NAME, never
|
|
361
|
+
// `registryPath` — a path is machine-local, and a fingerprint that
|
|
362
|
+
// differs between a laptop and CI reports a change nobody made.
|
|
363
|
+
profile: profileName,
|
|
364
|
+
};
|
|
346
365
|
}
|
|
347
366
|
|
|
348
367
|
/**
|
|
@@ -52,7 +52,7 @@ import { clockViolations } from "./clock.mjs";
|
|
|
52
52
|
import { describe, isPlainObject } from "../values.mjs";
|
|
53
53
|
|
|
54
54
|
/** The only keys a validated `origin` may carry. */
|
|
55
|
-
export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]);
|
|
55
|
+
export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]); // used by its own test
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* @typedef {object} OriginRecord
|
|
@@ -135,6 +135,7 @@ export function originViolations(raw, io = {}) {
|
|
|
135
135
|
* @throws {Error} naming every violation at once, prefixed by `at`.
|
|
136
136
|
*/
|
|
137
137
|
export function validateOrigin(raw, io = {}, at = "origin") {
|
|
138
|
+
// used by its own test
|
|
138
139
|
const violations = originViolations(raw, io).map((message) =>
|
|
139
140
|
message.startsWith("origin.")
|
|
140
141
|
? `${at}.${message.slice("origin.".length)}`
|
|
@@ -181,6 +182,7 @@ export function recordOrigin({ by, tool, clock }) {
|
|
|
181
182
|
* change, supersession, and bindings change is recorded as one of these.
|
|
182
183
|
*/
|
|
183
184
|
export const DECISION_LIFECYCLE_KINDS = Object.freeze([
|
|
185
|
+
// used by its own test
|
|
184
186
|
"status-transition",
|
|
185
187
|
"supersession",
|
|
186
188
|
"bindings-change",
|
|
@@ -234,6 +236,7 @@ export const DECISION_LIFECYCLE_KINDS = Object.freeze([
|
|
|
234
236
|
* the registry's `ADR_STATUSES`, a no-op event, or an invalid origin/clock.
|
|
235
237
|
*/
|
|
236
238
|
export function recordDecisionLifecycle({
|
|
239
|
+
// used by its own test
|
|
237
240
|
kind,
|
|
238
241
|
decisionId,
|
|
239
242
|
from = null,
|
|
@@ -70,6 +70,7 @@ function boundaryKey(from, to) {
|
|
|
70
70
|
|
|
71
71
|
/** The severity a state earns — the sort key a ranked proposal list uses. */
|
|
72
72
|
export const SEVERITY_ORDER = Object.freeze({
|
|
73
|
+
// used by its own test
|
|
73
74
|
unexpected: 4,
|
|
74
75
|
absent: 3,
|
|
75
76
|
match: 0,
|
|
@@ -133,6 +134,7 @@ function intentKeys(intent) {
|
|
|
133
134
|
* @returns {{project: ScoredElement, tags: ScoredElement[]}}
|
|
134
135
|
*/
|
|
135
136
|
export function scoreProject(project, keys, requiredTagsByProject) {
|
|
137
|
+
// used by its own test
|
|
136
138
|
const tags = project.data?.tags ?? project.tags ?? [];
|
|
137
139
|
const requiredTags = requiredTagsByProject.get(project.name) ?? [];
|
|
138
140
|
const element = { plane: "project", name: project.name };
|
|
@@ -209,6 +211,7 @@ export function scoreProject(project, keys, requiredTagsByProject) {
|
|
|
209
211
|
* @returns {ScoredElement}
|
|
210
212
|
*/
|
|
211
213
|
export function scoreEdge(edge, keys, intentForbiddenPairs, tagForbiddenPairs) {
|
|
214
|
+
// used by its own test
|
|
212
215
|
const key = `${edge.source} → ${edge.target}`;
|
|
213
216
|
const element = { plane: "edge", name: key, intentRow: null };
|
|
214
217
|
|
|
@@ -279,6 +282,7 @@ export function scoreEdge(edge, keys, intentForbiddenPairs, tagForbiddenPairs) {
|
|
|
279
282
|
* @returns {ScoredElement[]}
|
|
280
283
|
*/
|
|
281
284
|
export function scoreIntentRows(intent, judgeVerdict, observed, tagsByProject) {
|
|
285
|
+
// used by its own test
|
|
282
286
|
const rows = [];
|
|
283
287
|
const observedNames = new Set(observed.projects.map((p) => p.name));
|
|
284
288
|
const observedEdgeKeys = new Set(observed.edges.map((e) => `${e.source} → ${e.target}`));
|
|
@@ -43,11 +43,14 @@
|
|
|
43
43
|
* path — a path, never a second implementation.
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
+
import { describe, isNonEmptyString } from "../values.mjs";
|
|
47
|
+
|
|
46
48
|
/** The four canonical verdict values. */
|
|
47
49
|
export const VERDICTS = Object.freeze(["pass", "fail", "unknown", "not_applicable"]);
|
|
48
50
|
|
|
49
51
|
/** The single mapping from an envelope status to a verdict. */
|
|
50
52
|
export const VERDICT_FOR_STATUS = Object.freeze({
|
|
53
|
+
// used by its own test
|
|
51
54
|
ok: "pass",
|
|
52
55
|
findings: "fail",
|
|
53
56
|
"no-verdict": "unknown",
|
|
@@ -112,7 +115,7 @@ export function fitnessVerdict({ verdict, name, evidence, message, rows, notAppl
|
|
|
112
115
|
`expected one of ${VERDICTS.join(", ")}.`,
|
|
113
116
|
);
|
|
114
117
|
}
|
|
115
|
-
if (verdict === "not_applicable" && notApplicableReason
|
|
118
|
+
if (verdict === "not_applicable" && !isNonEmptyString(notApplicableReason)) {
|
|
116
119
|
throw new Error(
|
|
117
120
|
`archkeep: fitness function "${name}" returned "not_applicable" without ` +
|
|
118
121
|
`notApplicableReason — invariant I4: the reader must be told why the ` +
|
|
@@ -205,6 +208,21 @@ export function buildDecision(run) {
|
|
|
205
208
|
throw new Error("archkeep: buildDecision needs either a status or an explicit verdict");
|
|
206
209
|
}
|
|
207
210
|
const verdict = run.verdict ?? verdictForStatus(run.status);
|
|
211
|
+
// The vocabulary latch. `verdictForStatus` cannot produce a stranger, so
|
|
212
|
+
// this refuses exactly the explicitly-passed verdicts — and it must run
|
|
213
|
+
// BEFORE the status-conflict check below, so a malformed verdict reports
|
|
214
|
+
// the vocabulary it violated rather than a conflict it never had. Without
|
|
215
|
+
// this latch, "perhaps", "PASS", `42` and `{}` fell through the
|
|
216
|
+
// pass/fail/unknown arms into the not_applicable one and SHIPPED whenever
|
|
217
|
+
// `notApplicableReason` happened to be truthy — a byte-legal decision
|
|
218
|
+
// about a workspace nobody judged.
|
|
219
|
+
if (!isVerdict(verdict)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`archkeep: refusing to build a decision with verdict ${describe(run.verdict)} — ` +
|
|
222
|
+
`expected one of ${VERDICTS.join(", ")}. This is a bug in the command that ` +
|
|
223
|
+
`built the decision.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
208
226
|
if (
|
|
209
227
|
run.verdict !== undefined &&
|
|
210
228
|
run.status !== undefined &&
|
|
@@ -259,17 +277,32 @@ export function buildDecision(run) {
|
|
|
259
277
|
}
|
|
260
278
|
|
|
261
279
|
if (verdict === "unknown") {
|
|
280
|
+
// An absent reason (undefined/null) still defaults to the generic one —
|
|
281
|
+
// absence is not emptiness. A SUPPLIED reason must actually say
|
|
282
|
+
// something: "" and " " are byte-present but semantically absent, and
|
|
283
|
+
// a non-string reason would ship a `typeof` artifact where the reader
|
|
284
|
+
// was promised a sentence (I3).
|
|
262
285
|
const reason =
|
|
263
286
|
run.reason ??
|
|
264
287
|
(run.coverageComplete === true ? "no verdict was reached" : "coverage was incomplete");
|
|
288
|
+
if (!isNonEmptyString(reason)) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`archkeep: refusing to emit an "unknown" decision with a ${describe(run.reason)} reason — ` +
|
|
291
|
+
`I3 requires a non-empty reason naming why no verdict was reached. ` +
|
|
292
|
+
`This is a bug in the command that built the decision.`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
265
295
|
return withSampleTime({ verdict, reason }, run.sampleTime);
|
|
266
296
|
}
|
|
267
297
|
|
|
268
|
-
// verdict === "not_applicable" (I4).
|
|
269
|
-
|
|
298
|
+
// verdict === "not_applicable" (I4). The reason must be a string a reader
|
|
299
|
+
// could act on — the falsy check this replaces refused "" but SHIPPED
|
|
300
|
+
// " " and non-strings, the same byte-present-semantically-absent hole.
|
|
301
|
+
if (!isNonEmptyString(run.notApplicableReason)) {
|
|
270
302
|
throw new Error(
|
|
271
303
|
`archkeep: refusing to emit a "not_applicable" decision without notApplicableReason — ` +
|
|
272
|
-
`"did not apply" and "did not run" must never be indistinguishable
|
|
304
|
+
`"did not apply" and "did not run" must never be indistinguishable, and a reason of ` +
|
|
305
|
+
`${describe(run.notApplicableReason)} tells the reader nothing. ` +
|
|
273
306
|
`This is a bug in the command that built the decision.`,
|
|
274
307
|
);
|
|
275
308
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
{
|
|
26
26
|
"type": "source-evidence",
|
|
27
27
|
"path": "src/commands/context.mjs",
|
|
28
|
-
"assertion": "
|
|
28
|
+
"assertion": "commands/context.mjs imports providers — the designated orchestration path",
|
|
29
29
|
"sha256": "b505e3a7476c64af4b2daeb106e2f9cb05be82e314604229d508dda04b629a98"
|
|
30
30
|
}
|
|
31
31
|
],
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"type": "behavioral-test",
|
|
41
41
|
"path": "src/report/json.test.mjs",
|
|
42
42
|
"assertion": "JSON envelope throws on status=ok + incomplete coverage, status/exitCode disagreement, coverage.complete/notAnalyzed disagreement",
|
|
43
|
-
"sha256": "
|
|
43
|
+
"sha256": "8f587d3bd3f06519b90f760e7eafc091ca4e4cf26b67929ab180abe35c36c8f0"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"type": "source-evidence",
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"type": "behavioral-test",
|
|
67
67
|
"path": "src/intent/intent.test.mjs",
|
|
68
68
|
"assertion": "analysis output conforms to the frozen contract schema (no extra verdict/policy fields); analysis output is invariant under project tag changes (three workspaces differing only in project tag fields compared byte-for-byte)",
|
|
69
|
-
"sha256": "
|
|
69
|
+
"sha256": "85b98de1f9268c94020b8b728a3ebbf1e8b50f8c70731c5460c788ea00be3198"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"type": "architecture-test",
|
|
73
73
|
"path": "src/intent/intent.test.mjs",
|
|
74
74
|
"assertion": "the gate walks every production analysis module and fails when judging vocabulary (judge/forbid/permit/allow/ban) appears in code",
|
|
75
|
-
"sha256": "
|
|
75
|
+
"sha256": "85b98de1f9268c94020b8b728a3ebbf1e8b50f8c70731c5460c788ea00be3198"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"type": "source-evidence",
|
|
@@ -103,14 +103,20 @@
|
|
|
103
103
|
{
|
|
104
104
|
"type": "source-evidence",
|
|
105
105
|
"path": "src/commands/graph.mjs",
|
|
106
|
-
"assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped
|
|
107
|
-
"sha256": "
|
|
106
|
+
"assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped",
|
|
107
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"type": "source-evidence",
|
|
111
|
+
"path": "src/report/json.mjs",
|
|
112
|
+
"assertion": "SCHEMA_VERSION = 2 — the envelope's schemaVersion constant",
|
|
113
|
+
"sha256": "090418f6f60dec51638976dff658c55f8a932012bfc7c8bc59952b0f03543340"
|
|
108
114
|
},
|
|
109
115
|
{
|
|
110
116
|
"type": "source-evidence",
|
|
111
117
|
"path": "src/commands/graph.mjs",
|
|
112
118
|
"assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy",
|
|
113
|
-
"sha256": "
|
|
119
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
114
120
|
}
|
|
115
121
|
],
|
|
116
122
|
"status": "proven"
|
|
@@ -130,7 +136,7 @@
|
|
|
130
136
|
"type": "source-evidence",
|
|
131
137
|
"path": "src/commands/diff.mjs",
|
|
132
138
|
"assertion": "parseBaseline validates schemaVersion; refuses unknown versions",
|
|
133
|
-
"sha256": "
|
|
139
|
+
"sha256": "e28620e288b58e35916e8cd41de446b999bdc2c4e5fcb5f3a617ab0ee449b0fd"
|
|
134
140
|
}
|
|
135
141
|
],
|
|
136
142
|
"status": "proven"
|
|
@@ -150,7 +156,7 @@
|
|
|
150
156
|
"type": "source-evidence",
|
|
151
157
|
"path": "src/commands/diff.mjs",
|
|
152
158
|
"assertion": "computeDiff returns structural diff; policyMismatch detected via fingerprint; computeRuleImpact for depConstraints-only context",
|
|
153
|
-
"sha256": "
|
|
159
|
+
"sha256": "e28620e288b58e35916e8cd41de446b999bdc2c4e5fcb5f3a617ab0ee449b0fd"
|
|
154
160
|
}
|
|
155
161
|
],
|
|
156
162
|
"status": "proven"
|
|
@@ -230,7 +236,7 @@
|
|
|
230
236
|
"type": "behavioral-test",
|
|
231
237
|
"path": "src/intent/intent.test.mjs",
|
|
232
238
|
"assertion": "depConstraints verdicts from judgeEdge agree with evaluate in both directions (violating edge found by both, legal edge reported by neither); explain includes the same violations as evaluate at a given site",
|
|
233
|
-
"sha256": "
|
|
239
|
+
"sha256": "85b98de1f9268c94020b8b728a3ebbf1e8b50f8c70731c5460c788ea00be3198"
|
|
234
240
|
},
|
|
235
241
|
{
|
|
236
242
|
"type": "behavioral-test",
|
|
@@ -260,7 +266,7 @@
|
|
|
260
266
|
"type": "source-evidence",
|
|
261
267
|
"path": "src/commands/diff.mjs",
|
|
262
268
|
"assertion": "coverage.notes warns when ruleImpact is computed (depConstraints only, 3 of 15)",
|
|
263
|
-
"sha256": "
|
|
269
|
+
"sha256": "e28620e288b58e35916e8cd41de446b999bdc2c4e5fcb5f3a617ab0ee449b0fd"
|
|
264
270
|
},
|
|
265
271
|
{
|
|
266
272
|
"type": "documentation",
|
|
@@ -286,7 +292,7 @@
|
|
|
286
292
|
"type": "source-evidence",
|
|
287
293
|
"path": "src/commands/graph.mjs",
|
|
288
294
|
"assertion": "Plain string comparison throughout; never localeCompare",
|
|
289
|
-
"sha256": "
|
|
295
|
+
"sha256": "436902ab3ca994c233318437a896630f969a2c8a22b0f93e598d309faed39d49"
|
|
290
296
|
}
|
|
291
297
|
],
|
|
292
298
|
"status": "proven"
|
|
@@ -536,6 +536,7 @@ function scanTemplate(src, i) {
|
|
|
536
536
|
* same line structure.
|
|
537
537
|
*/
|
|
538
538
|
export function maskNonCode(src) {
|
|
539
|
+
// used by its own test
|
|
539
540
|
/**
|
|
540
541
|
* Chunks of the result, joined once at the end. Code is copied through in
|
|
541
542
|
* RUNS rather than a token at a time — `plainFrom` is where the current
|
package/src/lsp/diagnostics.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { DIAGNOSTIC_SEVERITY, SERVER_INFO } from "./protocol.mjs";
|
|
|
38
38
|
* prints beside the message to say which tool spoke. Taken from the server's
|
|
39
39
|
* own identity so the two can never disagree.
|
|
40
40
|
*/
|
|
41
|
-
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
41
|
+
export const DIAGNOSTIC_SOURCE = SERVER_INFO.name; // used by its own test
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The `code` on a diagnostic that reports the ABSENCE of a verdict rather than
|
|
@@ -46,7 +46,7 @@ export const DIAGNOSTIC_SOURCE = SERVER_INFO.name;
|
|
|
46
46
|
* against `MESSAGE_IDS` at load so a future upstream id cannot silently collide
|
|
47
47
|
* with it.
|
|
48
48
|
*/
|
|
49
|
-
export const ANALYSIS_FAILURE_CODE = "analysisFailure";
|
|
49
|
+
export const ANALYSIS_FAILURE_CODE = "analysisFailure"; // used by its own test
|
|
50
50
|
|
|
51
51
|
if (MESSAGE_IDS.includes(ANALYSIS_FAILURE_CODE)) {
|
|
52
52
|
throw new Error(
|
|
@@ -100,6 +100,7 @@ const QUOTES = new Set(['"', "'", "`"]);
|
|
|
100
100
|
* @returns {{start: {line: number, character: number}, end: {line: number, character: number}}}
|
|
101
101
|
*/
|
|
102
102
|
export function rangeAt(at, lines) {
|
|
103
|
+
// used by its own test
|
|
103
104
|
// A failure about the file as a whole carries no position (`contract.md`
|
|
104
105
|
// fixes it as an explicit `null`). It gets the first line, whole: a
|
|
105
106
|
// zero-width range at the origin renders as an invisible caret in most
|
package/src/lsp/protocol.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export const DIAGNOSTIC_SEVERITY = Object.freeze({
|
|
|
95
95
|
* more is not a supported conversation, and saying so beats waiting for bytes
|
|
96
96
|
* that will never arrive.
|
|
97
97
|
*/
|
|
98
|
-
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024;
|
|
98
|
+
export const MAX_CONTENT_LENGTH = 64 * 1024 * 1024; // used by its own test
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* LSP `MessageType`, as `window/showMessage` reports it.
|
|
@@ -211,5 +211,6 @@ export function uriToPath(uri) {
|
|
|
211
211
|
* @returns {string}
|
|
212
212
|
*/
|
|
213
213
|
export function pathToUri(path) {
|
|
214
|
+
// used by its own test
|
|
214
215
|
return pathToFileURL(path).href;
|
|
215
216
|
}
|
package/src/lsp/server.mjs
CHANGED
|
@@ -292,6 +292,7 @@ const POLYGLOT_GRAPH_MANIFESTS = Object.freeze([
|
|
|
292
292
|
* @returns {readonly string[]}
|
|
293
293
|
*/
|
|
294
294
|
export function watchedFilesFor(options, { unresolved = false } = {}) {
|
|
295
|
+
// used by its own test
|
|
295
296
|
return Object.freeze([
|
|
296
297
|
...(typeof options.boundaryConfig === "string" ? [options.boundaryConfig] : []),
|
|
297
298
|
...(unresolved
|
|
@@ -376,6 +377,7 @@ function markersAt(root) {
|
|
|
376
377
|
* convention chain can find while carrying files that need one.
|
|
377
378
|
*/
|
|
378
379
|
export function readWorkspaceOptions(root) {
|
|
380
|
+
// used by its own test
|
|
379
381
|
const { hasNx, hasNative } = markersAt(root);
|
|
380
382
|
if (hasNx && hasNative) {
|
|
381
383
|
throw new Error(
|
|
@@ -450,6 +452,7 @@ const WATCHER_REGISTRATION_ID = "archkeep/watched-files";
|
|
|
450
452
|
* text than the one on screen.
|
|
451
453
|
*/
|
|
452
454
|
export const SERVER_CAPABILITIES = Object.freeze({
|
|
455
|
+
// used by its own test
|
|
453
456
|
textDocumentSync: Object.freeze({
|
|
454
457
|
openClose: true,
|
|
455
458
|
change: TEXT_DOCUMENT_SYNC_KIND.full,
|
|
@@ -156,7 +156,7 @@ export { PROJECT_CONFIG_FILE, nodeTypeOf, buildDependencies };
|
|
|
156
156
|
* @returns {object} Whatever the JSON describes.
|
|
157
157
|
* @throws {Error} when neither parser can read it.
|
|
158
158
|
*/
|
|
159
|
-
|
|
159
|
+
const parseProjectJson = parseNxJson;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Every file git considers part of the working tree, workspace-relative and
|
|
@@ -205,6 +205,7 @@ const directoryOf = (file) => {
|
|
|
205
205
|
* @returns {{projects: {name: string, root: string, config: object}[], skipped: {file: string, reason: string}[]}}
|
|
206
206
|
*/
|
|
207
207
|
export function discoverProjects({ files, readFile }) {
|
|
208
|
+
// used by its own test
|
|
208
209
|
const projects = [];
|
|
209
210
|
const skipped = [];
|
|
210
211
|
for (const file of files) {
|
|
@@ -263,6 +264,7 @@ export function discoverProjects({ files, readFile }) {
|
|
|
263
264
|
* publishes the collision through `indexGaps`.
|
|
264
265
|
*/
|
|
265
266
|
export function buildNodes(projects) {
|
|
267
|
+
// used by its own test
|
|
266
268
|
// Null-prototype for the same reason `../providers/native/graph.mjs` and
|
|
267
269
|
// `../providers/moon.mjs` use them: every key here is a project NAME, and
|
|
268
270
|
// project names come from a `project.json`'s own `name` field —
|