@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
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
import { readFileSync } from "node:fs";
|
|
62
62
|
|
|
63
63
|
import { policyFrom } from "../config.mjs";
|
|
64
|
+
import { describe, isPlainObject } from "../values.mjs";
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* The top-level keys a profiles file may carry. `version` is checked AFTER
|
|
@@ -109,16 +110,6 @@ const BLOCK_KEYS = ["depConstraints", "moduleBoundaryOptions", "boundarySuppress
|
|
|
109
110
|
*/
|
|
110
111
|
const NAME_PATTERN = /^[a-zA-Z0-9_-]+$/u;
|
|
111
112
|
|
|
112
|
-
/** @type {(value: unknown) => value is Record<string, unknown>} */
|
|
113
|
-
const isPlainObject = (value) =>
|
|
114
|
-
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
-
|
|
116
|
-
function describe(value) {
|
|
117
|
-
if (Array.isArray(value)) return `an array (${JSON.stringify(value)})`;
|
|
118
|
-
if (value === null) return "null";
|
|
119
|
-
return `${typeof value} (${JSON.stringify(value) ?? String(value)})`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
113
|
/** A profile's declared block, kept ONLY for this command's own data. */
|
|
123
114
|
export function listNames(registry) {
|
|
124
115
|
return registry.profiles.map((profile) => profile.name);
|
|
@@ -304,23 +295,13 @@ export function resolveProfile(profiles, name, seen = new Set()) {
|
|
|
304
295
|
* @param {string} path Absolute path of the profiles file.
|
|
305
296
|
* @param {{readFile?: (path: string) => string|null}} [io] Injectable read,
|
|
306
297
|
* the same seam `../../options.mjs`'s readers take; answers `null` when the
|
|
307
|
-
* file is not there.
|
|
298
|
+
* file is not there. Defaults to `defaultProfileIo.readFile` — the sync
|
|
299
|
+
* `node:fs` read this module makes, the only place it touches the disk.
|
|
308
300
|
* @returns {{profiles: object[]}}
|
|
309
301
|
* @throws {Error} on a missing/unreadable/unparseable file, or on any
|
|
310
302
|
* profile-registry or reference-graph defect.
|
|
311
303
|
*/
|
|
312
|
-
export function loadProfileRegistry(
|
|
313
|
-
path,
|
|
314
|
-
{
|
|
315
|
-
readFile = (p) => {
|
|
316
|
-
try {
|
|
317
|
-
return readFileSync(p, "utf8");
|
|
318
|
-
} catch {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
} = {},
|
|
323
|
-
) {
|
|
304
|
+
export function loadProfileRegistry(path, { readFile = defaultProfileIo.readFile } = {}) {
|
|
324
305
|
const text = readFile(path);
|
|
325
306
|
if (text === null) {
|
|
326
307
|
throw new Error(`archkeep: cannot read profiles file ${path}`);
|
|
@@ -363,3 +344,19 @@ export function profilePolicy(registryPath, profileName, sourceLabel, io = {}) {
|
|
|
363
344
|
const effective = resolveProfile(registry.profiles, profileName);
|
|
364
345
|
return policyFrom(effective, `${sourceLabel} (profile "${profileName}")`);
|
|
365
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The default io: the sync `node:fs` read `loadProfileRegistry` makes, wrapped
|
|
350
|
+
* in the null-on-missing contract `../../options.mjs`'s readers share. This is
|
|
351
|
+
* the only place in this module the filesystem is named directly — a test
|
|
352
|
+
* injects a `readFile` and the module body never touches the disk on its own.
|
|
353
|
+
*/
|
|
354
|
+
const defaultProfileIo = Object.freeze({
|
|
355
|
+
readFile: (p) => {
|
|
356
|
+
try {
|
|
357
|
+
return readFileSync(p, "utf8");
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
});
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
import { ADR_STATUSES } from "./adr-registry.mjs";
|
|
51
51
|
import { clockViolations } from "./clock.mjs";
|
|
52
|
+
import { describe, isPlainObject } from "../values.mjs";
|
|
52
53
|
|
|
53
54
|
/** The only keys a validated `origin` may carry. */
|
|
54
55
|
export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]);
|
|
@@ -63,17 +64,6 @@ export const ORIGIN_KEYS = Object.freeze(["by", "tool", "on"]);
|
|
|
63
64
|
* `recordOrigin` produced it through the shared clock.
|
|
64
65
|
*/
|
|
65
66
|
|
|
66
|
-
/** @type {(value: unknown) => value is Record<string, unknown>} */
|
|
67
|
-
const isPlainObject = (value) =>
|
|
68
|
-
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
69
|
-
|
|
70
|
-
/** A value's type, for an error message that shows what was actually there. */
|
|
71
|
-
function describe(value) {
|
|
72
|
-
if (Array.isArray(value)) return `an array (${JSON.stringify(value)})`;
|
|
73
|
-
if (value === null) return "null";
|
|
74
|
-
return `${typeof value} (${JSON.stringify(value) ?? String(value)})`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
67
|
/**
|
|
78
68
|
* Everything wrong with a raw `origin` record at READ time, as messages; empty
|
|
79
69
|
* when it is well-formed. Shape only — an `on` committed in a declaration file
|
|
@@ -76,9 +76,6 @@ export const SEVERITY_ORDER = Object.freeze({
|
|
|
76
76
|
unknown: Infinity,
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
/** A scored element's state, in the vocabulary the text and JSON reports share. */
|
|
80
|
-
export const ELEMENT_STATES = Object.freeze(["match", "absent", "unexpected", "unknown"]);
|
|
81
|
-
|
|
82
79
|
/**
|
|
83
80
|
* A scored element.
|
|
84
81
|
*
|
|
@@ -57,9 +57,7 @@
|
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
59
|
import { originViolations } from "./provenance-record.mjs";
|
|
60
|
-
|
|
61
|
-
/** The shape of any `origin.on` producer. Re-exported for a row owner's own docs. */
|
|
62
|
-
export { clockViolations as clockValidation } from "./clock.mjs";
|
|
60
|
+
import { describe, isPlainObject } from "../values.mjs";
|
|
63
61
|
|
|
64
62
|
/** The four governance keys a row may carry, in the order reports list them. */
|
|
65
63
|
export const GOVERNANCE_ROW_KEYS = Object.freeze([
|
|
@@ -85,17 +83,6 @@ export const GOVERNANCE_ROW_KEYS = Object.freeze([
|
|
|
85
83
|
* @property {string[]} [fitnessBindings] Fitness ids this row is bound to.
|
|
86
84
|
*/
|
|
87
85
|
|
|
88
|
-
/** @type {(value: unknown) => value is Record<string, unknown>} */
|
|
89
|
-
const isPlainObject = (value) =>
|
|
90
|
-
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
91
|
-
|
|
92
|
-
/** A value's type, for an error message that shows what was actually there. */
|
|
93
|
-
function describe(value) {
|
|
94
|
-
if (Array.isArray(value)) return `an array (${JSON.stringify(value)})`;
|
|
95
|
-
if (value === null) return "null";
|
|
96
|
-
return `${typeof value} (${JSON.stringify(value) ?? String(value)})`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
86
|
/**
|
|
100
87
|
* Everything wrong with a row's `rationale`, `decisionRef`, or
|
|
101
88
|
* `fitnessBindings` — the three string-shaped governance keys. `origin` has
|
|
@@ -34,10 +34,13 @@
|
|
|
34
34
|
* claim the vocabulary makes and the hardest to disprove, so every other
|
|
35
35
|
* state exists to refuse it.
|
|
36
36
|
*
|
|
37
|
-
* The enforcer that makes the invariants executable
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* The enforcer that makes the invariants executable is `buildDecision`, in
|
|
38
|
+
* this file, beside the vocabulary it enforces — a vocabulary and the check
|
|
39
|
+
* that a verdict's evidence agrees with it are one subject, and splitting
|
|
40
|
+
* them across the report boundary had made the core verdict module
|
|
41
|
+
* (`../verdict.mjs`) depend on the presentation layer. `../report/evidence.mjs`
|
|
42
|
+
* re-exports `buildDecision` so the render-side callers keep their import
|
|
43
|
+
* path — a path, never a second implementation.
|
|
41
44
|
*/
|
|
42
45
|
|
|
43
46
|
/** The four canonical verdict values. */
|
|
@@ -125,3 +128,164 @@ export function fitnessVerdict({ verdict, name, evidence, message, rows, notAppl
|
|
|
125
128
|
...(notApplicableReason === undefined ? {} : { notApplicableReason }),
|
|
126
129
|
};
|
|
127
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The decision builder: turns a command's verdict counts into the `decision`
|
|
134
|
+
* the envelope optionally carries, enforcing the five invariants the module
|
|
135
|
+
* header states in code rather than leaving them to a docs page a later
|
|
136
|
+
* command author might not read.
|
|
137
|
+
*
|
|
138
|
+
* This function decides nothing about whether a finding IS one — the command
|
|
139
|
+
* that built the envelope owns that. What it decides is whether the verdict
|
|
140
|
+
* and its evidence AGREE, and it throws when they do not, the same posture
|
|
141
|
+
* `../report/json.mjs`'s `jsonEnvelope` takes for the three consistency rules
|
|
142
|
+
* it enforces: a mismatch here is a bug in the command, not a fact about the
|
|
143
|
+
* workspace.
|
|
144
|
+
*
|
|
145
|
+
* The shape it produces:
|
|
146
|
+
*
|
|
147
|
+
* {
|
|
148
|
+
* verdict: "pass" | "fail" | "unknown" | "not_applicable",
|
|
149
|
+
* reason?: string, // always present for unknown
|
|
150
|
+
* notApplicableReason?: string, // always present for not_applicable
|
|
151
|
+
* sampleTime?: string // opt-in, never on a deterministic envelope
|
|
152
|
+
* }
|
|
153
|
+
*
|
|
154
|
+
* A caller may pass `reason` for `unknown` — it names WHICH could-not-look
|
|
155
|
+
* condition fired (coverage incomplete, an unresolved intent boundary, a
|
|
156
|
+
* thrown analysis). Without it, `buildDecision` states the generic one. The
|
|
157
|
+
* reason field itself is always present on an `unknown` decision (I3).
|
|
158
|
+
*
|
|
159
|
+
* ## Determinism is the default
|
|
160
|
+
*
|
|
161
|
+
* The envelope this decision rides on is byte-deterministic
|
|
162
|
+
* (`docs/reference/json-output.md`: no timestamp, no random identifier). So
|
|
163
|
+
* `sampleTime` is OPT-IN by construction: a command passes it explicitly when
|
|
164
|
+
* it is an age/count capability (waivers, debt, health — the features
|
|
165
|
+
* `./clock.mjs` serves), and a command whose verdict must stay reproducible
|
|
166
|
+
* over an unchanged tree emits a decision with no time at all. That is how
|
|
167
|
+
* the determinism↔time tension is resolved — the clock is injectable (a test
|
|
168
|
+
* drives the same code with a fixed time), never asserted from the wall
|
|
169
|
+
* clock.
|
|
170
|
+
*
|
|
171
|
+
* ## One refusal per invariant
|
|
172
|
+
*
|
|
173
|
+
* I1 refuses a `pass` over incomplete coverage — the same refusal
|
|
174
|
+
* `jsonEnvelope` makes for `status: "ok"` over incomplete coverage, at the
|
|
175
|
+
* verdict layer — and a `pass` carrying findings. I2 refuses a `fail` that
|
|
176
|
+
* names no finding. I3 keeps a reason on every `unknown`, defaulting the
|
|
177
|
+
* generic one when the caller supplies none. I4 refuses a `not_applicable`
|
|
178
|
+
* without its `notApplicableReason`. I5 is I1's first check plus every caller
|
|
179
|
+
* choosing `unknown` wherever the run did not reach a verdict.
|
|
180
|
+
*
|
|
181
|
+
* `not_applicable` has no envelope status, so `buildDecision` reaches it only
|
|
182
|
+
* through an explicit `verdict` — the route a Fitness or Waiver capability
|
|
183
|
+
* takes. Engine behavior today never passes it: `jsonEnvelope` refuses a
|
|
184
|
+
* `decision.verdict` that contradicts the envelope's `status`, and no status
|
|
185
|
+
* maps to `not_applicable`, so the state is locked out of every envelope this
|
|
186
|
+
* release builds.
|
|
187
|
+
*
|
|
188
|
+
* @param {{
|
|
189
|
+
* verdict?: "pass"|"fail"|"unknown"|"not_applicable",
|
|
190
|
+
* status?: "ok"|"findings"|"no-verdict",
|
|
191
|
+
* coverageComplete: boolean,
|
|
192
|
+
* findings: number,
|
|
193
|
+
* reason?: string|null,
|
|
194
|
+
* notApplicableReason?: string|null,
|
|
195
|
+
* sampleTime?: string
|
|
196
|
+
* }} run
|
|
197
|
+
* @returns {{verdict: string, reason?: string, notApplicableReason?: string,
|
|
198
|
+
* sampleTime?: string}}
|
|
199
|
+
* @throws {Error} on any invariant violation (I1–I4).
|
|
200
|
+
*/
|
|
201
|
+
export function buildDecision(run) {
|
|
202
|
+
if (run.verdict === undefined && run.status === undefined) {
|
|
203
|
+
// No status, no explicit verdict — a builder called with neither is a
|
|
204
|
+
// programming error, not a fact about the workspace.
|
|
205
|
+
throw new Error("archkeep: buildDecision needs either a status or an explicit verdict");
|
|
206
|
+
}
|
|
207
|
+
const verdict = run.verdict ?? verdictForStatus(run.status);
|
|
208
|
+
if (
|
|
209
|
+
run.verdict !== undefined &&
|
|
210
|
+
run.status !== undefined &&
|
|
211
|
+
run.verdict !== verdictForStatus(run.status)
|
|
212
|
+
) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`archkeep: refusing to build a decision where verdict "${run.verdict}" contradicts status ` +
|
|
215
|
+
`"${run.status}" — status implies ${verdictForStatus(run.status)}, and a decision that ` +
|
|
216
|
+
`disagrees with its own status would make one of the two a lie. ` +
|
|
217
|
+
`This is a bug in the command that built the decision.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The `findings` count is the cardinal evidence number — a non-negative
|
|
222
|
+
// integer that the I1–I5 invariants all rely on. A missing, non-numeric, or
|
|
223
|
+
// negative value would silently falsify every comparison (`undefined > 0` is
|
|
224
|
+
// `false`), producing a clean verdict over a run whose counts were never set
|
|
225
|
+
// or are logically impossible — the exact silent direction this module exists
|
|
226
|
+
// to refuse.
|
|
227
|
+
if (typeof run.findings !== "number" || !Number.isFinite(run.findings) || run.findings < 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`archkeep: refusing to build a decision where findings is ${JSON.stringify(run.findings)} ` +
|
|
230
|
+
`— findings must be a non-negative number, or the verdict invariants cannot be enforced. ` +
|
|
231
|
+
`This is a bug in the command that built the decision.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (verdict === "pass") {
|
|
235
|
+
if (run.coverageComplete !== true) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`archkeep: refusing to emit a "pass" decision over incomplete coverage ` +
|
|
238
|
+
`(coverage.complete: ${run.coverageComplete}) — a run that could not fully read the ` +
|
|
239
|
+
`tree can never pass. This is a bug in the command that built the decision.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (run.findings > 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`archkeep: refusing to emit a "pass" decision with ${run.findings} finding(s) — ` +
|
|
245
|
+
`"pass" and "fail" cannot both be true of the same run. This is a bug in the command.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return withSampleTime({ verdict }, run.sampleTime);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (verdict === "fail") {
|
|
252
|
+
if (run.findings < 1) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`archkeep: refusing to emit a "fail" decision with no findings — a failing verdict ` +
|
|
255
|
+
`must name what failed. This is a bug in the command that built the decision.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return withSampleTime({ verdict }, run.sampleTime);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (verdict === "unknown") {
|
|
262
|
+
const reason =
|
|
263
|
+
run.reason ??
|
|
264
|
+
(run.coverageComplete === true ? "no verdict was reached" : "coverage was incomplete");
|
|
265
|
+
return withSampleTime({ verdict, reason }, run.sampleTime);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// verdict === "not_applicable" (I4).
|
|
269
|
+
if (!run.notApplicableReason) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`archkeep: refusing to emit a "not_applicable" decision without notApplicableReason — ` +
|
|
272
|
+
`"did not apply" and "did not run" must never be indistinguishable. ` +
|
|
273
|
+
`This is a bug in the command that built the decision.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return withSampleTime({ verdict, notApplicableReason: run.notApplicableReason }, run.sampleTime);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Adds `sampleTime` to the decision only when the caller opted into time —
|
|
281
|
+
* the determinism rule in `buildDecision`'s header. Absent `sampleTime`, the
|
|
282
|
+
* decision object carries exactly the invariant-bearing fields and nothing
|
|
283
|
+
* more.
|
|
284
|
+
*
|
|
285
|
+
* @param {object} decision
|
|
286
|
+
* @param {string|undefined} sampleTime
|
|
287
|
+
* @returns {object}
|
|
288
|
+
*/
|
|
289
|
+
function withSampleTime(decision, sampleTime) {
|
|
290
|
+
return sampleTime === undefined ? decision : { ...decision, sampleTime };
|
|
291
|
+
}
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"type": "architecture-test",
|
|
21
21
|
"path": "src/conformance/boundary.test.mjs",
|
|
22
22
|
"assertion": "SHIPPED_PACKAGES allow-list prevents unapproved dependencies; specifiersIn() walk catches all imports including createRequire; no provider import found in core layers",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "e4e24ed9f36e31126249c7d079b3656d17ac78ee4f12033321467ffa6666f033"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "source-evidence",
|
|
27
27
|
"path": "src/commands/context.mjs",
|
|
28
28
|
"assertion": "Only commands/context.mjs imports providers — the designated orchestration layer",
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "b505e3a7476c64af4b2daeb106e2f9cb05be82e314604229d508dda04b629a98"
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
32
|
"status": "proven"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"type": "source-evidence",
|
|
47
47
|
"path": "src/lsp/diagnose.mjs",
|
|
48
48
|
"assertion": "analyzed:false on every non-verdict path; empty diagnostic list only from two named places — plus a document whose only positioned failure is an external disclosure, which was judged and so is not published (#603)",
|
|
49
|
-
"sha256": "
|
|
49
|
+
"sha256": "0edeb309a9bba49f4e82d8bf82982363ae9a20bfc04421b208961854b84acd41"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"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": "adb25388f17c97577e97e6b65bd3dacc5811710fe3bb60a409a51ff4abcd945e"
|
|
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": "adb25388f17c97577e97e6b65bd3dacc5811710fe3bb60a409a51ff4abcd945e"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"type": "source-evidence",
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
"type": "source-evidence",
|
|
105
105
|
"path": "src/commands/graph.mjs",
|
|
106
106
|
"assertion": "Plain string comparison, never localeCompare; INTERNAL_DATA_FIELDS stripped; SCHEMA_VERSION = 2",
|
|
107
|
-
"sha256": "
|
|
107
|
+
"sha256": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"type": "source-evidence",
|
|
111
111
|
"path": "src/commands/graph.mjs",
|
|
112
112
|
"assertion": "computePolicyFingerprint produces SHA-256 of canonicalized policy",
|
|
113
|
-
"sha256": "
|
|
113
|
+
"sha256": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
114
114
|
}
|
|
115
115
|
],
|
|
116
116
|
"status": "proven"
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"type": "source-evidence",
|
|
131
131
|
"path": "src/commands/diff.mjs",
|
|
132
132
|
"assertion": "parseBaseline validates schemaVersion; refuses unknown versions",
|
|
133
|
-
"sha256": "
|
|
133
|
+
"sha256": "85e6400006a7e93c66e64547162c854320fa69d833a11cec42773703f522a163"
|
|
134
134
|
}
|
|
135
135
|
],
|
|
136
136
|
"status": "proven"
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"type": "source-evidence",
|
|
151
151
|
"path": "src/commands/diff.mjs",
|
|
152
152
|
"assertion": "computeDiff returns structural diff; policyMismatch detected via fingerprint; computeRuleImpact for depConstraints-only context",
|
|
153
|
-
"sha256": "
|
|
153
|
+
"sha256": "85e6400006a7e93c66e64547162c854320fa69d833a11cec42773703f522a163"
|
|
154
154
|
}
|
|
155
155
|
],
|
|
156
156
|
"status": "proven"
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
"type": "behavioral-test",
|
|
231
231
|
"path": "src/intent/intent.test.mjs",
|
|
232
232
|
"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": "
|
|
233
|
+
"sha256": "adb25388f17c97577e97e6b65bd3dacc5811710fe3bb60a409a51ff4abcd945e"
|
|
234
234
|
},
|
|
235
235
|
{
|
|
236
236
|
"type": "behavioral-test",
|
|
@@ -248,19 +248,19 @@
|
|
|
248
248
|
"type": "source-evidence",
|
|
249
249
|
"path": "src/commands/context-command.mjs",
|
|
250
250
|
"assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)",
|
|
251
|
-
"sha256": "
|
|
251
|
+
"sha256": "f906671ff4a5720d80ea5a3dab2779f1cfd3648fbc1e6a1f3a6d0e6efef21feb"
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
"type": "source-evidence",
|
|
255
255
|
"path": "src/commands/impact.mjs",
|
|
256
256
|
"assertion": "coverage.notes warns that per-edge violations cover only depConstraints (3 of 15 violation types)",
|
|
257
|
-
"sha256": "
|
|
257
|
+
"sha256": "152a53a0010d8cbd51d11e3ce6268fa50d1e2bcacab3d7bcb38dbd53fc120bbf"
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
"type": "source-evidence",
|
|
261
261
|
"path": "src/commands/diff.mjs",
|
|
262
262
|
"assertion": "coverage.notes warns when ruleImpact is computed (depConstraints only, 3 of 15)",
|
|
263
|
-
"sha256": "
|
|
263
|
+
"sha256": "85e6400006a7e93c66e64547162c854320fa69d833a11cec42773703f522a163"
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
"type": "documentation",
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"type": "source-evidence",
|
|
287
287
|
"path": "src/commands/graph.mjs",
|
|
288
288
|
"assertion": "Plain string comparison throughout; never localeCompare",
|
|
289
|
-
"sha256": "
|
|
289
|
+
"sha256": "72a28c1edcbcb209f74bd10a1a3691e25aaa848bac4bd54aceeacf8d415c8f13"
|
|
290
290
|
}
|
|
291
291
|
],
|
|
292
292
|
"status": "proven"
|
|
@@ -312,13 +312,13 @@
|
|
|
312
312
|
"type": "source-evidence",
|
|
313
313
|
"path": "src/providers/moon.mjs",
|
|
314
314
|
"assertion": "inferWorkspaceLayout returns null for partial layouts — same all-or-nothing contract as Nx and Native",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "2b874da56bda5bc6cfcfb4a984556ace0eafe3a6a98d964c78204bd96cc30e48"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"type": "behavioral-test",
|
|
319
319
|
"path": "src/providers/moon.test.mjs",
|
|
320
320
|
"assertion": "Partial layout (apps-only, libs-only) returns undefined workspaceLayout",
|
|
321
|
-
"sha256": "
|
|
321
|
+
"sha256": "906fcd38b82af875aecca2910231ec5d5bb551e2a6e7d452f35a0670326c8756"
|
|
322
322
|
}
|
|
323
323
|
],
|
|
324
324
|
"status": "proven"
|
package/src/lsp/diagnose.mjs
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { analyzeFile } from "../analysis/analyze.mjs";
|
|
45
45
|
import { isExternalSiteFailure, projectOwning } from "../analysis/source-util.mjs";
|
|
46
|
-
import { declaredEdgeViolationsForCheck } from "../
|
|
46
|
+
import { declaredEdgeViolationsForCheck } from "../rules/edge-constraints.mjs";
|
|
47
47
|
import { evaluate } from "../rules/index.mjs";
|
|
48
48
|
|
|
49
49
|
import {
|
|
@@ -170,7 +170,7 @@ export function diagnoseDocument({ sourceFile, text, index, config }) {
|
|
|
170
170
|
// Nx/`archkeep.json` `implicitDependencies` declaration — has no import site
|
|
171
171
|
// behind it, so it never becomes an `importSites` record for the rule engine
|
|
172
172
|
// to iterate. The CLI judges exactly those edges itself
|
|
173
|
-
// (`../
|
|
173
|
+
// (`../rules/edge-constraints.mjs`'s `declaredEdgeViolationsForCheck`,
|
|
174
174
|
// `cli.mjs check`), and without the same fold here the editor would paint a
|
|
175
175
|
// file clean while `check` exits 1 over the same declared edge — the
|
|
176
176
|
// boundary rule that never runs, dressed as a clean tree. Only the violations
|
package/src/lsp/server.mjs
CHANGED
|
@@ -393,7 +393,7 @@ export function readWorkspaceOptions(root) {
|
|
|
393
393
|
// A Moon root, checked only once neither marker file is there: a `.moon`
|
|
394
394
|
// beside `nx.json` keeps falling to `readPluginOptions` below exactly as it
|
|
395
395
|
// did, and `./workspace-index.mjs`'s `buildWorkspaceIndex` refuses the pair
|
|
396
|
-
// loudly through the one shared gate (`../
|
|
396
|
+
// loudly through the one shared gate (`../providers/model-gate.mjs`'s
|
|
397
397
|
// `requireSingleProjectModel`) rather than this function growing a second
|
|
398
398
|
// copy of that refusal. `moonMarkerAt` is the same dispatcher the index and
|
|
399
399
|
// the CLI read, so all three agree about which directory marks the tree —
|
|
@@ -134,7 +134,7 @@ import {
|
|
|
134
134
|
import { nodeTypeOf, PROJECT_CONFIG_FILE } from "../providers/native/discover.mjs";
|
|
135
135
|
import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
136
136
|
import { nativeProvider } from "../providers/native/index.mjs";
|
|
137
|
-
import { requireSingleProjectModel } from "../
|
|
137
|
+
import { requireSingleProjectModel } from "../providers/model-gate.mjs";
|
|
138
138
|
import { mergeImportEdges, moonProvider } from "../providers/moon.mjs";
|
|
139
139
|
|
|
140
140
|
export { PROJECT_CONFIG_FILE, nodeTypeOf, buildDependencies };
|
|
@@ -337,7 +337,7 @@ export function buildNodes(projects) {
|
|
|
337
337
|
* no project has no boundary to cross — a clean report, produced by not
|
|
338
338
|
* looking. Also when the root carries more than one project-model marker —
|
|
339
339
|
* a Moon directory beside `nx.json`/`archkeep.json`, both Moon spellings at
|
|
340
|
-
* once, or `nx.json` beside `archkeep.json` (`../
|
|
340
|
+
* once, or `nx.json` beside `archkeep.json` (`../providers/model-gate.mjs`'s
|
|
341
341
|
* `requireSingleProjectModel`, the same refusal `check` makes) — which
|
|
342
342
|
* config governs at all is a decision nobody made, refused the same way an
|
|
343
343
|
* unreadable `nx.json` is, through this function's caller in `./server.mjs`.
|
|
@@ -354,7 +354,7 @@ export function buildWorkspaceIndex({
|
|
|
354
354
|
const files = listFiles(root);
|
|
355
355
|
const readFile = (path) => readFileAt(root, path);
|
|
356
356
|
// Which provider may judge this root at all — the SAME gate
|
|
357
|
-
// (`../
|
|
357
|
+
// (`../providers/model-gate.mjs`'s `requireSingleProjectModel`) the CLI reads
|
|
358
358
|
// before any command runs, so a tree carrying a Moon directory beside
|
|
359
359
|
// `nx.json`/`archkeep.json` is refused here exactly as `check` refuses it,
|
|
360
360
|
// in the same words, from the one copy of the rule. Before the shared
|
package/src/options.mjs
CHANGED
|
@@ -322,7 +322,7 @@ export function readPluginOptions(workspaceRoot, { readFile = readFileOrNull } =
|
|
|
322
322
|
* `nx.json` → `plugins[].options.tsConfig` and a native root states it on
|
|
323
323
|
* `archkeep.json`'s own `tsConfig` field; Moon's own configuration carries no
|
|
324
324
|
* plugin-options table, and a `archkeep.json` beside `.moon/` is refused
|
|
325
|
-
* outright (`./
|
|
325
|
+
* outright (`./providers/model-gate.mjs`'s `requireSingleProjectModel`), so every
|
|
326
326
|
* door to a stated name is shut. What is left is convention, and one name is
|
|
327
327
|
* not enough of it: measured on a 94-project Vue Moon workspace whose `paths`
|
|
328
328
|
* table lives in `tsconfig.json` with no `tsconfig.base.json` beside it, a
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one gate deciding whether a workspace root may be judged at all: which
|
|
3
|
+
* project model it declares, and a loud refusal when it declares more than
|
|
4
|
+
* one.
|
|
5
|
+
*
|
|
6
|
+
* Lives in the providers layer because the decision it makes is a
|
|
7
|
+
* provider-selection decision — `../commands/context.mjs` composes a provider
|
|
8
|
+
* behind it, and `../lsp/workspace-index.mjs` branches on the same facts
|
|
9
|
+
* before choosing one — and neither face may hold a second copy of the rule.
|
|
10
|
+
* A second copy was exactly how the faces drifted apart once: the CLI refused
|
|
11
|
+
* a tree carrying a Moon directory beside `nx.json`/`archkeep.json` while the
|
|
12
|
+
* editor indexed it anyway — a clean diagnostic list over a tree nobody agreed
|
|
13
|
+
* could be judged (#223's silent shape, one level up).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { NX_CONFIG_FILE } from "../options.mjs";
|
|
20
|
+
import { ARCHKEEP_MODEL_FILE } from "./native/model.mjs";
|
|
21
|
+
import { moonMarkerAt } from "./moon.mjs";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The one gate deciding whether `root` may be judged at all: more than ONE
|
|
25
|
+
* project-model marker present is refused, naming what conflicts.
|
|
26
|
+
*
|
|
27
|
+
* Every entry point that picks a provider must answer this identically —
|
|
28
|
+
* `../commands/context.mjs`'s `resolveCommandContext` reads it before any
|
|
29
|
+
* command runs, and `../lsp/workspace-index.mjs`'s index build reads it before
|
|
30
|
+
* choosing a branch. Moon-versus-Moon coexistence (`.moon/` AND
|
|
31
|
+
* `.config/moon/`) is refused inside `./moon.mjs`'s `moonMarkerAt`, which this
|
|
32
|
+
* gate calls first; the cross-family pairs are refused here, all in the same
|
|
33
|
+
* terms: which model to judge against is a decision nobody made, not one this
|
|
34
|
+
* tool can make for them.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} root
|
|
37
|
+
* @param {{exists?: (path: string) => boolean}} [io] Injectable existence
|
|
38
|
+
* test (absolute paths), so a test drives this without a filesystem.
|
|
39
|
+
* @returns {{hasNx: boolean, hasNative: boolean, moonMarker: string|null}}
|
|
40
|
+
* The facts a provider choice needs; `moonMarker` names whichever Moon
|
|
41
|
+
* directory is present, `null` when neither spelling is.
|
|
42
|
+
* @throws {Error} when more than one marker is present.
|
|
43
|
+
*/
|
|
44
|
+
export function requireSingleProjectModel(root, { exists = existsSync } = {}) {
|
|
45
|
+
const moonMarker = moonMarkerAt(root, { exists });
|
|
46
|
+
const hasNx = exists(join(root, NX_CONFIG_FILE));
|
|
47
|
+
const hasNative = exists(join(root, ARCHKEEP_MODEL_FILE));
|
|
48
|
+
const refusal = (a, b) =>
|
|
49
|
+
new Error(
|
|
50
|
+
`archkeep: ${root} declares both ${a} and ${b} — this tool judges a workspace ` +
|
|
51
|
+
`against exactly one project model, and a tree carrying both is a decision nobody made ` +
|
|
52
|
+
`rather than one this tool can make for them. Remove whichever one is not the ` +
|
|
53
|
+
`workspace's real source of truth for projects and tags.`,
|
|
54
|
+
);
|
|
55
|
+
if (moonMarker !== null && hasNx) throw refusal(moonMarker, NX_CONFIG_FILE);
|
|
56
|
+
if (moonMarker !== null && hasNative) throw refusal(moonMarker, ARCHKEEP_MODEL_FILE);
|
|
57
|
+
if (hasNx && hasNative) throw refusal(NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE);
|
|
58
|
+
return { hasNx, hasNative, moonMarker };
|
|
59
|
+
}
|
package/src/providers/moon.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync } from "node:fs";
|
|
|
31
31
|
import { delimiter, join, posix } from "node:path";
|
|
32
32
|
|
|
33
33
|
import { environmentForTree, runProcess } from "../process.mjs";
|
|
34
|
+
import { isEnoent } from "../errors.mjs";
|
|
34
35
|
import { buildDependencies } from "./native/graph.mjs";
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -249,7 +250,7 @@ function resolveMoonEnv(workspaceRoot, { env = process.env, platform = process.p
|
|
|
249
250
|
* - **Archkeep** (from Nx's `implicitDependencies`, and `archkeep.json`'s own
|
|
250
251
|
* row of that name): `type: "implicit"` means *a human declared this edge
|
|
251
252
|
* and there is no import behind it*. That is precisely why
|
|
252
|
-
* `../
|
|
253
|
+
* `../rules/edge-constraints.mjs`'s `declaredEdgeViolationsForCheck`
|
|
253
254
|
* exists — such an edge never becomes an `importSites` record, so
|
|
254
255
|
* `evaluate()` structurally cannot reach it and `check` judges it as an edge
|
|
255
256
|
* instead. `../commands/drift.mjs` and `../commands/discover.mjs` exclude
|
|
@@ -905,16 +906,15 @@ function resolveMoonCli(workspaceRoot, { resolveMoon = () => "moon" } = {}) {
|
|
|
905
906
|
* `nxCli` guards against from the other direction, where only a
|
|
906
907
|
* `MODULE_NOT_FOUND` earns the "not installed" story.
|
|
907
908
|
*
|
|
908
|
-
* `../
|
|
909
|
-
*
|
|
910
|
-
*
|
|
909
|
+
* The shape test itself is `../errors.mjs`'s `isEnoent` — the name stays
|
|
910
|
+
* because what this file asks is not "is this ENOENT" but "does this failure
|
|
911
|
+
* carry an install action".
|
|
911
912
|
*
|
|
912
913
|
* @param {unknown} error
|
|
913
914
|
* @returns {boolean}
|
|
914
915
|
*/
|
|
915
916
|
function isMoonBinaryMissing(error) {
|
|
916
|
-
|
|
917
|
-
return thrown?.code === "ENOENT" || thrown?.cause?.code === "ENOENT";
|
|
917
|
+
return isEnoent(error);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
920
|
/**
|