@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.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/src/analysis/csharp.mjs +3 -1
  3. package/src/analysis/dotnet/csproj.mjs +5 -1
  4. package/src/analysis/dotnet/namespaces.mjs +1 -0
  5. package/src/analysis/go.mjs +6 -0
  6. package/src/analysis/java.mjs +2 -0
  7. package/src/analysis/jvm/gradle.mjs +3 -1
  8. package/src/analysis/jvm/maven.mjs +6 -1
  9. package/src/analysis/jvm/packages.mjs +1 -0
  10. package/src/analysis/jvm/resolve.mjs +4 -2
  11. package/src/analysis/kotlin.mjs +2 -0
  12. package/src/analysis/markdown.mjs +1 -0
  13. package/src/analysis/python.mjs +8 -0
  14. package/src/analysis/rust.mjs +5 -1
  15. package/src/analysis/source-util.mjs +1 -1
  16. package/src/analysis/typescript.mjs +2 -0
  17. package/src/architecture-intent/model.mjs +11 -7
  18. package/src/architecture-intent/selectors.mjs +2 -1
  19. package/src/commands/change-intent.mjs +10 -9
  20. package/src/commands/change.mjs +86 -23
  21. package/src/commands/check.mjs +11 -9
  22. package/src/commands/completeness.mjs +7 -6
  23. package/src/commands/coverage-acceptance.mjs +46 -0
  24. package/src/commands/custom-rules.mjs +1 -0
  25. package/src/commands/delta-classify.mjs +3 -0
  26. package/src/commands/delta-snapshot.mjs +27 -5
  27. package/src/commands/delta.mjs +73 -39
  28. package/src/commands/diff.mjs +3 -9
  29. package/src/commands/drift.mjs +1 -1
  30. package/src/commands/evaluation-primitives.mjs +4 -4
  31. package/src/commands/evolution.mjs +2 -0
  32. package/src/commands/explain.mjs +2 -0
  33. package/src/commands/graph.mjs +39 -17
  34. package/src/commands/history.mjs +36 -0
  35. package/src/commands/plan-context-command.mjs +4 -1
  36. package/src/commands/policy.mjs +5 -2
  37. package/src/commands/scenario-evaluation.mjs +1 -1
  38. package/src/commands/snapshot-meta.mjs +33 -0
  39. package/src/commands/trajectory.mjs +2 -1
  40. package/src/config.mjs +1 -1
  41. package/src/custom-rules/host.mjs +3 -3
  42. package/src/custom-rules/values.mjs +1 -4
  43. package/src/eslint-config.mjs +1 -0
  44. package/src/fixtures/evolution-lifecycle/workspace.mjs +15 -4
  45. package/src/go-work.mjs +1 -1
  46. package/src/governance/adr-registry.mjs +4 -1
  47. package/src/governance/debt-ledger.mjs +1 -1
  48. package/src/governance/decision-fitness.mjs +2 -0
  49. package/src/governance/decision-graph.mjs +1 -0
  50. package/src/governance/discovery-proposal.mjs +8 -2
  51. package/src/governance/evolution-event.mjs +42 -0
  52. package/src/governance/fitness-registry.mjs +16 -1
  53. package/src/governance/preset-fingerprints.json +14 -14
  54. package/src/governance/profile-registry.mjs +22 -3
  55. package/src/governance/provenance-record.mjs +4 -1
  56. package/src/governance/reconcile-score.mjs +4 -0
  57. package/src/governance/row-schema.mjs +1 -0
  58. package/src/governance/verdict.mjs +37 -4
  59. package/src/governance/waiver.mjs +1 -0
  60. package/src/intent/intent-manifest.json +18 -12
  61. package/src/intent/mask-non-code.mjs +1 -0
  62. package/src/lsp/diagnostics.mjs +3 -2
  63. package/src/lsp/protocol.mjs +2 -1
  64. package/src/lsp/server.mjs +3 -0
  65. package/src/lsp/workspace-index.mjs +3 -1
  66. package/src/providers/native/differential.fixtures.mjs +29 -11
  67. package/src/providers/native/index.mjs +2 -1
  68. package/src/providers/native/model.mjs +4 -0
  69. package/src/report/envelope-shape.mjs +2 -0
  70. package/src/report/json.mjs +4 -2
  71. package/src/report/sarif.mjs +21 -8
  72. package/src/report/snapshot-text.mjs +3 -3
  73. package/src/report/text.mjs +10 -2
  74. package/src/rules/match.mjs +7 -5
  75. package/src/rules/specifiers.mjs +2 -0
  76. package/src/rules/tags.mjs +3 -2
  77. package/src/rules/topology.mjs +6 -1
  78. package/src/values.mjs +11 -0
  79. package/src/workspace.mjs +1 -0
@@ -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
- export function intentDecisionRefRows(intent) {
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
- export function evaluateFindingsImpact(affectedProjects, availableFindings) {
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
- export function evaluateDebtImpact(affectedProjects, availableDebt, resolveProject = null) {
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
- export function evaluateBoundaryImpact(graph, constraintImpact, targetProject) {
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
- export function deriveEvidenceGates(evaluation) {
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
 
@@ -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) =>
@@ -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 — `fitness`
155
- * and `customRules`. Those are every field of a loaded policy that states
156
- * law: the first three decide which violations `evaluate` produces, the
157
- * fourth decides which fitness functions `check` folds into the same exit
158
- * code (`../governance/fitness-registry.mjs`), and the fifth names the rule
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. A field that can fail a
162
- * build and is not in the hash is a law that can be rewritten while `diff`
163
- * reports the policy unchanged the silent direction, and the reason the list
164
- * here and `policyFrom`'s return shape (`../config.mjs`) are revisited
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
- * `fitness` and `customRules` are included only when they are DECLARED, and
168
- * the absent case contributes no key rather than an empty array. A policy that
169
- * declares neither therefore fingerprints exactly as it did before those
170
- * fields were covered, so extending the hash did not move every existing
171
- * snapshot's value — only those whose law it was failing to describe.
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 document track is law the same way the two blocks above are: it
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
- // two above, so a policy declaring no block hashes exactly as it did
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 —
@@ -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
 
@@ -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
- export function hasProfiles(options) {
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
- export const MARKDOWN_EDGE_KINDS = Object.freeze(["resolvedExportOwner"]);
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
- export const REQUIRED_EXPORTS = Object.freeze({
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.
@@ -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
- export function tokenizeGoWorkLine(line, lineNumber) {
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
- export const ADR_FILE_PATTERN = /^(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/u;
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
- export function entryId(kind, source) {
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);