@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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"tool": {
|
|
4
|
+
"name": "@ecoma-io/archkeep",
|
|
5
|
+
"version": "0.26.0"
|
|
6
|
+
},
|
|
7
|
+
"command": "trajectory",
|
|
8
|
+
"workspace": {
|
|
9
|
+
"root": "/tmp/archkeep-golden-fixture",
|
|
10
|
+
"provider": "native",
|
|
11
|
+
"marker": "archkeep.json",
|
|
12
|
+
"provenance": {
|
|
13
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9",
|
|
14
|
+
"remote": null,
|
|
15
|
+
"dirty": false
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"exitCode": 0,
|
|
20
|
+
"coverage": {
|
|
21
|
+
"complete": true,
|
|
22
|
+
"projects": 3,
|
|
23
|
+
"analyzedFiles": 3,
|
|
24
|
+
"imports": 2,
|
|
25
|
+
"notAnalyzed": [],
|
|
26
|
+
"blindSpots": [],
|
|
27
|
+
"notes": [
|
|
28
|
+
"counts are snapshot-relative: 1 observation are stored graph snapshots — capture points, not commits, days, or captures attempted",
|
|
29
|
+
"rule-impact cannot be recomputed from stored snapshots — snapshots carry the graph and the policy fingerprint, not the constraint table or import sites, so no violation-level trajectory is reported. Run `delta` between two live points, or `check` at any commit."
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"result": {
|
|
33
|
+
"dir": "/tmp/archkeep-golden-fixture/.archkeep-history",
|
|
34
|
+
"observations": {
|
|
35
|
+
"count": 1,
|
|
36
|
+
"basis": "graph_snapshots",
|
|
37
|
+
"first": "0001-71341a04.json",
|
|
38
|
+
"last": "0001-71341a04.json",
|
|
39
|
+
"withProvenance": 1,
|
|
40
|
+
"dirtyProvenance": 0
|
|
41
|
+
},
|
|
42
|
+
"available": false,
|
|
43
|
+
"unavailableReason": "insufficient_history",
|
|
44
|
+
"transitions": {
|
|
45
|
+
"count": 0,
|
|
46
|
+
"architecture": 0,
|
|
47
|
+
"policy": 0,
|
|
48
|
+
"provider": 0,
|
|
49
|
+
"codeDrift": 0,
|
|
50
|
+
"incomparable": 0,
|
|
51
|
+
"unchanged": 0
|
|
52
|
+
},
|
|
53
|
+
"disclosures": {
|
|
54
|
+
"policyOneSided": 0,
|
|
55
|
+
"provenanceOneSided": 0,
|
|
56
|
+
"crossRepo": 0
|
|
57
|
+
},
|
|
58
|
+
"projects": {
|
|
59
|
+
"first": 3,
|
|
60
|
+
"current": 3,
|
|
61
|
+
"delta": null,
|
|
62
|
+
"addedEvents": null,
|
|
63
|
+
"removedEvents": null,
|
|
64
|
+
"changedEvents": null,
|
|
65
|
+
"introduced": null,
|
|
66
|
+
"resolved": null,
|
|
67
|
+
"persistent": null
|
|
68
|
+
},
|
|
69
|
+
"edges": {
|
|
70
|
+
"first": 2,
|
|
71
|
+
"current": 2,
|
|
72
|
+
"delta": null,
|
|
73
|
+
"addedEvents": null,
|
|
74
|
+
"removedEvents": null,
|
|
75
|
+
"changedEvents": null,
|
|
76
|
+
"introduced": null,
|
|
77
|
+
"resolved": null,
|
|
78
|
+
"persistent": null
|
|
79
|
+
},
|
|
80
|
+
"trends": null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
trajectory /tmp/archkeep-golden-fixture/.archkeep-history
|
|
2
|
+
1 observation (graph_snapshots), 0 transitions
|
|
3
|
+
✖ insufficient_history: a trajectory needs at least two observations — derived values are unavailable, not zero
|
|
4
|
+
signals architecture 0 · policy 0 · provider 0 · code drift 0 · incomparable 0 · unchanged 0
|
|
5
|
+
projects first 3 · current 3 · delta n/a · added n/a · removed n/a · changed n/a · introduced n/a · resolved n/a · persistent n/a
|
|
6
|
+
edges first 2 · current 2 · delta n/a · added n/a · removed n/a · introduced n/a · resolved n/a · persistent n/a
|
|
7
|
+
disclosures policy incomparable 0 · provenance incomparable 0 · cross-repo 0 · dirty captures 0 · with provenance 1
|
|
8
|
+
trends n/a
|
|
9
|
+
counts are snapshot-relative: 1 observation are stored graph snapshots — capture points, not commits, days, or captures attempted
|
|
10
|
+
rule-impact cannot be recomputed from stored snapshots — snapshots carry the graph and the policy fingerprint, not the constraint table or import sites, so no violation-level trajectory is reported. Run `delta` between two live points, or `check` at any commit.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
archkeep: no command given.
|
|
2
|
+
archkeep — module-boundary enforcement across every language in the workspace
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
archkeep check [<path>...] Check imports against the boundary rules
|
|
6
|
+
archkeep graph Print the project graph as a deterministic snapshot
|
|
7
|
+
archkeep diff <baseline> Compare two graph snapshots edge by edge
|
|
8
|
+
archkeep delta <baseline> | --capture Classify how boundary violations moved between a captured baseline and head
|
|
9
|
+
archkeep change <baseline> --intent <file> Reconcile a declared change intent against the architectural delta
|
|
10
|
+
archkeep discover [--propose] Report observed facts, and optionally propose candidate architecture
|
|
11
|
+
archkeep drift Compare the observed architecture against the declared intent
|
|
12
|
+
archkeep reconcile Compare the declared intent against the observed architecture
|
|
13
|
+
archkeep waivers List the boundary waivers and permanent suppressions on the table
|
|
14
|
+
archkeep fitness Judge every declared fitness function against the workspace
|
|
15
|
+
archkeep history <dir> Describe how the architecture evolved across snapshots
|
|
16
|
+
archkeep trajectory <dir> Aggregate the deterministic drift trajectory across snapshots
|
|
17
|
+
archkeep evolution Describe how the architecture evolved across a Git revision range
|
|
18
|
+
archkeep health [<snapshot-dir>] Describe architecture health metrics and trends
|
|
19
|
+
archkeep report [<snapshot-dir>] One governance document: how healthy the architecture is, and why
|
|
20
|
+
archkeep debt <dir> Print the architecture-debt ledger across snapshots
|
|
21
|
+
archkeep impact <project> List projects that depend on the named project
|
|
22
|
+
archkeep scenario <project> Evaluate a hypothetical change against the current workspace
|
|
23
|
+
archkeep explain <file:line:column> Explain the judgment for one import site
|
|
24
|
+
archkeep context <project> [--plan [<path>...]] Show the architecture constraints that apply to a project
|
|
25
|
+
archkeep provenance Describe where this run's facts came from and which rows carry an origin
|
|
26
|
+
archkeep decisions <id> Walk the full chain behind one recorded decision — decision to bound rows, projects, findings, and its verification level
|
|
27
|
+
archkeep adr [<id>] List recorded architecture decisions and what each binds
|
|
28
|
+
archkeep rules <list|info|verify|add> [<rule-name>] List official rules, show details, verify catalog integrity, or add a rule
|
|
29
|
+
archkeep --help Show this message
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--format text|sarif|json Terminal report (default), SARIF 2.1.0 for GitHub
|
|
33
|
+
code scanning, or the versioned JSON envelope
|
|
34
|
+
docs/reference/json-output.md documents
|
|
35
|
+
--output <file> Write the report to a file instead of stdout
|
|
36
|
+
--config <file> Read the boundary law from here instead of
|
|
37
|
+
<workspace root>/module-boundaries.config.mjs
|
|
38
|
+
--evidence-out <dir> Also write each custom rule's evidence bundle
|
|
39
|
+
into this existing directory, as <rule>.json —
|
|
40
|
+
the exact document the rule was judged over
|
|
41
|
+
--capture Print an evidence snapshot of the current tree
|
|
42
|
+
(raw import records, graph, coverage, policy
|
|
43
|
+
fingerprint) for a later delta run to compare against.
|
|
44
|
+
Without --output, the snapshot goes to stdout.
|
|
45
|
+
--event-out <dir> Append the delta's evolution event to this directory
|
|
46
|
+
(one canonical record per transition; idempotent — a
|
|
47
|
+
rerun over the same transition writes nothing new).
|
|
48
|
+
Absent: no event file is written
|
|
49
|
+
--intent <file> The change-intent manifest declaring the material
|
|
50
|
+
architectural consequences this change expects
|
|
51
|
+
(required; see docs/usage/change.md)
|
|
52
|
+
--propose Also derive candidate components, boundaries, tags and rules from
|
|
53
|
+
the observed facts — every candidate marked proposed and not
|
|
54
|
+
authoritative; nothing is ever written
|
|
55
|
+
--write-intent <file> Write the proposed components and rules to a valid
|
|
56
|
+
architecture-intent.json file. Only valid with --propose;
|
|
57
|
+
refuses to overwrite a file that already exists — the file
|
|
58
|
+
is a candidate for drift/reconcile and must be reviewed
|
|
59
|
+
before use
|
|
60
|
+
--base <rev> The baseline revision — a commit, branch, tag,
|
|
61
|
+
or HEAD~n; the first revision analyzed
|
|
62
|
+
--head <rev> The tip revision (default HEAD); must be a
|
|
63
|
+
linear descendant of --base with no merges between
|
|
64
|
+
--events <dir> Link the evolution event store in <dir> so debt entries carry
|
|
65
|
+
introducedBy/resolvedBy refs and a resolved list
|
|
66
|
+
--scenario-file <file> Path to the scenario JSON file describing the
|
|
67
|
+
hypothetical changes to evaluate
|
|
68
|
+
--plan Request the agent planning context: current
|
|
69
|
+
architecture, applicable policy (with Intent),
|
|
70
|
+
impact, current violations, drift, coverage, and
|
|
71
|
+
the commands that verify the change. Trailing
|
|
72
|
+
paths scope the change; deterministic and
|
|
73
|
+
never an LLM plan — Archkeep produces facts,
|
|
74
|
+
agents produce plans.
|
|
75
|
+
--history-dir <dir> Path to the workspace's history directory. When given and
|
|
76
|
+
the directory holds archived snapshots, the planning context
|
|
77
|
+
includes the architecture-debt snapshot: current violations,
|
|
78
|
+
exemptions, and gaps aged across the history. Used only with
|
|
79
|
+
`--plan`; ignored otherwise.
|
|
80
|
+
--catalog <path> Path to the catalog file
|
|
81
|
+
(default: node_modules/@ecoma-io/archkeep-rules/catalog.json)
|
|
82
|
+
--to <dir> Target directory for the .wasm file
|
|
83
|
+
(default: tools/rules/)
|
|
84
|
+
|
|
85
|
+
Projects and tags come from the project graph (archkeep.json or Nx); the rules come from
|
|
86
|
+
module-boundaries.config.mjs at the workspace root — the same table ESLint
|
|
87
|
+
reads, so both enforcers answer from one source. That filename is a
|
|
88
|
+
convention and can be changed per workspace: through archkeep.json's
|
|
89
|
+
`boundaryConfig` field, or the integration's `boundaryConfig` option in nx.json.
|
|
90
|
+
|
|
91
|
+
Naming paths scopes the run to those files. That is a fast local pre-check and
|
|
92
|
+
not the gate: the cycle and lazy-load rules judge the file graph as a whole, so
|
|
93
|
+
a scoped run can miss what a whole-workspace run would find.
|
|
94
|
+
|
|
95
|
+
A workspace with a go.work at its root also has its use list compared against
|
|
96
|
+
every project's go.mod, whatever paths scope the run — a module in one list
|
|
97
|
+
and not the other means a developer's go build and CI build different trees.
|
|
98
|
+
|
|
99
|
+
A workspace whose tsconfig declares a paths table also has each alias judged
|
|
100
|
+
for life: an alias whose every target points into directories that do not
|
|
101
|
+
exist resolves no import, so it fails the run the way a violation does. The
|
|
102
|
+
table itself is never re-resolved — the check reads the same parsed tsconfig
|
|
103
|
+
the import resolver uses.
|
|
104
|
+
|
|
105
|
+
Exit codes: 0 clean · 1 findings (violations, go.work drift, dead path aliases) · 2 usage error · 3 no verdict (a file could not be analyzed, or the run could not start)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"tool": {
|
|
4
|
+
"name": "@ecoma-io/archkeep",
|
|
5
|
+
"version": "0.26.0"
|
|
6
|
+
},
|
|
7
|
+
"command": "waivers",
|
|
8
|
+
"workspace": {
|
|
9
|
+
"root": "/tmp/archkeep-golden-fixture",
|
|
10
|
+
"provider": "native",
|
|
11
|
+
"marker": "archkeep.json",
|
|
12
|
+
"provenance": {
|
|
13
|
+
"commit": "1fd51709c377d99a6139891e1200291cf08aede9",
|
|
14
|
+
"remote": null,
|
|
15
|
+
"dirty": false
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"exitCode": 0,
|
|
20
|
+
"coverage": {
|
|
21
|
+
"complete": true,
|
|
22
|
+
"projects": 3,
|
|
23
|
+
"analyzedFiles": 3,
|
|
24
|
+
"imports": 2,
|
|
25
|
+
"notAnalyzed": [],
|
|
26
|
+
"blindSpots": [],
|
|
27
|
+
"notes": [
|
|
28
|
+
"remainingMs is the wall clock at the moment of this run, not a fact about the workspace — it is expected to differ between two runs of an unchanged tree and should be excluded from any diff or hash meant to detect real change. Every other field here is deterministic given the same law and the same tree."
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"result": {
|
|
32
|
+
"waivers": [],
|
|
33
|
+
"covered": 0,
|
|
34
|
+
"expired": 0,
|
|
35
|
+
"stale": 0,
|
|
36
|
+
"suppressions": [],
|
|
37
|
+
"suppressed": 0
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
no waivers — every boundary is enforced, nothing is being accepted temporarily or permanently
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"diagnostics": [
|
|
3
|
+
{
|
|
4
|
+
"code": "analysisFailure",
|
|
5
|
+
"message": "Module boundaries were not fully checked here: parse error: ',' expected.. Imports this server could not read are not covered by the verdict below.",
|
|
6
|
+
"range": {
|
|
7
|
+
"end": {
|
|
8
|
+
"character": 15,
|
|
9
|
+
"line": 0
|
|
10
|
+
},
|
|
11
|
+
"start": {
|
|
12
|
+
"character": 15,
|
|
13
|
+
"line": 0
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"severity": 2,
|
|
17
|
+
"source": "archkeep"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"code": "analysisFailure",
|
|
21
|
+
"message": "Module boundaries were not fully checked here: parse error: Expression expected.. Imports this server could not read are not covered by the verdict below.",
|
|
22
|
+
"range": {
|
|
23
|
+
"end": {
|
|
24
|
+
"character": 21,
|
|
25
|
+
"line": 1
|
|
26
|
+
},
|
|
27
|
+
"start": {
|
|
28
|
+
"character": 21,
|
|
29
|
+
"line": 1
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"severity": 2,
|
|
33
|
+
"source": "archkeep"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"code": "analysisFailure",
|
|
37
|
+
"message": "Module boundaries were not fully checked here: TypeScript cannot resolve './thing' from 'libs/inner/broken.ts'. Imports this server could not read are not covered by the verdict below.",
|
|
38
|
+
"range": {
|
|
39
|
+
"end": {
|
|
40
|
+
"character": 22,
|
|
41
|
+
"line": 0
|
|
42
|
+
},
|
|
43
|
+
"start": {
|
|
44
|
+
"character": 20,
|
|
45
|
+
"line": 0
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"severity": 2,
|
|
49
|
+
"source": "archkeep"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"uri": "file:///tmp/archkeep-lsp-golden-fixture/libs/inner/broken.ts",
|
|
53
|
+
"version": 1
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"diagnostics": [
|
|
3
|
+
{
|
|
4
|
+
"code": "onlyTagsConstraintViolation",
|
|
5
|
+
"message": "A project tagged with \"zone:inner\" can only depend on libs tagged with \"zone:inner\"",
|
|
6
|
+
"range": {
|
|
7
|
+
"end": {
|
|
8
|
+
"character": 27,
|
|
9
|
+
"line": 3
|
|
10
|
+
},
|
|
11
|
+
"start": {
|
|
12
|
+
"character": 1,
|
|
13
|
+
"line": 3
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"severity": 1,
|
|
17
|
+
"source": "archkeep"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"uri": "file:///tmp/archkeep-lsp-golden-fixture/libs/inner/main.go",
|
|
21
|
+
"version": 1
|
|
22
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"registrations": [
|
|
3
|
+
{
|
|
4
|
+
"id": "archkeep/watched-files",
|
|
5
|
+
"method": "workspace/didChangeWatchedFiles",
|
|
6
|
+
"registerOptions": {
|
|
7
|
+
"watchers": [
|
|
8
|
+
{
|
|
9
|
+
"globPattern": "**/module-boundaries.config.mjs"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"globPattern": "**/tsconfig.base.json"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"globPattern": "**/project.json"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"globPattern": "**/nx.json"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"globPattern": "**/archkeep.json"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"globPattern": "**/package.json"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"globPattern": "**/module-federation.config.js"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"globPattern": "**/module-federation.config.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"globPattern": "**/go.mod"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"globPattern": "**/go.work"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"globPattern": "**/Cargo.toml"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"globPattern": "**/pyproject.toml"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"globPattern": "**/pom.xml"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"globPattern": "**/settings.gradle"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"globPattern": "**/settings.gradle.kts"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"globPattern": "**/build.gradle"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"globPattern": "**/build.gradle.kts"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"globPattern": "**/moon.yml"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"globPattern": "**/.moon/workspace.yml"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"globPattern": "**/.config/moon/workspace.yml"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
package/src/go-work.mjs
CHANGED
|
@@ -71,29 +71,6 @@ import { isAbsolute, posix, relative, sep } from "node:path";
|
|
|
71
71
|
|
|
72
72
|
import { projectOwning } from "./analysis/source-util.mjs";
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* What each drift finding means — one entry per `messageId` a finding can
|
|
76
|
-
* carry. `../report/sarif.mjs` derives its rule descriptors from this table,
|
|
77
|
-
* the same arrangement it has with `../rules/messages.mjs`, so a kind added
|
|
78
|
-
* here cannot be nameless in a code-scanning upload.
|
|
79
|
-
*/
|
|
80
|
-
export const GO_WORK_MESSAGES = Object.freeze({
|
|
81
|
-
goWorkMissingUse:
|
|
82
|
-
"A project's go.mod is not in go.work's use list: a developer's go build and gopls skip a " +
|
|
83
|
-
"module the Nx graph covers, so dev machines and CI select different module sets.",
|
|
84
|
-
goWorkStaleUse:
|
|
85
|
-
"A go.work use entry names a directory with no tracked go.mod: go commands fail on developer " +
|
|
86
|
-
"machines while CI, which never reads go.work, stays green.",
|
|
87
|
-
goWorkUnmodeledUse:
|
|
88
|
-
"A go.work use entry names a module the Nx graph does not model: it builds on developer " +
|
|
89
|
-
"machines while nx affected and the boundary check never see it.",
|
|
90
|
-
goWorkOutsideUse:
|
|
91
|
-
"A go.work use entry points outside the workspace: developer builds include a module no run " +
|
|
92
|
-
"over this workspace can cover.",
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
export const GO_WORK_MESSAGE_IDS = Object.freeze(Object.keys(GO_WORK_MESSAGES));
|
|
96
|
-
|
|
97
74
|
/** A parse failure that names its line, so the failure record is actionable. */
|
|
98
75
|
const parseError = (line, reason) => new Error(`go.work:${line}: ${reason}`);
|
|
99
76
|
|
|
@@ -299,6 +276,14 @@ function useDirectory(usePath, workspaceRoot) {
|
|
|
299
276
|
* - **`goWorkOutsideUse`** — a `use` entry above the workspace root. Whatever
|
|
300
277
|
* it builds, no run over this workspace can cover it.
|
|
301
278
|
*
|
|
279
|
+
* One of the FOUR finding families — PD-13 (2026-09-06) outcome (c): no
|
|
280
|
+
* Finding supertype exists, and the relationship pin lives on
|
|
281
|
+
* `./rules/index.mjs`'s `violationOf` header. These findings stay this
|
|
282
|
+
* family's own shape and fold into the one verdict lane as count keys into
|
|
283
|
+
* `verdictFor` (`./verdict.mjs`). The canonical statement is the
|
|
284
|
+
* "Finding — the unowned concept" section of
|
|
285
|
+
* `../../../docs/architecture/refactor/SEMANTIC-MODEL.md`.
|
|
286
|
+
*
|
|
302
287
|
* @param {{ uses: { path: string, line: number, column: number }[],
|
|
303
288
|
* workspaceRoot: string,
|
|
304
289
|
* projects: { name: string, root: string }[],
|
|
@@ -191,6 +191,21 @@ export function escapeIdentityField(value) {
|
|
|
191
191
|
* string (#627) — while a triple with no delimiter in any field spells
|
|
192
192
|
* exactly what earlier versions spelled, byte for byte.
|
|
193
193
|
*
|
|
194
|
+
* One other spelling of edge identity exists in this package, for a
|
|
195
|
+
* different medium, and the two are deliberately not unified:
|
|
196
|
+
* `edgeIdentityKey` (`../commands/diff.mjs`) joins the same triple with
|
|
197
|
+
* `\0` for in-memory set arithmetic in the diff family — `computeDiff`'s
|
|
198
|
+
* index maps, `trajectory`'s persistence sets — and is never stored. Different
|
|
199
|
+
* media, one law: each family keeps exactly one identity constructor, and
|
|
200
|
+
* consumers consume a constructor's output rather than re-deriving a second
|
|
201
|
+
* spelling of it (`../../../../docs/adr/0008-snapshot-identity-per-family.md`,
|
|
202
|
+
* INV-6 in `../../../../docs/architecture/refactor/INVARIANTS.md`). An edge
|
|
203
|
+
* reaches this module only as its raw triple, never as a pre-built string
|
|
204
|
+
* from either spelling (`evolutionBoundary` below refuses one), and a
|
|
205
|
+
* `\0`-joined diff key written into an event would give one edge two
|
|
206
|
+
* spellings inside the store — every identity string it already holds reads
|
|
207
|
+
* `source>target:type`.
|
|
208
|
+
*
|
|
194
209
|
* @param {{source: string, target: string, type: string}} edge
|
|
195
210
|
* @returns {string}
|
|
196
211
|
*/
|
|
@@ -101,8 +101,14 @@ function shortId(id) {
|
|
|
101
101
|
* silently manufacture duplicates — the failure shape this store exists to
|
|
102
102
|
* rule out.
|
|
103
103
|
*
|
|
104
|
+
* It then runs the same vocabulary rules the read path enforces (see
|
|
105
|
+
* `eventVocabularyViolation`): a record outside the event vocabulary — a
|
|
106
|
+
* wrong `schemaVersion`, a classification or disposition outside its
|
|
107
|
+
* vocabulary — is refused BEFORE it can be persisted, so `writeEvent` never
|
|
108
|
+
* writes a record `readEvents` would refuse and brick the store on (#738).
|
|
109
|
+
*
|
|
104
110
|
* @param {object} event The event to write.
|
|
105
|
-
* @throws {Error} naming the mismatch.
|
|
111
|
+
* @throws {Error} naming the mismatch or the violated vocabulary rule.
|
|
106
112
|
*/
|
|
107
113
|
function validateEventForWrite(event) {
|
|
108
114
|
if (typeof event !== "object" || event === null || Array.isArray(event)) {
|
|
@@ -130,12 +136,50 @@ function validateEventForWrite(event) {
|
|
|
130
136
|
"tuple {base, head, declarationDigest}",
|
|
131
137
|
);
|
|
132
138
|
}
|
|
139
|
+
const violation = eventVocabularyViolation(event);
|
|
140
|
+
if (violation !== null) {
|
|
141
|
+
throw new Error(`archkeep: refusing to write the evolution event: ${violation}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The three vocabulary rules every stored event must pass — the schema
|
|
147
|
+
* version, the classification subset, and the disposition — as ONE copy both
|
|
148
|
+
* sides of the store run: `validateEventForWrite` refuses a write outside
|
|
149
|
+
* them, `validateEventRecord` refuses a read of one, so a record `writeEvent`
|
|
150
|
+
* persists is exactly a record `readEvents` accepts and a refused write can
|
|
151
|
+
* never brick a later read (#738).
|
|
152
|
+
*
|
|
153
|
+
* @param {object} event The record to check.
|
|
154
|
+
* @returns {string|null} The violated rule, or `null` when the record is
|
|
155
|
+
* within the vocabulary.
|
|
156
|
+
*/
|
|
157
|
+
function eventVocabularyViolation(event) {
|
|
158
|
+
if (event.schemaVersion !== EVOLUTION_EVENT_SCHEMA_VERSION) {
|
|
159
|
+
return `schemaVersion ${JSON.stringify(
|
|
160
|
+
event.schemaVersion,
|
|
161
|
+
)} is not ${EVOLUTION_EVENT_SCHEMA_VERSION}`;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
!Array.isArray(event.classifications) ||
|
|
165
|
+
event.classifications.some((entry) => !EVENT_CLASSIFICATIONS.includes(entry))
|
|
166
|
+
) {
|
|
167
|
+
return `classifications must be a subset of [${EVENT_CLASSIFICATIONS.join(", ")}]`;
|
|
168
|
+
}
|
|
169
|
+
if (!EVENT_DISPOSITIONS.includes(event.disposition)) {
|
|
170
|
+
return `disposition ${JSON.stringify(
|
|
171
|
+
event.disposition,
|
|
172
|
+
)} is not one of [${EVENT_DISPOSITIONS.join(", ")}]`;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
133
175
|
}
|
|
134
176
|
|
|
135
177
|
/**
|
|
136
|
-
* The three
|
|
137
|
-
* classification subset, and the disposition
|
|
138
|
-
*
|
|
178
|
+
* The three vocabulary rules every stored event must pass — the schema
|
|
179
|
+
* version, the classification subset, and the disposition — the ONE copy
|
|
180
|
+
* `eventVocabularyViolation` owns, shared with the write path so a record
|
|
181
|
+
* `writeEvent` accepts is exactly a record `readEvents` accepts. Any other
|
|
182
|
+
* shape is a malformed store — thrown, never read as an event.
|
|
139
183
|
*
|
|
140
184
|
* @param {object} parsed The parsed record.
|
|
141
185
|
* @param {string} path The file it came from, for the error message.
|
|
@@ -147,28 +191,9 @@ function validateEventRecord(parsed, path) {
|
|
|
147
191
|
`archkeep: malformed evolution event '${path}': the record is not a JSON object`,
|
|
148
192
|
);
|
|
149
193
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
parsed.schemaVersion,
|
|
154
|
-
)} is not ${EVOLUTION_EVENT_SCHEMA_VERSION}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
if (
|
|
158
|
-
!Array.isArray(parsed.classifications) ||
|
|
159
|
-
parsed.classifications.some((entry) => !EVENT_CLASSIFICATIONS.includes(entry))
|
|
160
|
-
) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`archkeep: malformed evolution event '${path}': classifications must be a subset of ` +
|
|
163
|
-
`[${EVENT_CLASSIFICATIONS.join(", ")}]`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
if (!EVENT_DISPOSITIONS.includes(parsed.disposition)) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`archkeep: malformed evolution event '${path}': disposition ${JSON.stringify(
|
|
169
|
-
parsed.disposition,
|
|
170
|
-
)} is not one of [${EVENT_DISPOSITIONS.join(", ")}]`,
|
|
171
|
-
);
|
|
194
|
+
const violation = eventVocabularyViolation(parsed);
|
|
195
|
+
if (violation !== null) {
|
|
196
|
+
throw new Error(`archkeep: malformed evolution event '${path}': ${violation}`);
|
|
172
197
|
}
|
|
173
198
|
}
|
|
174
199
|
|
|
@@ -182,9 +207,11 @@ function validateEventRecord(parsed, path) {
|
|
|
182
207
|
* against `io.root`. A missing directory is an empty store: it is created
|
|
183
208
|
* before the scan, so the first event in a fresh store lands at `0000`.
|
|
184
209
|
*
|
|
185
|
-
* The event's `id`/`dedupeKey` must match the canonical tuple
|
|
186
|
-
*
|
|
187
|
-
* lies about its content
|
|
210
|
+
* The event's `id`/`dedupeKey` must match the canonical tuple and the record
|
|
211
|
+
* must sit within the event vocabulary (see `validateEventForWrite`) — a
|
|
212
|
+
* caller cannot persist a record whose identity lies about its content, or
|
|
213
|
+
* whose vocabulary `readEvents` would refuse: the store never gains a file
|
|
214
|
+
* that would brick its own reads.
|
|
188
215
|
*
|
|
189
216
|
* @param {string} dir Absolute or relative path to the event store directory.
|
|
190
217
|
* @param {object} event The EvolutionEvent to append.
|
|
@@ -200,8 +227,9 @@ function validateEventRecord(parsed, path) {
|
|
|
200
227
|
* against and is REQUIRED for a write.
|
|
201
228
|
* @returns {{id: string, duplicate: boolean}} The event id, and whether this
|
|
202
229
|
* call wrote nothing because the event already existed.
|
|
203
|
-
* @throws {Error} on
|
|
204
|
-
* store, a containment violation, or a `wx`
|
|
230
|
+
* @throws {Error} on an event that fails validation (identity or vocabulary),
|
|
231
|
+
* an unreadable or malformed store, a containment violation, or a `wx`
|
|
232
|
+
* refusal.
|
|
205
233
|
*/
|
|
206
234
|
export function writeEvent(dir, event, io = {}) {
|
|
207
235
|
validateEventForWrite(event);
|