@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
package/src/go-work.mjs
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* go.work drift — the workspace-level check that a developer's `go` and the Nx
|
|
3
|
+
* graph select the same set of Go modules.
|
|
4
|
+
*
|
|
5
|
+
* A `go.work` at the workspace root decides, through its `use` directives,
|
|
6
|
+
* which modules `go build` and gopls load on a developer's machine. The Nx
|
|
7
|
+
* graph covers every project carrying `<projectRoot>/go.mod`, and nothing ties
|
|
8
|
+
* the two lists together: a project whose directory is missing from `use`
|
|
9
|
+
* builds differently on the dev machine than in CI, and the divergence is
|
|
10
|
+
* byte-for-byte invisible — both sides look fine on their own. That silence is
|
|
11
|
+
* the state this package exists to end, so both directions of drift are
|
|
12
|
+
* findings and `../cli.mjs` fails the run on them the way it fails on a
|
|
13
|
+
* boundary violation.
|
|
14
|
+
*
|
|
15
|
+
* The file is read statically, never by invoking `go` — the same refusal every
|
|
16
|
+
* resolver here holds (project `AGENTS.md`, "Static analysis by design"), for
|
|
17
|
+
* the same reason: this runs on machines that never installed the toolchain.
|
|
18
|
+
* Only a TRACKED `go.work` at the workspace root is read, which is the
|
|
19
|
+
* established keying — a workspace without one pays nothing and the report
|
|
20
|
+
* says nothing about it. There is no switch.
|
|
21
|
+
*
|
|
22
|
+
* **This check runs on the CLI surface only, not in the language server.** A
|
|
23
|
+
* drift finding describes the workspace — a list in one file against a graph —
|
|
24
|
+
* not any document being edited. The LSP publishes per-document diagnostics,
|
|
25
|
+
* and pinning a workspace-level finding to whichever file happens to be open
|
|
26
|
+
* would put the report somewhere its fix is not.
|
|
27
|
+
*
|
|
28
|
+
* The grammar this reads is the documented one (go.dev/ref/mod, "Workspaces" —
|
|
29
|
+
* `go.work` shares `go.mod`'s lexical elements): `//` comments and no block
|
|
30
|
+
* comments, interpreted strings whose `\X` escape is replaced by `X`, raw
|
|
31
|
+
* backquoted strings, identifiers and strings interchangeable, one directive
|
|
32
|
+
* per line, and a keyword factored over a `( … )` block. `use` takes one
|
|
33
|
+
* directory path, absolute or relative to the directory holding `go.work`.
|
|
34
|
+
*
|
|
35
|
+
* Known parse limits, deliberate and pinned by tests. The Go analyzers' limits
|
|
36
|
+
* each err toward a spurious record; a manifest read errs the same way a
|
|
37
|
+
* malformed boundary config does — it stops the run loudly — because here the
|
|
38
|
+
* silent direction is an EMPTY use list, and a malformed `go.work` read as "no
|
|
39
|
+
* drift" is exactly the false green this check exists to remove:
|
|
40
|
+
*
|
|
41
|
+
* - **A `go.work` this parser cannot read throws, never returns fewer
|
|
42
|
+
* entries.** An unterminated block or string, a `use` without a path, stray
|
|
43
|
+
* tokens, or a keyword outside go.work's five (below) each name their line;
|
|
44
|
+
* `../cli.mjs` turns the throw into a whole-file failure, so the run exits 3
|
|
45
|
+
* (no verdict), not 0.
|
|
46
|
+
* - **A string may not span a line.** Legal `go.mod` syntax keeps strings on
|
|
47
|
+
* one line; a raw string left open runs to the end of the line and throws as
|
|
48
|
+
* unterminated rather than silently swallowing the directives below it.
|
|
49
|
+
* - **Only go.work's own five directive keywords are skipped as unread,
|
|
50
|
+
* blocks included.** `use` is read; `go`, `toolchain`, `godebug` and
|
|
51
|
+
* `replace` are recognized and passed over without being mistaken for
|
|
52
|
+
* paths. Any other keyword throws — verified against `go work edit` itself
|
|
53
|
+
* (go1.24.7), which rejects one as `"unknown directive"` rather than
|
|
54
|
+
* skipping it, so text that is not a go.work file at all (an HTML error
|
|
55
|
+
* page from a bad download, a truncated checkout) is read as unparseable,
|
|
56
|
+
* never as zero `use` entries.
|
|
57
|
+
* - **Paths are compared with `/` separators.** A `\`-separated Windows path
|
|
58
|
+
* or a drive-letter absolute path matches no project root and surfaces as a
|
|
59
|
+
* drift finding naming the text the file really contains — loud, never
|
|
60
|
+
* silent.
|
|
61
|
+
*
|
|
62
|
+
* The comparison follows the graph's model — one module per project root,
|
|
63
|
+
* `<projectRoot>/go.mod` (project `AGENTS.md`, "One module/crate/package per
|
|
64
|
+
* project root"). Consequences, both deliberate: a nested `go.mod` is never
|
|
65
|
+
* REQUIRED to appear in `use`, because the graph does not model it either; but
|
|
66
|
+
* a `use` entry that names one is reported, because that is the moment the
|
|
67
|
+
* developer's build and the graph demonstrably diverge — the module builds
|
|
68
|
+
* locally while `nx affected` and the boundary check never see it.
|
|
69
|
+
*/
|
|
70
|
+
import { isAbsolute, posix, relative, sep } from "node:path";
|
|
71
|
+
|
|
72
|
+
import { projectOwning } from "./analysis/source-util.mjs";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What each drift finding means — one entry per `messageId` a finding can
|
|
76
|
+
* carry. `../report/sarif.mjs` derives its rule descriptors from this table,
|
|
77
|
+
* the same arrangement it has with `../rules/messages.mjs`, so a kind added
|
|
78
|
+
* here cannot be nameless in a code-scanning upload.
|
|
79
|
+
*/
|
|
80
|
+
export const GO_WORK_MESSAGES = Object.freeze({
|
|
81
|
+
goWorkMissingUse:
|
|
82
|
+
"A project's go.mod is not in go.work's use list: a developer's go build and gopls skip a " +
|
|
83
|
+
"module the Nx graph covers, so dev machines and CI select different module sets.",
|
|
84
|
+
goWorkStaleUse:
|
|
85
|
+
"A go.work use entry names a directory with no tracked go.mod: go commands fail on developer " +
|
|
86
|
+
"machines while CI, which never reads go.work, stays green.",
|
|
87
|
+
goWorkUnmodeledUse:
|
|
88
|
+
"A go.work use entry names a module the Nx graph does not model: it builds on developer " +
|
|
89
|
+
"machines while nx affected and the boundary check never see it.",
|
|
90
|
+
goWorkOutsideUse:
|
|
91
|
+
"A go.work use entry points outside the workspace: developer builds include a module no run " +
|
|
92
|
+
"over this workspace can cover.",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const GO_WORK_MESSAGE_IDS = Object.freeze(Object.keys(GO_WORK_MESSAGES));
|
|
96
|
+
|
|
97
|
+
/** A parse failure that names its line, so the failure record is actionable. */
|
|
98
|
+
const parseError = (line, reason) => new Error(`go.work:${line}: ${reason}`);
|
|
99
|
+
|
|
100
|
+
/** The characters that end an identifier token — go.mod's own boundaries. */
|
|
101
|
+
const IDENTIFIER_BOUNDARY = new Set([" ", "\t", "\r", '"', "`", "(", ")"]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One line of go.work as tokens: `(`/`)` punctuation, identifiers, and the
|
|
105
|
+
* unquoted values of interpreted and raw strings. A `//` between tokens starts
|
|
106
|
+
* a comment that runs to the end of the line; inside an identifier a `/` is an
|
|
107
|
+
* ordinary path character, which is how `./a` and `example.com/x` stay whole.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} line One line, without its newline.
|
|
110
|
+
* @param {number} lineNumber 1-based, for error messages.
|
|
111
|
+
* @returns {{ value: string, column: number, punct: boolean }[]} `column` is
|
|
112
|
+
* 1-based and points at the token's first character in the line.
|
|
113
|
+
* @throws {Error} on an unterminated string — a string left open would
|
|
114
|
+
* otherwise swallow the rest of the line silently.
|
|
115
|
+
*/
|
|
116
|
+
export function tokenizeGoWorkLine(line, lineNumber) {
|
|
117
|
+
const tokens = [];
|
|
118
|
+
let at = 0;
|
|
119
|
+
while (at < line.length) {
|
|
120
|
+
const char = line[at];
|
|
121
|
+
if (char === " " || char === "\t" || char === "\r") {
|
|
122
|
+
at += 1;
|
|
123
|
+
} else if (char === "/" && line[at + 1] === "/") {
|
|
124
|
+
break;
|
|
125
|
+
} else if (char === "(" || char === ")") {
|
|
126
|
+
tokens.push({ value: char, column: at + 1, punct: true });
|
|
127
|
+
at += 1;
|
|
128
|
+
} else if (char === '"') {
|
|
129
|
+
// Interpreted string: each `\X` escape is replaced by `X`, which is the
|
|
130
|
+
// documented unquoting — `\"` is `"`, and `\n` is the letter n.
|
|
131
|
+
let value = "";
|
|
132
|
+
let cursor = at + 1;
|
|
133
|
+
while (cursor < line.length && line[cursor] !== '"') {
|
|
134
|
+
if (line[cursor] === "\\" && cursor + 1 < line.length) {
|
|
135
|
+
value += line[cursor + 1];
|
|
136
|
+
cursor += 2;
|
|
137
|
+
} else {
|
|
138
|
+
value += line[cursor];
|
|
139
|
+
cursor += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (cursor >= line.length) {
|
|
143
|
+
throw parseError(lineNumber, "unterminated quoted string");
|
|
144
|
+
}
|
|
145
|
+
tokens.push({ value, column: at + 1, punct: false });
|
|
146
|
+
at = cursor + 1;
|
|
147
|
+
} else if (char === "`") {
|
|
148
|
+
// Raw string: no escapes. One that never closes would swallow every
|
|
149
|
+
// directive after it, so it throws instead — the header's second limit.
|
|
150
|
+
const close = line.indexOf("`", at + 1);
|
|
151
|
+
if (close === -1) {
|
|
152
|
+
throw parseError(lineNumber, "unterminated raw string (strings may not span a line)");
|
|
153
|
+
}
|
|
154
|
+
tokens.push({ value: line.slice(at + 1, close), column: at + 1, punct: false });
|
|
155
|
+
at = close + 1;
|
|
156
|
+
} else {
|
|
157
|
+
let end = at;
|
|
158
|
+
while (end < line.length && !IDENTIFIER_BOUNDARY.has(line[end])) end += 1;
|
|
159
|
+
tokens.push({ value: line.slice(at, end), column: at + 1, punct: false });
|
|
160
|
+
at = end;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return tokens;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* go.work's own directive keywords — verified against `go work edit` itself
|
|
168
|
+
* (go1.24.7): a keyword outside this set is `go`'s own `"unknown directive"`
|
|
169
|
+
* parse error, not a directive the format tolerates. `use` is the only one
|
|
170
|
+
* this parser reads; the other four are recognized so their lines (and, for
|
|
171
|
+
* `replace`, their block bodies) can be skipped without being mistaken for
|
|
172
|
+
* paths — the header's "only five keywords are skipped as unread" limit.
|
|
173
|
+
*/
|
|
174
|
+
const KNOWN_DIRECTIVES = new Set(["go", "toolchain", "use", "replace", "godebug"]);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Every `use` directive in a go.work, single-line and block form, with the
|
|
178
|
+
* position of each path as written.
|
|
179
|
+
*
|
|
180
|
+
* Malformed input **throws** rather than returning what was readable so far:
|
|
181
|
+
* this list's emptiness is the fact the drift check turns on, and a use list
|
|
182
|
+
* truncated by a parse problem would read as projects the developer removed —
|
|
183
|
+
* or, worse, as no drift at all (the header's first limit). A keyword outside
|
|
184
|
+
* go.work's own five throws for the same reason: text that only coincidentally
|
|
185
|
+
* tokenizes (an HTML error page, a truncated download) must not read as a
|
|
186
|
+
* go.work with zero `use` entries.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} text The go.work file's contents.
|
|
189
|
+
* @returns {{ path: string, line: number, column: number }[]} In file order;
|
|
190
|
+
* `path` is unquoted but not normalized, so a finding can cite it as written.
|
|
191
|
+
* @throws {Error} naming the offending line.
|
|
192
|
+
*/
|
|
193
|
+
export function parseGoWorkUse(text) {
|
|
194
|
+
const uses = [];
|
|
195
|
+
/** @type {{ keyword: string, line: number }|null} */
|
|
196
|
+
let block = null;
|
|
197
|
+
const lines = text.split("\n");
|
|
198
|
+
for (let index = 0; index < lines.length; index++) {
|
|
199
|
+
const lineNumber = index + 1;
|
|
200
|
+
const tokens = tokenizeGoWorkLine(lines[index], lineNumber);
|
|
201
|
+
if (tokens.length === 0) continue;
|
|
202
|
+
|
|
203
|
+
if (block !== null) {
|
|
204
|
+
if (tokens[0].punct && tokens[0].value === ")") {
|
|
205
|
+
if (tokens.length > 1) {
|
|
206
|
+
throw parseError(lineNumber, "unexpected text after the ')' closing a block");
|
|
207
|
+
}
|
|
208
|
+
block = null;
|
|
209
|
+
} else if (block.keyword !== "use") {
|
|
210
|
+
// Another directive's block body — skipped, never read as paths.
|
|
211
|
+
} else if (tokens.length !== 1 || tokens[0].punct) {
|
|
212
|
+
throw parseError(lineNumber, "a use block line must hold exactly one directory path");
|
|
213
|
+
} else {
|
|
214
|
+
uses.push({ path: tokens[0].value, line: lineNumber, column: tokens[0].column });
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const [keyword, ...rest] = tokens;
|
|
220
|
+
if (keyword.punct) {
|
|
221
|
+
throw parseError(lineNumber, `unexpected '${keyword.value}' outside any block`);
|
|
222
|
+
}
|
|
223
|
+
if (rest.length === 1 && rest[0].punct && rest[0].value === "(") {
|
|
224
|
+
if (!KNOWN_DIRECTIVES.has(keyword.value)) {
|
|
225
|
+
throw parseError(lineNumber, `unknown block type: ${keyword.value}`);
|
|
226
|
+
}
|
|
227
|
+
block = { keyword: keyword.value, line: lineNumber };
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (keyword.value !== "use") {
|
|
231
|
+
if (!KNOWN_DIRECTIVES.has(keyword.value)) {
|
|
232
|
+
throw parseError(lineNumber, `unknown directive: ${keyword.value}`);
|
|
233
|
+
}
|
|
234
|
+
continue; // go, toolchain, replace, godebug one-liners
|
|
235
|
+
}
|
|
236
|
+
if (rest.length !== 1 || rest[0].punct) {
|
|
237
|
+
throw parseError(
|
|
238
|
+
lineNumber,
|
|
239
|
+
rest.length === 0
|
|
240
|
+
? "'use' needs a directory path"
|
|
241
|
+
: "'use' takes exactly one directory path (the block form puts '(' at the end of the line)",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
uses.push({ path: rest[0].value, line: lineNumber, column: rest[0].column });
|
|
245
|
+
}
|
|
246
|
+
if (block !== null) {
|
|
247
|
+
throw parseError(block.line, `'${block.keyword} (' block is never closed`);
|
|
248
|
+
}
|
|
249
|
+
return uses;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Nx spells the workspace-root project's root as "." where files use "". */
|
|
253
|
+
const normalizeProjectRoot = (root) => (root === "." || root == null ? "" : root);
|
|
254
|
+
|
|
255
|
+
/** A directory for display, with the one root spelling that needs words. */
|
|
256
|
+
const displayDir = (dir) => (dir === "" ? "the workspace root" : dir);
|
|
257
|
+
|
|
258
|
+
/** The `use` argument that would name `dir`, for a fix the reader can paste. */
|
|
259
|
+
const useSpelling = (dir) => (dir === "" ? "." : `./${dir}`);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A use path as a workspace-relative directory, or `null` when it points
|
|
263
|
+
* outside the workspace. `go.work` sits at the workspace root, so a relative
|
|
264
|
+
* path is relative to exactly that directory.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} usePath As written (unquoted).
|
|
267
|
+
* @param {string} workspaceRoot Absolute.
|
|
268
|
+
* @returns {string|null} `""` for the root itself.
|
|
269
|
+
*/
|
|
270
|
+
function useDirectory(usePath, workspaceRoot) {
|
|
271
|
+
if (isAbsolute(usePath)) {
|
|
272
|
+
const rel = relative(workspaceRoot, usePath).split(sep).join("/");
|
|
273
|
+
return rel === ".." || rel.startsWith("../") || isAbsolute(rel) ? null : rel;
|
|
274
|
+
}
|
|
275
|
+
let dir = posix.normalize(usePath);
|
|
276
|
+
while (dir.endsWith("/")) dir = dir.slice(0, -1);
|
|
277
|
+
if (dir === ".") dir = "";
|
|
278
|
+
return dir === ".." || dir.startsWith("../") ? null : dir;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The drift between a go.work use list and the graph's Go module projects —
|
|
283
|
+
* pure, facts as arguments, so the tests need no filesystem.
|
|
284
|
+
*
|
|
285
|
+
* Both directions are findings, because both mean the developer's `go` and CI
|
|
286
|
+
* disagree about which modules exist:
|
|
287
|
+
*
|
|
288
|
+
* - **`goWorkMissingUse`** — a project with a tracked `<projectRoot>/go.mod`
|
|
289
|
+
* whose directory no `use` entry names. The graph judges it; the developer's
|
|
290
|
+
* build skips it.
|
|
291
|
+
* - **`goWorkStaleUse`** — a `use` entry with no tracked `go.mod` at its
|
|
292
|
+
* directory. `go` itself refuses such an entry, so the developer's build is
|
|
293
|
+
* broken (or the `go.mod` is untracked, which reads the same to a tool whose
|
|
294
|
+
* file set is `git ls-files`) while CI never notices.
|
|
295
|
+
* - **`goWorkUnmodeledUse`** — a `use` entry whose directory has a tracked
|
|
296
|
+
* `go.mod` but is no project root: a module nested inside a project, or
|
|
297
|
+
* inside no project at all. It builds locally; the graph draws no edge and
|
|
298
|
+
* no boundary verdict for it (the header's modeling note).
|
|
299
|
+
* - **`goWorkOutsideUse`** — a `use` entry above the workspace root. Whatever
|
|
300
|
+
* it builds, no run over this workspace can cover it.
|
|
301
|
+
*
|
|
302
|
+
* @param {{ uses: { path: string, line: number, column: number }[],
|
|
303
|
+
* workspaceRoot: string,
|
|
304
|
+
* projects: { name: string, root: string }[],
|
|
305
|
+
* files: string[] }} facts `files` is the tracked-file list; `projects`
|
|
306
|
+
* comes from the graph.
|
|
307
|
+
* @returns {{ findings: { messageId: string, file: string, line: number|null,
|
|
308
|
+
* column: number|null, message: string, directory: string|null,
|
|
309
|
+
* project: string|null }[], moduleProjects: number }} `moduleProjects`
|
|
310
|
+
* counts the go.mod-carrying projects, so the report can state the check's
|
|
311
|
+
* coverage beside its verdict.
|
|
312
|
+
*/
|
|
313
|
+
export function compareGoWork({ uses, workspaceRoot, projects, files }) {
|
|
314
|
+
const tracked = new Set(files);
|
|
315
|
+
const goModAt = (dir) => (dir === "" ? "go.mod" : `${dir}/go.mod`);
|
|
316
|
+
const normalizedProjects = projects.map((project) => ({
|
|
317
|
+
name: project.name,
|
|
318
|
+
root: normalizeProjectRoot(project.root),
|
|
319
|
+
}));
|
|
320
|
+
const moduleProjects = normalizedProjects.filter((project) => tracked.has(goModAt(project.root)));
|
|
321
|
+
const projectByRoot = new Map(moduleProjects.map((project) => [project.root, project]));
|
|
322
|
+
|
|
323
|
+
const findings = [];
|
|
324
|
+
const usedDirs = new Set();
|
|
325
|
+
for (const use of uses) {
|
|
326
|
+
const site = { file: "go.work", line: use.line, column: use.column };
|
|
327
|
+
const dir = useDirectory(use.path, workspaceRoot);
|
|
328
|
+
if (dir === null) {
|
|
329
|
+
findings.push({
|
|
330
|
+
messageId: "goWorkOutsideUse",
|
|
331
|
+
...site,
|
|
332
|
+
directory: null,
|
|
333
|
+
project: null,
|
|
334
|
+
message:
|
|
335
|
+
`go.work uses "${use.path}", which lies outside the workspace — a developer's go ` +
|
|
336
|
+
`build includes a module that no graph, affected run, or boundary check over this ` +
|
|
337
|
+
`workspace can ever cover.`,
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
usedDirs.add(dir);
|
|
342
|
+
if (projectByRoot.has(dir)) continue;
|
|
343
|
+
if (tracked.has(goModAt(dir))) {
|
|
344
|
+
const owner = projectOwning(normalizedProjects, goModAt(dir));
|
|
345
|
+
findings.push({
|
|
346
|
+
messageId: "goWorkUnmodeledUse",
|
|
347
|
+
...site,
|
|
348
|
+
directory: dir,
|
|
349
|
+
project: owner?.name ?? null,
|
|
350
|
+
message:
|
|
351
|
+
`go.work uses "${use.path}", whose go.mod is ` +
|
|
352
|
+
(owner
|
|
353
|
+
? `nested inside project "${owner.name}" — the graph models one module per project ` +
|
|
354
|
+
`root (<projectRoot>/go.mod), so this module builds on developer machines while ` +
|
|
355
|
+
`nx affected and the boundary check draw no edges for it. Split it into its own ` +
|
|
356
|
+
`Nx project.`
|
|
357
|
+
: `inside no Nx project — the module builds on developer machines while nx affected ` +
|
|
358
|
+
`and the boundary check never see it. Declare a project there so the graph ` +
|
|
359
|
+
`models it.`),
|
|
360
|
+
});
|
|
361
|
+
} else {
|
|
362
|
+
findings.push({
|
|
363
|
+
messageId: "goWorkStaleUse",
|
|
364
|
+
...site,
|
|
365
|
+
directory: dir,
|
|
366
|
+
project: null,
|
|
367
|
+
message:
|
|
368
|
+
`go.work uses "${use.path}", but the workspace has no tracked go.mod at ` +
|
|
369
|
+
`${displayDir(dir)} — the entry names a module this workspace does not contain ` +
|
|
370
|
+
`(moved, deleted, or untracked), so go commands fail on developer machines while CI, ` +
|
|
371
|
+
`which never reads go.work, stays green. Remove the entry, or track the module.`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const project of moduleProjects) {
|
|
377
|
+
if (usedDirs.has(project.root)) continue;
|
|
378
|
+
findings.push({
|
|
379
|
+
messageId: "goWorkMissingUse",
|
|
380
|
+
file: "go.work",
|
|
381
|
+
line: null,
|
|
382
|
+
column: null,
|
|
383
|
+
directory: project.root,
|
|
384
|
+
project: project.name,
|
|
385
|
+
message:
|
|
386
|
+
`go.work does not use ${displayDir(project.root)} — the graph covers project ` +
|
|
387
|
+
`"${project.name}" (${goModAt(project.root)}), but a developer's go build and gopls ` +
|
|
388
|
+
`skip it, so dev machines and CI select different module sets. Add ` +
|
|
389
|
+
`"use ${useSpelling(project.root)}" to go.work.`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { findings, moduleProjects: moduleProjects.length };
|
|
394
|
+
}
|