@ecoma-io/archkeep 0.26.0 → 0.27.1
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 +4 -3
- package/package.json +2 -1
- package/src/architecture-intent/model.mjs +2 -2
- package/src/commands/README.md +7 -4
- package/src/commands/analyze-capability.mjs +2 -2
- package/src/commands/change-intent.mjs +2 -2
- package/src/commands/change.mjs +9 -5
- package/src/commands/check-capability.mjs +2 -2
- package/src/commands/check.mjs +3 -5
- package/src/commands/compare-capability.mjs +2 -2
- package/src/commands/completeness.mjs +2 -2
- package/src/commands/delta-snapshot.mjs +17 -0
- package/src/commands/delta.mjs +9 -5
- package/src/commands/diff.mjs +2 -2
- package/src/commands/evolution.mjs +10 -0
- package/src/commands/explain-capability.mjs +2 -2
- package/src/commands/fitness.mjs +2 -2
- package/src/commands/govern-capability.mjs +2 -2
- package/src/commands/inspect-capability.mjs +2 -2
- package/src/commands/rules-capability.mjs +2 -2
- package/src/commands/scenario-evaluation.mjs +97 -3
- package/src/corpus/goldens/adr.json +2 -2
- package/src/corpus/goldens/change.json +6 -5
- package/src/corpus/goldens/change.text +2 -2
- package/src/corpus/goldens/check.json +2 -2
- package/src/corpus/goldens/context.json +2 -2
- package/src/corpus/goldens/debt.json +4 -4
- package/src/corpus/goldens/debt.text +2 -2
- package/src/corpus/goldens/decisions.json +2 -2
- package/src/corpus/goldens/delta.json +4 -4
- package/src/corpus/goldens/delta.text +1 -1
- package/src/corpus/goldens/diff.json +4 -4
- package/src/corpus/goldens/diff.text +1 -1
- package/src/corpus/goldens/discover.json +2 -2
- package/src/corpus/goldens/drift.json +2 -2
- package/src/corpus/goldens/evolution.json +4 -2
- package/src/corpus/goldens/explain.json +2 -2
- package/src/corpus/goldens/fitness.json +2 -2
- package/src/corpus/goldens/graph.json +2 -2
- package/src/corpus/goldens/health.json +2 -2
- package/src/corpus/goldens/history.json +3 -3
- package/src/corpus/goldens/history.text +1 -1
- package/src/corpus/goldens/impact.json +2 -2
- package/src/corpus/goldens/provenance.json +2 -2
- package/src/corpus/goldens/reconcile.json +2 -2
- package/src/corpus/goldens/report.json +2 -2
- package/src/corpus/goldens/report.text +1 -1
- package/src/corpus/goldens/scenario.json +2 -2
- package/src/corpus/goldens/trajectory.json +3 -3
- package/src/corpus/goldens/trajectory.text +1 -1
- package/src/corpus/goldens/waivers.json +2 -2
- package/src/go-work.mjs +4 -5
- package/src/governance/debt-ledger.mjs +5 -2
- package/src/governance/evolution-event.mjs +1 -2
- package/src/governance/reconcile-score.mjs +59 -9
- package/src/intent/intent-manifest.json +8 -8
- package/src/providers/nx-static.mjs +4 -4
- package/src/report/text.mjs +1 -1
- package/src/rules/edge-constraints.mjs +4 -5
- package/src/rules/index.mjs +8 -10
- package/src/tsconfig-paths.mjs +4 -5
- package/src/fixtures/evolution-lifecycle/workspace.mjs +0 -248
package/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
* declared constraint. `check`, `fitness`, `delta` and `change` are the
|
|
58
58
|
* verbs whose verdicts carry this code, plus `rules verify` — the
|
|
59
59
|
* artifact-integrity fold, a bounded verification surface, not a fifth
|
|
60
|
-
* architecture carrier (
|
|
60
|
+
* architecture carrier (`docs/reference/exit-codes.md` owns the
|
|
61
61
|
* roster). Every other verb only ever reads.
|
|
62
62
|
* 2 usage error — unknown command, unknown flag, missing argument, path
|
|
63
63
|
* outside the tree
|
|
@@ -105,7 +105,8 @@ import { containmentViolation } from "./src/containment.mjs";
|
|
|
105
105
|
import { UsageError } from "./src/errors.mjs";
|
|
106
106
|
// Verb imports route through the capability facades: each
|
|
107
107
|
// `<word>-capability.mjs` module is its word's explicit verb roster — pure
|
|
108
|
-
// re-exports, zero judgment
|
|
108
|
+
// re-exports, zero judgment. The vocabulary's roster lives in
|
|
109
|
+
// docs/concepts/architecture.md's "The 24 commands".
|
|
109
110
|
// Non-verb helper modules keep their direct imports.
|
|
110
111
|
import {
|
|
111
112
|
discoverCommand,
|
|
@@ -1331,7 +1332,7 @@ async function runWaivers(options, { cwd, env }) {
|
|
|
1331
1332
|
* (D-09). `check` folds the same `fail` into its own exit 1 by presence, so the
|
|
1332
1333
|
* two faces agree; the verbs whose verdict carries that code are `check`,
|
|
1333
1334
|
* `fitness`, `delta` and `change`, plus the `rules verify` artifact-integrity
|
|
1334
|
-
* fold (
|
|
1335
|
+
* fold (`docs/reference/exit-codes.md` owns the roster).
|
|
1335
1336
|
*
|
|
1336
1337
|
* @param {{format: string, output: string|null, config: string|null, paths: string[]}} options
|
|
1337
1338
|
* @param {{cwd: string, env: {out: Function, err: Function, readGraph?: Function, listFiles?: Function}}} runContext
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecoma-io/archkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Architecture authority for human and agentic software development — deterministic, evidence-backed enforcement of declared architecture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"!src/custom-rules/wasm-fixture.mjs",
|
|
74
74
|
"!src/report/envelope-shape.json",
|
|
75
75
|
"!src/custom-rules/evidence-golden.json",
|
|
76
|
+
"!src/fixtures/",
|
|
76
77
|
"presets/",
|
|
77
78
|
"LICENSE",
|
|
78
79
|
"README.md"
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* is a deterministic comparison (`./judge.mjs`). NO LLM/AI anywhere in the core.
|
|
13
13
|
*
|
|
14
14
|
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
15
|
-
*
|
|
16
|
-
* (`../../../../docs/architecture
|
|
15
|
+
* four surfaces, never one type
|
|
16
|
+
* (`../../../../docs/concepts/architecture.md`, "The 24 commands"). This module is
|
|
17
17
|
* the workspace-declared law: the grammar, validation, and loading of
|
|
18
18
|
* `architecture-intent.json`, judged against the observed graph by `check`
|
|
19
19
|
* and `drift`. The other three: the v1.0 evidence-manifest registry
|
package/src/commands/README.md
CHANGED
|
@@ -54,8 +54,10 @@ the resolution order.
|
|
|
54
54
|
introduced/resolved/unchanged/unknown (`./delta-classify.mjs`), with
|
|
55
55
|
unresolvable import sites carried as their own category, never counted as
|
|
56
56
|
violations. Refuses an unreadable, malformed, foreign-schema, or
|
|
57
|
-
incomplete-coverage baseline, a
|
|
58
|
-
|
|
57
|
+
incomplete-coverage baseline, a `command`-carrying report envelope (a graph
|
|
58
|
+
snapshot is `diff`'s input, not delta evidence), a provider mismatch
|
|
59
|
+
(stricter than `diff`'s note — violation identity across two project models
|
|
60
|
+
is not evidence),
|
|
59
61
|
incomplete head coverage, and an Nx workspace with polyglot manifests but no
|
|
60
62
|
plugin registration; a policy-fingerprint change is a loud coverage note, not
|
|
61
63
|
a refusal. A verdict, not a description: a non-waived introduced violation is
|
|
@@ -84,8 +86,9 @@ the resolution order.
|
|
|
84
86
|
pins); an unproven base identity or an undeterminable
|
|
85
87
|
constraint is exit 3, and constraints are left unevaluated over a base the
|
|
86
88
|
run cannot vouch for. Refuses a manifest that fails shape or reference
|
|
87
|
-
validation, an unreadable/malformed/incomplete baseline, a
|
|
88
|
-
mismatch, incomplete head coverage,
|
|
89
|
+
validation, an unreadable/malformed/incomplete baseline, a `command`-carrying
|
|
90
|
+
report envelope in its place, a provider mismatch, incomplete head coverage,
|
|
91
|
+
and the unregistered-plugin graph.
|
|
89
92
|
|
|
90
93
|
- **`impact`** (`./impact.mjs`'s `impactCommand`) — reverse reachability from
|
|
91
94
|
the project graph: given a project name, lists every project that transitively
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
8
8
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
9
9
|
* the facade so the vocabulary's code referent is visible to the import
|
|
10
|
-
* graph, not just to a comment
|
|
11
|
-
* ../../../../docs/architecture
|
|
10
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
11
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
12
12
|
*/
|
|
13
13
|
export { discoverCommand, intentJsonFromProposal, intentWriteRefusal } from "./discover.mjs";
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* as the reviewable answer to "what did this change do to the architecture?".
|
|
11
11
|
*
|
|
12
12
|
* Which "intent" this is: one of four unrelated surfaces sharing the word —
|
|
13
|
-
*
|
|
14
|
-
* (`../../../../docs/architecture
|
|
13
|
+
* four surfaces, never one type
|
|
14
|
+
* (`../../../../docs/concepts/architecture.md`, "The 24 commands"). This module is
|
|
15
15
|
* the declared-change grammar: one change's expected architectural
|
|
16
16
|
* consequences, verified by the `change` command against the actual delta.
|
|
17
17
|
* The other three: the workspace-declared law (`../architecture-intent/` —
|
package/src/commands/change.mjs
CHANGED
|
@@ -76,8 +76,9 @@
|
|
|
76
76
|
* exit 3, a `coverage` block naming every file and site the run could not
|
|
77
77
|
* judge — where a parser and `--output` can read it; the rest are throws →
|
|
78
78
|
* exit 3 upstream: a manifest that fails shape or reference validation, an
|
|
79
|
-
* unreadable/malformed/foreign-schema baseline,
|
|
80
|
-
*
|
|
79
|
+
* unreadable/malformed/foreign-schema baseline, a `command`-carrying report
|
|
80
|
+
* envelope in its place, incomplete baseline coverage, a provider mismatch,
|
|
81
|
+
* an unregistered-plugin graph over polyglot manifests,
|
|
81
82
|
* and a run with no boundary law (constraints and the law fingerprint need
|
|
82
83
|
* one).
|
|
83
84
|
*
|
|
@@ -480,8 +481,8 @@ function changeFoldInputProblem(reconciliation, constraints, unprovenReasons) {
|
|
|
480
481
|
* before any list, so a malformed one reaching it unguarded can name the
|
|
481
482
|
* whole run unproven or matched for reasons the counts never supported. The
|
|
482
483
|
* literals stay hand-rolled per site by decision — the carrier folds are the
|
|
483
|
-
* pinned baseline
|
|
484
|
-
* (`docs/
|
|
484
|
+
* pinned baseline, not a table to be converged
|
|
485
|
+
* (`docs/reference/exit-codes.md`'s fold-site statement).
|
|
485
486
|
*
|
|
486
487
|
* @param {{matched: object[], unexpected: object[], missingExpected: object[]}} reconciliation
|
|
487
488
|
* @param {{verdict: string}[]} constraints Judged `fitnessVerdict` rows —
|
|
@@ -1000,10 +1001,11 @@ export async function changeCommand(
|
|
|
1000
1001
|
// `findings`, never in `debt`. An unproven base (F-CHG-1) or an unjudgeable
|
|
1001
1002
|
// intent is a no-verdict: no ids are emitted, an in-band note says so, and a
|
|
1002
1003
|
// change run never fabricates ledger ids over evidence it cannot vouch for.
|
|
1003
|
-
/** @type {{introduced: string[], resolved: string[], note?: string}} */
|
|
1004
|
+
/** @type {{judged: boolean, introduced: string[], resolved: string[], note?: string}} the marker is in-band on every shape: `false` on a fail-closed path, `true` from `debtChangeDiff` */
|
|
1004
1005
|
let debt;
|
|
1005
1006
|
if (baseEngineGraph === null) {
|
|
1006
1007
|
debt = {
|
|
1008
|
+
judged: false,
|
|
1007
1009
|
introduced: [],
|
|
1008
1010
|
resolved: [],
|
|
1009
1011
|
note: "base identity unproven — no architecture debt diff can be trusted",
|
|
@@ -1013,6 +1015,7 @@ export async function changeCommand(
|
|
|
1013
1015
|
const archIntent = await (loadIntentOverride ?? loadIntent)(root, { tracked });
|
|
1014
1016
|
if (archIntent === undefined || archIntent === null) {
|
|
1015
1017
|
debt = {
|
|
1018
|
+
judged: false,
|
|
1016
1019
|
introduced: [],
|
|
1017
1020
|
resolved: [],
|
|
1018
1021
|
note: `no '${INTENT_FILE}' tracked — the change event carries no architecture debt ids`,
|
|
@@ -1024,6 +1027,7 @@ export async function changeCommand(
|
|
|
1024
1027
|
}
|
|
1025
1028
|
} catch (error) {
|
|
1026
1029
|
debt = {
|
|
1030
|
+
judged: false,
|
|
1027
1031
|
introduced: [],
|
|
1028
1032
|
resolved: [],
|
|
1029
1033
|
note: `architecture intent could not be judged — no debt ids emitted (${error.message})`,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
6
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
7
|
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
-
* graph, not just to a comment
|
|
9
|
-
* ../../../../docs/architecture
|
|
8
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
9
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
10
10
|
*/
|
|
11
11
|
export { check, sortViolations } from "./check.mjs";
|
|
12
12
|
export { fitness } from "./fitness.mjs";
|
package/src/commands/check.mjs
CHANGED
|
@@ -522,14 +522,12 @@ export async function check(
|
|
|
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
524
|
//
|
|
525
|
-
//
|
|
526
|
-
// (`../../../../docs/architecture/refactor/DECISIONS.md`): the one place
|
|
525
|
+
// This fold is the documented normalization seam — the one place
|
|
527
526
|
// `judgeEdge`'s markdown-pairing verdicts are reshaped into the exact
|
|
528
527
|
// `Violation` record `violationOf` (`../rules/index.mjs`) builds — the
|
|
529
528
|
// families then fold into the one verdict lane as count keys into
|
|
530
|
-
// `verdictFor` (`../verdict.mjs`). The canonical statement is
|
|
531
|
-
// "
|
|
532
|
-
// `../../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
|
|
529
|
+
// `verdictFor` (`../verdict.mjs`). The canonical statement is
|
|
530
|
+
// `../../../../docs/concepts/evidence.md`'s "Four finding families".
|
|
533
531
|
/** @type {object[]} */
|
|
534
532
|
const markdownRaw = [];
|
|
535
533
|
if (config !== null && markdownTrack !== null && config.depConstraints.length > 0) {
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
8
8
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
9
9
|
* the facade so the vocabulary's code referent is visible to the import
|
|
10
|
-
* graph, not just to a comment
|
|
11
|
-
* ../../../../docs/architecture
|
|
10
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
11
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
12
12
|
*/
|
|
13
13
|
export { diff } from "./diff.mjs";
|
|
14
14
|
export { captureBaseline, delta } from "./delta.mjs";
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
* refusal contract (`./coverage-verdict.mjs`): `coverageVerdict` refuses
|
|
34
34
|
* `no-verdict` (exit 3), and no status here is derived from it or folds
|
|
35
35
|
* into it. Two models of "did we see everything", owned by different
|
|
36
|
-
* waves — register R1 in
|
|
37
|
-
* keeps them separate.
|
|
36
|
+
* waves — register R1 in `../../../../docs/concepts/evidence.md`'s "Register
|
|
37
|
+
* R1" keeps them separate.
|
|
38
38
|
*
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
@@ -320,6 +320,10 @@ export function readEvidenceSnapshot(path, io = {}) {
|
|
|
320
320
|
*
|
|
321
321
|
* Refusals, each loud:
|
|
322
322
|
* - unreadable/malformed JSON — named with the path and the parse error;
|
|
323
|
+
* - a `command` field — that marker belongs to a report envelope (the graph
|
|
324
|
+
* family), so the document is not delta evidence at all; the family is
|
|
325
|
+
* decided before the schemaVersion refusals, whose "newer version" advice
|
|
326
|
+
* would be false for a file this binary itself wrote;
|
|
323
327
|
* - a `schemaVersion` that is not the integer this format uses — a FUTURE
|
|
324
328
|
* version refuses too: a reader that half-understood a newer format would
|
|
325
329
|
* classify over evidence it misread;
|
|
@@ -360,6 +364,19 @@ export function parseEvidenceSnapshot(text, path) {
|
|
|
360
364
|
);
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
// A `command` field marks a report envelope — the graph family's document,
|
|
368
|
+
// not delta evidence. The family decides before the schemaVersion refusals
|
|
369
|
+
// below: that number is the OTHER format's version, so its "newer version;
|
|
370
|
+
// upgrade" advice would be false for a file this binary itself wrote (#810).
|
|
371
|
+
if (typeof parsed.command === "string") {
|
|
372
|
+
const pointer = parsed.command === "graph" ? " For graph snapshots use 'diff <baseline>'." : "";
|
|
373
|
+
throw new Error(
|
|
374
|
+
`archkeep: the evidence snapshot '${path}' has a 'command' field — it is not a delta ` +
|
|
375
|
+
`evidence snapshot, it is a '${parsed.command}' envelope. delta requires an evidence ` +
|
|
376
|
+
`snapshot (from 'delta --capture').${pointer}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
363
380
|
const problems = [];
|
|
364
381
|
if (!Number.isInteger(parsed.schemaVersion)) {
|
|
365
382
|
problems.push(
|
package/src/commands/delta.mjs
CHANGED
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
* status "no-verdict", exit 3, a `coverage` block naming every file and site
|
|
30
30
|
* the run could not judge — where a parser and `--output` can read it; the
|
|
31
31
|
* rest are throws, exit 3 upstream:
|
|
32
|
-
* - a baseline that cannot be read, parsed, or holds a foreign schemaVersion
|
|
33
|
-
*
|
|
32
|
+
* - a baseline that cannot be read, parsed, or holds a foreign schemaVersion,
|
|
33
|
+
* or a report envelope — a document carrying a `command` field (a graph
|
|
34
|
+
* snapshot is `diff`'s input), refused as the wrong family before any
|
|
35
|
+
* schemaVersion reading (`./delta-snapshot.mjs`'s loader owns those);
|
|
34
36
|
* - a provider mismatch between baseline and this run (`providerMismatch`) —
|
|
35
37
|
* a THROW here where `diff` settles for a note, because violation IDENTITY
|
|
36
38
|
* across two different project models is not trustworthy: the same tree
|
|
@@ -479,8 +481,8 @@ function deltaFoldInputProblem(classification, custom) {
|
|
|
479
481
|
* The input latch runs first: a bucket the fold cannot read is refused as a
|
|
480
482
|
* no-verdict whose single reason names it (`refused`), never folded past as a
|
|
481
483
|
* silently-empty count. The literals stay hand-rolled per site by decision —
|
|
482
|
-
* the carrier folds are the pinned baseline
|
|
483
|
-
*
|
|
484
|
+
* the carrier folds are the pinned baseline, not a table to be converged
|
|
485
|
+
* (`docs/reference/exit-codes.md`'s fold-site statement).
|
|
484
486
|
*
|
|
485
487
|
* Pure and exported for the fold's own tests; `deltaCommand` is its only
|
|
486
488
|
* production caller.
|
|
@@ -1029,12 +1031,13 @@ export async function deltaCommand(
|
|
|
1029
1031
|
// gate; the fail-closed branches are an absent intent and an unjudgeable
|
|
1030
1032
|
// one, each emitting no ids and an in-band note rather than a fabricated
|
|
1031
1033
|
// clean ledger.
|
|
1032
|
-
/** @type {{introduced: string[], resolved: string[], note?: string}} */
|
|
1034
|
+
/** @type {{judged: boolean, introduced: string[], resolved: string[], note?: string}} the marker is in-band on every shape: `false` on a fail-closed path, `true` from `debtChangeDiff` */
|
|
1033
1035
|
let debt;
|
|
1034
1036
|
try {
|
|
1035
1037
|
const archIntent = await (loadIntentOverride ?? loadIntent)(root, { tracked });
|
|
1036
1038
|
if (archIntent === undefined || archIntent === null) {
|
|
1037
1039
|
debt = {
|
|
1040
|
+
judged: false,
|
|
1038
1041
|
introduced: [],
|
|
1039
1042
|
resolved: [],
|
|
1040
1043
|
note: `no '${INTENT_FILE}' tracked — the delta event carries no architecture debt ids`,
|
|
@@ -1046,6 +1049,7 @@ export async function deltaCommand(
|
|
|
1046
1049
|
}
|
|
1047
1050
|
} catch (error) {
|
|
1048
1051
|
debt = {
|
|
1052
|
+
judged: false,
|
|
1049
1053
|
introduced: [],
|
|
1050
1054
|
resolved: [],
|
|
1051
1055
|
note: `architecture intent could not be judged — no debt ids emitted (${error.message})`,
|
package/src/commands/diff.mjs
CHANGED
|
@@ -245,8 +245,8 @@ function buildHeadSnapshot(commandContext) {
|
|
|
245
245
|
* `edgeEvolutionIdentity` (`../governance/evolution-event.mjs`), the escaped
|
|
246
246
|
* `source>target:type` string evolution events carry, and the two spellings
|
|
247
247
|
* are deliberately not unified — each medium keeps exactly one identity
|
|
248
|
-
* constructor (`../../../../docs/adr/0008-snapshot-identity-per-family.md
|
|
249
|
-
*
|
|
248
|
+
* constructor (`../../../../docs/adr/0008-snapshot-identity-per-family.md`).
|
|
249
|
+
* An edge
|
|
250
250
|
* crosses from a structural diff into an event as its raw
|
|
251
251
|
* `{source, target, type}` triple, mapped through that function at the
|
|
252
252
|
* command boundary; a `\0`-joined key written into an event record would
|
|
@@ -940,6 +940,16 @@ export function buildEvolutionSummary(comparisons) {
|
|
|
940
940
|
});
|
|
941
941
|
return { verdictDeltas };
|
|
942
942
|
}
|
|
943
|
+
if (axis === "debt") {
|
|
944
|
+
// The aggregate states whether the debt it unions was judged at every
|
|
945
|
+
// transition (`debtChangeDiff`'s marker) — a summary over a transition
|
|
946
|
+
// that could not judge its debt never reads as a judged clean union.
|
|
947
|
+
return {
|
|
948
|
+
judged: comparisons.every((c) => c.debt === undefined || c.debt.judged === true),
|
|
949
|
+
introduced: collect("introduced"),
|
|
950
|
+
resolved: collect("resolved"),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
943
953
|
return {
|
|
944
954
|
introduced: collect("introduced"),
|
|
945
955
|
resolved: collect("resolved"),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
6
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
7
|
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
-
* graph, not just to a comment
|
|
9
|
-
* ../../../../docs/architecture
|
|
8
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
9
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
10
10
|
*/
|
|
11
11
|
export { explain } from "./explain.mjs";
|
package/src/commands/fitness.mjs
CHANGED
|
@@ -122,8 +122,8 @@ export function declaresFitness(config) {
|
|
|
122
122
|
* exit over a run the fold could not read. A verdict outside the four states
|
|
123
123
|
* is refused as a no-verdict whose single reason names it (`refused`), never
|
|
124
124
|
* folded past. The literals stay hand-rolled per site by decision — the
|
|
125
|
-
* carrier folds are the pinned baseline
|
|
126
|
-
* converged (`docs/
|
|
125
|
+
* carrier folds are the pinned baseline, not a table to be
|
|
126
|
+
* converged (`docs/reference/exit-codes.md`'s fold-site statement).
|
|
127
127
|
*
|
|
128
128
|
* `overall.decisions` is validated for the same reason the delta fold
|
|
129
129
|
* validates buckets the fold itself does not count first: `result.functions`
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
6
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
7
|
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
-
* graph, not just to a comment
|
|
9
|
-
* ../../../../docs/architecture
|
|
8
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
9
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
10
10
|
*/
|
|
11
11
|
export { waivers } from "./waivers.mjs";
|
|
12
12
|
export { report } from "./report.mjs";
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
7
7
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
8
8
|
* the facade so the vocabulary's code referent is visible to the import
|
|
9
|
-
* graph, not just to a comment
|
|
10
|
-
* ../../../../docs/architecture
|
|
9
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
10
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
11
11
|
*/
|
|
12
12
|
export { graph } from "./graph.mjs";
|
|
13
13
|
export { impact } from "./impact.mjs";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Pure surface module: no judgment, no defaults, no logic — every export is
|
|
6
6
|
* a re-export of a command module. `cli.mjs` routes its verb imports through
|
|
7
7
|
* the facade so the vocabulary's code referent is visible to the import
|
|
8
|
-
* graph, not just to a comment
|
|
9
|
-
* ../../../../docs/architecture
|
|
8
|
+
* graph, not just to a comment — the vocabulary's roster lives in
|
|
9
|
+
* ../../../../docs/concepts/architecture.md's "The 24 commands".
|
|
10
10
|
*/
|
|
11
11
|
export {
|
|
12
12
|
rulesAddCommand,
|
|
@@ -152,11 +152,13 @@ function cloneGraph(graph) {
|
|
|
152
152
|
*
|
|
153
153
|
* @param {object} graph The base graph to apply changes to.
|
|
154
154
|
* @param {DependencyChange[]} changes The hypothetical changes.
|
|
155
|
-
* @returns {{graph: object, applied: string[],
|
|
155
|
+
* @returns {{graph: object, applied: string[],
|
|
156
|
+
* mutations: {type: string, source: string, target: string}[], refused: string[]}}
|
|
156
157
|
*/
|
|
157
158
|
function applyChanges(graph, changes) {
|
|
158
159
|
const cloned = cloneGraph(graph);
|
|
159
160
|
const applied = [];
|
|
161
|
+
const mutations = [];
|
|
160
162
|
const refused = [];
|
|
161
163
|
|
|
162
164
|
for (const change of changes) {
|
|
@@ -203,6 +205,7 @@ function applyChanges(graph, changes) {
|
|
|
203
205
|
source: change.source,
|
|
204
206
|
});
|
|
205
207
|
applied.push(`added dependency: ${change.source} → ${change.target} (${change.edgeType})`);
|
|
208
|
+
mutations.push({ type: "dependency_added", source: change.source, target: change.target });
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
if (change.type === "dependency_removed") {
|
|
@@ -238,6 +241,7 @@ function applyChanges(graph, changes) {
|
|
|
238
241
|
existing.splice(idx, 1);
|
|
239
242
|
const typeLabel = change.edgeType ? ` (${change.edgeType})` : "";
|
|
240
243
|
applied.push(`removed dependency: ${change.source} → ${change.target}${typeLabel}`);
|
|
244
|
+
mutations.push({ type: "dependency_removed", source: change.source, target: change.target });
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
// Clean up empty dependency arrays
|
|
@@ -247,7 +251,7 @@ function applyChanges(graph, changes) {
|
|
|
247
251
|
}
|
|
248
252
|
}
|
|
249
253
|
|
|
250
|
-
return { graph: cloned, applied, refused };
|
|
254
|
+
return { graph: cloned, applied, mutations, refused };
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
/**
|
|
@@ -410,6 +414,50 @@ function resolveBaseRevision(root, userBase) {
|
|
|
410
414
|
};
|
|
411
415
|
}
|
|
412
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Merges constraint-impact rows for the named project's own changed edges
|
|
419
|
+
* into one side's rows (#809), preserving the row shape the primitive
|
|
420
|
+
* returns — no new fields, no second judgment.
|
|
421
|
+
*
|
|
422
|
+
* One row per project: when the side already carries a row for the project,
|
|
423
|
+
* the edge, constraint-row and violation lists union without duplicates
|
|
424
|
+
* (constraint rows match by identity — they are the config's own row
|
|
425
|
+
* objects; violations by value, since `judgeEdge` builds fresh objects);
|
|
426
|
+
* otherwise the row is appended. Appending keeps every pre-existing row
|
|
427
|
+
* byte-identical and the order deterministic, because it follows the
|
|
428
|
+
* input changes' order.
|
|
429
|
+
*
|
|
430
|
+
* @param {{project: string, edges: object[], constraintRows: object[],
|
|
431
|
+
* violations: object[]}[]} rows The side's constraint impact, mutated in place.
|
|
432
|
+
* @param {{project: string, edges: object[], constraintRows: object[],
|
|
433
|
+
* violations: object[]}[]} merged Rows computed for the changed edge.
|
|
434
|
+
*/
|
|
435
|
+
function mergeConstraintImpact(rows, merged) {
|
|
436
|
+
for (const row of merged) {
|
|
437
|
+
const existing = rows.find((r) => r.project === row.project);
|
|
438
|
+
if (!existing) {
|
|
439
|
+
rows.push(row);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const edge of row.edges) {
|
|
443
|
+
if (!existing.edges.some((e) => e.target === edge.target && e.type === edge.type)) {
|
|
444
|
+
existing.edges.push(edge);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const constraintRow of row.constraintRows) {
|
|
448
|
+
if (!existing.constraintRows.includes(constraintRow)) {
|
|
449
|
+
existing.constraintRows.push(constraintRow);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const violation of row.violations) {
|
|
453
|
+
const identity = JSON.stringify(violation);
|
|
454
|
+
if (!existing.violations.some((v) => JSON.stringify(v) === identity)) {
|
|
455
|
+
existing.violations.push(violation);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
413
461
|
/**
|
|
414
462
|
* Evaluates a scenario against the current workspace.
|
|
415
463
|
*
|
|
@@ -452,7 +500,12 @@ export function evaluateScenario(
|
|
|
452
500
|
}
|
|
453
501
|
|
|
454
502
|
// Step 3: Apply scenario changes to the graph
|
|
455
|
-
const {
|
|
503
|
+
const {
|
|
504
|
+
graph: scenarioGraph,
|
|
505
|
+
applied,
|
|
506
|
+
mutations,
|
|
507
|
+
refused,
|
|
508
|
+
} = applyChanges(graph, scenarioInput.changes);
|
|
456
509
|
|
|
457
510
|
// Step 4: Compute scenario impact
|
|
458
511
|
const scenarioImpact = computeImpact(projectName, scenarioGraph);
|
|
@@ -468,6 +521,47 @@ export function evaluateScenario(
|
|
|
468
521
|
);
|
|
469
522
|
}
|
|
470
523
|
|
|
524
|
+
// Step 4b: the named project's own changed edges (#809).
|
|
525
|
+
//
|
|
526
|
+
// `computeImpactConstraints` judges edges INTO the target from its
|
|
527
|
+
// dependents, so a change whose SOURCE is the named project never enters
|
|
528
|
+
// that frame — an edge pointing out of the target is invisible to it, and
|
|
529
|
+
// the source-named run reported `unchanged` where the target-named run of
|
|
530
|
+
// the same change reported the violation. The rows for the changed edge
|
|
531
|
+
// are computed here by the same primitive — never a second judgment — and
|
|
532
|
+
// merged into the frame the edge actually belongs to: additions into the
|
|
533
|
+
// scenario side (the edge exists only there), removals into the current
|
|
534
|
+
// side (it exists only there).
|
|
535
|
+
if (config && config.depConstraints) {
|
|
536
|
+
for (const mutation of mutations) {
|
|
537
|
+
if (mutation.source !== projectName) continue;
|
|
538
|
+
if (mutation.type === "dependency_added") {
|
|
539
|
+
mergeConstraintImpact(
|
|
540
|
+
scenarioConstraintImpact,
|
|
541
|
+
computeImpactConstraints(
|
|
542
|
+
mutation.target,
|
|
543
|
+
[mutation.source],
|
|
544
|
+
scenarioGraph.nodes,
|
|
545
|
+
scenarioGraph.dependencies,
|
|
546
|
+
config.depConstraints,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (mutation.type === "dependency_removed") {
|
|
551
|
+
mergeConstraintImpact(
|
|
552
|
+
currentConstraintImpact,
|
|
553
|
+
computeImpactConstraints(
|
|
554
|
+
mutation.target,
|
|
555
|
+
[mutation.source],
|
|
556
|
+
graph.nodes,
|
|
557
|
+
graph.dependencies,
|
|
558
|
+
config.depConstraints,
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
471
565
|
// Step 5: Build decision impact for both sides
|
|
472
566
|
const currentDecisionImpact = buildDecisionImpact(root, currentConstraintImpact, config);
|
|
473
567
|
const scenarioDecisionImpact = buildDecisionImpact(root, scenarioConstraintImpact, config);
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"tool": {
|
|
4
4
|
"name": "@ecoma-io/archkeep",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.27.1"
|
|
6
6
|
},
|
|
7
7
|
"command": "adr",
|
|
8
8
|
"workspace": {
|
|
9
|
-
"root": "
|
|
9
|
+
"root": "<fixture-root>",
|
|
10
10
|
"provider": "native",
|
|
11
11
|
"marker": "docs/adr",
|
|
12
12
|
"provenance": null
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"tool": {
|
|
4
4
|
"name": "@ecoma-io/archkeep",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.27.1"
|
|
6
6
|
},
|
|
7
7
|
"command": "change",
|
|
8
8
|
"workspace": {
|
|
9
|
-
"root": "
|
|
9
|
+
"root": "<fixture-root>",
|
|
10
10
|
"provider": "native",
|
|
11
11
|
"marker": "archkeep.json",
|
|
12
12
|
"provenance": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"result": {
|
|
30
30
|
"intent": {
|
|
31
|
-
"file": "
|
|
31
|
+
"file": "<fixture-root>/.archkeep-change.json",
|
|
32
32
|
"version": "1",
|
|
33
33
|
"base": {
|
|
34
34
|
"commit": "1fd51709c377d99a6139891e1200291cf08aede9"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"baseline": {
|
|
45
|
-
"path": "
|
|
45
|
+
"path": "<fixture-root>/.archkeep-delta.json",
|
|
46
46
|
"tool": {
|
|
47
47
|
"name": "@ecoma-io/archkeep",
|
|
48
|
-
"version": "0.
|
|
48
|
+
"version": "0.27.1"
|
|
49
49
|
},
|
|
50
50
|
"provider": "native",
|
|
51
51
|
"provenance": {
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"decisions": []
|
|
88
88
|
},
|
|
89
89
|
"debt": {
|
|
90
|
+
"judged": true,
|
|
90
91
|
"introduced": [],
|
|
91
92
|
"resolved": []
|
|
92
93
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
intent
|
|
2
|
-
baseline
|
|
1
|
+
intent <fixture-root>/.archkeep-change.json — base 1fd51709
|
|
2
|
+
baseline <fixture-root>/.archkeep-delta.json — 1fd51709, 2 records, 3 projects
|
|
3
3
|
head 1fd51709, 3 projects
|
|
4
4
|
reconciliation ✔ MATCHED — the delta is exactly the declared change
|
|
5
5
|
workspace law 0 live violations under the current law — informational; archkeep check remains the authoritative verdict
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"tool": {
|
|
4
4
|
"name": "@ecoma-io/archkeep",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.27.1"
|
|
6
6
|
},
|
|
7
7
|
"command": "check",
|
|
8
8
|
"workspace": {
|
|
9
|
-
"root": "
|
|
9
|
+
"root": "<fixture-root>",
|
|
10
10
|
"provider": "native",
|
|
11
11
|
"marker": "archkeep.json",
|
|
12
12
|
"provenance": {
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"tool": {
|
|
4
4
|
"name": "@ecoma-io/archkeep",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.27.1"
|
|
6
6
|
},
|
|
7
7
|
"command": "context",
|
|
8
8
|
"workspace": {
|
|
9
|
-
"root": "
|
|
9
|
+
"root": "<fixture-root>",
|
|
10
10
|
"provider": "native",
|
|
11
11
|
"marker": "archkeep.json",
|
|
12
12
|
"provenance": {
|