@ecoma-io/archkeep 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +262 -0
  3. package/cli.mjs +2792 -0
  4. package/index.mjs +85 -0
  5. package/lsp.mjs +81 -0
  6. package/nx.mjs +24 -0
  7. package/package.json +81 -0
  8. package/presets/clean-architecture.json +78 -0
  9. package/presets/ddd-bounded-contexts.json +88 -0
  10. package/presets/hexagonal.json +68 -0
  11. package/presets/layered.json +92 -0
  12. package/presets/modular-monolith.json +85 -0
  13. package/presets/vertical-slice.json +68 -0
  14. package/src/analysis/analyze.mjs +218 -0
  15. package/src/analysis/contract.md +259 -0
  16. package/src/analysis/go.mjs +414 -0
  17. package/src/analysis/manifest-util.mjs +68 -0
  18. package/src/analysis/python.mjs +1266 -0
  19. package/src/analysis/registry.mjs +74 -0
  20. package/src/analysis/rust.mjs +674 -0
  21. package/src/analysis/source-util.mjs +230 -0
  22. package/src/analysis/typescript.mjs +1034 -0
  23. package/src/analysis/vue.mjs +156 -0
  24. package/src/architecture-intent/intent-fingerprint.mjs +29 -0
  25. package/src/architecture-intent/judge.mjs +539 -0
  26. package/src/architecture-intent/model.mjs +703 -0
  27. package/src/architecture-intent/selectors.mjs +170 -0
  28. package/src/canonical.mjs +48 -0
  29. package/src/commands/README.md +266 -0
  30. package/src/commands/adr.mjs +248 -0
  31. package/src/commands/check.mjs +989 -0
  32. package/src/commands/context-command.mjs +212 -0
  33. package/src/commands/context.mjs +790 -0
  34. package/src/commands/custom-rules.mjs +428 -0
  35. package/src/commands/debt.mjs +218 -0
  36. package/src/commands/diff.mjs +523 -0
  37. package/src/commands/discover.mjs +159 -0
  38. package/src/commands/drift.mjs +473 -0
  39. package/src/commands/edge-constraints.mjs +355 -0
  40. package/src/commands/explain.mjs +359 -0
  41. package/src/commands/fitness.mjs +226 -0
  42. package/src/commands/graph.mjs +297 -0
  43. package/src/commands/health.mjs +213 -0
  44. package/src/commands/history.mjs +614 -0
  45. package/src/commands/impact.mjs +226 -0
  46. package/src/commands/plan-context-command.mjs +496 -0
  47. package/src/commands/policy.mjs +138 -0
  48. package/src/commands/provenance-command.mjs +352 -0
  49. package/src/commands/provenance.mjs +159 -0
  50. package/src/commands/reconcile.mjs +219 -0
  51. package/src/commands/report.mjs +553 -0
  52. package/src/commands/snapshot-meta.mjs +107 -0
  53. package/src/commands/waivers.mjs +240 -0
  54. package/src/config.mjs +1308 -0
  55. package/src/containment.mjs +234 -0
  56. package/src/custom-rules/evidence.mjs +340 -0
  57. package/src/custom-rules/host.mjs +1023 -0
  58. package/src/custom-rules/values.mjs +43 -0
  59. package/src/entry-point.mjs +55 -0
  60. package/src/errors.mjs +36 -0
  61. package/src/eslint-config.mjs +542 -0
  62. package/src/go-work.mjs +394 -0
  63. package/src/governance/adr-registry.mjs +539 -0
  64. package/src/governance/clock.mjs +69 -0
  65. package/src/governance/debt-ledger.mjs +274 -0
  66. package/src/governance/discovery-proposal.mjs +423 -0
  67. package/src/governance/fitness-registry.mjs +504 -0
  68. package/src/governance/fitness-rules.mjs +668 -0
  69. package/src/governance/metrics.mjs +392 -0
  70. package/src/governance/preset-fingerprints.json +16 -0
  71. package/src/governance/profile-registry.mjs +366 -0
  72. package/src/governance/provenance-record.mjs +177 -0
  73. package/src/governance/reconcile-candidates.mjs +301 -0
  74. package/src/governance/reconcile-score.mjs +503 -0
  75. package/src/governance/row-schema.mjs +208 -0
  76. package/src/governance/verdict.mjs +127 -0
  77. package/src/governance/waiver.mjs +105 -0
  78. package/src/graph/create-dependencies.mjs +96 -0
  79. package/src/intent/intent-manifest.json +347 -0
  80. package/src/intent/mask-non-code.mjs +640 -0
  81. package/src/lsp/boundary-config.mjs +225 -0
  82. package/src/lsp/diagnose.mjs +202 -0
  83. package/src/lsp/diagnostics.mjs +241 -0
  84. package/src/lsp/protocol.mjs +215 -0
  85. package/src/lsp/server.mjs +922 -0
  86. package/src/lsp/workspace-index.mjs +891 -0
  87. package/src/nx-json.mjs +95 -0
  88. package/src/options.mjs +611 -0
  89. package/src/process.mjs +91 -0
  90. package/src/providers/moon.mjs +733 -0
  91. package/src/providers/native/README.md +204 -0
  92. package/src/providers/native/coverage.mjs +74 -0
  93. package/src/providers/native/differential.fixtures.mjs +1277 -0
  94. package/src/providers/native/discover.mjs +431 -0
  95. package/src/providers/native/graph.mjs +234 -0
  96. package/src/providers/native/index.mjs +152 -0
  97. package/src/providers/native/model.mjs +755 -0
  98. package/src/providers/nx.mjs +178 -0
  99. package/src/report/README.md +89 -0
  100. package/src/report/adr-text.mjs +129 -0
  101. package/src/report/context-text.mjs +109 -0
  102. package/src/report/debt-text.mjs +105 -0
  103. package/src/report/diff-text.mjs +219 -0
  104. package/src/report/discover-text.mjs +186 -0
  105. package/src/report/drift-text.mjs +194 -0
  106. package/src/report/envelope-shape.mjs +161 -0
  107. package/src/report/evidence.mjs +157 -0
  108. package/src/report/explain-text.mjs +159 -0
  109. package/src/report/graph-text.mjs +116 -0
  110. package/src/report/health-text.mjs +123 -0
  111. package/src/report/history-text.mjs +204 -0
  112. package/src/report/impact-text.mjs +128 -0
  113. package/src/report/json.mjs +173 -0
  114. package/src/report/plan-context-text.mjs +159 -0
  115. package/src/report/provenance-text.mjs +78 -0
  116. package/src/report/reconcile-text.mjs +159 -0
  117. package/src/report/report-text.mjs +264 -0
  118. package/src/report/sarif.mjs +953 -0
  119. package/src/report/text.mjs +823 -0
  120. package/src/report/waivers-text.mjs +100 -0
  121. package/src/rules/README.md +123 -0
  122. package/src/rules/index.mjs +962 -0
  123. package/src/rules/match.mjs +1708 -0
  124. package/src/rules/messages.mjs +73 -0
  125. package/src/rules/reachability.mjs +224 -0
  126. package/src/rules/specifiers.mjs +300 -0
  127. package/src/rules/tags.mjs +238 -0
  128. package/src/rules/topology.mjs +333 -0
  129. package/src/tsconfig-paths.mjs +237 -0
  130. package/src/verdict.mjs +145 -0
  131. package/src/workspace.mjs +580 -0
@@ -0,0 +1,204 @@
1
+ # `src/providers/native/` — the `archkeep.json` project model
2
+
3
+ A `ProjectModelProvider` for a workspace with no `nx.json` and no Nx installed:
4
+ `model.mjs` loads and validates `archkeep.json`, `discover.mjs` resolves the
5
+ project list declared∪inferred from it, `coverage.mjs` judges which tracked,
6
+ analyzable files none of those projects own, and `graph.mjs` reduces both into
7
+ the same `{nodes, dependencies}` shape `../nx.mjs`'s Nx path builds from a real
8
+ `nx graph`. `index.mjs` composes the four into `nativeProvider`, the object
9
+ `../../../cli.mjs` selects when a workspace root carries `archkeep.json` rather than
10
+ `nx.json`.
11
+
12
+ Every module here is reachable with no `nx` package resolvable and no
13
+ TypeScript compiler loaded at import time — `index.mjs`'s header documents the
14
+ one import (`../../analysis/analyze.mjs`, for `languageOf` in `coverage.mjs`)
15
+ that does load `typescript`, and why that one is fine: `typescript` is a
16
+ mandatory dependency of this package, unlike `nx`, which is an optional peer
17
+ that a workspace running the native path has no reason to install.
18
+
19
+ ## Declared limits
20
+
21
+ Six things this provider does not attempt, each because reaching further would
22
+ either duplicate a fact this package derives elsewhere or answer a question
23
+ nobody asked it:
24
+
25
+ 1. **Git is required.** Project discovery and coverage both walk the tracked
26
+ file list `../../workspace.mjs`'s `listTrackedFiles` returns from `git
27
+ ls-files`; a tree with no git repository has no file list to discover
28
+ projects or judge coverage from, and `../../../cli.mjs` reports that loudly
29
+ (exit 3) rather than reading an empty list as an empty, clean workspace.
30
+ 2. **No `externalNodes` are emitted.** `../../rules/specifiers.mjs`'s
31
+ `findTransitiveExternalDependencies` already treats an absent
32
+ `graph.externalNodes` as "none", and `../../rules/index.mjs`'s
33
+ `externalNodeFor` synthesises a node for an external target on demand — a
34
+ provider declaring npm-registry bookkeeping here would be a second source of
35
+ truth for a fact this package already derives from the analysis records.
36
+ 3. **`data.targets` names, never runs.** `archkeep.json`'s declared
37
+ projects may carry a `targets` field (`model.mjs` validates its shape) and
38
+ `graph.mjs` synthesises `{[name]: {executor: "archkeep:declared"}}` for each
39
+ declared name, which is enough for `../../rules/topology.mjs`'s
40
+ `hasBuildExecutor` to read the project as buildable and for
41
+ `enforceBuildableLibDependency` to run on a native tree. What it does not
42
+ do is anything an executor or a config would: this provider has no build
43
+ system to ask, so `"archkeep:declared"` is a fixed placeholder string, never
44
+ a real executor identity a rule could branch on.
45
+ 4. **`workspaceLayout` is taken from the model, or absent — never inferred**
46
+ from directory names. A workspace that wants `appsDir`/`libsDir` judged
47
+ states them in `archkeep.json`; one that states neither gets the rule
48
+ engine's own default (`../../rules/index.mjs`), the same default the Nx path
49
+ falls back to when Nx's own `workspaceLayout` is unset.
50
+ 5. **Tag spellings are not validated against any vocabulary.** `archkeep.json`
51
+ can declare or infer any string as a tag; whether `layer:adapter` is a tag
52
+ this workspace's boundary config actually constrains is a `module-boundaries.config.mjs`
53
+ question, judged by `../../rules/`, never by this provider.
54
+ 6. **No tag is ever inferred from `package.json`.** Nx's own built-in
55
+ js/package-json plugin synthesises `npm:public`/`npm:private` from a
56
+ directory's `package.json` `private` field the moment that file exists,
57
+ with no opt-in — this provider does not replicate that inference, by
58
+ design: `discover.mjs` reads a `package.json` only for its `name`, never
59
+ its `private` field, so a native tree gets no tag from `package.json` it did
60
+ not declare or that a `projectRules` row did not add explicitly. The silent
61
+ direction this refusal creates: a workspace migrating from Nx to
62
+ `archkeep.json`, carrying `depConstraints` rows keyed on `npm:public` or
63
+ `npm:private`, will see those constraints stop matching anything the moment
64
+ the tree drops `nx.json` — not because the projects changed, but because
65
+ the tag that used to appear automatically no longer does. The fix is one
66
+ line per affected project: add the matching tag to that project's
67
+ `projects.declared` row or to a `projectRules` row that matches it, the
68
+ same way `differential.fixtures.mjs`'s `composite` fixture states
69
+ `npm:private` on `pkgnamed` explicitly rather than relying on inference
70
+ that does not happen here.
71
+
72
+ ## Two failure classes, both loud
73
+
74
+ A **model defect** — a `projectRules` row matching no project, a declared root
75
+ with no tracked file, two projects resolving to the same name, a stale
76
+ `coverage.exempt` waiver, a workspace describing zero projects — throws out of
77
+ `discover()`. `../../../cli.mjs` turns that into exit 3: the workspace description
78
+ itself does not hold up against the tree, so nothing downstream of discovery
79
+ can be trusted enough to try.
80
+
81
+ An **unclaimed file** — a tracked, analyzable file no discovered project owns —
82
+ is not a defect in the model; it is a coverage hole in the _tree_, and
83
+ `coverage.mjs` reports it as a whole-file `fileFailure`
84
+ (`../../analysis/source-util.mjs`) riding in `discover()`'s return value rather
85
+ than thrown. `../../../cli.mjs` merges it into the same `failures` array a
86
+ language analyzer's own unreadable-file failure already occupies, so
87
+ `../../report/text.mjs`'s `formatFailures` and the exit-3 `unchecked` count both
88
+ need no native-specific branch to report it.
89
+
90
+ ## What proves this provider against a tree it was not tested on
91
+
92
+ Every test above this line runs against a fixture this package's own tests
93
+ built — an in-memory tree, or (`differential.integration.test.mjs`'s Oracle 1)
94
+ a synthetic tree constructed under `packages/archkeep/` itself, close enough to
95
+ resolve this repository's own `node_modules` for the Nx side of that
96
+ comparison. Neither is what a real consumer does: `pnpm pack`, `pnpm install`
97
+ the tarball into a workspace this repository never built, and run the bin
98
+ entries as installed.
99
+
100
+ `differential.integration.test.mjs`'s Oracle 1 covers seven axes across three
101
+ fixture pairs, not one pair per axis — the cost driver is the real `nx graph
102
+ --file=` spawn, so the axes are packed to hold the file to three spawns total.
103
+ The shared machinery it drives — the fixture builders, `diffGraphs`, `LEDGER`,
104
+ and the breach checks — lives in `./differential.fixtures.mjs`, a plain module
105
+ with no `vitest` import, so a future differential can reuse it without running
106
+ this suite's own cases as a side effect of the import.
107
+
108
+ `simple` (unchanged) stays the minimal, single-violation pair a failure is
109
+ diagnosable from without reading a diff row. `composite` packs six
110
+ identity/topology axes into one tree — name precedence (a declared name, a
111
+ `package.json` name, and a bare `basename(root)` fallback), project type (the
112
+ `-e2e` suffix rule), the workspace root itself as a project (`root: ""`), tag
113
+ union across all THREE sources at once on one project (a declared row, a
114
+ `projectRules` row, and its own `project.json`, all on `parent`), implicit
115
+ dependencies (a literal project name spelled on a declared row, a literal name
116
+ spelled in `project.json`, and a `tag:`-pattern entry), and a project nested
117
+ inside another project's own directory — and asserts each axis by name via
118
+ `diffGraphs`, a per-node/per-edge/per-verdict comparison, rather than one
119
+ `deepEqual` over two whole graphs. `layout` isolates `workspaceLayout` alone,
120
+ because it is workspace-global rather than per-project and folding it into
121
+ `composite` would move every other axis's expected shape at once. The two
122
+ providers now AGREE on this pair — both report the same two violations —
123
+ because `readProjectGraph` in `../nx.mjs` merges `nx.json`'s own
124
+ `workspaceLayout` back onto the graph `nx graph --file=` returns (that
125
+ function's own header), so the Nx side reads the identical non-default
126
+ `libsDir`/`appsDir` the native side already read from `archkeep.json`. `LEDGER`
127
+ is empty and frozen for this pair — the one row it ever carried
128
+ (https://github.com/ecoma-io/archkeep/issues/31) retired with that merge — and
129
+ the ledger's stale-row check (`classifyDifferences`'s `stale` return,
130
+ asserted in `differential.integration.test.mjs`'s "the ledger's stale-row
131
+ rule") is what keeps that claim honest: a row reintroduced here without a
132
+ matching difference to explain would fail the suite rather than sit unread.
133
+
134
+ Only ONE direction is ledgerable, and it is enforced structurally rather than
135
+ by convention: a `LedgerRow` now carries a `direction` field
136
+ (`differential.fixtures.mjs`'s `LEDGER_DIRECTIONS`), and its only member is
137
+ `"native-only"` — native reporting something Nx does not. `classifyDifferences`
138
+ throws, unconditionally, on any verdict-count difference where Nx's count
139
+ exceeds native's, before it ever checks whether a `LEDGER` row matches that
140
+ difference's subject and field — no row, however its `reason` is worded, gets
141
+ a vote on that direction. `emptyVerdictBreaches` catches the aggregate shape of
142
+ the same failure (an engine reporting zero violations on a pair built to
143
+ contain one) and `perMessageBreaches` catches it per `messageId` even when
144
+ neither side's total is literally zero (`{nx: 3, native: 1}` on one rule is a
145
+ breach exactly as much as `{nx: 1, native: 0}` is); both take no `ledger`
146
+ parameter at all, so the suppression is not just untested, it has nowhere to be
147
+ written.
148
+
149
+ `../../../../../scripts/verify-package.mjs` closes that gap for both providers,
150
+ not only Nx. It packs the real tarball once and installs it into TWO throwaway
151
+ consumer workspaces — one with `nx.json`, one with `archkeep.json` and no `nx`
152
+ package requested at all — then runs the same three questions against both: the
153
+ checker exits 0 on a clean tree and states what it inspected, exits 1 on a
154
+ violating one naming the rule and the site, and the language server answers
155
+ `initialize` when launched through the symlinked path an installed plugin is
156
+ launched by. It also asserts `nx` genuinely does not resolve in the native
157
+ consumer's `node_modules` — the optional-peer claim (`../../../AGENTS.md`'s
158
+ "Nx is a peer dependency but an optional one") checked against a real install,
159
+ not only against the manifest's `peerDependenciesMeta`. It runs in CI on every
160
+ pull request (`../../../../../.github/workflows/ci.yml`'s "Prove the packed
161
+ artifact works outside this workspace" step already invoked it for the Nx
162
+ path; the native path rides the same invocation, no separate CI step needed)
163
+ and again in the release lane before `npm publish`.
164
+
165
+ `verify-package.mjs` still proves the provider only against fixtures someone
166
+ here built, even installed for real — the checker exits the right code on a
167
+ tree this repository authored to exit that code. Two more things close the gap
168
+ `verify-package.mjs` cannot: source nobody here wrote, and this repository's
169
+ own tree.
170
+
171
+ `../../../../../scripts/differential-real-trees.mjs`'s native leg answers "does
172
+ this provider's discovery-plus-graph pipeline reproduce a real tool's own
173
+ answer, on a tree neither of us built." It derives a `archkeep.json`-equivalent
174
+ model mechanically from a real Nx workspace's own `nx graph --file=` output
175
+ (`deriveNativeModel`, one `projects.declared` row per node) — never
176
+ hand-authored, so the model is a measurement of the real tree rather than a
177
+ fixture this package's author already knew the answer to — runs
178
+ `nativeProvider.discover`/`buildGraph` over it, and compares the node set, edge
179
+ set, and rule verdicts against the same tree's real Nx-graph-based run,
180
+ classified through the differential's existing ledger. The first real run
181
+ (2026-08-12, against `code-pushup` at its pinned commit) found a populated
182
+ ledger, not an empty one — the expected outcome, argued in that file's own
183
+ `LEDGER` doc comment — and every row traces to a real, investigated cause
184
+ rather than an unknown: Nx's own root-project spelling (`root: "."`) needing
185
+ renormalisation before it reaches `archkeep.json`'s dialect (which rejects that
186
+ exact spelling by name), and two narrower, pre-existing gaps in the shared
187
+ analysis pipeline that this leg surfaced but does not own fixing (a root-"."
188
+ project's own files going unowned by `createWorkspace`, and a TypeScript
189
+ import-type query the analyzer's AST walk does not visit) — both logged in
190
+ `LEDGER` with the reason, and both out of scope for this provider to change:
191
+ either fix moves what every consumer's boundary check reports on an unchanged
192
+ workspace, which is a breaking change on its own, argued separately from this
193
+ provider's own correctness.
194
+
195
+ `../../../../../.github/workflows/ci.yml`'s "Check this repository's own
196
+ module boundaries (native provider)" step answers the other half: does this
197
+ provider meet THIS repository's own real source, under a tag vocabulary
198
+ (`type:package`, `scope:nx`) nothing under `src/` has any knowledge of — the
199
+ same argument the Nx-based self-check step just above it makes for that path.
200
+ That step's own comment carries the full account of why this repository
201
+ cannot carry a root `archkeep.json` alongside its own `nx.json`, and how the
202
+ throwaway copy the step runs against is built and proved to share this
203
+ repository's real `module-boundaries.config.mjs` — read it there rather than
204
+ here, so the mechanism has one description instead of two that could drift.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Coverage judgment for a native workspace: which tracked, analyzable files
3
+ * no discovered project owns.
4
+ *
5
+ * Nx has no equivalent question — every file Nx's own graph omits is simply
6
+ * outside any project as far as `nx graph` is concerned, and this package's
7
+ * Nx path (`../nx.mjs`, `../../workspace.mjs`) has no unclaimed-file check of
8
+ * its own: both compute imports and violations only for files a project
9
+ * already claims. A native workspace gets no `nx graph` to lean on, so `archkeep.json` states
10
+ * the projects itself — and a file that fits none of them is exactly the
11
+ * silent hole `../../../../../AGENTS.md`'s invariant is about: it is analyzed by
12
+ * nothing, judged by nothing, and an empty violation list would read
13
+ * identically to a file that really was clean.
14
+ *
15
+ * An unclaimed file becomes a whole-file `fileFailure`
16
+ * (`../../analysis/source-util.mjs`) — the SAME record shape a language
17
+ * analyzer produces for a file it could not read, so `../../../cli.mjs` and
18
+ * `../../report/text.mjs`'s `formatFailures` need no native-specific branch
19
+ * to report it: `formatFailures` already splits whole-file holes from
20
+ * site-level ones (`isWholeFileFailure`, `failure.line === null`).
21
+ */
22
+ import { languageOf } from "../../analysis/registry.mjs";
23
+ import { fileFailure } from "../../analysis/source-util.mjs";
24
+ import { matchesGlob } from "./model.mjs";
25
+
26
+ /**
27
+ * Judges coverage over a discovered native workspace.
28
+ *
29
+ * `files` and `claimed` ride alongside `unclaimed`/`exempted`/`stale` rather
30
+ * than replacing any of them — additive, so an existing caller destructuring
31
+ * this object keeps working unchanged. They exist for the same reason
32
+ * `../../../AGENTS.md`'s "What is a stub" section gives `cli.mjs check` its
33
+ * own inspected-file count: "no violations" is a claim about coverage too,
34
+ * and a caller that wants to state what it inspected (not just what it
35
+ * found) needs the denominator, not only the failing files.
36
+ *
37
+ * @param {{files: string[], projectOf: (file: string) => string|undefined, exempt: {path: string, reason: string}[]}} args
38
+ * @returns {{
39
+ * files: number,
40
+ * claimed: number,
41
+ * unclaimed: string[],
42
+ * exempted: string[],
43
+ * stale: {path: string, reason: string}[],
44
+ * failures: {sourceFile: string, line: null, column: null, reason: string}[],
45
+ * }}
46
+ */
47
+ export function judgeCoverage({ files, projectOf, exempt }) {
48
+ const analyzable = files.filter((file) => languageOf(file) !== null);
49
+ const unowned = analyzable.filter((file) => projectOf(file) === undefined);
50
+
51
+ const matchesFor = exempt.map((row) => ({
52
+ row,
53
+ files: unowned.filter((file) => matchesGlob(file, row.path)),
54
+ }));
55
+ const exemptedFiles = new Set(matchesFor.flatMap((m) => m.files));
56
+ const unclaimed = unowned.filter((file) => !exemptedFiles.has(file));
57
+ const stale = matchesFor.filter((m) => m.files.length === 0).map((m) => m.row);
58
+
59
+ return {
60
+ files: analyzable.length,
61
+ claimed: analyzable.length - unowned.length,
62
+ unclaimed,
63
+ exempted: [...exemptedFiles],
64
+ stale,
65
+ failures: unclaimed.map((file) =>
66
+ fileFailure(
67
+ file,
68
+ "is not owned by any project declared or inferred from archkeep.json — every " +
69
+ "analyzable tracked file must belong to exactly one project, or be named in " +
70
+ "archkeep.json's coverage.exempt with a reason",
71
+ ),
72
+ ),
73
+ };
74
+ }