@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue SFC analyzer — the official SFC parser finds the `<script>` blocks, the
|
|
3
|
+
* TypeScript analyzer next door reads the imports inside them.
|
|
4
|
+
*
|
|
5
|
+
* ## Positions are mapped by blanking, not by arithmetic
|
|
6
|
+
*
|
|
7
|
+
* A diagnostic that names the wrong line is worse than no diagnostic: it sends
|
|
8
|
+
* a reader to code that is not the problem, and it does it silently. The
|
|
9
|
+
* obvious implementation — analyze `block.content`, then add the block's start
|
|
10
|
+
* line to every result — has an off-by-one at the first line of the block
|
|
11
|
+
* (whose column is offset too, not just its line), and it needs a second,
|
|
12
|
+
* different correction for a second `<script>` block.
|
|
13
|
+
*
|
|
14
|
+
* So no arithmetic happens at all. Each block is analyzed in a copy of the
|
|
15
|
+
* WHOLE file with every character outside that block replaced by a space,
|
|
16
|
+
* newlines kept. The text handed to TypeScript therefore has the block's code
|
|
17
|
+
* at exactly the offsets, lines, and columns it occupies in the `.vue` file,
|
|
18
|
+
* and every position TypeScript reports is already a `.vue` position. There is
|
|
19
|
+
* nothing left to get wrong, and `@vue/compiler-sfc` guarantees the one fact
|
|
20
|
+
* it relies on: `text.slice(block.loc.start.offset, block.loc.end.offset)` is
|
|
21
|
+
* exactly `block.content`.
|
|
22
|
+
*
|
|
23
|
+
* The cost is one string copy per script block, which is bounded by the file.
|
|
24
|
+
*
|
|
25
|
+
* ## Why the parser is loaded lazily
|
|
26
|
+
*
|
|
27
|
+
* `vue/compiler-sfc` is a public entry point of `vue`, a workspace dependency,
|
|
28
|
+
* and the SFC format is Vue's own — a hand-rolled `<script>` extractor would
|
|
29
|
+
* be a second answer to a question its author already answers, and would be
|
|
30
|
+
* wrong first on the cases that motivate a real parser (a `</script>` inside a
|
|
31
|
+
* string, `<script>` and `<script setup>` together, a custom block).
|
|
32
|
+
*
|
|
33
|
+
* It is still loaded on first use rather than at import time, through
|
|
34
|
+
* `createRequire`, for two reasons. This tool runs over trees that have no Vue
|
|
35
|
+
* at all — a pure Go or Rust workspace does not depend on it — and a
|
|
36
|
+
* top-level import would make a missing `vue` break Go, Rust, and Python
|
|
37
|
+
* analysis in a workspace that has no `.vue` file to analyze. And the contract
|
|
38
|
+
* says an analyzer never throws: a missing parser becomes a failure record
|
|
39
|
+
* naming what is absent, like any other thing this layer could not do.
|
|
40
|
+
*/
|
|
41
|
+
import { createRequire } from "node:module";
|
|
42
|
+
|
|
43
|
+
import { emptyResult, fileFailure } from "./source-util.mjs";
|
|
44
|
+
import { analyzeTypeScript } from "./typescript.mjs";
|
|
45
|
+
|
|
46
|
+
/** The SFC parser's specifier, named once — the failure message quotes it. */
|
|
47
|
+
const COMPILER_SFC = "vue/compiler-sfc";
|
|
48
|
+
|
|
49
|
+
/** Resolved once, success or failure, and remembered either way. */
|
|
50
|
+
let parserLoad = null;
|
|
51
|
+
|
|
52
|
+
function sfcParse() {
|
|
53
|
+
if (parserLoad === null) {
|
|
54
|
+
try {
|
|
55
|
+
parserLoad = { parse: createRequire(import.meta.url)(COMPILER_SFC).parse, error: null };
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
parserLoad = { parse: null, error: cause?.message ?? String(cause) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (parserLoad.parse === null) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`'${COMPILER_SFC}' is not installed, so no .vue file can be analyzed: ${parserLoad.error}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return parserLoad.parse;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `text` with everything outside `[start, end)` replaced by spaces. */
|
|
69
|
+
function isolate(text, start, end) {
|
|
70
|
+
const blank = (part) => part.replace(/[^\n]/g, " ");
|
|
71
|
+
return blank(text.slice(0, start)) + text.slice(start, end) + blank(text.slice(end));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A compiler-sfc error as a failure record. Its `loc` is already 1-based in
|
|
76
|
+
* the `.vue` file's own coordinates; a plain `SyntaxError` carries none, and
|
|
77
|
+
* becomes a file-level failure.
|
|
78
|
+
*/
|
|
79
|
+
function sfcFailure(sourceFile, error) {
|
|
80
|
+
const start = error?.loc?.start;
|
|
81
|
+
return {
|
|
82
|
+
sourceFile,
|
|
83
|
+
line: start?.line ?? null,
|
|
84
|
+
column: start?.column ?? null,
|
|
85
|
+
reason: `Vue SFC parse error: ${error?.message ?? error}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Analyzes one `.vue` file.
|
|
91
|
+
*
|
|
92
|
+
* Both script blocks are analyzed when both exist — `<script>` and
|
|
93
|
+
* `<script setup>` legitimately coexist, and an import in either is an import
|
|
94
|
+
* of the component. A file with no script block yields the empty envelope and
|
|
95
|
+
* no failure: a template-only SFC imports nothing, which is not an error. A
|
|
96
|
+
* `<script>`/`<script setup>` tag the SFC parser could not recover to EOF is
|
|
97
|
+
* different — its content is unknown, not empty — and yields a whole-file
|
|
98
|
+
* failure rather than a silently clean result.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ sourceFile: string, text: string, workspace: object }} request
|
|
101
|
+
* @returns {{ imports: object[], failures: object[] }}
|
|
102
|
+
*/
|
|
103
|
+
export function analyzeVue({ sourceFile, text, workspace }) {
|
|
104
|
+
const result = emptyResult();
|
|
105
|
+
try {
|
|
106
|
+
const parse = sfcParse();
|
|
107
|
+
const { descriptor, errors } = parse(text, { filename: sourceFile });
|
|
108
|
+
for (const error of errors) result.failures.push(sfcFailure(sourceFile, error));
|
|
109
|
+
|
|
110
|
+
for (const block of [descriptor.script, descriptor.scriptSetup]) {
|
|
111
|
+
if (!block) continue;
|
|
112
|
+
// `ignoreEmpty` (compiler-sfc's own default) already drops a genuinely
|
|
113
|
+
// empty, well-formed `<script>`/`<script setup>` to `null` before this
|
|
114
|
+
// loop ever sees it — verified against the installed compiler-sfc's own
|
|
115
|
+
// source. The only way a truthy block reaches here with a zero-width
|
|
116
|
+
// span (`loc.start.offset === loc.end.offset`) and no `src` is the SFC
|
|
117
|
+
// parser failing to find the block's end tag before EOF: it still
|
|
118
|
+
// returns a block object, but one carrying none of the file's bytes,
|
|
119
|
+
// which `isolate` below would otherwise blank to nothing and read as
|
|
120
|
+
// "no imports" — indistinguishable from a legitimate template-only
|
|
121
|
+
// component. `!block.src` is required because `<script src="./x.ts" />`
|
|
122
|
+
// legitimately has an empty inline body and the SAME zero-width span —
|
|
123
|
+
// its content lives in a file this analyzer does not read, an
|
|
124
|
+
// already-accepted limit, not this failure.
|
|
125
|
+
if (block.loc.start.offset === block.loc.end.offset && !block.src) {
|
|
126
|
+
result.failures.push(
|
|
127
|
+
fileFailure(
|
|
128
|
+
sourceFile,
|
|
129
|
+
`Vue SFC parse error: a <script${block === descriptor.scriptSetup ? " setup" : ""}> ` +
|
|
130
|
+
`block could not be recovered (its end tag was not found), so any imports inside ` +
|
|
131
|
+
`it are unknown`,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const isolated = isolate(text, block.loc.start.offset, block.loc.end.offset);
|
|
137
|
+
const analyzed = analyzeTypeScript({
|
|
138
|
+
sourceFile,
|
|
139
|
+
text: isolated,
|
|
140
|
+
workspace,
|
|
141
|
+
// No `lang` means plain JavaScript, which is Vue's own default.
|
|
142
|
+
lang: block.lang ?? "js",
|
|
143
|
+
});
|
|
144
|
+
result.imports.push(...analyzed.imports);
|
|
145
|
+
result.failures.push(...analyzed.failures);
|
|
146
|
+
}
|
|
147
|
+
} catch (cause) {
|
|
148
|
+
result.failures.push(
|
|
149
|
+
fileFailure(sourceFile, `Vue analysis failed: ${cause?.message ?? cause}`),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
// Two blocks are analyzed in sequence, so their records arrive block by
|
|
153
|
+
// block; `contract.md` promises source order over the file.
|
|
154
|
+
result.imports.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The intent fingerprint: canonical SHA-256 of the parsed intent, so a
|
|
3
|
+
* consumer can tell "the tree drifted" from "the contract changed" between two
|
|
4
|
+
* runs.
|
|
5
|
+
*
|
|
6
|
+
* This is the same canonicalization `computePolicyFingerprint` uses (the
|
|
7
|
+
* shared `canonicalizeJson` sorts object keys at every depth, never array
|
|
8
|
+
* elements) — an intentional change that deletes an intent row, or re-orders
|
|
9
|
+
* one, moves the fingerprint even when the observed tree is unchanged. The
|
|
10
|
+
* fingerprint is *directional* metadata a human or a pipeline diffing two
|
|
11
|
+
* `drift --format json` outputs reads; the engine does not keep a baseline
|
|
12
|
+
* intent file and auto-detect changes to it, because that is `diff`'s job and
|
|
13
|
+
* would be a second mechanism. What it does not do is pass judgment: a
|
|
14
|
+
* fingerprint difference is exactly as meaningful as the intent rows that
|
|
15
|
+
* moved, and only the author knows whether the move was intentional — so the
|
|
16
|
+
* engine reports the change, never calls it drift.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
import { canonicalizeJson } from "../canonical.mjs";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} intent The normalized intent model from `./model.mjs`.
|
|
24
|
+
* @returns {string} A hex-encoded SHA-256 fingerprint.
|
|
25
|
+
*/
|
|
26
|
+
export function computeIntentFingerprint(intent) {
|
|
27
|
+
const canonical = canonicalizeJson(intent);
|
|
28
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
29
|
+
}
|