@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.
- package/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preamble every command shares: which workspace, which provider, which
|
|
3
|
+
* files, and what a whole-tree read of them found. `../../cli.mjs`'s `check`
|
|
4
|
+
* built exactly this before `--format json` existed; this module is that
|
|
5
|
+
* preamble lifted out so a second command can reuse it rather than reimplement
|
|
6
|
+
* it a rule at a time. What is deliberately NOT here: the boundary policy
|
|
7
|
+
* (`../config.mjs`'s `loadBoundaryConfig`/`loadBoundaryConfigFile`), the
|
|
8
|
+
* go.work drift check, and the tsconfig paths hygiene check — all three are
|
|
9
|
+
* `check`'s own concerns, judged from what this module hands back, not part of
|
|
10
|
+
* establishing "which tree, which files, what did reading them find" that a
|
|
11
|
+
* later command (`graph`, `explain`, `impact`, `diff`) needs identically.
|
|
12
|
+
*
|
|
13
|
+
* Everything below is a straight extraction: no branch here changes what
|
|
14
|
+
* `check` decided before this module existed, because the byte-for-byte
|
|
15
|
+
* unchanged verdict on an unchanged tree is itself part of the contract
|
|
16
|
+
* (`../../../../AGENTS.md`, "a change to what is reported on an unchanged
|
|
17
|
+
* workspace is a breaking change").
|
|
18
|
+
*
|
|
19
|
+
* `../providers/native/index.mjs`'s header states that module imports nothing
|
|
20
|
+
* from `../workspace.mjs`; this module is the one place both a provider and
|
|
21
|
+
* `../workspace.mjs` are composed, which is why it lives beside the commands
|
|
22
|
+
* that need the composition rather than inside either provider
|
|
23
|
+
* (`./README.md`).
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { containmentViolation } from "../containment.mjs";
|
|
29
|
+
import { languageOf } from "../analysis/registry.mjs";
|
|
30
|
+
import { pythonUnmodelledFailures } from "../analysis/python.mjs";
|
|
31
|
+
import { fileFailure } from "../analysis/source-util.mjs";
|
|
32
|
+
import {
|
|
33
|
+
DEFAULT_OPTIONS,
|
|
34
|
+
NX_CONFIG_FILE,
|
|
35
|
+
pluginIsRegistered,
|
|
36
|
+
readMoonOptions,
|
|
37
|
+
readPluginOptions,
|
|
38
|
+
} from "../options.mjs";
|
|
39
|
+
import { readProjectGraph } from "../providers/nx.mjs";
|
|
40
|
+
import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
41
|
+
import {
|
|
42
|
+
MOON_DIR,
|
|
43
|
+
MOON_ALT_DIR,
|
|
44
|
+
mergeImportEdges,
|
|
45
|
+
moonMarkerAt,
|
|
46
|
+
moonProvider,
|
|
47
|
+
} from "../providers/moon.mjs";
|
|
48
|
+
import { nativeProvider } from "../providers/native/index.mjs";
|
|
49
|
+
import {
|
|
50
|
+
analyzeWorkspace,
|
|
51
|
+
annotateMFERemotes,
|
|
52
|
+
annotatePackageFacts,
|
|
53
|
+
createWorkspace,
|
|
54
|
+
findWorkspaceRoot,
|
|
55
|
+
listTrackedFiles,
|
|
56
|
+
polyglotManifests,
|
|
57
|
+
selectFiles,
|
|
58
|
+
} from "../workspace.mjs";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Workspace-relative read from `root` — the same default `createWorkspace`
|
|
62
|
+
* builds when no reader is injected (`../workspace.mjs`), duplicated here
|
|
63
|
+
* rather than imported because it is needed BEFORE a `Workspace` exists to
|
|
64
|
+
* read from: `nativeProvider.discover` needs one to load `archkeep.json`
|
|
65
|
+
* itself.
|
|
66
|
+
*
|
|
67
|
+
* Carries the same containment rule as that default reader: a tracked symlink
|
|
68
|
+
* whose realpath leaves the workspace would hand the reader outside bytes as
|
|
69
|
+
* the workspace's own declaration — a model file read that way is a whole
|
|
70
|
+
* verdict built on attacker-controlled input, reported clean. Refusing (null)
|
|
71
|
+
* makes the read a loud "cannot load" failure rather than a silent
|
|
72
|
+
* read-and-judge (`../containment.mjs`, the G-10 closure).
|
|
73
|
+
*
|
|
74
|
+
* @param {string} root
|
|
75
|
+
* @returns {(path: string) => string|null}
|
|
76
|
+
*/
|
|
77
|
+
function readWorkspaceRoot(root) {
|
|
78
|
+
return (path) => {
|
|
79
|
+
const abs = join(root, path);
|
|
80
|
+
if (containmentViolation(root, abs) !== null) return null;
|
|
81
|
+
try {
|
|
82
|
+
return readFileSync(abs, "utf8");
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The real-filesystem reader `pluginIsRegistered` (`../options.mjs`) gets
|
|
91
|
+
* when no seam overrides it. Unlike `readWorkspaceRoot` above, this one takes
|
|
92
|
+
* an ALREADY-ABSOLUTE path: `pluginIsRegistered` builds
|
|
93
|
+
* `${workspaceRoot}/${NX_CONFIG_FILE}` itself before calling its reader, so a
|
|
94
|
+
* `join(root, path)`-style reader would double the root onto an already-full
|
|
95
|
+
* path.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} path
|
|
98
|
+
* @returns {string|null}
|
|
99
|
+
*/
|
|
100
|
+
function readFileAbsolute(path) {
|
|
101
|
+
try {
|
|
102
|
+
return readFileSync(path, "utf8");
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Which Moon directory marks `root` — or none. Presence facts only; see
|
|
110
|
+
* `requireSingleProjectModel` below for the one-decision gate.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} root
|
|
113
|
+
* @returns {{hasNx: boolean, hasNative: boolean, hasMoon: boolean}}
|
|
114
|
+
*/
|
|
115
|
+
export function markersAt(root) {
|
|
116
|
+
return {
|
|
117
|
+
hasNx: existsSync(join(root, NX_CONFIG_FILE)),
|
|
118
|
+
hasNative: existsSync(join(root, ARCHKEEP_MODEL_FILE)),
|
|
119
|
+
hasMoon: moonMarkerAt(root) !== null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The one gate deciding whether `root` may be judged at all: more than ONE
|
|
125
|
+
* project-model marker present is refused, naming what conflicts.
|
|
126
|
+
*
|
|
127
|
+
* Every entry point that picks a provider must answer this identically —
|
|
128
|
+
* `resolveCommandContext` below reads it before any command runs, and
|
|
129
|
+
* `../lsp/workspace-index.mjs`'s index build reads it before choosing a
|
|
130
|
+
* branch. A second copy of the condition was exactly how the faces drifted
|
|
131
|
+
* apart once: the CLI refused a tree carrying a Moon directory beside
|
|
132
|
+
* `nx.json`/`archkeep.json` while the editor indexed it anyway — a clean
|
|
133
|
+
* diagnostic list over a tree nobody agreed could be judged (#223's silent
|
|
134
|
+
* shape, one level up). Moon-versus-Moon coexistence (`.moon/` AND
|
|
135
|
+
* `.config/moon/`) is refused inside `../providers/moon.mjs`'s
|
|
136
|
+
* `moonMarkerAt`, which this gate calls first; the cross-family pairs are
|
|
137
|
+
* refused here, all in the same terms: which model to judge against is a
|
|
138
|
+
* decision nobody made, not one this tool can make for them.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} root
|
|
141
|
+
* @param {{exists?: (path: string) => boolean}} [io] Injectable existence
|
|
142
|
+
* test (absolute paths), so a test drives this without a filesystem.
|
|
143
|
+
* @returns {{hasNx: boolean, hasNative: boolean, moonMarker: string|null}}
|
|
144
|
+
* The facts a provider choice needs; `moonMarker` names whichever Moon
|
|
145
|
+
* directory is present, `null` when neither spelling is.
|
|
146
|
+
* @throws {Error} when more than one marker is present.
|
|
147
|
+
*/
|
|
148
|
+
export function requireSingleProjectModel(root, { exists = existsSync } = {}) {
|
|
149
|
+
const moonMarker = moonMarkerAt(root, { exists });
|
|
150
|
+
const hasNx = exists(join(root, NX_CONFIG_FILE));
|
|
151
|
+
const hasNative = exists(join(root, ARCHKEEP_MODEL_FILE));
|
|
152
|
+
const refusal = (a, b) =>
|
|
153
|
+
new Error(
|
|
154
|
+
`archkeep: ${root} declares both ${a} and ${b} — this tool judges a workspace ` +
|
|
155
|
+
`against exactly one project model, and a tree carrying both is a decision nobody made ` +
|
|
156
|
+
`rather than one this tool can make for them. Remove whichever one is not the ` +
|
|
157
|
+
`workspace's real source of truth for projects and tags.`,
|
|
158
|
+
);
|
|
159
|
+
if (moonMarker !== null && hasNx) throw refusal(moonMarker, NX_CONFIG_FILE);
|
|
160
|
+
if (moonMarker !== null && hasNative) throw refusal(moonMarker, ARCHKEEP_MODEL_FILE);
|
|
161
|
+
if (hasNx && hasNative) throw refusal(NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE);
|
|
162
|
+
return { hasNx, hasNative, moonMarker };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Every marker a workspace root may be recognised by, as one list.
|
|
167
|
+
*
|
|
168
|
+
* Exported because `cli.mjs` walks for a root a second time — `--help` and
|
|
169
|
+
* `adr` need one before a `CommandContext` exists — and that walk kept its own
|
|
170
|
+
* copy of this list. The copy went stale: it named `nx.json` and `archkeep.json`
|
|
171
|
+
* only, so `adr` answered "no workspace root" on every Moon tree while naming
|
|
172
|
+
* `.moon` in the very message, and `--help` fell back to defaults there. The
|
|
173
|
+
* two callers still differ in posture — one throws where the other returns a
|
|
174
|
+
* default — but they may not differ in what a workspace root IS.
|
|
175
|
+
*
|
|
176
|
+
* @type {string[]}
|
|
177
|
+
*/
|
|
178
|
+
export const WORKSPACE_MARKERS = [NX_CONFIG_FILE, ARCHKEEP_MODEL_FILE, MOON_DIR, MOON_ALT_DIR];
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @typedef {object} CommandContext
|
|
182
|
+
* @property {string} root Absolute workspace root.
|
|
183
|
+
* @property {"nx"|"native"|"moon"} provider Which project-model provider answered.
|
|
184
|
+
* @property {string} marker `nx.json`, `archkeep.json`, `.moon`, or `.config/moon` — whichever
|
|
185
|
+
* `root` carries, and the one this run's provider came from.
|
|
186
|
+
* @property {object} graph `{nodes, dependencies}`, from `readProjectGraph`
|
|
187
|
+
* or `nativeProvider.buildGraph`.
|
|
188
|
+
* @property {object} workspace The `Workspace` `../workspace.mjs`'s
|
|
189
|
+
* `createWorkspace` returns.
|
|
190
|
+
* @property {string[]} tracked Every tracked file, from `listFiles(root)`.
|
|
191
|
+
* @property {{imports: object[], failures: object[], analyzed: number,
|
|
192
|
+
* analyzedFiles: string[], exemptedFiles: string[]}} analysis The
|
|
193
|
+
* whole-tree-then-scoped (native) or scoped-then-analyzed (nx/moon) result —
|
|
194
|
+
* see the branches below for why the order is not the same on all three.
|
|
195
|
+
* `exemptedFiles` is always `[]` on nx/moon: `coverage.exempt` is a
|
|
196
|
+
* native-only `archkeep.json` key.
|
|
197
|
+
* @property {{boundaryConfig: string|object, tsConfig: object|undefined,
|
|
198
|
+
* boundaryConfigDeclared: boolean, profiles?: string, inline?: boolean}} options
|
|
199
|
+
* What this workspace names its boundary law, its shared tsconfig, and —
|
|
200
|
+
* when it uses one — its named profile registry. `check`'s `--config` flag,
|
|
201
|
+
* if given, still wins over `options.boundaryConfig`; that override is
|
|
202
|
+
* `check`'s decision, not this module's.
|
|
203
|
+
* `boundaryConfigDeclared` is not a name but the provenance of one: `true`
|
|
204
|
+
* when this workspace's own config named a boundary law (`nx.json`'s
|
|
205
|
+
* `plugins[].options.boundaryConfig`, or `archkeep.json`'s field in either
|
|
206
|
+
* its filename or its inline-policy spelling), `false` when the name above
|
|
207
|
+
* is `../options.mjs`'s `DEFAULT_OPTIONS` convention and nobody wrote one.
|
|
208
|
+
* Always present, on all three providers. A command that may tolerate a law
|
|
209
|
+
* file that is not there — `graph`, which describes the project graph and
|
|
210
|
+
* judges nothing against a constraint row — reads this to tell "no law was
|
|
211
|
+
* ever written" from "the law this workspace named has been renamed or
|
|
212
|
+
* deleted", which the merged `boundaryConfig` string alone cannot. That
|
|
213
|
+
* module's header owns the argument.
|
|
214
|
+
* @property {{registered: boolean, manifests: string[]}} pluginGap Whether
|
|
215
|
+
* this workspace's own provider is the one Nx would actually run, and which
|
|
216
|
+
* tracked polyglot manifests sit under a project root either way. Always
|
|
217
|
+
* `{registered: true, manifests: []}` on a native or Moon workspace: there
|
|
218
|
+
* is no Nx plugin registration to be missing, because there is no `nx.json`.
|
|
219
|
+
* Computed and returned, but not consulted by `check`'s refusal logic —
|
|
220
|
+
* `../../../../docs/usage/` names the gap this fills and the issue tracking it
|
|
221
|
+
* wiring it in.
|
|
222
|
+
* @property {{files: string[], languages: string[]}} unownedGap The tracked
|
|
223
|
+
* analyzable files no project owns that this run deliberately does NOT fail
|
|
224
|
+
* on — `unownedAnalyzableFiles` below owns the argument, and
|
|
225
|
+
* `./check.mjs` turns a non-empty `files` into the `"unowned-files"`
|
|
226
|
+
* `coverageGaps` entry. Always `{files: [], languages: []}` on a native
|
|
227
|
+
* workspace: there, every unclaimed analyzable file is already a whole-file
|
|
228
|
+
* failure (`../providers/native/coverage.mjs`'s `judgeCoverage`) and so
|
|
229
|
+
* already refuses the run with exit 3 — a gap beside it would be a second,
|
|
230
|
+
* quieter voice for a state that is answered loudly.
|
|
231
|
+
* @property {{file: string, project: string}[]} owned Every tracked file that
|
|
232
|
+
* belongs to a project, paired with its owning project — the ownership map
|
|
233
|
+
* `createWorkspace` already built. A command that needs to know WHICH project
|
|
234
|
+
* owns a file (the planning context's path→project scoping, `./plan-context-command.mjs`)
|
|
235
|
+
* reads this rather than re-deriving ownership a second way.
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The languages Nx cannot draw an edge for and ESLint cannot parse at all —
|
|
240
|
+
* `../../../AGENTS.md`'s own opening line: "Nx reads TypeScript and
|
|
241
|
+
* JavaScript imports and so `nx affected` and `@nx/enforce-module-boundaries`
|
|
242
|
+
* work there; for the other three both go quiet." TypeScript, JavaScript
|
|
243
|
+
* (and their `.mjs`/`.cjs`/`.jsx`/`.tsx` siblings) and Vue are deliberately
|
|
244
|
+
* OUTSIDE this set: Nx's own graph already draws their edges from real
|
|
245
|
+
* imports, and `@nx/enforce-module-boundaries` already lints them through
|
|
246
|
+
* ESLint's normal file scoping, so a root-level tooling script in one of
|
|
247
|
+
* those languages sitting outside every declared project is the ordinary,
|
|
248
|
+
* unremarkable shape of an Nx or Moon workspace — this very repository's own
|
|
249
|
+
* root carries a whole `scripts/` directory of them, plus `.opencode/plugins/`
|
|
250
|
+
* and `commitlint.config.mjs`, inside neither of its two Moon projects — not
|
|
251
|
+
* a gap this tool introduces. `../workspace.mjs`'s `polyglotManifests` already
|
|
252
|
+
* draws this exact same line for the unregistered-plugin coverage gap, over
|
|
253
|
+
* the three languages' manifests rather than their sources; this is that
|
|
254
|
+
* same boundary restated for `unclaimedFileFailures` below, which has no
|
|
255
|
+
* manifest to key off since an unclaimed file is, by definition, one with no
|
|
256
|
+
* project (and so no `go.mod`/`Cargo.toml`/`pyproject.toml`) to belong to.
|
|
257
|
+
*
|
|
258
|
+
* Scoping to these three is not a smaller fix chosen for convenience — it is
|
|
259
|
+
* the fix: widening this to every analyzable language turns `check` into a
|
|
260
|
+
* breaking change for nearly every real Nx/Moon consumer, measured by running
|
|
261
|
+
* it over this repository's own tree, whose tooling layer is exactly this
|
|
262
|
+
* shape and whose own `check` must keep exiting 0.
|
|
263
|
+
*/
|
|
264
|
+
const UNCLAIMED_CHECK_LANGUAGES = new Set(["go", "rust", "python"]);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Tracked Go, Rust or Python files that no project in `owned` claims — the
|
|
268
|
+
* same "unclaimed file" question `../providers/native/coverage.mjs`'s
|
|
269
|
+
* `judgeCoverage` answers for a native workspace (over every analyzable
|
|
270
|
+
* language there, `UNCLAIMED_CHECK_LANGUAGES` above argues why this narrower
|
|
271
|
+
* set is the right one here), asked for the Nx and Moon branches below,
|
|
272
|
+
* neither of which has a discovery step of its own to answer it from: both
|
|
273
|
+
* build their graph from `nx graph`/`moon project-graph` rather than from
|
|
274
|
+
* `archkeep.json`, and `../providers/native/coverage.mjs`'s own header names
|
|
275
|
+
* the gap this closes — "this package's Nx path ... has no unclaimed-file
|
|
276
|
+
* check of its own: both compute imports and violations only for files a
|
|
277
|
+
* project already claims."
|
|
278
|
+
*
|
|
279
|
+
* `owned` already IS the claimed half of this question: `createWorkspace`
|
|
280
|
+
* (`../workspace.mjs`) computed it over the FULL tracked-file list by the
|
|
281
|
+
* same longest-root-prefix match `../providers/native/coverage.mjs`'s
|
|
282
|
+
* `projectOf` uses, silently dropping any file that matched no project
|
|
283
|
+
* (`../workspace.mjs`'s own header — "A file no project owns ... is dropped
|
|
284
|
+
* here rather than read and analyzed for a verdict that cannot exist").
|
|
285
|
+
* Comparing against the set `createWorkspace` already produced, rather than
|
|
286
|
+
* matching roots a second time, is what keeps this answer from being able to
|
|
287
|
+
* disagree with what "owned" already means for this graph.
|
|
288
|
+
*
|
|
289
|
+
* A file this returns is exactly the silent hole `../../../../AGENTS.md`'s
|
|
290
|
+
* invariant refuses: analyzed by nothing, judged by nothing, and an empty
|
|
291
|
+
* violation list reading identically to a file that really was clean. It
|
|
292
|
+
* ignores path scoping on purpose — `tracked`, not the caller's scoped
|
|
293
|
+
* selection — the same workspace-wide posture native's own unclaimed check
|
|
294
|
+
* already has (its failures ride in `discovered.failures` below, unfiltered
|
|
295
|
+
* by `paths`), because a `check <path>` run must not be able to hide an
|
|
296
|
+
* orphan file elsewhere in the tree by naming a path that excludes it.
|
|
297
|
+
*
|
|
298
|
+
* Returns the SAME whole-file `fileFailure` shape (`../analysis/source-util.mjs`)
|
|
299
|
+
* a language analyzer produces for a file it could not read, so `../../cli.mjs`'s
|
|
300
|
+
* existing `unchecked`/`coverage.complete` logic — already built to treat any
|
|
301
|
+
* whole-file failure as a coverage hole — picks these up with no change of its
|
|
302
|
+
* own, exactly as it already does for native's. The wording names
|
|
303
|
+
* `providerLabel` rather than native's `archkeep.json`/`coverage.exempt`
|
|
304
|
+
* vocabulary, because neither Nx nor Moon has an exemption mechanism this
|
|
305
|
+
* tool reads — inventing one is out of scope here; this only detects and
|
|
306
|
+
* reports.
|
|
307
|
+
*
|
|
308
|
+
* @param {{tracked: string[], owned: {file: string, project: string}[], providerLabel: string}} args
|
|
309
|
+
* @returns {object[]}
|
|
310
|
+
*/
|
|
311
|
+
function unclaimedFileFailures({ tracked, owned, providerLabel }) {
|
|
312
|
+
const ownedFiles = new Set(owned.map(({ file }) => file));
|
|
313
|
+
return tracked
|
|
314
|
+
.filter((file) => UNCLAIMED_CHECK_LANGUAGES.has(languageOf(file)) && !ownedFiles.has(file))
|
|
315
|
+
.map((file) =>
|
|
316
|
+
fileFailure(
|
|
317
|
+
file,
|
|
318
|
+
`is not owned by any project in ${providerLabel} — every tracked Go, Rust or Python file ` +
|
|
319
|
+
`must belong to exactly one declared project, so its cross-project imports can be checked`,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The OTHER half of the same question: tracked analyzable files no project
|
|
326
|
+
* owns whose language `UNCLAIMED_CHECK_LANGUAGES` above deliberately leaves
|
|
327
|
+
* out — TypeScript, JavaScript and Vue. That set's own argument stands
|
|
328
|
+
* unchanged and is not widened here: those three keep producing no failure,
|
|
329
|
+
* no `notAnalyzed` entry, no `coverage.complete: false` and no exit 3.
|
|
330
|
+
*
|
|
331
|
+
* What they were also producing was nothing at all. `createWorkspace`
|
|
332
|
+
* (`../workspace.mjs`) drops a file no project owns, so such a file left no
|
|
333
|
+
* trace on any surface: `coverage.analyzedFiles` counted only owned files,
|
|
334
|
+
* `notAnalyzed`/`coverageGaps`/`notes` stayed empty, and a run over a tree
|
|
335
|
+
* with fifty of them printed the same bytes as a run over a tree with none —
|
|
336
|
+
* measured on this repository, where 50 of 425 tracked analyzable files sit
|
|
337
|
+
* outside both Moon projects — 49 of them reported, once the run's own
|
|
338
|
+
* boundary config is subtracted by `unownedGapWithoutRunConfiguration` below. Tolerated is a decision; invisible is the
|
|
339
|
+
* silent direction `../../../../AGENTS.md`'s invariant refuses, and the two
|
|
340
|
+
* are not the same thing.
|
|
341
|
+
*
|
|
342
|
+
* So this reports rather than judges: `./check.mjs` shapes what this returns
|
|
343
|
+
* into a `coverageGaps` entry — the same degraded-coverage channel
|
|
344
|
+
* `../workspace.mjs`'s `polyglotManifests` already feeds through
|
|
345
|
+
* `pluginGap`, which likewise changes no exit code and no verdict. `files` is
|
|
346
|
+
* the whole list rather than a count, so the JSON envelope carries something
|
|
347
|
+
* a reader can act on and the text face can bound its own rendering
|
|
348
|
+
* (`../report/text.mjs`) without either surface having to trust a number it
|
|
349
|
+
* cannot check.
|
|
350
|
+
*
|
|
351
|
+
* Empty `files` is the answer for a workspace where every analyzable file is
|
|
352
|
+
* owned, and `./check.mjs` contributes no gap entry at all then: a gap that
|
|
353
|
+
* always fires teaches a reader to skip the line it is written on.
|
|
354
|
+
*
|
|
355
|
+
* Workspace-scoped like `unclaimedFileFailures` above, and for the same
|
|
356
|
+
* reason — a `check <path>` must not be able to hide an orphan elsewhere in
|
|
357
|
+
* the tree by naming a path that excludes it.
|
|
358
|
+
*
|
|
359
|
+
* **This list still holds the files the run reads as its own configuration**,
|
|
360
|
+
* and `unownedGapWithoutRunConfiguration` below is what removes them. The
|
|
361
|
+
* split is forced rather than stylistic: `resolvePolicy` needs a resolved
|
|
362
|
+
* `CommandContext` to run, so it runs AFTER this does, and until it has run
|
|
363
|
+
* nothing here knows which law actually governed the run. A `--config`
|
|
364
|
+
* override, a profile name, or an inline policy object all decide that later.
|
|
365
|
+
* Subtracting the DECLARED name here would exclude a file the run never read
|
|
366
|
+
* while listing the one it did — worse than not filtering at all.
|
|
367
|
+
*
|
|
368
|
+
* @param {{tracked: string[], owned: {file: string, project: string}[]}} args
|
|
369
|
+
* @returns {{files: string[], languages: string[]}} `languages` is the sorted
|
|
370
|
+
* distinct set of languages `files` spans — derived here, beside the filter
|
|
371
|
+
* that decided the list, so no face can name a language the list does not
|
|
372
|
+
* contain.
|
|
373
|
+
*/
|
|
374
|
+
function unownedAnalyzableFiles({ tracked, owned }) {
|
|
375
|
+
const ownedFiles = new Set(owned.map(({ file }) => file));
|
|
376
|
+
const files = tracked.filter((file) => {
|
|
377
|
+
const language = languageOf(file);
|
|
378
|
+
return language !== null && !UNCLAIMED_CHECK_LANGUAGES.has(language) && !ownedFiles.has(file);
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
files,
|
|
382
|
+
languages: [...new Set(files.map((file) => languageOf(file)))].sort(),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The same gap with the files this run read as its own configuration removed,
|
|
388
|
+
* and the languages recounted over what is left.
|
|
389
|
+
*
|
|
390
|
+
* A file the run read as configuration is not a file the run failed to cover:
|
|
391
|
+
* it is not source judged by the boundary law, it IS the boundary law, and
|
|
392
|
+
* "no verdict covers this file" is vacuous when said of it. The concrete
|
|
393
|
+
* failure without this is worse than vacuous, and
|
|
394
|
+
* `../config-spelling.integration.test.mjs` is what proved it: a law spelled
|
|
395
|
+
* `module-boundaries.config.mjs` and one spelled `law/custom.mjs` produced
|
|
396
|
+
* different reports, so RENAMING THE LAW CHANGED THE VERDICT.
|
|
397
|
+
*
|
|
398
|
+
* It takes the names as arguments rather than reading `CommandContext.options`
|
|
399
|
+
* because the caller is the only layer that knows them. `options.boundaryConfig`
|
|
400
|
+
* is what the workspace DECLARED; the law that actually ran may be a `--config`
|
|
401
|
+
* override or a profile, which `./policy.mjs`'s `resolvePolicy` reports as its
|
|
402
|
+
* workspace-relative `source` — and that resolution cannot happen before
|
|
403
|
+
* `resolveCommandContext`, because it takes the context as an argument.
|
|
404
|
+
*
|
|
405
|
+
* Names are normalised toward the spelling `git ls-files` uses, because that
|
|
406
|
+
* is what `tracked` holds: an `nx.json` may legitimately declare
|
|
407
|
+
* `"./module-boundaries.config.mjs"` or a backslash-separated path, and an
|
|
408
|
+
* unnormalised compare would silently fail to exclude it. An absolute path
|
|
409
|
+
* matches nothing and is left alone — no tracked entry is absolute, so it
|
|
410
|
+
* cannot collide with one.
|
|
411
|
+
*
|
|
412
|
+
* @param {{files: string[], languages: string[]}} gap
|
|
413
|
+
* @param {(string|object|null|undefined)[]} configNames Every name this run
|
|
414
|
+
* read as configuration. Non-strings are ignored, which is how an inline
|
|
415
|
+
* policy object (`archkeep.json`'s object form) and an absent profile both
|
|
416
|
+
* pass through without naming a file.
|
|
417
|
+
* @returns {{files: string[], languages: string[]}}
|
|
418
|
+
*/
|
|
419
|
+
export function unownedGapWithoutRunConfiguration(gap, configNames) {
|
|
420
|
+
const excluded = new Set(
|
|
421
|
+
configNames
|
|
422
|
+
.filter((name) => typeof name === "string")
|
|
423
|
+
.map((name) => name.replace(/\\/gu, "/").replace(/^\.\//u, "")),
|
|
424
|
+
);
|
|
425
|
+
if (excluded.size === 0) return gap;
|
|
426
|
+
const files = gap.files.filter((file) => !excluded.has(file));
|
|
427
|
+
if (files.length === gap.files.length) return gap;
|
|
428
|
+
return {
|
|
429
|
+
files,
|
|
430
|
+
languages: [...new Set(files.map((file) => languageOf(file)))].sort(),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Resolves everything a command needs before it can ask its own question:
|
|
436
|
+
* which workspace, which provider, which files, and what analyzing them
|
|
437
|
+
* found.
|
|
438
|
+
*
|
|
439
|
+
* Throws rather than returning a partial context on every condition that
|
|
440
|
+
* would otherwise leave a caller building a verdict over a tree it could not
|
|
441
|
+
* fully read — no workspace root, both markers present, or a requested path
|
|
442
|
+
* outside the workspace or matching no tracked file at all
|
|
443
|
+
* (`../workspace.mjs`'s `selectFiles`). That is the empty-result invariant
|
|
444
|
+
* (`../../../../AGENTS.md`) applied one layer before any command's own
|
|
445
|
+
* report: a context half-built is exactly the silent direction the invariant
|
|
446
|
+
* refuses.
|
|
447
|
+
*
|
|
448
|
+
* @param {{cwd: string, paths?: string[]}} request
|
|
449
|
+
* @param {{readGraph?: Function, listFiles?: Function, readFile?: (path: string) => string|null}} [io]
|
|
450
|
+
* The seams that reach outside this process — Nx, git, and (on the Nx
|
|
451
|
+
* branch) the `nx.json` read behind `pluginGap.registered` — injectable for
|
|
452
|
+
* the same reason `check` always took the first two: a test drives the real
|
|
453
|
+
* analysis over a fixture tree with none of them touching a real
|
|
454
|
+
* filesystem or subprocess. `readFile` takes an ALREADY-ABSOLUTE path, the
|
|
455
|
+
* shape `pluginIsRegistered` (`../options.mjs`) calls its reader with.
|
|
456
|
+
* @returns {CommandContext}
|
|
457
|
+
*/
|
|
458
|
+
export function resolveCommandContext(
|
|
459
|
+
{ cwd, paths = [] },
|
|
460
|
+
{ readGraph, listFiles = listTrackedFiles, readFile = readFileAbsolute } = {},
|
|
461
|
+
) {
|
|
462
|
+
// All three markers in one walk (`../workspace.mjs`'s `findWorkspaceRoot`), so a
|
|
463
|
+
// native root nested under an unrelated Nx tree — or vice versa — is found
|
|
464
|
+
// from the working directory the same way either alone would be. Which
|
|
465
|
+
// marker(s) the returned directory actually carries is then read back
|
|
466
|
+
// below, because a walk that STOPPED at the first marker it saw could never
|
|
467
|
+
// tell "only archkeep.json here" from "both, one level up".
|
|
468
|
+
const root = findWorkspaceRoot(cwd, WORKSPACE_MARKERS);
|
|
469
|
+
if (root === null) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`archkeep: no workspace root above ${cwd} — looked for an nx.json, a archkeep.json, or a ` +
|
|
472
|
+
`.moon (or .config/moon) directory in every parent. The tree to judge is found from the working directory, ` +
|
|
473
|
+
`never from this tool's own location: installed from the registry, this tool lives under ` +
|
|
474
|
+
`the consumer's node_modules and the two are always different trees.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
// Which provider may judge at all — the one gate
|
|
478
|
+
// (`requireSingleProjectModel` above) every entry point shares, CLI and
|
|
479
|
+
// language server alike. Moon-versus-Moon rides it through `moonMarkerAt`.
|
|
480
|
+
const { hasNative, moonMarker } = requireSingleProjectModel(root);
|
|
481
|
+
const hasMoon = moonMarker !== null;
|
|
482
|
+
|
|
483
|
+
// Resolve the default graph reader based on provider: Moon workspaces read
|
|
484
|
+
// their graph from `moon project-graph --json`, Nx workspaces from
|
|
485
|
+
// `nx graph --file=`. The native provider uses a two-call discover/buildGraph
|
|
486
|
+
// contract instead, so it never goes through `readGraph` at all.
|
|
487
|
+
const defaultReadGraph = hasMoon ? moonProvider.readProjectGraph : readProjectGraph;
|
|
488
|
+
const effectiveReadGraph = readGraph ?? defaultReadGraph;
|
|
489
|
+
|
|
490
|
+
const tracked = listFiles(root);
|
|
491
|
+
let graph;
|
|
492
|
+
let workspace;
|
|
493
|
+
let owned;
|
|
494
|
+
let options;
|
|
495
|
+
let imports;
|
|
496
|
+
let failures;
|
|
497
|
+
let analyzed;
|
|
498
|
+
let analyzedFiles;
|
|
499
|
+
let pluginGap;
|
|
500
|
+
let unownedGap;
|
|
501
|
+
let exemptedFiles;
|
|
502
|
+
|
|
503
|
+
if (hasNative) {
|
|
504
|
+
// No `nx graph`, no `nx.json`, and — verified by this branch existing at
|
|
505
|
+
// all — no `nx` needing to be installed: `nativeProvider` is imported
|
|
506
|
+
// from `../providers/native/index.mjs`, which imports nothing from
|
|
507
|
+
// `../providers/nx.mjs` and nothing that resolves the `nx` package.
|
|
508
|
+
const readFile = readWorkspaceRoot(root);
|
|
509
|
+
const discovered = nativeProvider.discover({ root, files: tracked, readFile });
|
|
510
|
+
// A graph with nodes but no dependencies yet — `createWorkspace` only
|
|
511
|
+
// ever reads `data.root` off each node, and dependencies are not known
|
|
512
|
+
// until the import sites below are analyzed against these same projects.
|
|
513
|
+
const preGraph = {
|
|
514
|
+
nodes: Object.fromEntries(
|
|
515
|
+
discovered.projects.map((project) => [
|
|
516
|
+
project.name,
|
|
517
|
+
{ name: project.name, data: { root: project.root } },
|
|
518
|
+
]),
|
|
519
|
+
),
|
|
520
|
+
};
|
|
521
|
+
({ workspace, owned } = createWorkspace({
|
|
522
|
+
root,
|
|
523
|
+
graph: preGraph,
|
|
524
|
+
files: tracked,
|
|
525
|
+
tsConfig: discovered.model.tsConfig,
|
|
526
|
+
}));
|
|
527
|
+
|
|
528
|
+
// On the Nx path `graph.dependencies` comes from `nx graph`, computed
|
|
529
|
+
// over the WHOLE workspace regardless of `paths`, so scoping only ever
|
|
530
|
+
// narrows which import sites are handed back for reporting. The native
|
|
531
|
+
// path has no such independent source — `nativeProvider.buildGraph`
|
|
532
|
+
// DERIVES `dependencies` from import sites — so analyzing only the
|
|
533
|
+
// requested scope first would drop every project outside it from the
|
|
534
|
+
// dependency graph itself, and a cycle or a transitive violation that
|
|
535
|
+
// only closes once the rest of the tree's imports are counted would go
|
|
536
|
+
// unreported. Every owned file is analyzed here, unconditionally;
|
|
537
|
+
// `selected` below only filters which of the resulting sites are handed
|
|
538
|
+
// back.
|
|
539
|
+
const wholeTreeAnalysis = analyzeWorkspace(
|
|
540
|
+
workspace,
|
|
541
|
+
owned.map(({ file }) => file),
|
|
542
|
+
);
|
|
543
|
+
graph = nativeProvider.buildGraph({
|
|
544
|
+
discovered,
|
|
545
|
+
importSites: wholeTreeAnalysis.imports,
|
|
546
|
+
});
|
|
547
|
+
annotateMFERemotes(graph.nodes, workspace.readFile);
|
|
548
|
+
annotatePackageFacts(graph.nodes, workspace.readFile);
|
|
549
|
+
|
|
550
|
+
// `boundaryConfigDeclared` is carried straight off the model rather than
|
|
551
|
+
// re-derived here: `../providers/native/model.mjs`'s
|
|
552
|
+
// `normalizeNativeModel` is the only code that still sees the raw
|
|
553
|
+
// `archkeep.json`, so it is the only place that can answer whether the
|
|
554
|
+
// file named a law. Re-deriving it from `options.boundaryConfig` at this
|
|
555
|
+
// point is exactly the mistake this whole thread exists to undo — a
|
|
556
|
+
// declared name and the convention default are the same string by then.
|
|
557
|
+
options = {
|
|
558
|
+
boundaryConfig: discovered.model.boundaryConfig,
|
|
559
|
+
tsConfig: discovered.model.tsConfig,
|
|
560
|
+
boundaryConfigDeclared: discovered.model.boundaryConfigDeclared,
|
|
561
|
+
...(typeof discovered.model.boundaryConfig === "string" ? {} : { inline: true }),
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const selected = selectFiles(
|
|
565
|
+
owned.map(({ file }) => file),
|
|
566
|
+
paths,
|
|
567
|
+
{ root, cwd, tracked },
|
|
568
|
+
);
|
|
569
|
+
const selectedFiles = new Set(selected);
|
|
570
|
+
imports = wholeTreeAnalysis.imports.filter((site) => selectedFiles.has(site.sourceFile));
|
|
571
|
+
// Unclaimed analyzable files — this branch's own source is native
|
|
572
|
+
// discovery's `discovered.failures` (`../providers/native/coverage.mjs`'s
|
|
573
|
+
// `judgeCoverage`, reached through `nativeProvider.discover` above), which
|
|
574
|
+
// already carries the unclaimed-file list alongside any unparseable-
|
|
575
|
+
// manifest failure. The Nx and Moon branches below have no such discovery
|
|
576
|
+
// step to answer the same question from, so they compute the equivalent
|
|
577
|
+
// list themselves (`unclaimedFileFailures` above) — three different
|
|
578
|
+
// sources feeding the SAME whole-file failure shape a language analyzer
|
|
579
|
+
// produces for an unreadable file, so nothing downstream needs to know
|
|
580
|
+
// which provider found the gap.
|
|
581
|
+
failures = [
|
|
582
|
+
...wholeTreeAnalysis.failures.filter((failure) => selectedFiles.has(failure.sourceFile)),
|
|
583
|
+
...discovered.failures,
|
|
584
|
+
// Workspace-scoped on purpose, the same posture the two unclaimed
|
|
585
|
+
// equivalents above hold: a wildcard run must not be able to hide a
|
|
586
|
+
// project whose manifest it cannot read by naming a path that excludes
|
|
587
|
+
// it (`../analysis/python.mjs`'s `pythonUnmodelledFailures`).
|
|
588
|
+
...pythonUnmodelledFailures(workspace),
|
|
589
|
+
];
|
|
590
|
+
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
591
|
+
analyzed = analyzedFiles.length;
|
|
592
|
+
// Unaffected by `paths`: an exempted file is by definition unowned by any
|
|
593
|
+
// project, so it was never a candidate for `owned`/`selected` in the
|
|
594
|
+
// first place — the same reason `pluginGap` below is a workspace-wide
|
|
595
|
+
// fact rather than a scoped one.
|
|
596
|
+
exemptedFiles = discovered.exempted;
|
|
597
|
+
|
|
598
|
+
// There is no Nx plugin registration to be missing on a workspace that
|
|
599
|
+
// has no `nx.json` at all.
|
|
600
|
+
pluginGap = { registered: true, manifests: [] };
|
|
601
|
+
// Nothing to report as a tolerated gap either: native's own coverage
|
|
602
|
+
// judgment already fails on EVERY unclaimed analyzable file, in every
|
|
603
|
+
// language, and those failures are in `discovered.failures` above — the
|
|
604
|
+
// run refuses with exit 3 rather than tolerating them, which is the
|
|
605
|
+
// deliberate difference between this provider and the two below
|
|
606
|
+
// (`../providers/native/coverage.mjs`). Stated rather than left off, for
|
|
607
|
+
// the reason `pluginGap` is: a reader must not have to tell "false" from
|
|
608
|
+
// "this branch forgot".
|
|
609
|
+
unownedGap = { files: [], languages: [] };
|
|
610
|
+
} else if (hasMoon) {
|
|
611
|
+
// Moon provider — reads graph from `moon project-graph --json`, the same
|
|
612
|
+
// one-call contract as the Nx path: Moon already resolved projects, tags
|
|
613
|
+
// and edges before this package ever asked. Both option names are
|
|
614
|
+
// convention here, because Moon carries no `plugins[].options` table and
|
|
615
|
+
// a `archkeep.json` beside `.moon/` is refused outright
|
|
616
|
+
// (`../providers/moon.mjs`) — so there is nowhere in a Moon workspace to
|
|
617
|
+
// name either file, and `readMoonOptions` is where they are decided
|
|
618
|
+
// rather than read. `boundaryConfigDeclared: false` therefore states a
|
|
619
|
+
// fact about Moon rather than a fallback, and it is written out rather
|
|
620
|
+
// than left off so the key is present on all three providers: a reader
|
|
621
|
+
// that had to tell "false" from "this branch forgot" would be back to
|
|
622
|
+
// guessing provenance, which is the defect.
|
|
623
|
+
//
|
|
624
|
+
// The two names are NOT symmetrical, which is why this is a call and not
|
|
625
|
+
// a literal. `boundaryConfig` is the default outright. `tsConfig` walks
|
|
626
|
+
// the short ordered chain `MOON_TSCONFIG_CHAIN` — a workspace whose paths
|
|
627
|
+
// table lives in `tsconfig.json` rather than `tsconfig.base.json` was
|
|
628
|
+
// previously judged against a file it does not have, where every aliased
|
|
629
|
+
// import resolves to nothing and the report is a wall of crossings with
|
|
630
|
+
// no line saying the table was never found. `listFiles` is a thunk the
|
|
631
|
+
// chain only calls on the branch that needs it (neither candidate
|
|
632
|
+
// present), so a workspace carrying one pays nothing for the question.
|
|
633
|
+
options = readMoonOptions(root, { listFiles: () => tracked });
|
|
634
|
+
|
|
635
|
+
graph = effectiveReadGraph(root);
|
|
636
|
+
({ workspace, owned } = createWorkspace({
|
|
637
|
+
root,
|
|
638
|
+
graph,
|
|
639
|
+
files: tracked,
|
|
640
|
+
tsConfig: options.tsConfig,
|
|
641
|
+
}));
|
|
642
|
+
annotateMFERemotes(graph.nodes, workspace.readFile);
|
|
643
|
+
annotatePackageFacts(graph.nodes, workspace.readFile);
|
|
644
|
+
|
|
645
|
+
// Unlike the Nx path, Moon's own graph carries NO edge this package's own
|
|
646
|
+
// analysis found: `moon project-graph --json` only knows a project
|
|
647
|
+
// depends on another when `moon.yml` says `dependsOn`, because Moon has no
|
|
648
|
+
// plugin hook this package can register the way `../nx.mjs`'s
|
|
649
|
+
// `createDependencies` registers into Nx's own graph computation. A Go,
|
|
650
|
+
// Rust or Python import crossing a project boundary with no hand-written
|
|
651
|
+
// `dependsOn` entry is therefore invisible to `graph.dependencies` and to
|
|
652
|
+
// every verdict computed from it — architecture-intent, drift, cycles,
|
|
653
|
+
// `impact`, `diff` — while `check`'s own import-site rules judge it fine,
|
|
654
|
+
// because those read analysis records directly rather than the graph.
|
|
655
|
+
// Analyzing the whole tree first, before `paths` narrows anything, mirrors
|
|
656
|
+
// the native branch above for the same reason it states there: an edge
|
|
657
|
+
// from a project outside the scoped paths is still part of the graph a
|
|
658
|
+
// cycle or a transitive violation is judged against.
|
|
659
|
+
const wholeTreeAnalysis = analyzeWorkspace(
|
|
660
|
+
workspace,
|
|
661
|
+
owned.map(({ file }) => file),
|
|
662
|
+
);
|
|
663
|
+
const projectOfFile = new Map(owned.map(({ file, project }) => [file, project]));
|
|
664
|
+
// Moon's own graph carries only edges Moon itself resolved (`dependsOn`);
|
|
665
|
+
// the imports this tree writes are folded in here by the same merge the
|
|
666
|
+
// language server's index runs — one implementation
|
|
667
|
+
// (`../providers/moon.mjs`'s `mergeImportEdges`), so the two faces cannot
|
|
668
|
+
// disagree about which edges exist.
|
|
669
|
+
mergeImportEdges(graph, {
|
|
670
|
+
importSites: wholeTreeAnalysis.imports,
|
|
671
|
+
projectOf: (file) => projectOfFile.get(file),
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const selected = selectFiles(
|
|
675
|
+
owned.map(({ file }) => file),
|
|
676
|
+
paths,
|
|
677
|
+
{ root, cwd, tracked },
|
|
678
|
+
);
|
|
679
|
+
const selectedFiles = new Set(selected);
|
|
680
|
+
imports = wholeTreeAnalysis.imports.filter((site) => selectedFiles.has(site.sourceFile));
|
|
681
|
+
// Unclaimed analyzable files — `unclaimedFileFailures` above — join the
|
|
682
|
+
// scoped read failures unconditionally, the same workspace-wide posture
|
|
683
|
+
// native's own `discovered.failures` has (this branch's header already
|
|
684
|
+
// analyzes the whole tree before `paths` narrows anything, for the same
|
|
685
|
+
// reason).
|
|
686
|
+
failures = [
|
|
687
|
+
...wholeTreeAnalysis.failures.filter((failure) => selectedFiles.has(failure.sourceFile)),
|
|
688
|
+
...unclaimedFileFailures({ tracked, owned, providerLabel: "the Moon project graph" }),
|
|
689
|
+
...pythonUnmodelledFailures(workspace),
|
|
690
|
+
];
|
|
691
|
+
analyzedFiles = wholeTreeAnalysis.analyzedFiles.filter((file) => selectedFiles.has(file));
|
|
692
|
+
analyzed = analyzedFiles.length;
|
|
693
|
+
// `coverage.exempt` is a native-only key (`../providers/native/coverage.mjs`'s
|
|
694
|
+
// header: "Nx has no equivalent question") — Moon carries no such list.
|
|
695
|
+
exemptedFiles = [];
|
|
696
|
+
|
|
697
|
+
// There is no Nx plugin registration to be missing on a workspace that
|
|
698
|
+
// has no `nx.json` at all.
|
|
699
|
+
pluginGap = { registered: true, manifests: [] };
|
|
700
|
+
// The tolerated half of the same unclaimed-file question the failures
|
|
701
|
+
// above answer for Go, Rust and Python — counted and reported rather than
|
|
702
|
+
// judged (`unownedAnalyzableFiles`).
|
|
703
|
+
unownedGap = unownedAnalyzableFiles({ tracked, owned });
|
|
704
|
+
} else {
|
|
705
|
+
// What this workspace calls the two files whose names are conventions
|
|
706
|
+
// rather than contracts. Read before the graph, because it decides which
|
|
707
|
+
// tsconfig `createWorkspace` resolves paths against.
|
|
708
|
+
const pluginOptions = readPluginOptions(root);
|
|
709
|
+
options = {
|
|
710
|
+
boundaryConfig: pluginOptions.boundaryConfig,
|
|
711
|
+
tsConfig: pluginOptions.tsConfig,
|
|
712
|
+
// Straight off `readPluginOptions`, for the reason the native branch
|
|
713
|
+
// above states: `nx.json`'s `plugins[].options` is the last place the
|
|
714
|
+
// declaration is still distinguishable from `DEFAULT_OPTIONS`.
|
|
715
|
+
boundaryConfigDeclared: pluginOptions.boundaryConfigDeclared,
|
|
716
|
+
...(pluginOptions.profiles === undefined ? {} : { profiles: pluginOptions.profiles }),
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
graph = effectiveReadGraph(root);
|
|
720
|
+
({ workspace, owned } = createWorkspace({
|
|
721
|
+
root,
|
|
722
|
+
graph,
|
|
723
|
+
files: tracked,
|
|
724
|
+
tsConfig: pluginOptions.tsConfig,
|
|
725
|
+
}));
|
|
726
|
+
// `nx graph --file=` does not carry the Module Federation fact — see
|
|
727
|
+
// `annotateMFERemotes` — so it is computed here, before any rule runs, or
|
|
728
|
+
// every import of a real remote app would be a false `noImportsOfApps`.
|
|
729
|
+
annotateMFERemotes(graph.nodes, workspace.readFile);
|
|
730
|
+
// Nor the two `package.json` facts — `data.entryPoints` and
|
|
731
|
+
// `data.declaredPackages`, see `annotatePackageFacts` — which decide the
|
|
732
|
+
// secondary-entry-point exemptions and `noTransitiveDependencies`.
|
|
733
|
+
annotatePackageFacts(graph.nodes, workspace.readFile);
|
|
734
|
+
|
|
735
|
+
const selected = selectFiles(
|
|
736
|
+
owned.map(({ file }) => file),
|
|
737
|
+
paths,
|
|
738
|
+
{ root, cwd, tracked },
|
|
739
|
+
);
|
|
740
|
+
({ imports, failures, analyzed, analyzedFiles } = analyzeWorkspace(workspace, selected));
|
|
741
|
+
// Unclaimed analyzable files — `unclaimedFileFailures` above — join
|
|
742
|
+
// unconditionally, the same workspace-wide posture native's own
|
|
743
|
+
// `discovered.failures` has, so a scoped `check <path>` cannot hide an
|
|
744
|
+
// orphan file elsewhere in the tree by naming a path that excludes it.
|
|
745
|
+
failures = [
|
|
746
|
+
...failures,
|
|
747
|
+
...unclaimedFileFailures({ tracked, owned, providerLabel: "the Nx project graph" }),
|
|
748
|
+
...pythonUnmodelledFailures(workspace),
|
|
749
|
+
];
|
|
750
|
+
// Same reason as the Moon branch above: `coverage.exempt` is a native-only
|
|
751
|
+
// concept, so an Nx workspace has nothing to report here.
|
|
752
|
+
exemptedFiles = [];
|
|
753
|
+
|
|
754
|
+
pluginGap = {
|
|
755
|
+
registered: pluginIsRegistered(root, { readFile }),
|
|
756
|
+
manifests: polyglotManifests(tracked, workspace.projects),
|
|
757
|
+
};
|
|
758
|
+
// Same as the Moon branch above, and computed from `tracked` rather than
|
|
759
|
+
// `selected` for the same reason `unclaimedFileFailures` is.
|
|
760
|
+
unownedGap = unownedAnalyzableFiles({ tracked, owned });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// `moonMarker` — resolved once at the top, where coexistence was refused —
|
|
764
|
+
// names whichever Moon directory this root actually carries, so diagnostics
|
|
765
|
+
// can name it correctly.
|
|
766
|
+
return {
|
|
767
|
+
root,
|
|
768
|
+
provider: hasMoon ? "moon" : hasNative ? "native" : "nx",
|
|
769
|
+
marker: hasMoon ? moonMarker : hasNative ? ARCHKEEP_MODEL_FILE : NX_CONFIG_FILE,
|
|
770
|
+
graph,
|
|
771
|
+
workspace,
|
|
772
|
+
tracked,
|
|
773
|
+
analysis: { imports, failures, analyzed, analyzedFiles, exemptedFiles },
|
|
774
|
+
options,
|
|
775
|
+
pluginGap,
|
|
776
|
+
unownedGap,
|
|
777
|
+
// Every tracked file that belongs to a project, paired with its project —
|
|
778
|
+
// the ownership map `createWorkspace` already built (`own ./workspace.mjs`).
|
|
779
|
+
// A command that needs to know WHICH project owns a file (the planning
|
|
780
|
+
// context's path→project scoping) reads this rather than re-deriving
|
|
781
|
+
// ownership a second way. Not part of any existing command's consumption.
|
|
782
|
+
owned,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Re-exported so a caller that only needs "does this tree look like a
|
|
787
|
+
// workspace at all" (`../../cli.mjs`'s `optionsForUsage`) is not forced to
|
|
788
|
+
// duplicate the marker check a second time; `DEFAULT_OPTIONS` rides along for
|
|
789
|
+
// the same reason, since the two are always read together there.
|
|
790
|
+
export { DEFAULT_OPTIONS };
|