@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
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
|
@@ -112,6 +112,12 @@ import { compareSnapshotMetadata } from "./snapshot-meta.mjs";
|
|
|
112
112
|
* Provider and provenance changes surface through the transition
|
|
113
113
|
* classification instead.
|
|
114
114
|
*
|
|
115
|
+
* This function is the graph-snapshot family's ONE identity — evolution event
|
|
116
|
+
* sides consume its output through `eventSnapshotSide` below, never a second
|
|
117
|
+
* derivation (`../../../../docs/adr/0008-snapshot-identity-per-family.md`
|
|
118
|
+
* records the one-identity-per-family decision, including why the evidence
|
|
119
|
+
* snapshot family deliberately carries no id at all).
|
|
120
|
+
*
|
|
115
121
|
* @param {{projects: object[], dependencies: object[], policy?: {fingerprint: string}|null}} snapshot
|
|
116
122
|
* @returns {string} A hex-encoded SHA-256.
|
|
117
123
|
*/
|
|
@@ -136,6 +142,34 @@ export function snapshotIdentity({ projects, dependencies, policy }) {
|
|
|
136
142
|
return createHash("sha256").update(canonical).digest("hex");
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* An evolution event's identity side: the reference to ONE state — a revision
|
|
147
|
+
* when one is known, plus the snapshot identity of the graph that side was
|
|
148
|
+
* judged over. `delta` and `change` build every side through this one
|
|
149
|
+
* function so the identity spelling cannot drift between them, and a storage
|
|
150
|
+
* path can never re-enter the tuple a committed event store dedupes on
|
|
151
|
+
* (`docs/concepts/evolution.md`'s "Nor does a storage path"). `history` and
|
|
152
|
+
* `evolution` spell the same `{revision, snapshot}` shape directly, from a
|
|
153
|
+
* snapshot whose identity is already computed.
|
|
154
|
+
*
|
|
155
|
+
* @param {{revision?: string|null, projects: object[], dependencies: object[],
|
|
156
|
+
* policyFingerprint?: string|null}} side
|
|
157
|
+
* @returns {{revision?: string, snapshot: string}} The identity side.
|
|
158
|
+
*/
|
|
159
|
+
export function eventSnapshotSide({ revision, projects, dependencies, policyFingerprint }) {
|
|
160
|
+
return {
|
|
161
|
+
...(typeof revision === "string" ? { revision } : {}),
|
|
162
|
+
snapshot: snapshotIdentity({
|
|
163
|
+
projects,
|
|
164
|
+
dependencies,
|
|
165
|
+
policy:
|
|
166
|
+
policyFingerprint === undefined || policyFingerprint === null
|
|
167
|
+
? null
|
|
168
|
+
: { fingerprint: policyFingerprint },
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
/**
|
|
140
174
|
* Reads and validates every snapshot in the history directory.
|
|
141
175
|
*
|
|
@@ -229,6 +263,7 @@ export function readSnapshots(dir, root) {
|
|
|
229
263
|
* @returns {string} First 8 hex characters.
|
|
230
264
|
*/
|
|
231
265
|
export function shortId(id) {
|
|
266
|
+
// used by its own test
|
|
232
267
|
return id.slice(0, 8);
|
|
233
268
|
}
|
|
234
269
|
|
|
@@ -248,6 +283,7 @@ export function shortId(id) {
|
|
|
248
283
|
* @returns {string} Zero-padded sequence, at least four digits.
|
|
249
284
|
*/
|
|
250
285
|
export function nextSequence(read) {
|
|
286
|
+
// used by its own test
|
|
251
287
|
let max = 0;
|
|
252
288
|
for (const file of read.files) {
|
|
253
289
|
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
|
|
@@ -23,6 +23,39 @@
|
|
|
23
23
|
* `null` before treating either as a boolean.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The one wording for the dirty-baseline disclosure, shared by every command
|
|
28
|
+
* that compares a snapshot pair (`diff`, `delta`, `change`). Three copies of
|
|
29
|
+
* one disclosure spell drift three ways, so the wording lives beside the fact
|
|
30
|
+
* it translates. A consumer that pins the baseline by its own contract names
|
|
31
|
+
* that pin (`pinned`) — `change`'s contract carries `base.commit`; a diff or
|
|
32
|
+
* delta pair has no such pin, so its note names the commit the snapshot
|
|
33
|
+
* itself claims.
|
|
34
|
+
*
|
|
35
|
+
* @param {boolean} pinned Whether the consumer's contract pins the base commit.
|
|
36
|
+
* @returns {string} The disclosure note.
|
|
37
|
+
*/
|
|
38
|
+
export function dirtyBaselineNote(pinned) {
|
|
39
|
+
return pinned
|
|
40
|
+
? "the baseline was captured from a dirty working tree — its evidence is not a reproducible " +
|
|
41
|
+
"claim about the commit the contract pins"
|
|
42
|
+
: "the baseline was captured from a dirty working tree — its evidence is not a reproducible " +
|
|
43
|
+
"claim about the commit it names";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The one wording for the dirty-head disclosure — byte-identical in every
|
|
48
|
+
* consumer, so it is stated exactly once.
|
|
49
|
+
*
|
|
50
|
+
* @returns {string} The disclosure note.
|
|
51
|
+
*/
|
|
52
|
+
export function dirtyHeadNote() {
|
|
53
|
+
return (
|
|
54
|
+
"this run's working tree is dirty — the head side describes uncommitted state, not the " +
|
|
55
|
+
"commit HEAD names"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
/**
|
|
27
60
|
* Compares the provider, provenance, and policy fingerprint of two graph
|
|
28
61
|
* envelopes.
|
|
@@ -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",
|
|
@@ -25,10 +25,7 @@
|
|
|
25
25
|
* to reach it.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
export { isPlainObject } from "../values.mjs";
|
|
29
|
-
|
|
30
|
-
/** @type {(value: unknown) => boolean} */
|
|
31
|
-
export const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
|
|
28
|
+
export { isPlainObject, isNonEmptyString } from "../values.mjs";
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
31
|
* A value's type, for a refusal that shows what was actually there.
|
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(
|
|
@@ -36,6 +36,7 @@ const IDENTITY = ["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpg
|
|
|
36
36
|
* than blocking the worker thread forever.
|
|
37
37
|
*/
|
|
38
38
|
export function git(cwd, ...args) {
|
|
39
|
+
// used by its own test
|
|
39
40
|
return execFileSync("git", args, {
|
|
40
41
|
cwd,
|
|
41
42
|
env: environmentForTree(),
|
|
@@ -47,12 +48,14 @@ export function git(cwd, ...args) {
|
|
|
47
48
|
|
|
48
49
|
/** Writes `text` to `root/relativePath`, creating parent directories. */
|
|
49
50
|
export function writeIn(root, relativePath, text) {
|
|
51
|
+
// used by its own test
|
|
50
52
|
mkdirSync(join(root, relativePath, ".."), { recursive: true });
|
|
51
53
|
writeFileSync(join(root, relativePath), text);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** Stages every change and commits with the fixture identity; returns the SHA. */
|
|
55
57
|
export function commit(root, message) {
|
|
58
|
+
// used by its own test
|
|
56
59
|
git(root, ...IDENTITY, "add", "-A");
|
|
57
60
|
git(root, ...IDENTITY, "commit", "-q", "-m", message);
|
|
58
61
|
return git(root, "rev-parse", "HEAD").trim();
|
|
@@ -109,12 +112,12 @@ const OPTIONS = `export const moduleBoundaryOptions = {
|
|
|
109
112
|
};
|
|
110
113
|
`;
|
|
111
114
|
|
|
112
|
-
export const ALPHA_CLEAN = `package alpha
|
|
115
|
+
export const ALPHA_CLEAN = `package alpha // used by its own test
|
|
113
116
|
|
|
114
117
|
func Name() string { return "alpha" }
|
|
115
118
|
`;
|
|
116
119
|
|
|
117
|
-
export const ALPHA_REACHING = `package alpha
|
|
120
|
+
export const ALPHA_REACHING = `package alpha // used by its own test
|
|
118
121
|
|
|
119
122
|
import (
|
|
120
123
|
"example.com/beta"
|
|
@@ -123,7 +126,7 @@ import (
|
|
|
123
126
|
func Name() string { return "alpha" + beta.Suffix() }
|
|
124
127
|
`;
|
|
125
128
|
|
|
126
|
-
export const BETA = `package beta
|
|
129
|
+
export const BETA = `package beta // used by its own test
|
|
127
130
|
|
|
128
131
|
func Suffix() string { return "-beta" }
|
|
129
132
|
`;
|
|
@@ -136,6 +139,7 @@ func Suffix() string { return "-beta" }
|
|
|
136
139
|
* @param {{rows?: string, fitness?: string}} [law]
|
|
137
140
|
*/
|
|
138
141
|
export function writeLaw(root, { rows = "", fitness } = {}) {
|
|
142
|
+
// used by its own test
|
|
139
143
|
writeIn(
|
|
140
144
|
root,
|
|
141
145
|
"module-boundaries.config.mjs",
|
|
@@ -149,7 +153,7 @@ export function writeLaw(root, { rows = "", fitness } = {}) {
|
|
|
149
153
|
* evolution CLI integration fixtures use, so an allowed alpha→beta edge never
|
|
150
154
|
* trips a boundary rule.
|
|
151
155
|
*/
|
|
152
|
-
export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags: ["layer:b"] },`;
|
|
156
|
+
export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags: ["layer:b"] },`; // used by its own test
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
159
|
* Writes `architecture-intent.json` at `root`. `sections` carries the top-level
|
|
@@ -157,6 +161,7 @@ export const ALLOW_A_TO_B = ` { sourceTag: "layer:a", onlyDependOnLibsWithTags:
|
|
|
157
161
|
* `dependencies`, …); `version` defaults to "1".
|
|
158
162
|
*/
|
|
159
163
|
export function writeIntent(root, sections) {
|
|
164
|
+
// used by its own test
|
|
160
165
|
writeIn(root, "architecture-intent.json", `${JSON.stringify(sections, null, 2)}\n`);
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -165,6 +170,7 @@ export function writeIntent(root, sections) {
|
|
|
165
170
|
* `record` is the frontmatter map (`{id, status, supersedes?, bindings?}`).
|
|
166
171
|
*/
|
|
167
172
|
export function writeAdr(root, filename, record) {
|
|
173
|
+
// used by its own test
|
|
168
174
|
const lines = ["---", `id: ${record.id}`, `status: ${record.status}`];
|
|
169
175
|
if (record.supersedes?.length) {
|
|
170
176
|
lines.push("supersedes:");
|
|
@@ -184,6 +190,7 @@ export function writeAdr(root, filename, record) {
|
|
|
184
190
|
* (`../cli.mjs`), never a shell-out to a binary named `archkeep`.
|
|
185
191
|
*/
|
|
186
192
|
export async function runEvolution(cwd, argv) {
|
|
193
|
+
// used by its own test
|
|
187
194
|
const out = [];
|
|
188
195
|
const err = [];
|
|
189
196
|
const exitCode = await runCli(argv, {
|
|
@@ -201,6 +208,7 @@ export async function runEvolution(cwd, argv) {
|
|
|
201
208
|
* @param {{head?: string, eventOut?: string, format?: string}} [options]
|
|
202
209
|
*/
|
|
203
210
|
export function evolutionArgs(base, { head, eventOut, format = "json" } = {}) {
|
|
211
|
+
// used by its own test
|
|
204
212
|
const args = ["evolution", "--base", base];
|
|
205
213
|
if (head) args.push("--head", head);
|
|
206
214
|
if (eventOut) args.push("--event-out", eventOut);
|
|
@@ -212,6 +220,7 @@ export function evolutionArgs(base, { head, eventOut, format = "json" } = {}) {
|
|
|
212
220
|
* Parses the `--format json` envelope out of a successful evolution run.
|
|
213
221
|
*/
|
|
214
222
|
export function parseEnvelope(run) {
|
|
223
|
+
// used by its own test
|
|
215
224
|
if (run.exitCode !== EXIT.ok) throw new Error(`evolution exited ${run.exitCode}: ${run.err}`);
|
|
216
225
|
return JSON.parse(run.out);
|
|
217
226
|
}
|
|
@@ -226,6 +235,7 @@ export function readEvents(dir) {
|
|
|
226
235
|
|
|
227
236
|
/** The event store's file names in `dir`, in filename order. */
|
|
228
237
|
export function eventFiles(dir) {
|
|
238
|
+
// used by its own test
|
|
229
239
|
return readdirSync(dir)
|
|
230
240
|
.filter((name) => name.endsWith(".json") && !name.endsWith(".json.tmp"))
|
|
231
241
|
.sort();
|
|
@@ -233,5 +243,6 @@ export function eventFiles(dir) {
|
|
|
233
243
|
|
|
234
244
|
/** Removes a throwaway workspace. */
|
|
235
245
|
export function dispose(root) {
|
|
246
|
+
// used by its own test
|
|
236
247
|
rmSync(root, { recursive: true, force: true });
|
|
237
248
|
}
|
package/src/go-work.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const IDENTIFIER_BOUNDARY = new Set([" ", "\t", "\r", '"', "`", "(", ")"]);
|
|
|
113
113
|
* @throws {Error} on an unterminated string — a string left open would
|
|
114
114
|
* otherwise swallow the rest of the line silently.
|
|
115
115
|
*/
|
|
116
|
-
|
|
116
|
+
function tokenizeGoWorkLine(line, lineNumber) {
|
|
117
117
|
const tokens = [];
|
|
118
118
|
let at = 0;
|
|
119
119
|
while (at < line.length) {
|
|
@@ -124,7 +124,7 @@ export function hasAuthority(status) {
|
|
|
124
124
|
* format outgrows 999 records without breaking; the slug is dash-separated
|
|
125
125
|
* lowercase words.
|
|
126
126
|
*/
|
|
127
|
-
|
|
127
|
+
const ADR_FILE_PATTERN = /^(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/u;
|
|
128
128
|
|
|
129
129
|
/** An ADR id — `NNN-slug` — must match the filename it lives in. */
|
|
130
130
|
export const ADR_ID_PATTERN = /^\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
@@ -236,6 +236,7 @@ function parseProseFields(body) {
|
|
|
236
236
|
* that already appears earlier in the same block.
|
|
237
237
|
*/
|
|
238
238
|
export function parseFrontmatterFields(text, at) {
|
|
239
|
+
// used by its own test
|
|
239
240
|
/** @type {Record<string, string|string[]|undefined>} */
|
|
240
241
|
const fields = {};
|
|
241
242
|
let currentList = null;
|
|
@@ -307,6 +308,7 @@ function toList(value) {
|
|
|
307
308
|
* @throws {Error} naming every violation at once.
|
|
308
309
|
*/
|
|
309
310
|
export function validateRecord({ id, frontmatter, body = "" }) {
|
|
311
|
+
// used by its own test
|
|
310
312
|
const fields = frontmatter === null ? {} : parseFrontmatterFields(frontmatter, id);
|
|
311
313
|
const violations = [];
|
|
312
314
|
|
|
@@ -400,6 +402,7 @@ export function validateRecord({ id, frontmatter, body = "" }) {
|
|
|
400
402
|
* @throws {Error} naming every lineage violation at once.
|
|
401
403
|
*/
|
|
402
404
|
export function validateLineage(records) {
|
|
405
|
+
// used by its own test
|
|
403
406
|
const byId = new Map(records.map((record) => [record.id, record]));
|
|
404
407
|
/** @type {Map<string, string[]>} */
|
|
405
408
|
const supersededBy = new Map(records.map((record) => [record.id, []]));
|
|
@@ -143,7 +143,7 @@ function owningProjectForPath(path, byName) {
|
|
|
143
143
|
* @param {string} source The entry's `source` (its keying field).
|
|
144
144
|
* @returns {string} The stable hex id.
|
|
145
145
|
*/
|
|
146
|
-
|
|
146
|
+
function entryId(kind, source) {
|
|
147
147
|
const semanticKind = kind === "expired-waiver" ? "waiver" : kind;
|
|
148
148
|
return createHash("sha256")
|
|
149
149
|
.update(canonicalizeJson({ kind: semanticKind, source }))
|
|
@@ -61,6 +61,7 @@ import { hasAuthority } from "./adr-registry.mjs";
|
|
|
61
61
|
|
|
62
62
|
/** The closed set of per-decision fitness levels `computeDecisionFitness` emits. */
|
|
63
63
|
export const DECISION_FITNESS_LEVELS = Object.freeze([
|
|
64
|
+
// used by its own test
|
|
64
65
|
"enforced",
|
|
65
66
|
"partially-enforced",
|
|
66
67
|
"violated",
|
|
@@ -70,6 +71,7 @@ export const DECISION_FITNESS_LEVELS = Object.freeze([
|
|
|
70
71
|
|
|
71
72
|
/** Whether a level names a red (never-healthy) direction. */
|
|
72
73
|
export function isRedDirection(level) {
|
|
74
|
+
// used by its own test
|
|
73
75
|
return level === "violated" || level === "unverifiable";
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -382,6 +382,7 @@ export function forwardDecision(decisionId, ctx) {
|
|
|
382
382
|
* @returns {GraphWalk}
|
|
383
383
|
*/
|
|
384
384
|
export function reverseRow(rowRef, ctx) {
|
|
385
|
+
// used by its own test
|
|
385
386
|
const g = newWalk();
|
|
386
387
|
|
|
387
388
|
const row = ctx.rows.find((candidate) => candidate.id === rowRef);
|