@ecoma-io/archkeep 0.24.1 → 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.
Files changed (125) hide show
  1. package/cli.mjs +166 -506
  2. package/commands.mjs +7 -1
  3. package/package.json +1 -1
  4. package/src/architecture-intent/judge.mjs +1 -1
  5. package/src/architecture-intent/model.mjs +34 -0
  6. package/src/canonical.mjs +2 -1
  7. package/src/commands/adr-for-workspace.mjs +63 -0
  8. package/src/commands/analyze-capability.mjs +13 -0
  9. package/src/commands/change-intent.mjs +11 -0
  10. package/src/commands/change.mjs +315 -55
  11. package/src/commands/check-capability.mjs +13 -0
  12. package/src/commands/check.mjs +13 -3
  13. package/src/commands/compare-capability.mjs +20 -0
  14. package/src/commands/completeness.mjs +12 -0
  15. package/src/commands/context-command.mjs +31 -0
  16. package/src/commands/coverage-verdict.mjs +15 -2
  17. package/src/commands/debt.mjs +30 -0
  18. package/src/commands/decisions.mjs +59 -10
  19. package/src/commands/delta-snapshot.mjs +25 -4
  20. package/src/commands/delta.mjs +301 -67
  21. package/src/commands/diff.mjs +48 -9
  22. package/src/commands/discover.mjs +37 -0
  23. package/src/commands/drift.mjs +45 -0
  24. package/src/commands/evolution.mjs +22 -2
  25. package/src/commands/explain-capability.mjs +11 -0
  26. package/src/commands/explain.mjs +24 -0
  27. package/src/commands/fitness.mjs +119 -10
  28. package/src/commands/govern-capability.mjs +16 -0
  29. package/src/commands/graph.mjs +39 -0
  30. package/src/commands/health.mjs +34 -0
  31. package/src/commands/history.mjs +119 -2
  32. package/src/commands/impact.mjs +23 -0
  33. package/src/commands/inspect-capability.mjs +16 -0
  34. package/src/commands/plan-context-command.mjs +35 -0
  35. package/src/commands/policy.mjs +42 -4
  36. package/src/commands/provenance-command.mjs +15 -24
  37. package/src/commands/report.mjs +38 -0
  38. package/src/commands/rules-capability.mjs +16 -0
  39. package/src/commands/rules.mjs +3 -1
  40. package/src/commands/scenario.mjs +23 -0
  41. package/src/commands/snapshot-meta.mjs +33 -0
  42. package/src/commands/trajectory.mjs +35 -0
  43. package/src/commands/waivers.mjs +30 -0
  44. package/src/corpus/goldens/.gitkeep +0 -0
  45. package/src/corpus/goldens/adr.json +62 -0
  46. package/src/corpus/goldens/adr.text +6 -0
  47. package/src/corpus/goldens/change.json +98 -0
  48. package/src/corpus/goldens/change.text +6 -0
  49. package/src/corpus/goldens/check.json +90 -0
  50. package/src/corpus/goldens/check.sarif +479 -0
  51. package/src/corpus/goldens/check.text +9 -0
  52. package/src/corpus/goldens/context.json +47 -0
  53. package/src/corpus/goldens/context.text +6 -0
  54. package/src/corpus/goldens/debt.json +59 -0
  55. package/src/corpus/goldens/debt.text +7 -0
  56. package/src/corpus/goldens/decisions.json +74 -0
  57. package/src/corpus/goldens/decisions.text +7 -0
  58. package/src/corpus/goldens/delta.json +94 -0
  59. package/src/corpus/goldens/delta.sarif +472 -0
  60. package/src/corpus/goldens/delta.text +5 -0
  61. package/src/corpus/goldens/diff.json +58 -0
  62. package/src/corpus/goldens/diff.text +4 -0
  63. package/src/corpus/goldens/discover.json +77 -0
  64. package/src/corpus/goldens/discover.text +10 -0
  65. package/src/corpus/goldens/drift.json +43 -0
  66. package/src/corpus/goldens/drift.text +3 -0
  67. package/src/corpus/goldens/evolution.json +328 -0
  68. package/src/corpus/goldens/evolution.text +32 -0
  69. package/src/corpus/goldens/explain.json +60 -0
  70. package/src/corpus/goldens/explain.text +7 -0
  71. package/src/corpus/goldens/fitness.json +45 -0
  72. package/src/corpus/goldens/fitness.text +3 -0
  73. package/src/corpus/goldens/graph.json +78 -0
  74. package/src/corpus/goldens/graph.text +10 -0
  75. package/src/corpus/goldens/health.json +72 -0
  76. package/src/corpus/goldens/health.text +10 -0
  77. package/src/corpus/goldens/help.text +104 -0
  78. package/src/corpus/goldens/history.json +43 -0
  79. package/src/corpus/goldens/history.text +4 -0
  80. package/src/corpus/goldens/impact.json +327 -0
  81. package/src/corpus/goldens/impact.text +10 -0
  82. package/src/corpus/goldens/provenance.json +453 -0
  83. package/src/corpus/goldens/provenance.text +14 -0
  84. package/src/corpus/goldens/reconcile.json +168 -0
  85. package/src/corpus/goldens/reconcile.text +3 -0
  86. package/src/corpus/goldens/report.json +168 -0
  87. package/src/corpus/goldens/report.text +42 -0
  88. package/src/corpus/goldens/rules verify.json +0 -0
  89. package/src/corpus/goldens/rules verify.text +0 -0
  90. package/src/corpus/goldens/scenario.json +441 -0
  91. package/src/corpus/goldens/scenario.text +28 -0
  92. package/src/corpus/goldens/trajectory.json +83 -0
  93. package/src/corpus/goldens/trajectory.text +10 -0
  94. package/src/corpus/goldens/usage-error.text +105 -0
  95. package/src/corpus/goldens/waivers.json +40 -0
  96. package/src/corpus/goldens/waivers.text +1 -0
  97. package/src/corpus/goldens-lsp/initialize.result.json +15 -0
  98. package/src/corpus/goldens-lsp/publish-cleared-empty.json +4 -0
  99. package/src/corpus/goldens-lsp/publish-fixed-empty.json +5 -0
  100. package/src/corpus/goldens-lsp/publish-unparseable.json +54 -0
  101. package/src/corpus/goldens-lsp/publish-violation.json +22 -0
  102. package/src/corpus/goldens-lsp/register-watched-files.json +72 -0
  103. package/src/custom-rules/values.mjs +1 -4
  104. package/src/go-work.mjs +8 -23
  105. package/src/governance/evolution-event.mjs +15 -0
  106. package/src/governance/evolution-store.mjs +59 -31
  107. package/src/governance/fitness-registry.mjs +14 -1
  108. package/src/governance/provenance-record.mjs +0 -152
  109. package/src/governance/verdict.mjs +62 -7
  110. package/src/intent/intent-manifest.json +22 -16
  111. package/src/lsp/server.mjs +5 -8
  112. package/src/lsp/workspace-index.mjs +55 -220
  113. package/src/options.mjs +10 -0
  114. package/src/providers/native/discover.mjs +13 -12
  115. package/src/providers/native/model.mjs +8 -3
  116. package/src/providers/nx-static.mjs +231 -0
  117. package/src/report/json.mjs +7 -4
  118. package/src/report/sarif.mjs +8 -3
  119. package/src/rules/edge-constraints.mjs +9 -0
  120. package/src/rules/index.mjs +26 -10
  121. package/src/rules/messages.mjs +64 -14
  122. package/src/rules/specifiers.mjs +21 -20
  123. package/src/tsconfig-paths.mjs +8 -14
  124. package/src/values.mjs +11 -0
  125. package/src/verdict.mjs +127 -16
@@ -6,33 +6,33 @@
6
6
  * (`../rules/README.md`). Under Nx that graph arrives from Nx. A language
7
7
  * server has no Nx — it is spawned by an editor, in a directory, with nothing
8
8
  * else — so this module builds the same shape from whichever source of truth
9
- * the root actually carries: the tracked `project.json` files when there is no
10
- * `archkeep.json` at the root, `../providers/native/`'s own `discover()`/
11
- * `buildGraph()` when there is (`buildNativeWorkspaceIndex` below), and
12
- * `../providers/moon.mjs`'s one-call `readProjectGraph` when the root carries
13
- * `.moon/` or `.config/moon/` (`buildMoonWorkspaceIndex`) the same provider
14
- * object `../commands/context.mjs` hands `check`, so the editor's graph and
15
- * the CLI's come from one dispatch rather than two (#223). Before that Moon
16
- * branch existed, this module fell through to `discoverProjects` on a
17
- * `.moon`-rooted tree which finds a project only by its `project.json`, a
18
- * file a Moon workspace never has and built a zero-node index that read as
19
- * clean while `check` exited 1 on the same tree.
20
- *
21
- * `nodeTypeOf`, `PROJECT_CONFIG_FILE` and `buildDependencies` are imported
22
- * from `../providers/native/`, not defined here that package is where a
23
- * `project.json`-shaped graph is built for BOTH the Nx-less native provider
24
- * and this server, so the two do not grow separate copies of Nx's own
25
- * `getProjectType` rule and its implicit-dependency expansion
26
- * (`../../AGENTS.md`, "`src/providers/` is the only layer allowed to build a
27
- * graph"). `discoverProjects` and `buildNodes` below are the Nx-shaped
28
- * branch's own project discovery reading `project.json` is correct THERE,
29
- * because a root with no `archkeep.json` has no other source of truth to read.
30
- *
31
- * ## Why the native branch cannot reuse `discoverProjects`
9
+ * the root actually carries: `../providers/nx-static.mjs`'s static acquisition
10
+ * from the tracked `project.json` files when there is no `archkeep.json` at
11
+ * the root, `../providers/native/`'s own `discover()`/`buildGraph()` when
12
+ * there is (`buildNativeWorkspaceIndex` below), and `../providers/moon.mjs`'s
13
+ * one-call `readProjectGraph` when the root carries `.moon/` or
14
+ * `.config/moon/` (`buildMoonWorkspaceIndex`) every acquisition through the
15
+ * provider layer (`../../AGENTS.md`, "`src/providers/` is the only layer
16
+ * allowed to build a graph"), never per-surface. Before the Moon branch
17
+ * existed, this module fell through to static discovery on a `.moon`-rooted
18
+ * tree which finds a project only by its `project.json`, a file a Moon
19
+ * workspace never has and built a zero-node index that read as clean while
20
+ * `check` exited 1 on the same tree.
21
+ *
22
+ * The static acquisition is the provider layer's, not this file's: what a
23
+ * `project.json` is, how its name resolves, which node type a project gets,
24
+ * how duplicate names are recorded all of it lives in
25
+ * `../providers/nx-static.mjs`, beside the CLI-backed `readProjectGraph` it
26
+ * is the no-Nx counterpart of. This branch is the composition the seam
27
+ * contract assigns the caller: provider nodes in, annotations
28
+ * (`annotateMFERemotes`/`annotatePackageFacts`) and the workspace and the
29
+ * analysis and the edge folds composed around them, gap records out.
30
+ *
31
+ * ## Why the native branch cannot reuse the static acquisition
32
32
  *
33
33
  * A native workspace can declare or infer a project with no `project.json` at
34
- * all — the whole point of `archkeep.json` is not needing one — and
35
- * `discoverProjects` below finds nothing there. A silently missing project is
34
+ * all — the whole point of `archkeep.json` is not needing one — and static
35
+ * discovery finds nothing there. A silently missing project is
36
36
  * indistinguishable from a project that legitimately has no boundary
37
37
  * violations, which is exactly the hole `../../../../AGENTS.md`'s invariant
38
38
  * refuses ("An empty result is a claim, not a shrug"). So a root carrying
@@ -56,10 +56,11 @@
56
56
  *
57
57
  * No project name, no directory layout, no tag vocabulary (`../../AGENTS.md` —
58
58
  * the tool is installed into workspaces it has never seen). Everything below is
59
- * derived: projects from the `project.json` files that exist (Nx-shaped
60
- * branch) or from `archkeep.json`'s declared∪inferred model (native branch),
61
- * node types from `projectType` by Nx's own rule either way, tags from each
62
- * project's own list, edges from the imports the analyzers actually find.
59
+ * derived: projects from the `project.json` files that exist (static
60
+ * acquisition) or from `archkeep.json`'s declared∪inferred model (native
61
+ * branch), node types from `projectType` by Nx's own rule either way, tags
62
+ * from each project's own list, edges from the imports the analyzers actually
63
+ * find.
63
64
  *
64
65
  * ## Why the file list comes from git
65
66
  *
@@ -113,12 +114,7 @@ import { join } from "node:path";
113
114
  import { analyzeFile } from "../analysis/analyze.mjs";
114
115
  import { fileFailure, isWholeFileFailure } from "../analysis/source-util.mjs";
115
116
  import { containmentViolation } from "../containment.mjs";
116
- import { parseNxJson } from "../nx-json.mjs";
117
- import {
118
- NX_CONFIG_FILE,
119
- readWorkspaceLayout,
120
- requireCompleteWorkspaceLayout,
121
- } from "../options.mjs";
117
+ import { NX_CONFIG_FILE, readWorkspaceLayout } from "../options.mjs";
122
118
  import {
123
119
  analyzeWorkspace,
124
120
  annotateMFERemotes,
@@ -126,37 +122,17 @@ import {
126
122
  createWorkspace,
127
123
  listTrackedFiles,
128
124
  } from "../workspace.mjs";
125
+ import { PROJECT_CONFIG_FILE } from "../providers/native/discover.mjs";
129
126
  import { buildDependencies, mergeDeclaredEdges } from "../providers/native/graph.mjs";
130
127
  import {
131
128
  resolveDeclaredManifestEdges,
132
129
  resolveDeclaredManifestFailures,
133
130
  } from "../graph/create-dependencies.mjs";
134
- import { nodeTypeOf, PROJECT_CONFIG_FILE } from "../providers/native/discover.mjs";
135
131
  import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
136
132
  import { nativeProvider } from "../providers/native/index.mjs";
137
133
  import { requireSingleProjectModel } from "../providers/model-gate.mjs";
138
134
  import { mergeImportEdges, moonProvider } from "../providers/moon.mjs";
139
-
140
- export { PROJECT_CONFIG_FILE, nodeTypeOf, buildDependencies };
141
-
142
- /**
143
- * One `project.json` — or the `package.json` beside it — read the way Nx reads
144
- * it, which is NOT `JSON.parse`.
145
- *
146
- * The reader itself is `../nx-json.mjs`, shared with `../options.mjs` because
147
- * `nx.json` has to be read the same way for the same reason. This name stays as
148
- * the local one because the stakes are specific to a project config, and worth
149
- * stating where a reader of this module will look for them: losing a
150
- * `project.json` here is the worst failure this server can have. The project
151
- * leaves the graph; an import into it then resolves as external rather than
152
- * cross-project; the rule engine's npm branch returns before the tag checks
153
- * run; and the editor paints a real violation clean.
154
- *
155
- * @param {string} text
156
- * @returns {object} Whatever the JSON describes.
157
- * @throws {Error} when neither parser can read it.
158
- */
159
- const parseProjectJson = parseNxJson;
135
+ import { readStaticProjectGraph } from "../providers/nx-static.mjs";
160
136
 
161
137
  /**
162
138
  * Every file git considers part of the working tree, workspace-relative and
@@ -188,127 +164,6 @@ export function listWorkspaceFiles(root) {
188
164
  }
189
165
  }
190
166
 
191
- /** The directory part of a workspace-relative path; `""` at the tree root. */
192
- const directoryOf = (file) => {
193
- const slash = file.lastIndexOf("/");
194
- return slash === -1 ? "" : file.slice(0, slash);
195
- };
196
-
197
- /**
198
- * The projects declared in a tree, from its `project.json` files.
199
- *
200
- * A `project.json` that will not parse is SKIPPED and reported, not thrown on:
201
- * one project being edited must not blank the graph for the other nineteen. The
202
- * caller decides how loud to be about the ones that were skipped.
203
- *
204
- * @param {{files: string[], readFile: (path: string) => string|null}} tree
205
- * @returns {{projects: {name: string, root: string, config: object}[], skipped: {file: string, reason: string}[]}}
206
- */
207
- export function discoverProjects({ files, readFile }) {
208
- // used by its own test
209
- const projects = [];
210
- const skipped = [];
211
- for (const file of files) {
212
- if (file !== PROJECT_CONFIG_FILE && !file.endsWith(`/${PROJECT_CONFIG_FILE}`)) continue;
213
- const text = readFile(file);
214
- if (text === null) {
215
- skipped.push({ file, reason: "could not be read" });
216
- continue;
217
- }
218
- let config;
219
- try {
220
- config = parseProjectJson(text);
221
- } catch (cause) {
222
- skipped.push({ file, reason: `is not valid JSON: ${cause?.message ?? cause}` });
223
- continue;
224
- }
225
- const root = directoryOf(file);
226
- // Nx's own precedence: the name a project states, then the one its
227
- // `package.json` states, then the directory it lives in.
228
- const packageName = (() => {
229
- const manifest = readFile(root === "" ? "package.json" : `${root}/package.json`);
230
- if (manifest === null) return undefined;
231
- try {
232
- // The same parser, because Nx reads this file with the same
233
- // `readJsonFile` — a `package.json` Nx can name a project from must
234
- // not become a project named after its directory here.
235
- return parseProjectJson(manifest).name;
236
- } catch {
237
- return undefined;
238
- }
239
- })();
240
- const name =
241
- config.name ?? packageName ?? (root === "" ? "" : root.slice(root.lastIndexOf("/") + 1));
242
- if (typeof name !== "string" || name === "") {
243
- skipped.push({ file, reason: "declares no usable project name" });
244
- continue;
245
- }
246
- projects.push({ name, root, config });
247
- }
248
- return { projects, skipped };
249
- }
250
-
251
- /**
252
- * The graph nodes for a project list, in Nx's shape: `data` is the project's
253
- * own configuration with `tags` guaranteed present, because `../rules/tags.mjs`
254
- * reads it unguarded and an absent list is not the same fact as an empty one.
255
- *
256
- * @param {{name: string, root: string, config: object}[]} projects
257
- * @returns {{nodes: Record<string, object>, duplicateProjects: {name: string, roots: string[]}[]}}
258
- * `duplicateProjects` names every name two or more projects resolved to and
259
- * every root that claimed it (#375): a silent `nodes[name] = …` overwrite
260
- * drops the shadowed project from the graph, its files match no root, and
261
- * the editor publishes no diagnostics for real boundary crossings — the
262
- * exact silent direction `../../../../AGENTS.md`'s invariant refuses. The
263
- * first project still wins in `nodes` (the index stays usable); the caller
264
- * publishes the collision through `indexGaps`.
265
- */
266
- export function buildNodes(projects) {
267
- // used by its own test
268
- // Null-prototype for the same reason `../providers/native/graph.mjs` and
269
- // `../providers/moon.mjs` use them: every key here is a project NAME, and
270
- // project names come from a `project.json`'s own `name` field —
271
- // attacker-supplied the moment a pull request adds a project called
272
- // `__proto__`. A plain `{}` answers `nodes["__proto__"] = …` by repointing
273
- // the object's OWN prototype rather than adding an entry, so the project
274
- // vanishes from `graph.nodes` while `filesOf` still attributes it files — a
275
- // real cross-project import into it then read a poisoned Node as a graph
276
- // node and flips/throws on every rule that touches it. `Object.create(null)`
277
- // has no inherited `__proto__` accessor to collide with, so the name behaves
278
- // like every other project name: a real, own, enumerable entry.
279
- const nodes = Object.create(null);
280
- /** @type {Map<string, string>} name → root of the first project that claimed it. */
281
- const seenNames = new Map();
282
- /** @type {Map<string, string[]>} name → every root that resolved to it, for names claimed twice or more. */
283
- const duplicateMap = new Map();
284
-
285
- for (const { name, root, config } of projects) {
286
- if (seenNames.has(name)) {
287
- // Duplicate name detected — record it for loud reporting
288
- if (!duplicateMap.has(name)) {
289
- duplicateMap.set(name, [seenNames.get(name)]);
290
- }
291
- duplicateMap.get(name).push(root);
292
- // Skip adding the duplicate to nodes — first project wins
293
- continue;
294
- }
295
- seenNames.set(name, root);
296
- nodes[name] = {
297
- name,
298
- type: nodeTypeOf(name, config.projectType),
299
- data: { ...config, root, tags: config.tags ?? [] },
300
- };
301
- }
302
-
303
- // Convert the duplicate map to the expected output format
304
- const duplicateProjects = [];
305
- for (const [name, roots] of duplicateMap.entries()) {
306
- duplicateProjects.push({ name, roots });
307
- }
308
-
309
- return { nodes, duplicateProjects };
310
- }
311
-
312
167
  /**
313
168
  * Everything a diagnosis needs about the tree, computed once.
314
169
  *
@@ -372,7 +227,7 @@ export function buildWorkspaceIndex({
372
227
  const { hasNx, moonMarker } = requireSingleProjectModel(root, { exists: pathExists });
373
228
  // A root carrying ARCHKEEP_MODEL_FILE has a project model this module does
374
229
  // not read from `project.json` at all — see this file's header — so it is
375
- // handed to the native branch below rather than to `discoverProjects`.
230
+ // handed to the native branch below rather than to the static acquisition.
376
231
  //
377
232
  // Detected by READING the file, not by whether git tracks it: `../../cli.mjs`
378
233
  // and this server's own `readWorkspaceOptions` (`./server.mjs`'s `markersAt`)
@@ -381,7 +236,7 @@ export function buildWorkspaceIndex({
381
236
  // to the tree but not yet `git add`ed) exists by that test. Dispatching here
382
237
  // on `files.includes(...)` instead — `files` is the TRACKED list `listFiles`
383
238
  // returns — disagreed with both of them: this branch would fall through to
384
- // `discoverProjects`, find no `project.json` for a native-only tree, and
239
+ // the static acquisition, find no `project.json` for a native-only tree, and
385
240
  // build a zero-node, zero-edge index that publishes `analyzed: true` with an
386
241
  // empty diagnostic list on a workspace `archkeep check` exits 1 on — the gap
387
242
  // machinery below has no entry for "wrong provider" to report. `readFile`
@@ -395,8 +250,14 @@ export function buildWorkspaceIndex({
395
250
  return buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph });
396
251
  }
397
252
 
398
- const { projects, skipped } = discoverProjects({ files, readFile });
399
- const { nodes, duplicateProjects } = buildNodes(projects);
253
+ // Static acquisition see this file's header. The provider returns the
254
+ // nodes, the projects it skipped on the way, the duplicate-name records, and
255
+ // `nx.json`'s `workspaceLayout` merged onto the graph (`./nx.mjs`
256
+ // `readProjectGraph`'s own merge, in its caught-into-a-record form — that
257
+ // function's doc owns why the merge exists at all); this branch composes
258
+ // everything else around them.
259
+ const { nodes, skippedProjects, duplicateProjects, workspaceLayout, workspaceLayoutFailure } =
260
+ readStaticProjectGraph({ root, files, readFile, readLayout });
400
261
  // The same Module Federation fact the CLI path computes, from the same
401
262
  // predicate (`../workspace.mjs` → `annotateMFERemotes`): a CLI verdict and an
402
263
  // editor verdict on the same import must match, and the field failing closed
@@ -408,8 +269,8 @@ export function buildWorkspaceIndex({
408
269
  // write would fail closed — extra reports, not waived ones — but the two
409
270
  // faces would then disagree about the same import, which is the line
410
271
  // `src/lsp/` exists to hold. It also DELETES a stale `entryPoints` or
411
- // `declaredPackages` riding in from `project.json` (`buildNodes` spreads that
412
- // config into `data` verbatim), because an unmeasured claim that waives
272
+ // `declaredPackages` riding in from `project.json` (`../providers/nx-static.mjs`'s
273
+ // `buildNodes` spreads that config into `data` verbatim), because an unmeasured claim that waives
413
274
  // violations is the silent direction.
414
275
  annotatePackageFacts(nodes, readFile);
415
276
 
@@ -443,34 +304,9 @@ export function buildWorkspaceIndex({
443
304
  // silently missing edges.
444
305
  const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
445
306
 
446
- // `nx.json`'s `workspaceLayout` reaches the rule engine here the same way
447
- // `../providers/nx.mjs`'s `readProjectGraph` merges it onto the graph it
448
- // returns to `cli.mjs` — see that function's own doc for why a merge step
449
- // exists at all (`nx graph --file=` itself emits no such key) and why a
450
- // declared-but-incomplete layout is refused rather than completed
451
- // (`requireCompleteWorkspaceLayout`, `../options.mjs`). Without this, an
452
- // editor open on a workspace with a non-default `appsDir`/`libsDir` would
453
- // draw no diagnostic for exactly the import `archkeep check` flags on the
454
- // same tree — the language server's own stated rule (this package's
455
- // `AGENTS.md`, "An empty diagnostic list must mean 'no violation'"),
456
- // violated from the direction it exists to catch. A read/validation
457
- // failure is caught rather than thrown onward — one malformed `nx.json`
458
- // must not blank the whole index — and recorded as `workspaceLayoutFailure`
459
- // for `indexGaps` to turn into a diagnostic, the same shape
460
- // `nativeModelFailure` already uses for the native branch's own
461
- // model-load failure.
462
- let workspaceLayout;
463
- let workspaceLayoutFailure = null;
464
- try {
465
- const declared = requireCompleteWorkspaceLayout(readLayout(root));
466
- if (declared !== null) workspaceLayout = declared;
467
- } catch (cause) {
468
- workspaceLayoutFailure = cause?.message ?? String(cause);
469
- }
470
-
471
307
  // An Nx-marked root that yielded no project at all is a tree this branch
472
308
  // could not see the shape of, not a tree with nothing in it.
473
- // `discoverProjects` above finds a project only by its `project.json`, and a
309
+ // The static acquisition finds a project only by its `project.json`, and a
474
310
  // PACKAGE-BASED Nx workspace has none: its projects are declared in
475
311
  // `package.json` files, which this module reads only to resolve the NAME of a
476
312
  // project a `project.json` already found. `../providers/nx.mjs`'s
@@ -517,7 +353,7 @@ export function buildWorkspaceIndex({
517
353
  files,
518
354
  workspace,
519
355
  graph,
520
- skippedProjects: skipped,
356
+ skippedProjects,
521
357
  fileFailures: [...fileFailures, ...manifestRefusalFailures],
522
358
  // Retained past the graph build — the evidence half of `evaluate()`'s
523
359
  // input (`./diagnose.mjs` composes its run from these). See this module
@@ -589,8 +425,8 @@ function analyzeTrackedFiles({ files, workspace }) {
589
425
 
590
426
  /**
591
427
  * The native branch of `buildWorkspaceIndex`: drives `../providers/native/`'s
592
- * two-call contract (`discover()` then `buildGraph()`) instead of
593
- * `discoverProjects`/`buildNodes`, because a `archkeep.json` project can have no
428
+ * two-call contract (`discover()` then `buildGraph()`) instead of the static
429
+ * acquisition (`../providers/nx-static.mjs`), because a `archkeep.json` project can have no
594
430
  * `project.json` at all — see this module's header.
595
431
  *
596
432
  * @param {{root: string, files: string[], readFile: (path: string) => string|null, tsConfig?: string}} args
@@ -711,10 +547,9 @@ function buildNativeWorkspaceIndex({ root, files, readFile, tsConfig }) {
711
547
  /**
712
548
  * The Moon branch of `buildWorkspaceIndex`: drives `../providers/moon.mjs`'s
713
549
  * one-call contract (`readProjectGraph`) — the same provider object
714
- * `../commands/context.mjs` hands `check` on this tree — instead of
715
- * `discoverProjects`/`buildNodes`, because a Moon project has no `project.json`
716
- * for those to find. See this module's header for the fall-through this branch
717
- * replaces.
550
+ * `../commands/context.mjs` hands `check` on this tree — instead of the static
551
+ * acquisition, because a Moon project has no `project.json` for it to find. See
552
+ * this module's header for the fall-through this branch replaces.
718
553
  *
719
554
  * @param {{root: string, files: string[], readFile: (path: string) => string|null, tsConfig?: string, readGraph: (root: string) => object}} args
720
555
  * @returns {ReturnType<typeof buildWorkspaceIndex>}
@@ -859,7 +694,7 @@ function buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph })
859
694
  * command whose failure a developer has to resolve.
860
695
  * - **An `nxModelFailure` gap is that same family again, for the Nx-shaped
861
696
  * branch's own project discovery.** Present only while `nx.json` marks the
862
- * root AND `discoverProjects` found no project in it at all — never merely
697
+ * root AND the static acquisition found no project in it at all — never merely
863
698
  * because the root carries `nx.json`. A package-based Nx workspace (projects
864
699
  * declared in `package.json`, no `project.json` anywhere) is the shape that
865
700
  * reaches it, and reading that shape is NOT what this server does about it:
@@ -873,8 +708,8 @@ function buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph })
873
708
  * equivalent of `nativeModelFailure`, not a second copy of it.** It is
874
709
  * present only while `NX_CONFIG_FILE`'s own `workspaceLayout` is malformed
875
710
  * or declared partially (`../options.mjs`'s `readWorkspaceLayout` /
876
- * `requireCompleteWorkspaceLayout`, called from `buildWorkspaceIndex`
877
- * above) — never on the native branch, where the identically-shaped
711
+ * `requireCompleteWorkspaceLayout`, called from the static acquisition
712
+ * `../providers/nx-static.mjs`) — never on the native branch, where the identically-shaped
878
713
  * failure already surfaces as `nativeModelFailure` instead (see
879
714
  * `buildNativeWorkspaceIndex`). It **clears itself** the same way: `nx.json`
880
715
  * is already a watched file (`./server.mjs`), so fixing it republishes
package/src/options.mjs CHANGED
@@ -120,6 +120,16 @@ export const DEFAULT_OPTIONS = Object.freeze({
120
120
  /** The file Nx reads to learn a workspace exists — and where the options live. */
121
121
  export const NX_CONFIG_FILE = "nx.json";
122
122
 
123
+ /**
124
+ * The native workspace's root marker and model file — `nx.json`'s
125
+ * counterpart for a tree with no Nx at all. The options layer owns every
126
+ * filename a workspace's declaration can be read from, so the name is
127
+ * defined here rather than beside the loader that parses it
128
+ * (`./providers/native/model.mjs`, which re-exports this binding): help
129
+ * text and `--output` guards need the name without importing the loader.
130
+ */
131
+ export const ARCHKEEP_MODEL_FILE = "archkeep.json";
132
+
123
133
  /**
124
134
  * Merges a raw options object over the defaults, refusing anything it does not
125
135
  * recognise.
@@ -8,7 +8,7 @@
8
8
  * manifest matching `projects.infer` (`project.json`, `package.json`, `go.mod`,
9
9
  * `Cargo.toml`, `pyproject.toml` unless the model says otherwise) contributes
10
10
  * one more UNLESS its directory is already a declared root. Name precedence
11
- * reproduces Nx's own exactly — `discoverProjects` in `../../lsp/workspace-index.mjs`
11
+ * reproduces Nx's own exactly — `../nx-static.mjs`'s `discoverProjects`
12
12
  * is the oracle: `config.name ?? packageName ?? directoryBasename` — so a tree
13
13
  * that used to run Nx and now runs `archkeep.json` names its projects the same
14
14
  * way it always did.
@@ -21,15 +21,16 @@
21
21
  * (`../../rules/tags.mjs`) only ever needs to know a project HAS a tag, never
22
22
  * where it came from.
23
23
  *
24
- * `nodeTypeOf` and `PROJECT_CONFIG_FILE` live here rather than in
24
+ * `nodeTypeOf` and `PROJECT_CONFIG_FILE` live here rather than beside their
25
+ * consumers — `../nx-static.mjs` (the static Nx acquisition) and
25
26
  * `../../lsp/workspace-index.mjs`, which used to define them: this module is
26
27
  * the promotion target (`../../../AGENTS.md`, "`src/providers/` is the layer
27
- * that supplies a graph to `evaluate()`"), and `../../lsp/workspace-index.mjs`
28
- * now imports both from here. Defining them there instead would mean
29
- * importing `../../lsp/workspace-index.mjs` from this module which imports
30
- * `../../workspace.mjs`, which loads the TypeScript compiler at module scope
31
- * (`../../process.mjs`'s header) for two functions that need nothing beyond
32
- * a string.
28
+ * that supplies a graph to `evaluate()`"), and both consumers import them
29
+ * from here. Defining them in the acquisition instead would point this
30
+ * module at its own consumer for two functions that need nothing beyond a
31
+ * string; defining them in `../../lsp/workspace-index.mjs` would be worse
32
+ * that module imports `../../workspace.mjs`, which loads the TypeScript
33
+ * compiler at module scope (`../../process.mjs`'s header).
33
34
  */
34
35
  import { basenameMatches } from "../../analysis/manifest-util.mjs";
35
36
  import { fileFailure } from "../../analysis/source-util.mjs";
@@ -80,7 +81,7 @@ const basenameOf = (root) => (root === "" ? "" : root.slice(root.lastIndexOf("/"
80
81
  /**
81
82
  * `project.json` at `projectRoot`, parsed the same JSONC-tolerant way
82
83
  * `../../nx-json.mjs` reads every other config this package trusts —
83
- * `../../lsp/workspace-index.mjs` reads its own copy of `project.json` the
84
+ * `../nx-static.mjs` reads its copy of `project.json` the
84
85
  * same way, so a trailing comma or a comment that Nx itself accepts is not a
85
86
  * reason for this provider to disagree with it.
86
87
  *
@@ -146,7 +147,7 @@ function readProjectManifest(projectRoot, readFile, isTracked) {
146
147
  * `nx`, `dist/plugins/package-json.js`'s `createNodeFromPackageJson` reads
147
148
  * every tracked `package.json` that way, the same `jsonc-parser` Nx reads
148
149
  * `nx.json` and `project.json` with — even though `npm install` itself is
149
- * strict JSON about the same file. `../../lsp/workspace-index.mjs`'s
150
+ * strict JSON about the same file. `../nx-static.mjs`'s
150
151
  * `discoverProjects`, the oracle this module's own header cites for name
151
152
  * precedence, reads `package.json` through that same reader
152
153
  * (`parseProjectJson`, its local name for `parseNxJson`) for exactly that
@@ -355,7 +356,7 @@ export function discoverNativeProjects({ root, files, readFile, model }) {
355
356
  // gating this read on `manifest` being falsy skipped `package.json`
356
357
  // entirely for that project, landing straight on the directory basename
357
358
  // and skipping the middle rung of the precedence chain below.
358
- // `../../lsp/workspace-index.mjs`'s `discoverProjects` — the oracle this
359
+ // `../nx-static.mjs`'s `discoverProjects` — the oracle this
359
360
  // module's own header names for this exact precedence — reads
360
361
  // `package.json` the same unconditional way for the same reason.
361
362
  const { name: packageName, failure: packageFailure } = readPackageName(
@@ -364,7 +365,7 @@ export function discoverNativeProjects({ root, files, readFile, model }) {
364
365
  isTracked,
365
366
  );
366
367
  if (packageFailure) failures.push(packageFailure);
367
- // Nx's own precedence, reproduced exactly (`../../lsp/workspace-index.mjs`,
368
+ // Nx's own precedence, reproduced exactly (`../nx-static.mjs`,
368
369
  // `discoverProjects`): a declared name, then `project.json`'s, then
369
370
  // `package.json`'s, then the directory basename.
370
371
  const name = declared?.name ?? manifest?.name ?? packageName ?? basenameOf(projectRoot);
@@ -44,12 +44,17 @@
44
44
  * shows the first is the slower way to get to a working config.
45
45
  */
46
46
  import { globComplexityError, projectPatternError, safeMatchesGlob } from "../../rules/match.mjs";
47
- import { resolveOptions } from "../../options.mjs";
47
+ import { ARCHKEEP_MODEL_FILE, resolveOptions } from "../../options.mjs";
48
48
  import { findBoundaryConfigViolations, policyKeyViolations } from "../../config.mjs";
49
49
  import { describe, isPlainObject, isStringArray } from "../../values.mjs";
50
50
 
51
- /** The file this provider treats as a workspace root marker and its model. */
52
- export const ARCHKEEP_MODEL_FILE = "archkeep.json";
51
+ /**
52
+ * The file this provider treats as a workspace root marker and its model —
53
+ * defined once in `../../options.mjs`, the filename-knowing layer, and
54
+ * re-exported here under the same binding this module and every provider
55
+ * consumer already import.
56
+ */
57
+ export { ARCHKEEP_MODEL_FILE };
53
58
 
54
59
  /**
55
60
  * Copies every character of `text` to `out` up to and including the closing