@ecoma-io/archkeep 0.25.0 → 0.26.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/cli.mjs +166 -506
- package/commands.mjs +7 -1
- package/package.json +1 -1
- package/src/architecture-intent/judge.mjs +1 -1
- package/src/architecture-intent/model.mjs +34 -0
- package/src/canonical.mjs +2 -1
- package/src/commands/adr-for-workspace.mjs +63 -0
- package/src/commands/analyze-capability.mjs +13 -0
- package/src/commands/change-intent.mjs +11 -0
- package/src/commands/change.mjs +247 -34
- package/src/commands/check-capability.mjs +13 -0
- package/src/commands/check.mjs +13 -3
- package/src/commands/compare-capability.mjs +20 -0
- package/src/commands/completeness.mjs +12 -0
- package/src/commands/context-command.mjs +31 -0
- package/src/commands/coverage-verdict.mjs +15 -2
- package/src/commands/debt.mjs +30 -0
- package/src/commands/decisions.mjs +59 -10
- package/src/commands/delta.mjs +263 -49
- package/src/commands/diff.mjs +45 -0
- package/src/commands/discover.mjs +37 -0
- package/src/commands/drift.mjs +45 -0
- package/src/commands/evolution.mjs +22 -2
- package/src/commands/explain-capability.mjs +11 -0
- package/src/commands/explain.mjs +24 -0
- package/src/commands/fitness.mjs +119 -10
- package/src/commands/govern-capability.mjs +16 -0
- package/src/commands/graph.mjs +39 -0
- package/src/commands/health.mjs +34 -0
- package/src/commands/history.mjs +85 -2
- package/src/commands/impact.mjs +23 -0
- package/src/commands/inspect-capability.mjs +16 -0
- package/src/commands/plan-context-command.mjs +35 -0
- package/src/commands/policy.mjs +42 -4
- package/src/commands/provenance-command.mjs +15 -24
- package/src/commands/report.mjs +38 -0
- package/src/commands/rules-capability.mjs +16 -0
- package/src/commands/rules.mjs +3 -1
- package/src/commands/scenario.mjs +23 -0
- package/src/commands/trajectory.mjs +35 -0
- package/src/commands/waivers.mjs +30 -0
- package/src/corpus/goldens/.gitkeep +0 -0
- package/src/corpus/goldens/adr.json +62 -0
- package/src/corpus/goldens/adr.text +6 -0
- package/src/corpus/goldens/change.json +98 -0
- package/src/corpus/goldens/change.text +6 -0
- package/src/corpus/goldens/check.json +90 -0
- package/src/corpus/goldens/check.sarif +479 -0
- package/src/corpus/goldens/check.text +9 -0
- package/src/corpus/goldens/context.json +47 -0
- package/src/corpus/goldens/context.text +6 -0
- package/src/corpus/goldens/debt.json +59 -0
- package/src/corpus/goldens/debt.text +7 -0
- package/src/corpus/goldens/decisions.json +74 -0
- package/src/corpus/goldens/decisions.text +7 -0
- package/src/corpus/goldens/delta.json +94 -0
- package/src/corpus/goldens/delta.sarif +472 -0
- package/src/corpus/goldens/delta.text +5 -0
- package/src/corpus/goldens/diff.json +58 -0
- package/src/corpus/goldens/diff.text +4 -0
- package/src/corpus/goldens/discover.json +77 -0
- package/src/corpus/goldens/discover.text +10 -0
- package/src/corpus/goldens/drift.json +43 -0
- package/src/corpus/goldens/drift.text +3 -0
- package/src/corpus/goldens/evolution.json +328 -0
- package/src/corpus/goldens/evolution.text +32 -0
- package/src/corpus/goldens/explain.json +60 -0
- package/src/corpus/goldens/explain.text +7 -0
- package/src/corpus/goldens/fitness.json +45 -0
- package/src/corpus/goldens/fitness.text +3 -0
- package/src/corpus/goldens/graph.json +78 -0
- package/src/corpus/goldens/graph.text +10 -0
- package/src/corpus/goldens/health.json +72 -0
- package/src/corpus/goldens/health.text +10 -0
- package/src/corpus/goldens/help.text +104 -0
- package/src/corpus/goldens/history.json +43 -0
- package/src/corpus/goldens/history.text +4 -0
- package/src/corpus/goldens/impact.json +327 -0
- package/src/corpus/goldens/impact.text +10 -0
- package/src/corpus/goldens/provenance.json +453 -0
- package/src/corpus/goldens/provenance.text +14 -0
- package/src/corpus/goldens/reconcile.json +168 -0
- package/src/corpus/goldens/reconcile.text +3 -0
- package/src/corpus/goldens/report.json +168 -0
- package/src/corpus/goldens/report.text +42 -0
- package/src/corpus/goldens/rules verify.json +0 -0
- package/src/corpus/goldens/rules verify.text +0 -0
- package/src/corpus/goldens/scenario.json +441 -0
- package/src/corpus/goldens/scenario.text +28 -0
- package/src/corpus/goldens/trajectory.json +83 -0
- package/src/corpus/goldens/trajectory.text +10 -0
- package/src/corpus/goldens/usage-error.text +105 -0
- package/src/corpus/goldens/waivers.json +40 -0
- package/src/corpus/goldens/waivers.text +1 -0
- package/src/corpus/goldens-lsp/initialize.result.json +15 -0
- package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
- package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
- package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
- package/src/corpus/goldens-lsp/publish-violation.json +22 -0
- package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
- package/src/go-work.mjs +8 -23
- package/src/governance/evolution-event.mjs +15 -0
- package/src/governance/evolution-store.mjs +59 -31
- package/src/governance/provenance-record.mjs +0 -152
- package/src/governance/verdict.mjs +26 -3
- package/src/intent/intent-manifest.json +14 -14
- package/src/lsp/server.mjs +5 -8
- package/src/lsp/workspace-index.mjs +55 -220
- package/src/options.mjs +10 -0
- package/src/providers/native/discover.mjs +13 -12
- package/src/providers/native/model.mjs +8 -3
- package/src/providers/nx-static.mjs +231 -0
- package/src/report/json.mjs +3 -2
- package/src/report/sarif.mjs +8 -3
- package/src/rules/edge-constraints.mjs +9 -0
- package/src/rules/index.mjs +26 -10
- package/src/rules/messages.mjs +64 -14
- package/src/rules/specifiers.mjs +21 -20
- package/src/tsconfig-paths.mjs +8 -14
- package/src/verdict.mjs +127 -16
package/commands.mjs
CHANGED
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
* one. What is deliberately absent: the `run*` drivers and argv parsing
|
|
26
26
|
* (`cli.mjs` alone owns what a process's stdout and exit code mean), and the
|
|
27
27
|
* renderers (`src/report/` shapes bytes for humans; an importer reads the
|
|
28
|
-
* envelopes the commands already return).
|
|
28
|
+
* envelopes the commands already return). `adrForWorkspace`
|
|
29
|
+
* (`./src/commands/adr-for-workspace.mjs`) is on the roster without bending
|
|
30
|
+
* that: it composes the workspace preamble two faces (`cli.mjs`'s `runAdr`
|
|
31
|
+
* and the MCP history adapter) used to duplicate — one of them past this
|
|
32
|
+
* subpath — and returns exactly what `adrCommand` returns, printing no byte
|
|
33
|
+
* and deciding no exit code of its own.
|
|
29
34
|
*
|
|
30
35
|
* The seams the CLI threads are threaded the same way here: `readGraph` and
|
|
31
36
|
* `listFiles` are injectable on every entry point that reaches outside the
|
|
@@ -45,6 +50,7 @@ export { driftCommand } from "./src/commands/drift.mjs";
|
|
|
45
50
|
export { planContextCommand } from "./src/commands/plan-context-command.mjs";
|
|
46
51
|
export { historyCommand } from "./src/commands/history.mjs";
|
|
47
52
|
export { adrCommand } from "./src/commands/adr.mjs";
|
|
53
|
+
export { adrForWorkspace } from "./src/commands/adr-for-workspace.mjs";
|
|
48
54
|
export { discoverCommand } from "./src/commands/discover.mjs";
|
|
49
55
|
export { reconcileCommand } from "./src/commands/reconcile.mjs";
|
|
50
56
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecoma-io/archkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Architecture authority for human and agentic software development — deterministic, evidence-backed enforcement of declared architecture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
@@ -34,7 +34,7 @@ import { buildReachability, getPath, pathExists } from "../../src/rules/reachabi
|
|
|
34
34
|
/**
|
|
35
35
|
* What a finding means — one entry per `messageId`, the arrangement
|
|
36
36
|
* `../../src/report/sarif.mjs` derives its rule descriptors from, the same as
|
|
37
|
-
*
|
|
37
|
+
* the per-domain tables in `../rules/messages.mjs`.
|
|
38
38
|
*/
|
|
39
39
|
export const INTENT_MESSAGES = Object.freeze({
|
|
40
40
|
intentForbiddenEdge:
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* observed architecture stays the graph Archkeep derives from source; governance
|
|
12
12
|
* is a deterministic comparison (`./judge.mjs`). NO LLM/AI anywhere in the core.
|
|
13
13
|
*
|
|
14
|
+
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
15
|
+
* PD-13 outcome (c), four surfaces, never one type
|
|
16
|
+
* (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
|
|
17
|
+
* the workspace-declared law: the grammar, validation, and loading of
|
|
18
|
+
* `architecture-intent.json`, judged against the observed graph by `check`
|
|
19
|
+
* and `drift`. The other three: the v1.0 evidence-manifest registry
|
|
20
|
+
* (`../intent/` — `intent-manifest.json` and its executable gate), the
|
|
21
|
+
* declared-change grammar (`../commands/change-intent.mjs`, one change's
|
|
22
|
+
* expected consequences, verified by the `change` command), and the fileless
|
|
23
|
+
* run-envelope intent shape (`result.intent` in a `--format json` envelope).
|
|
24
|
+
*
|
|
14
25
|
* This module mirrors `../../config.mjs`'s and
|
|
15
26
|
* `../../providers/native/model.mjs`'s split: a pure `(raw) -> string[]`
|
|
16
27
|
* validator and a thin loader that reads, parses, validates and throws — one
|
|
@@ -696,3 +707,26 @@ export async function loadIntent(root, { read = readFileFromDisk, tracked } = {}
|
|
|
696
707
|
}
|
|
697
708
|
return normalizeIntent(raw);
|
|
698
709
|
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* The driver gate over `loadIntent`: load the workspace's declared law only
|
|
713
|
+
* when the tracked-file list names it, `null` otherwise.
|
|
714
|
+
*
|
|
715
|
+
* Three `cli.mjs` drivers (decisions, health, report) need the same answer:
|
|
716
|
+
* an intent file `git ls-files` does not list is absence — the callers render
|
|
717
|
+
* their no-intent face — while a tracked-but-unreadable file is a no-verdict
|
|
718
|
+
* throw that the shared catch folds to exit 3. The membership pre-test is a
|
|
719
|
+
* pure equivalence, not a containment guard: `loadIntent` early-returns for
|
|
720
|
+
* untracked input before any containment check, so this helper changes no
|
|
721
|
+
* verdict on any input.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} root Absolute workspace root.
|
|
724
|
+
* @param {string[]} tracked The `git ls-files` list.
|
|
725
|
+
* @returns {Promise<object|null>} The normalized, validated model, or `null`
|
|
726
|
+
* when the file is not tracked.
|
|
727
|
+
* @throws {Error} exactly what `loadIntent` throws — tracked-but-unreadable
|
|
728
|
+
* stays a no-verdict, never absence.
|
|
729
|
+
*/
|
|
730
|
+
export async function loadIntentIfTracked(root, tracked) {
|
|
731
|
+
return tracked.includes(INTENT_FILE) ? loadIntent(root, { tracked }) : null;
|
|
732
|
+
}
|
package/src/canonical.mjs
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* correct.
|
|
12
12
|
*
|
|
13
13
|
* Used by `computePolicyFingerprint` (`../commands/graph.mjs`), the intent
|
|
14
|
-
* fingerprint (`./intent-fingerprint.mjs`), the
|
|
14
|
+
* fingerprint (`./architecture-intent/intent-fingerprint.mjs`), the
|
|
15
|
+
* evidence-snapshot serializer
|
|
15
16
|
* (`./commands/delta-snapshot.mjs`, through the exported replacer below), and
|
|
16
17
|
* anything else a fingerprint or a byte-deterministic file is produced from —
|
|
17
18
|
* one canonicalizer, in one place, so two serializations cannot drift.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `adr` preamble, composed once: the workspace root walked up from a
|
|
3
|
+
* working directory, the tracked file list, then `./adr.mjs`'s `adrCommand`.
|
|
4
|
+
* `../../cli.mjs`'s `runAdr` and the MCP history adapter
|
|
5
|
+
* (`../../../archkeep-mcp/src/engine.mjs`) each inlined these three steps,
|
|
6
|
+
* and the adapter's copy reached past the `./commands` subpath into the
|
|
7
|
+
* engine's root entry for `findWorkspaceRoot` and `listTrackedFiles` — the
|
|
8
|
+
* one import in that package that made it something other than a client of
|
|
9
|
+
* the command layer. The composition lives here, beside the command it
|
|
10
|
+
* feeds, so both faces run one path and neither needs the root entry for it.
|
|
11
|
+
*
|
|
12
|
+
* It holds no policy of its own. The two preamble decisions move verbatim,
|
|
13
|
+
* because inventing either here would be a second opinion about what a
|
|
14
|
+
* workspace is:
|
|
15
|
+
*
|
|
16
|
+
* - the root is `findWorkspaceRoot(cwd, WORKSPACE_MARKERS)` — the same
|
|
17
|
+
* marker list `./context.mjs`'s `resolveCommandContext` walks, whose own
|
|
18
|
+
* header owns why callers of a workspace root may not differ in what a
|
|
19
|
+
* workspace root IS;
|
|
20
|
+
* - the files are `(io.listFiles ?? listTrackedFiles)(root)` — the injectable
|
|
21
|
+
* default every command reads through, so a test drives the read over a
|
|
22
|
+
* fixture tree with no git.
|
|
23
|
+
*
|
|
24
|
+
* Everything a caller might decide stays with the caller. The `null` — no
|
|
25
|
+
* ancestor of `cwd` is a workspace root — is the caller's refusal to render,
|
|
26
|
+
* never an answer: the CLI words its own refusal and picks its own exit
|
|
27
|
+
* code, the adapter throws its own, and a caller that ignored the null
|
|
28
|
+
* crashes one line later instead of reading a clean result. The throws
|
|
29
|
+
* (`adrCommand`'s unreadable registry, `listTrackedFiles`' failed
|
|
30
|
+
* `git ls-files`) propagate unchanged for the same reason. And it prints no
|
|
31
|
+
* byte and decides no exit code, so it is not one of the `run*` drivers
|
|
32
|
+
* `../../commands.mjs` leaves to `../../cli.mjs` — it returns exactly what
|
|
33
|
+
* the command returns.
|
|
34
|
+
*/
|
|
35
|
+
import { findWorkspaceRoot, listTrackedFiles } from "../workspace.mjs";
|
|
36
|
+
|
|
37
|
+
import { adrCommand } from "./adr.mjs";
|
|
38
|
+
import { WORKSPACE_MARKERS } from "./context.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs the `adr` command for the workspace `cwd` falls in.
|
|
42
|
+
*
|
|
43
|
+
* @param {{cwd: string}} request The working directory the root is walked up
|
|
44
|
+
* from — the same walk `resolveCommandContext` makes, without the project
|
|
45
|
+
* graph it builds.
|
|
46
|
+
* @param {{id?: string}} [options] Forwarded to `adrCommand` unchanged.
|
|
47
|
+
* @param {{listFiles?: Function}} [io] The tracked-file seam, typed like
|
|
48
|
+
* every command's (`Function`, the shape `./check.mjs` and
|
|
49
|
+
* `./context.mjs` declare), defaulted to `git ls-files`
|
|
50
|
+
* (`../workspace.mjs`) — injected where a test answers for git.
|
|
51
|
+
* @returns {object|null} What `adrCommand` returns, verbatim; `null` when no
|
|
52
|
+
* ancestor of `cwd` is a workspace root — the caller's refusal to render,
|
|
53
|
+
* never a clean answer (the module header owns why the message is not
|
|
54
|
+
* worded here).
|
|
55
|
+
* @throws {Error} on an unreadable registry or a failed `git ls-files`,
|
|
56
|
+
* exactly as `adrCommand` and `listTrackedFiles` throw — each caller maps
|
|
57
|
+
* that the way it already did.
|
|
58
|
+
*/
|
|
59
|
+
export function adrForWorkspace({ cwd }, options = {}, io = {}) {
|
|
60
|
+
const root = findWorkspaceRoot(cwd, WORKSPACE_MARKERS);
|
|
61
|
+
if (root === null) return null;
|
|
62
|
+
return adrCommand(root, options, { tracked: (io.listFiles ?? listTrackedFiles)(root) });
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `analyze` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `discover`, with the two proposal→intent steps the
|
|
4
|
+
* `--write-intent` lane drives — the serialized bytes and the target
|
|
5
|
+
* refusal.
|
|
6
|
+
*
|
|
7
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
8
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
9
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
10
|
+
* graph, not just to a comment (PD-18,
|
|
11
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
12
|
+
*/
|
|
13
|
+
export { discoverCommand, intentJsonFromProposal, intentWriteRefusal } from "./discover.mjs";
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
* architectural delta (`./change.mjs`), and then belongs to the pull request
|
|
10
10
|
* as the reviewable answer to "what did this change do to the architecture?".
|
|
11
11
|
*
|
|
12
|
+
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
13
|
+
* PD-13 outcome (c), four surfaces, never one type
|
|
14
|
+
* (`../../../../docs/architecture/refactor/DECISIONS.md`). This module is
|
|
15
|
+
* the declared-change grammar: one change's expected architectural
|
|
16
|
+
* consequences, verified by the `change` command against the actual delta.
|
|
17
|
+
* The other three: the workspace-declared law (`../architecture-intent/` —
|
|
18
|
+
* the `architecture-intent.json` file this module deliberately does not
|
|
19
|
+
* extend, per the paragraph below), the v1.0 evidence-manifest registry
|
|
20
|
+
* (`../intent/`), and the fileless run-envelope intent shape
|
|
21
|
+
* (`result.intent` in a `--format json` envelope).
|
|
22
|
+
*
|
|
12
23
|
* This file mirrors `../architecture-intent/model.mjs`'s split — a pure
|
|
13
24
|
* `(raw) -> string[]` validator, a thin loader that reads, parses, validates
|
|
14
25
|
* and throws one Error naming every violation at once — and deliberately does
|
package/src/commands/change.mjs
CHANGED
|
@@ -101,11 +101,12 @@ import {
|
|
|
101
101
|
} from "./delta.mjs";
|
|
102
102
|
import { providerMismatch, readEvidenceSnapshot } from "./delta-snapshot.mjs";
|
|
103
103
|
import { coverageRefusal, coverageVerdict } from "./coverage-verdict.mjs";
|
|
104
|
+
import { isAbsolute, resolve } from "node:path";
|
|
104
105
|
import { blindSpotRows } from "../analysis/source-util.mjs";
|
|
105
106
|
import { cyclicProjects } from "../governance/fitness-rules.mjs";
|
|
106
107
|
import { eventSnapshotSide } from "./history.mjs";
|
|
107
108
|
import { VERDICTS, fitnessVerdict, isVerdict } from "../governance/verdict.mjs";
|
|
108
|
-
import { describe } from "../values.mjs";
|
|
109
|
+
import { describe, isStringArray } from "../values.mjs";
|
|
109
110
|
import { buildDecision } from "../report/evidence.mjs";
|
|
110
111
|
import { jsonEnvelope, renderJson } from "../report/json.mjs";
|
|
111
112
|
import { formatChangeReport } from "../report/change-text.mjs";
|
|
@@ -126,6 +127,8 @@ import { writeEvent } from "../governance/evolution-store.mjs";
|
|
|
126
127
|
import { judgeIntent } from "../architecture-intent/judge.mjs";
|
|
127
128
|
import { INTENT_FILE, loadIntent } from "../architecture-intent/model.mjs";
|
|
128
129
|
import { debtChangeDiff } from "../governance/debt-ledger.mjs";
|
|
130
|
+
import { resolveCommandContext } from "./context.mjs";
|
|
131
|
+
import { resolvePolicy } from "./policy.mjs";
|
|
129
132
|
|
|
130
133
|
/**
|
|
131
134
|
* One expected-fact row as the report and JSON carry it. Kept in one builder
|
|
@@ -430,6 +433,124 @@ export function reconcileDisposition(verdict, constraints = []) {
|
|
|
430
433
|
return "accepted";
|
|
431
434
|
}
|
|
432
435
|
|
|
436
|
+
/**
|
|
437
|
+
* The input latch for `changeFold`: names the first way its inputs are not a
|
|
438
|
+
* shape the fold can count, or `null` when they are. A returned problem
|
|
439
|
+
* becomes the fold's refusal — never a throw, the same in-band lane
|
|
440
|
+
* `deltaFold`'s latch takes one module over.
|
|
441
|
+
*
|
|
442
|
+
* @param {object} reconciliation From `reconcileMaterialDelta`.
|
|
443
|
+
* @param {object[]} constraints Judged `fitnessVerdict` rows.
|
|
444
|
+
* @param {string[]} unprovenReasons Why the base identity could not be proven.
|
|
445
|
+
* @returns {string|null}
|
|
446
|
+
*/
|
|
447
|
+
function changeFoldInputProblem(reconciliation, constraints, unprovenReasons) {
|
|
448
|
+
for (const key of ["matched", "unexpected", "missingExpected"]) {
|
|
449
|
+
if (!Array.isArray(reconciliation?.[key])) {
|
|
450
|
+
return `the change fold counts "reconciliation.${key}", which is ${describe(reconciliation?.[key])} — a list that is not an array counts as empty in every lane, and an empty reconciliation reads "matched". This is a bug in archkeep, not a fact about the workspace.`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!Array.isArray(constraints)) {
|
|
454
|
+
return `the change fold counts "constraints", which is ${describe(constraints)} — a list that is not an array counts as empty in every lane, and an empty constraint list reads "every declared constraint passing". This is a bug in archkeep, not a fact about the workspace.`;
|
|
455
|
+
}
|
|
456
|
+
for (const [index, row] of constraints.entries()) {
|
|
457
|
+
if (!isVerdict(row?.verdict)) {
|
|
458
|
+
return `the change fold counts the verdict of constraint row ${index}, which is ${describe(row?.verdict)} — a verdict outside ${VERDICTS.join(", ")} matches neither the fail nor the unknown lane, and reads as consent. This is a bug in archkeep, not a fact about the workspace.`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!isStringArray(unprovenReasons)) {
|
|
462
|
+
return `the change fold reads "unprovenReasons", which is ${describe(unprovenReasons)} — reasons that are not a string array read as "no reasons" below, and an unproven base then reads "matched". This is a bug in archkeep, not a fact about the workspace.`;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The `change` exit fold, lifted out of `changeCommand` as a pure function so
|
|
469
|
+
* the mapping from the reconciliation lists and the judged constraints to the
|
|
470
|
+
* verdict is a fact a test can pin (`reconcileDisposition` directly above is
|
|
471
|
+
* the arrangement's precedent — it latches the EVENT disposition; this is the
|
|
472
|
+
* STATUS fold the envelope and the exit code read). Lane order: an unproven
|
|
473
|
+
* base identity or any undetermined constraint → `no-verdict`/3; else any
|
|
474
|
+
* unexpected fact, missing expected fact, or failed constraint →
|
|
475
|
+
* `findings`/1; else `ok`/0.
|
|
476
|
+
*
|
|
477
|
+
* The input latch runs first: input the fold cannot read is refused as a
|
|
478
|
+
* no-verdict whose single reason names it (`refused`), never folded past as a
|
|
479
|
+
* silently-empty count — `reconciliationVerdict` reads `unprovenReasons`
|
|
480
|
+
* before any list, so a malformed one reaching it unguarded can name the
|
|
481
|
+
* whole run unproven or matched for reasons the counts never supported. The
|
|
482
|
+
* literals stay hand-rolled per site by decision — the carrier folds are the
|
|
483
|
+
* pinned baseline INV-2 names, not a table to be converged
|
|
484
|
+
* (`docs/architecture/refactor/AUTHORITY-MAP.md`).
|
|
485
|
+
*
|
|
486
|
+
* @param {{matched: object[], unexpected: object[], missingExpected: object[]}} reconciliation
|
|
487
|
+
* @param {{verdict: string}[]} constraints Judged `fitnessVerdict` rows —
|
|
488
|
+
* empty when none were declared or the base identity was unproven.
|
|
489
|
+
* @param {string[]} unprovenReasons Why the base identity could not be proven.
|
|
490
|
+
* @returns {{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
491
|
+
* decision: object, verdict: "matched"|"undeclared"|"unfulfilled"|"unproven",
|
|
492
|
+
* refused?: undefined}|
|
|
493
|
+
* {status: "no-verdict", exitCode: 3, decision: object, verdict?: undefined,
|
|
494
|
+
* refused: string}}
|
|
495
|
+
* `verdict` rides every judged lane because the event's `declaredIntentRows`
|
|
496
|
+
* and `result.reconciliation` report it whichever lane fired; a refusal
|
|
497
|
+
* carries `refused` and no verdict, and is the one return whose
|
|
498
|
+
* `decision.reason` names a malformed input rather than a fact about the
|
|
499
|
+
* trees.
|
|
500
|
+
*/
|
|
501
|
+
export function changeFold(reconciliation, constraints, unprovenReasons) {
|
|
502
|
+
const problem = changeFoldInputProblem(reconciliation, constraints, unprovenReasons);
|
|
503
|
+
if (problem !== null) {
|
|
504
|
+
return {
|
|
505
|
+
status: "no-verdict",
|
|
506
|
+
exitCode: 3,
|
|
507
|
+
decision: buildDecision({
|
|
508
|
+
status: "no-verdict",
|
|
509
|
+
coverageComplete: true,
|
|
510
|
+
findings: 0,
|
|
511
|
+
reason: problem,
|
|
512
|
+
}),
|
|
513
|
+
refused: problem,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const verdict = reconciliationVerdict(reconciliation, unprovenReasons);
|
|
517
|
+
const failedConstraints = constraints.filter((row) => row.verdict === "fail").length;
|
|
518
|
+
const unknownConstraints = constraints.filter((row) => row.verdict === "unknown").length;
|
|
519
|
+
const findings =
|
|
520
|
+
reconciliation.unexpected.length + reconciliation.missingExpected.length + failedConstraints;
|
|
521
|
+
|
|
522
|
+
if (verdict === "unproven" || unknownConstraints > 0) {
|
|
523
|
+
return {
|
|
524
|
+
status: "no-verdict",
|
|
525
|
+
exitCode: 3,
|
|
526
|
+
decision: buildDecision({
|
|
527
|
+
status: "no-verdict",
|
|
528
|
+
coverageComplete: true,
|
|
529
|
+
findings: 0,
|
|
530
|
+
reason:
|
|
531
|
+
verdict === "unproven"
|
|
532
|
+
? `the change intent could not be verified against the declared base: ${unprovenReasons[0]}`
|
|
533
|
+
: `${unknownConstraints} declared constraint${unknownConstraints === 1 ? "" : "s"} could not be determined`,
|
|
534
|
+
}),
|
|
535
|
+
verdict,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (findings > 0) {
|
|
539
|
+
return {
|
|
540
|
+
status: "findings",
|
|
541
|
+
exitCode: 1,
|
|
542
|
+
decision: buildDecision({ status: "findings", coverageComplete: true, findings }),
|
|
543
|
+
verdict,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
status: "ok",
|
|
548
|
+
exitCode: 0,
|
|
549
|
+
decision: buildDecision({ status: "ok", coverageComplete: true, findings: 0 }),
|
|
550
|
+
verdict,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
433
554
|
/**
|
|
434
555
|
* Judges the constraints the contract declares, through the shared engine —
|
|
435
556
|
* both sides re-judged under the CURRENT law and one shared instant, exactly
|
|
@@ -539,7 +660,7 @@ function judgeDeclaredConstraints(intent, io) {
|
|
|
539
660
|
* architecture-intent seam `drift` uses (defaults to `loadIntent`); the
|
|
540
661
|
* change event's `debt` diff judges the intent over this run's base and
|
|
541
662
|
* head graphs and would be untestable without it.
|
|
542
|
-
* @returns {Promise<{status: "ok"|"findings"|"no-verdict",
|
|
663
|
+
* @returns {Promise<{status: "ok"|"findings"|"no-verdict", exitCode: 0|1|3,
|
|
543
664
|
* changeIntent?: object, coverage: object, report: {text: string, json: string}}>}
|
|
544
665
|
* `status: "no-verdict"` from the coverage refusal (#608) carries no
|
|
545
666
|
* `changeIntent` payload — the reconciliation was withheld, and the
|
|
@@ -746,38 +867,13 @@ export async function changeCommand(
|
|
|
746
867
|
liveViolations = headEval.violations.length;
|
|
747
868
|
}
|
|
748
869
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
let status;
|
|
757
|
-
/** @type {0|1|3} */
|
|
758
|
-
let exitCode;
|
|
759
|
-
let decision;
|
|
760
|
-
if (verdict === "unproven" || unknownConstraints > 0) {
|
|
761
|
-
status = "no-verdict";
|
|
762
|
-
exitCode = 3;
|
|
763
|
-
decision = buildDecision({
|
|
764
|
-
status,
|
|
765
|
-
coverageComplete: true,
|
|
766
|
-
findings: 0,
|
|
767
|
-
reason:
|
|
768
|
-
verdict === "unproven"
|
|
769
|
-
? `the change intent could not be verified against the declared base: ${unprovenReasons[0]}`
|
|
770
|
-
: `${unknownConstraints} declared constraint${unknownConstraints === 1 ? "" : "s"} could not be determined`,
|
|
771
|
-
});
|
|
772
|
-
} else if (findings > 0) {
|
|
773
|
-
status = "findings";
|
|
774
|
-
exitCode = 1;
|
|
775
|
-
decision = buildDecision({ status, coverageComplete: true, findings });
|
|
776
|
-
} else {
|
|
777
|
-
status = "ok";
|
|
778
|
-
exitCode = 0;
|
|
779
|
-
decision = buildDecision({ status, coverageComplete: true, findings: 0 });
|
|
780
|
-
}
|
|
870
|
+
// The verdict comes from the one fold — `changeFold` above owns the lane
|
|
871
|
+
// order and the input latch — but it is TAKEN after the notes and coverage
|
|
872
|
+
// blocks below, so a refusal can carry the disclosures (policy-changed,
|
|
873
|
+
// dirty sides) the run already computed: a withheld verdict that dropped
|
|
874
|
+
// them would hide a weaker-evidence fact from the only envelope the run
|
|
875
|
+
// emits. Nothing between this comment and the fold call reads the fold's
|
|
876
|
+
// inputs or outputs.
|
|
781
877
|
|
|
782
878
|
/** @type {string[]} */
|
|
783
879
|
const notes = [];
|
|
@@ -815,6 +911,40 @@ export async function changeCommand(
|
|
|
815
911
|
notes,
|
|
816
912
|
};
|
|
817
913
|
|
|
914
|
+
// The exit fold, at the input boundary: input the fold cannot read is
|
|
915
|
+
// refused in-band (no-verdict, exit 3, the field named), never counted past
|
|
916
|
+
// as a silently-empty list. The refusal withholds `result` and the event
|
|
917
|
+
// exactly the coverage refusal does: a comparison whose counts could not be
|
|
918
|
+
// read has no verdict to report, and the refusal rides the envelope beside
|
|
919
|
+
// the coverage notes the run already earned.
|
|
920
|
+
const fold = changeFold(reconciliation, constraints, unprovenReasons);
|
|
921
|
+
if (fold.refused !== undefined) {
|
|
922
|
+
const refusalCoverage = { ...coverage, notes: [...notes, fold.refused] };
|
|
923
|
+
return {
|
|
924
|
+
status: fold.status,
|
|
925
|
+
exitCode: fold.exitCode,
|
|
926
|
+
coverage: refusalCoverage,
|
|
927
|
+
report: {
|
|
928
|
+
text: `change: no verdict — ${fold.refused}\n`,
|
|
929
|
+
json: renderJson(
|
|
930
|
+
jsonEnvelope({
|
|
931
|
+
command: "change",
|
|
932
|
+
context: { root, provider, marker, provenance: headProvenance },
|
|
933
|
+
status: fold.status,
|
|
934
|
+
exitCode: fold.exitCode,
|
|
935
|
+
coverage: refusalCoverage,
|
|
936
|
+
// The refusal-withheld payload, stated the way `coverageRefusal`
|
|
937
|
+
// states it: `jsonEnvelope` requires the key, the refusal has no
|
|
938
|
+
// result to report.
|
|
939
|
+
result: undefined,
|
|
940
|
+
decision: fold.decision,
|
|
941
|
+
}),
|
|
942
|
+
),
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const { status, exitCode, decision, verdict } = fold;
|
|
947
|
+
|
|
818
948
|
// Wave 3 (design §1, §2, §5): the evolution classification and the reconcile
|
|
819
949
|
// event. The classification is ALWAYS computed — it rides the envelope
|
|
820
950
|
// result in memory even when no event is written (design §4) — and the
|
|
@@ -1030,6 +1160,7 @@ export async function changeCommand(
|
|
|
1030
1160
|
|
|
1031
1161
|
return {
|
|
1032
1162
|
status,
|
|
1163
|
+
exitCode,
|
|
1033
1164
|
changeIntent: result,
|
|
1034
1165
|
coverage,
|
|
1035
1166
|
report: {
|
|
@@ -1044,3 +1175,85 @@ export async function changeCommand(
|
|
|
1044
1175
|
},
|
|
1045
1176
|
};
|
|
1046
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* The paths `change` runs over, resolved once and shared by the CLI entry
|
|
1181
|
+
* and the output guard below — one spelling of "where does this flag point",
|
|
1182
|
+
* the same relative-to-cwd resolution every path flag uses.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {{intent: string, paths: string[]}} options This run's parsed flags.
|
|
1185
|
+
* @param {string} cwd The run's working directory.
|
|
1186
|
+
* @returns {{baselinePath: string, intentPath: string}}
|
|
1187
|
+
*/
|
|
1188
|
+
function changePaths(options, cwd) {
|
|
1189
|
+
return {
|
|
1190
|
+
baselinePath: isAbsolute(options.paths[0]) ? options.paths[0] : resolve(cwd, options.paths[0]),
|
|
1191
|
+
intentPath: isAbsolute(options.intent) ? options.intent : resolve(cwd, options.intent),
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* `change`'s self-footgun guard, the same shape `history`'s holds: writing
|
|
1197
|
+
* the reconciliation report over the very manifest this run just read would
|
|
1198
|
+
* destroy the declaration it verified, with the loss surfacing only later —
|
|
1199
|
+
* the first time someone tries to re-run the verification. Declared by the
|
|
1200
|
+
* command that owns the law and enforced by the driver's write door; `null`
|
|
1201
|
+
* means no refusal.
|
|
1202
|
+
*
|
|
1203
|
+
* @param {{output: string|null, intent: string, paths: string[]}} options
|
|
1204
|
+
* This run's parsed flags.
|
|
1205
|
+
* @param {string} cwd The run's working directory, for relative flag
|
|
1206
|
+
* resolution.
|
|
1207
|
+
* @returns {string|null} The refusal message, or `null` when the output is
|
|
1208
|
+
* safe.
|
|
1209
|
+
*/
|
|
1210
|
+
export function changeOutputRefusal(options, cwd) {
|
|
1211
|
+
if (!options.output) return null;
|
|
1212
|
+
const outputAbs = isAbsolute(options.output)
|
|
1213
|
+
? resolve(options.output)
|
|
1214
|
+
: resolve(cwd, options.output);
|
|
1215
|
+
if (outputAbs === changePaths(options, cwd).intentPath) {
|
|
1216
|
+
return (
|
|
1217
|
+
`archkeep: --output '${options.output}' resolves to the change-intent manifest itself — ` +
|
|
1218
|
+
`overwriting the declaration with its own reconciliation report would destroy it. ` +
|
|
1219
|
+
`Write the report somewhere else.`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* `change` as the CLI drives it: the baseline and manifest paths resolved,
|
|
1227
|
+
* then the shared preamble — command context, boundary law, the optional
|
|
1228
|
+
* `--event-out` directory — so `../../cli.mjs`'s driver only wires options,
|
|
1229
|
+
* IO seams, and where output lands (`./README.md`). The engine this returns
|
|
1230
|
+
* from is `changeCommand` above, unchanged.
|
|
1231
|
+
*
|
|
1232
|
+
* @param {{config: string|null, eventOut?: string|null, intent: string, paths: string[]}} options
|
|
1233
|
+
* This run's parsed flags; `paths[0]` is the baseline evidence snapshot.
|
|
1234
|
+
* @param {{cwd: string, readGraph?: Function, listFiles?: Function}} io The
|
|
1235
|
+
* seams a test injects, the same ones `check` takes.
|
|
1236
|
+
* @returns {Promise<object>} `changeCommand`'s result, unmodified.
|
|
1237
|
+
*/
|
|
1238
|
+
export async function change(options, { cwd, readGraph, listFiles }) {
|
|
1239
|
+
const { baselinePath, intentPath } = changePaths(options, cwd);
|
|
1240
|
+
const commandContext = resolveCommandContext({ cwd }, { readGraph, listFiles });
|
|
1241
|
+
// Declared constraints are judged under whichever law THIS run resolves,
|
|
1242
|
+
// and the envelope records that law's fingerprint beside the baseline's —
|
|
1243
|
+
// the same loading every judging command does (`resolvePolicy`),
|
|
1244
|
+
// profile-aware the same way `check` is.
|
|
1245
|
+
const { config } = await resolvePolicy(options, commandContext, cwd);
|
|
1246
|
+
// `--event-out` names the reconcile event store directory, resolved from
|
|
1247
|
+
// cwd like the other path flags; `undefined` when absent, so a run
|
|
1248
|
+
// without the flag writes no event and stays byte-identical.
|
|
1249
|
+
const eventOut =
|
|
1250
|
+
typeof options.eventOut === "string" && options.eventOut !== ""
|
|
1251
|
+
? isAbsolute(options.eventOut)
|
|
1252
|
+
? options.eventOut
|
|
1253
|
+
: resolve(cwd, options.eventOut)
|
|
1254
|
+
: undefined;
|
|
1255
|
+
return changeCommand(baselinePath, intentPath, commandContext, {
|
|
1256
|
+
config,
|
|
1257
|
+
...(eventOut === undefined ? {} : { eventOut }),
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `check` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `check`, `fitness`, `scenario`.
|
|
4
|
+
*
|
|
5
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
+
* graph, not just to a comment (PD-18,
|
|
9
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
10
|
+
*/
|
|
11
|
+
export { check, sortViolations } from "./check.mjs";
|
|
12
|
+
export { fitness } from "./fitness.mjs";
|
|
13
|
+
export { scenario } from "./scenario.mjs";
|
package/src/commands/check.mjs
CHANGED
|
@@ -209,7 +209,7 @@ function declaredEdgeManifest({ provider, graph }, sourceProject) {
|
|
|
209
209
|
* evidenceOut?: string|null}} options
|
|
210
210
|
* @param {{cwd: string, readGraph?: Function, listFiles?: Function,
|
|
211
211
|
* listUntracked?: Function}} context
|
|
212
|
-
* @returns {Promise<{report: string, violations: number, declaredEdgeFindings: number,
|
|
212
|
+
* @returns {Promise<{exitCode: 0|1|3, report: string, violations: number, declaredEdgeFindings: number,
|
|
213
213
|
* goWorkDrift: number, tsconfigPathsDead: number, intentFindings: number,
|
|
214
214
|
* intentUnresolved: number, intentUnresolvedDecisionRefs: number, fitnessFail: number,
|
|
215
215
|
* fitnessUnknown: number, customRuleFail: number, customRuleUnknown: number,
|
|
@@ -521,6 +521,15 @@ export async function check(
|
|
|
521
521
|
// from the MARKER (the line a reader edits is the document's), project and
|
|
522
522
|
// constraint from the edge — so suppression, waiver annotation, sorting and
|
|
523
523
|
// the SARIF face all treat it as the ordinary violation it is.
|
|
524
|
+
//
|
|
525
|
+
// PD-13 (2026-09-06) pins THIS fold as the documented normalization seam
|
|
526
|
+
// (`../../../../docs/architecture/refactor/DECISIONS.md`): the one place
|
|
527
|
+
// `judgeEdge`'s markdown-pairing verdicts are reshaped into the exact
|
|
528
|
+
// `Violation` record `violationOf` (`../rules/index.mjs`) builds — the
|
|
529
|
+
// families then fold into the one verdict lane as count keys into
|
|
530
|
+
// `verdictFor` (`../verdict.mjs`). The canonical statement is the
|
|
531
|
+
// "Finding — the unowned concept" section of
|
|
532
|
+
// `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
|
|
524
533
|
/** @type {object[]} */
|
|
525
534
|
const markdownRaw = [];
|
|
526
535
|
if (config !== null && markdownTrack !== null && config.depConstraints.length > 0) {
|
|
@@ -1289,6 +1298,7 @@ export async function check(
|
|
|
1289
1298
|
});
|
|
1290
1299
|
|
|
1291
1300
|
return {
|
|
1301
|
+
exitCode: verdict.exitCode,
|
|
1292
1302
|
report,
|
|
1293
1303
|
violations: violations.length,
|
|
1294
1304
|
waived,
|
|
@@ -1311,8 +1321,8 @@ export async function check(
|
|
|
1311
1321
|
customRulesDeclared: customRules !== null,
|
|
1312
1322
|
analyzed,
|
|
1313
1323
|
unchecked,
|
|
1314
|
-
// The
|
|
1315
|
-
//
|
|
1324
|
+
// The exit authority is `verdict` above, surfaced on this return: the
|
|
1325
|
+
// process exit code comes straight from this field, so a count the
|
|
1316
1326
|
// envelope saw but the exit code did not would let the two disagree.
|
|
1317
1327
|
blindSpots: blindSpotCount,
|
|
1318
1328
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compare` capability facade — the word's verb roster as explicit named
|
|
3
|
+
* re-exports: `diff`, `delta` (both modes), `change`, `drift`, `reconcile`,
|
|
4
|
+
* `history`, `trajectory`, `evolution`, plus the output guards the two
|
|
5
|
+
* verbs that write reports declare (`change`, `history`, `trajectory`).
|
|
6
|
+
*
|
|
7
|
+
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
8
|
+
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
9
|
+
* the facade so the vocabulary's code referent is visible to the import
|
|
10
|
+
* graph, not just to a comment (PD-18,
|
|
11
|
+
* ../../../../docs/architecture/refactor/DECISIONS.md).
|
|
12
|
+
*/
|
|
13
|
+
export { diff } from "./diff.mjs";
|
|
14
|
+
export { captureBaseline, delta } from "./delta.mjs";
|
|
15
|
+
export { change, changeOutputRefusal } from "./change.mjs";
|
|
16
|
+
export { drift } from "./drift.mjs";
|
|
17
|
+
export { reconcileCommand } from "./reconcile.mjs";
|
|
18
|
+
export { history, historyOutputRefusal } from "./history.mjs";
|
|
19
|
+
export { trajectoryCommand, trajectoryOutputRefusal } from "./trajectory.mjs";
|
|
20
|
+
export { evolutionCommand } from "./evolution.mjs";
|
|
@@ -24,6 +24,18 @@
|
|
|
24
24
|
* `overallComplete` implies ALL gates pass. Any failed gate MUST prevent
|
|
25
25
|
* `overallComplete = true`.
|
|
26
26
|
*
|
|
27
|
+
* ## Not the coverage refusal (register R1)
|
|
28
|
+
*
|
|
29
|
+
* These statuses grade the composed evaluation — how completely the
|
|
30
|
+
* Evidence-Complete gates were satisfied for the statement being made. A
|
|
31
|
+
* different question is whether the run saw enough input to make any claim
|
|
32
|
+
* at all, and that one is answered only by the graph family's shared
|
|
33
|
+
* refusal contract (`./coverage-verdict.mjs`): `coverageVerdict` refuses
|
|
34
|
+
* `no-verdict` (exit 3), and no status here is derived from it or folds
|
|
35
|
+
* into it. Two models of "did we see everything", owned by different
|
|
36
|
+
* waves — register R1 in `docs/architecture/refactor/SEMANTIC-MODEL.md`
|
|
37
|
+
* keeps them separate.
|
|
38
|
+
*
|
|
27
39
|
* @module
|
|
28
40
|
*/
|
|
29
41
|
|