@ecoma-io/archkeep 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +262 -0
  3. package/cli.mjs +2792 -0
  4. package/index.mjs +85 -0
  5. package/lsp.mjs +81 -0
  6. package/nx.mjs +24 -0
  7. package/package.json +81 -0
  8. package/presets/clean-architecture.json +78 -0
  9. package/presets/ddd-bounded-contexts.json +88 -0
  10. package/presets/hexagonal.json +68 -0
  11. package/presets/layered.json +92 -0
  12. package/presets/modular-monolith.json +85 -0
  13. package/presets/vertical-slice.json +68 -0
  14. package/src/analysis/analyze.mjs +218 -0
  15. package/src/analysis/contract.md +259 -0
  16. package/src/analysis/go.mjs +414 -0
  17. package/src/analysis/manifest-util.mjs +68 -0
  18. package/src/analysis/python.mjs +1266 -0
  19. package/src/analysis/registry.mjs +74 -0
  20. package/src/analysis/rust.mjs +674 -0
  21. package/src/analysis/source-util.mjs +230 -0
  22. package/src/analysis/typescript.mjs +1034 -0
  23. package/src/analysis/vue.mjs +156 -0
  24. package/src/architecture-intent/intent-fingerprint.mjs +29 -0
  25. package/src/architecture-intent/judge.mjs +539 -0
  26. package/src/architecture-intent/model.mjs +703 -0
  27. package/src/architecture-intent/selectors.mjs +170 -0
  28. package/src/canonical.mjs +48 -0
  29. package/src/commands/README.md +266 -0
  30. package/src/commands/adr.mjs +248 -0
  31. package/src/commands/check.mjs +989 -0
  32. package/src/commands/context-command.mjs +212 -0
  33. package/src/commands/context.mjs +790 -0
  34. package/src/commands/custom-rules.mjs +428 -0
  35. package/src/commands/debt.mjs +218 -0
  36. package/src/commands/diff.mjs +523 -0
  37. package/src/commands/discover.mjs +159 -0
  38. package/src/commands/drift.mjs +473 -0
  39. package/src/commands/edge-constraints.mjs +355 -0
  40. package/src/commands/explain.mjs +359 -0
  41. package/src/commands/fitness.mjs +226 -0
  42. package/src/commands/graph.mjs +297 -0
  43. package/src/commands/health.mjs +213 -0
  44. package/src/commands/history.mjs +614 -0
  45. package/src/commands/impact.mjs +226 -0
  46. package/src/commands/plan-context-command.mjs +496 -0
  47. package/src/commands/policy.mjs +138 -0
  48. package/src/commands/provenance-command.mjs +352 -0
  49. package/src/commands/provenance.mjs +159 -0
  50. package/src/commands/reconcile.mjs +219 -0
  51. package/src/commands/report.mjs +553 -0
  52. package/src/commands/snapshot-meta.mjs +107 -0
  53. package/src/commands/waivers.mjs +240 -0
  54. package/src/config.mjs +1308 -0
  55. package/src/containment.mjs +234 -0
  56. package/src/custom-rules/evidence.mjs +340 -0
  57. package/src/custom-rules/host.mjs +1023 -0
  58. package/src/custom-rules/values.mjs +43 -0
  59. package/src/entry-point.mjs +55 -0
  60. package/src/errors.mjs +36 -0
  61. package/src/eslint-config.mjs +542 -0
  62. package/src/go-work.mjs +394 -0
  63. package/src/governance/adr-registry.mjs +539 -0
  64. package/src/governance/clock.mjs +69 -0
  65. package/src/governance/debt-ledger.mjs +274 -0
  66. package/src/governance/discovery-proposal.mjs +423 -0
  67. package/src/governance/fitness-registry.mjs +504 -0
  68. package/src/governance/fitness-rules.mjs +668 -0
  69. package/src/governance/metrics.mjs +392 -0
  70. package/src/governance/preset-fingerprints.json +16 -0
  71. package/src/governance/profile-registry.mjs +366 -0
  72. package/src/governance/provenance-record.mjs +177 -0
  73. package/src/governance/reconcile-candidates.mjs +301 -0
  74. package/src/governance/reconcile-score.mjs +503 -0
  75. package/src/governance/row-schema.mjs +208 -0
  76. package/src/governance/verdict.mjs +127 -0
  77. package/src/governance/waiver.mjs +105 -0
  78. package/src/graph/create-dependencies.mjs +96 -0
  79. package/src/intent/intent-manifest.json +347 -0
  80. package/src/intent/mask-non-code.mjs +640 -0
  81. package/src/lsp/boundary-config.mjs +225 -0
  82. package/src/lsp/diagnose.mjs +202 -0
  83. package/src/lsp/diagnostics.mjs +241 -0
  84. package/src/lsp/protocol.mjs +215 -0
  85. package/src/lsp/server.mjs +922 -0
  86. package/src/lsp/workspace-index.mjs +891 -0
  87. package/src/nx-json.mjs +95 -0
  88. package/src/options.mjs +611 -0
  89. package/src/process.mjs +91 -0
  90. package/src/providers/moon.mjs +733 -0
  91. package/src/providers/native/README.md +204 -0
  92. package/src/providers/native/coverage.mjs +74 -0
  93. package/src/providers/native/differential.fixtures.mjs +1277 -0
  94. package/src/providers/native/discover.mjs +431 -0
  95. package/src/providers/native/graph.mjs +234 -0
  96. package/src/providers/native/index.mjs +152 -0
  97. package/src/providers/native/model.mjs +755 -0
  98. package/src/providers/nx.mjs +178 -0
  99. package/src/report/README.md +89 -0
  100. package/src/report/adr-text.mjs +129 -0
  101. package/src/report/context-text.mjs +109 -0
  102. package/src/report/debt-text.mjs +105 -0
  103. package/src/report/diff-text.mjs +219 -0
  104. package/src/report/discover-text.mjs +186 -0
  105. package/src/report/drift-text.mjs +194 -0
  106. package/src/report/envelope-shape.mjs +161 -0
  107. package/src/report/evidence.mjs +157 -0
  108. package/src/report/explain-text.mjs +159 -0
  109. package/src/report/graph-text.mjs +116 -0
  110. package/src/report/health-text.mjs +123 -0
  111. package/src/report/history-text.mjs +204 -0
  112. package/src/report/impact-text.mjs +128 -0
  113. package/src/report/json.mjs +173 -0
  114. package/src/report/plan-context-text.mjs +159 -0
  115. package/src/report/provenance-text.mjs +78 -0
  116. package/src/report/reconcile-text.mjs +159 -0
  117. package/src/report/report-text.mjs +264 -0
  118. package/src/report/sarif.mjs +953 -0
  119. package/src/report/text.mjs +823 -0
  120. package/src/report/waivers-text.mjs +100 -0
  121. package/src/rules/README.md +123 -0
  122. package/src/rules/index.mjs +962 -0
  123. package/src/rules/match.mjs +1708 -0
  124. package/src/rules/messages.mjs +73 -0
  125. package/src/rules/reachability.mjs +224 -0
  126. package/src/rules/specifiers.mjs +300 -0
  127. package/src/rules/tags.mjs +238 -0
  128. package/src/rules/topology.mjs +333 -0
  129. package/src/tsconfig-paths.mjs +237 -0
  130. package/src/verdict.mjs +145 -0
  131. package/src/workspace.mjs +580 -0
@@ -0,0 +1,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
+ }