al-sem 0.0.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/package.json +64 -0
  4. package/scripts/d40-diff.ts +44 -0
  5. package/scripts/fetch-native-parser.ts +179 -0
  6. package/scripts/precision-sample.ts +99 -0
  7. package/scripts/precision-study.ts +42 -0
  8. package/scripts/precision-tabulate.ts +52 -0
  9. package/src/cli/baseline.ts +31 -0
  10. package/src/cli/diff.ts +199 -0
  11. package/src/cli/events-chains.ts +56 -0
  12. package/src/cli/events-fanout.ts +87 -0
  13. package/src/cli/exit-code.ts +30 -0
  14. package/src/cli/fingerprint-indexes.ts +130 -0
  15. package/src/cli/fingerprint-query.ts +543 -0
  16. package/src/cli/fingerprint-witness.ts +493 -0
  17. package/src/cli/fingerprint.ts +292 -0
  18. package/src/cli/format-compact-json.ts +45 -0
  19. package/src/cli/format-events.ts +77 -0
  20. package/src/cli/format-fingerprint.ts +295 -0
  21. package/src/cli/format-html.ts +503 -0
  22. package/src/cli/format-json.ts +13 -0
  23. package/src/cli/format-policy.ts +95 -0
  24. package/src/cli/format-sarif.ts +186 -0
  25. package/src/cli/format-terminal.ts +153 -0
  26. package/src/cli/index.ts +566 -0
  27. package/src/cli/policy.ts +204 -0
  28. package/src/config/roots-config.ts +302 -0
  29. package/src/deps/cache-versions.ts +74 -0
  30. package/src/deps/canonical-json.ts +27 -0
  31. package/src/deps/dependency-artifact.ts +144 -0
  32. package/src/deps/dependency-cache.ts +262 -0
  33. package/src/deps/dependency-dag.ts +128 -0
  34. package/src/deps/dependency-package-discovery.ts +85 -0
  35. package/src/deps/dependency-pipeline.ts +483 -0
  36. package/src/deps/dependency-projection.ts +211 -0
  37. package/src/deps/dependency-resolver.ts +154 -0
  38. package/src/deps/workspace-dependencies.ts +114 -0
  39. package/src/detectors/capability-query.ts +145 -0
  40. package/src/detectors/confidence.ts +52 -0
  41. package/src/detectors/d1-db-op-in-loop.ts +457 -0
  42. package/src/detectors/d10-self-modifying-loop.ts +114 -0
  43. package/src/detectors/d11-modify-without-get.ts +129 -0
  44. package/src/detectors/d12-dead-integration-event.ts +81 -0
  45. package/src/detectors/d13-cross-app-internal-call.ts +105 -0
  46. package/src/detectors/d14-dead-routine.ts +151 -0
  47. package/src/detectors/d16-obsolete-routine-call.ts +94 -0
  48. package/src/detectors/d17-min-version-drift.ts +157 -0
  49. package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
  50. package/src/detectors/d19-unused-parameter.ts +116 -0
  51. package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
  52. package/src/detectors/d20-unreachable-after-exit.ts +92 -0
  53. package/src/detectors/d21-read-without-load.ts +128 -0
  54. package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
  55. package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
  56. package/src/detectors/d3-load-state.ts +72 -0
  57. package/src/detectors/d3-missing-setloadfields.ts +234 -0
  58. package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
  59. package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
  60. package/src/detectors/d34-commit-in-loop.ts +206 -0
  61. package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
  62. package/src/detectors/d36-late-setloadfields.ts +162 -0
  63. package/src/detectors/d37-validate-without-persist.ts +271 -0
  64. package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
  65. package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
  66. package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
  67. package/src/detectors/d40-transitive-load-missing.ts +217 -0
  68. package/src/detectors/d41-transitive-filter-loss.ts +200 -0
  69. package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
  70. package/src/detectors/d43-event-ishandled-skip.ts +257 -0
  71. package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
  72. package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
  73. package/src/detectors/d5-set-based-opportunity.ts +162 -0
  74. package/src/detectors/d7-recursive-event-expansion.ts +151 -0
  75. package/src/detectors/d8-commit-in-transaction.ts +132 -0
  76. package/src/detectors/d9-transaction-span-summary.ts +107 -0
  77. package/src/detectors/detector-context.ts +121 -0
  78. package/src/detectors/finding-grouping.ts +61 -0
  79. package/src/detectors/path-merge.ts +174 -0
  80. package/src/detectors/registry.ts +176 -0
  81. package/src/detectors/table-display.ts +42 -0
  82. package/src/diff/diff-abi.ts +195 -0
  83. package/src/diff/diff-capabilities.ts +179 -0
  84. package/src/diff/diff-engine.ts +146 -0
  85. package/src/diff/diff-events.ts +323 -0
  86. package/src/diff/diff-identity.ts +73 -0
  87. package/src/diff/diff-indexes.ts +199 -0
  88. package/src/diff/diff-permissions.ts +260 -0
  89. package/src/diff/diff-policy.ts +101 -0
  90. package/src/diff/diff-preflight.ts +66 -0
  91. package/src/diff/diff-renames.ts +104 -0
  92. package/src/diff/diff-schema.ts +232 -0
  93. package/src/diff/format-diff.ts +148 -0
  94. package/src/engine/attribute-parser.ts +50 -0
  95. package/src/engine/capability-cone.ts +531 -0
  96. package/src/engine/combined-graph.ts +357 -0
  97. package/src/engine/control-flow-walker.ts +1317 -0
  98. package/src/engine/dispatch-sites.ts +199 -0
  99. package/src/engine/effect-lattice.ts +81 -0
  100. package/src/engine/entry-points.ts +57 -0
  101. package/src/engine/event-flow.ts +524 -0
  102. package/src/engine/event-relay.ts +92 -0
  103. package/src/engine/op-classification.ts +92 -0
  104. package/src/engine/path-walker.ts +189 -0
  105. package/src/engine/reverse-call-graph.ts +23 -0
  106. package/src/engine/root-classifier-overlay.ts +194 -0
  107. package/src/engine/root-classifier.ts +135 -0
  108. package/src/engine/scc.ts +110 -0
  109. package/src/engine/source-anchor.ts +25 -0
  110. package/src/engine/summary-context.ts +104 -0
  111. package/src/engine/summary-engine.ts +296 -0
  112. package/src/engine/summary-runner.ts +560 -0
  113. package/src/engine/transaction-spans.ts +112 -0
  114. package/src/engine/uncertainty-util.ts +54 -0
  115. package/src/hash.ts +31 -0
  116. package/src/index/attribute-from-node.ts +141 -0
  117. package/src/index/callee-from-node.ts +181 -0
  118. package/src/index/capability/background.ts +90 -0
  119. package/src/index/capability/commit.ts +44 -0
  120. package/src/index/capability/dispatch.ts +164 -0
  121. package/src/index/capability/events.ts +65 -0
  122. package/src/index/capability/extractor.ts +124 -0
  123. package/src/index/capability/file-blob.ts +137 -0
  124. package/src/index/capability/http.ts +159 -0
  125. package/src/index/capability/hyperlink.ts +60 -0
  126. package/src/index/capability/isolated-storage.ts +179 -0
  127. package/src/index/capability/table.ts +113 -0
  128. package/src/index/capability/telemetry.ts +84 -0
  129. package/src/index/capability/ui.ts +55 -0
  130. package/src/index/capability/value-source.ts +202 -0
  131. package/src/index/expression-from-node.ts +117 -0
  132. package/src/index/indexer.ts +102 -0
  133. package/src/index/intraprocedural-body.ts +1467 -0
  134. package/src/index/intraprocedural-ops.ts +253 -0
  135. package/src/index/intraprocedural-refs.ts +188 -0
  136. package/src/index/object-indexer.ts +279 -0
  137. package/src/index/routine-indexer.ts +282 -0
  138. package/src/index/routine-signature.ts +46 -0
  139. package/src/index/variable-indexer.ts +134 -0
  140. package/src/index/variable-initializer-extractor.ts +155 -0
  141. package/src/index/variable-type-normalizer.ts +83 -0
  142. package/src/index.ts +267 -0
  143. package/src/mcp/server.ts +72 -0
  144. package/src/mcp/session.ts +49 -0
  145. package/src/mcp/tools/explain-path.ts +75 -0
  146. package/src/mcp/tools/get-analysis-health.ts +62 -0
  147. package/src/mcp/tools/get-finding.ts +47 -0
  148. package/src/mcp/tools/get-routine-summary.ts +126 -0
  149. package/src/mcp/tools/list-findings.ts +85 -0
  150. package/src/mcp/tools/list-hotspots.ts +78 -0
  151. package/src/mcp/tools/list-rollups.ts +103 -0
  152. package/src/mcp/tools/validators.ts +25 -0
  153. package/src/model/attributes.ts +120 -0
  154. package/src/model/callee.ts +45 -0
  155. package/src/model/capability.ts +187 -0
  156. package/src/model/coverage.ts +85 -0
  157. package/src/model/entities.ts +628 -0
  158. package/src/model/expression.ts +98 -0
  159. package/src/model/finding.ts +110 -0
  160. package/src/model/graph-edge.ts +93 -0
  161. package/src/model/graph.ts +62 -0
  162. package/src/model/identity.ts +81 -0
  163. package/src/model/ids.ts +90 -0
  164. package/src/model/index.ts +13 -0
  165. package/src/model/model.ts +51 -0
  166. package/src/model/permission.ts +76 -0
  167. package/src/model/root-classification.ts +116 -0
  168. package/src/model/stable-identity.ts +102 -0
  169. package/src/model/summary.ts +96 -0
  170. package/src/parser/ast.ts +82 -0
  171. package/src/parser/native/ffi.ts +145 -0
  172. package/src/parser/native/parse-index-pool.ts +148 -0
  173. package/src/parser/native/parse-index-worker.ts +94 -0
  174. package/src/parser/native/wrapper.ts +353 -0
  175. package/src/parser/parser-init.ts +43 -0
  176. package/src/perf/profiler.ts +66 -0
  177. package/src/policy/policy-default.yaml +83 -0
  178. package/src/policy/policy-engine.ts +339 -0
  179. package/src/policy/policy-loader.ts +257 -0
  180. package/src/policy/policy-schema.json +379 -0
  181. package/src/policy/policy-types.ts +81 -0
  182. package/src/policy/predicate-compiler.ts +151 -0
  183. package/src/policy/predicate-evaluator.ts +267 -0
  184. package/src/policy/predicate-fields.ts +439 -0
  185. package/src/projection/actionable-anchor.ts +48 -0
  186. package/src/projection/finding-filters.ts +44 -0
  187. package/src/projection/finding-fingerprint.ts +54 -0
  188. package/src/projection/finding-groups.ts +41 -0
  189. package/src/projection/finding-summary.ts +110 -0
  190. package/src/projection/rollup-findings.ts +105 -0
  191. package/src/providers/discover.ts +88 -0
  192. package/src/providers/external.ts +46 -0
  193. package/src/providers/types.ts +36 -0
  194. package/src/providers/workspace.ts +117 -0
  195. package/src/resolve/call-resolver.ts +117 -0
  196. package/src/resolve/coverage.ts +61 -0
  197. package/src/resolve/event-graph.ts +166 -0
  198. package/src/resolve/implicit-edges.ts +53 -0
  199. package/src/resolve/record-types.ts +36 -0
  200. package/src/resolve/resolver.ts +23 -0
  201. package/src/resolve/semantic-graph.ts +29 -0
  202. package/src/resolve/symbol-table.ts +69 -0
  203. package/src/snapshot/app-snapshot.ts +74 -0
  204. package/src/snapshot/compose.ts +100 -0
  205. package/src/snapshot/derive/callsite-evidence.ts +76 -0
  206. package/src/snapshot/derive/capability-facts.ts +70 -0
  207. package/src/snapshot/derive/contracts.ts +131 -0
  208. package/src/snapshot/derive/coverage.ts +35 -0
  209. package/src/snapshot/derive/event-declarations.ts +140 -0
  210. package/src/snapshot/derive/identity-table.ts +58 -0
  211. package/src/snapshot/derive/inputs.ts +91 -0
  212. package/src/snapshot/derive/operation-evidence.ts +70 -0
  213. package/src/snapshot/derive/permissions.ts +186 -0
  214. package/src/snapshot/derive/root-classifications.ts +56 -0
  215. package/src/snapshot/derive/schema.ts +130 -0
  216. package/src/snapshot/derive/typed-edges.ts +60 -0
  217. package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
  218. package/src/snapshot/deserialize.ts +40 -0
  219. package/src/snapshot/serialize-cbor-gz.ts +12 -0
  220. package/src/snapshot/serialize-cbor.ts +19 -0
  221. package/src/snapshot/serialize-json.ts +22 -0
  222. package/src/snapshot/shard.ts +134 -0
  223. package/src/snapshot/types.ts +181 -0
  224. package/src/symbols/app-manifest.ts +96 -0
  225. package/src/symbols/app-package-zip.ts +50 -0
  226. package/src/symbols/embedded-source-reader.ts +41 -0
  227. package/src/symbols/package-hash.ts +81 -0
  228. package/src/symbols/symbol-reader.ts +101 -0
  229. package/src/symbols/symbol-reference-parser.ts +378 -0
  230. package/src/symbols/symbol-reference-reader.ts +27 -0
  231. package/tsconfig.json +18 -0
@@ -0,0 +1,292 @@
1
+ import { existsSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { analyzeWorkspace } from "../index.ts";
4
+ import type { Diagnostic } from "../model/finding.ts";
5
+ import type { SemanticModel } from "../model/model.ts";
6
+ import { ROOT_KIND_VALUES, type RootKind } from "../model/root-classification.ts";
7
+ import { buildPrimaryAppModel } from "../snapshot/app-snapshot.ts";
8
+ import { composeSnapshot } from "../snapshot/compose.ts";
9
+ import { serializeCborGz } from "../snapshot/serialize-cbor-gz.ts";
10
+ import { serializeCbor } from "../snapshot/serialize-cbor.ts";
11
+ import { serializeJson } from "../snapshot/serialize-json.ts";
12
+ import { serializeSharded } from "../snapshot/shard.ts";
13
+ import type { SnapshotFormat } from "../snapshot/types.ts";
14
+ import { type FingerprintFilters, fingerprintQuery } from "./fingerprint-query.ts";
15
+ import { formatFingerprint } from "./format-fingerprint.ts";
16
+
17
+ export type FingerprintOutputFormat = "human" | SnapshotFormat;
18
+
19
+ export interface FingerprintOptions {
20
+ workspace: string;
21
+ format?: FingerprintOutputFormat;
22
+ out?: string;
23
+ shard?: "primary-only" | "all";
24
+ deterministic?: boolean;
25
+ alsemVersion?: string;
26
+ /**
27
+ * Skip loading `roots.config.json` even when it exists on disk. When
28
+ * both this flag is set AND the config file exists, the snapshot's
29
+ * `inputsMetadata.rootsConfigIgnored` is set to true so consumers can
30
+ * tell "config ignored" from "no config at all".
31
+ */
32
+ noRootsConfig?: boolean;
33
+ roots?: readonly string[];
34
+ routineSelectors?: readonly string[];
35
+ includeInherited?: boolean;
36
+ witness?: false | number | "all";
37
+ strict?: boolean;
38
+ debug?: boolean;
39
+ verbosity?: "compact" | "full";
40
+ color?: boolean;
41
+ /** Internal flag used by tests to record whether flags were user-specified. */
42
+ _specifiedFlags?: ReadonlySet<string>;
43
+ }
44
+
45
+ export class FingerprintCliError extends Error {
46
+ constructor(
47
+ public exitCode: 1 | 2,
48
+ message: string,
49
+ ) {
50
+ super(message);
51
+ }
52
+ }
53
+
54
+ const VALID_OUTPUT_FORMATS: readonly FingerprintOutputFormat[] = [
55
+ "human",
56
+ "json",
57
+ "cbor",
58
+ "cbor.gz",
59
+ ];
60
+
61
+ function isSerializerFormat(f: FingerprintOutputFormat): f is SnapshotFormat {
62
+ return f === "json" || f === "cbor" || f === "cbor.gz";
63
+ }
64
+
65
+ function validateRoots(values: readonly string[]): RootKind[] {
66
+ const valid = new Set<string>(ROOT_KIND_VALUES);
67
+ const out: RootKind[] = [];
68
+ for (const v of values) {
69
+ if (!valid.has(v)) {
70
+ throw new FingerprintCliError(
71
+ 1,
72
+ `unknown root kind '${v}'; valid: ${ROOT_KIND_VALUES.join(", ")}`,
73
+ );
74
+ }
75
+ out.push(v as RootKind);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function normalizeWitness(w: FingerprintOptions["witness"]): false | number | "all" {
81
+ if (w === undefined) return 3;
82
+ if (w === false || w === "all") return w;
83
+ if (typeof w === "number") {
84
+ if (w < 0 || w > 256) {
85
+ throw new FingerprintCliError(1, `--witness must be in 0..256 or 'all' (got ${w})`);
86
+ }
87
+ return w;
88
+ }
89
+ throw new FingerprintCliError(1, "invalid --witness value");
90
+ }
91
+
92
+ function rejectIllegalCombos(opts: FingerprintOptions, format: FingerprintOutputFormat): void {
93
+ const specified = opts._specifiedFlags ?? new Set<string>();
94
+ const queryFlags: readonly string[] = [
95
+ "roots",
96
+ "routineSelectors",
97
+ "witness",
98
+ "includeInherited",
99
+ ];
100
+ if (opts.shard !== undefined) {
101
+ for (const f of queryFlags) {
102
+ if (specified.has(f)) {
103
+ throw new FingerprintCliError(
104
+ 1,
105
+ `--shard cannot be combined with --${f === "routineSelectors" ? "routine" : f === "includeInherited" ? "include-inherited" : f}`,
106
+ );
107
+ }
108
+ }
109
+ if (format === "human") {
110
+ throw new FingerprintCliError(1, "--shard requires --format=json|cbor|cbor.gz");
111
+ }
112
+ }
113
+ if (isSerializerFormat(format)) {
114
+ for (const f of queryFlags) {
115
+ if (specified.has(f)) {
116
+ throw new FingerprintCliError(
117
+ 1,
118
+ `--${f === "routineSelectors" ? "routine" : f === "includeInherited" ? "include-inherited" : f} is only valid with --format=human`,
119
+ );
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ function defaultFormat(opts: FingerprintOptions): FingerprintOutputFormat {
126
+ if (opts.format !== undefined) {
127
+ if (!VALID_OUTPUT_FORMATS.includes(opts.format)) {
128
+ throw new FingerprintCliError(
129
+ 1,
130
+ `unknown --format '${opts.format}'; valid: ${VALID_OUTPUT_FORMATS.join(", ")}`,
131
+ );
132
+ }
133
+ return opts.format;
134
+ }
135
+ return opts.shard !== undefined ? "json" : "human";
136
+ }
137
+
138
+ export async function runFingerprint(opts: FingerprintOptions): Promise<number> {
139
+ let format: FingerprintOutputFormat;
140
+ try {
141
+ format = defaultFormat(opts);
142
+ rejectIllegalCombos(opts, format);
143
+ } catch (err) {
144
+ if (err instanceof FingerprintCliError) {
145
+ process.stderr.write(`${err.message}\n`);
146
+ return err.exitCode;
147
+ }
148
+ throw err;
149
+ }
150
+
151
+ const noRootsConfig = opts.noRootsConfig === true;
152
+ const isApp =
153
+ existsSync(opts.workspace) &&
154
+ statSync(opts.workspace).isFile() &&
155
+ opts.workspace.toLowerCase().endsWith(".app");
156
+ let model: SemanticModel;
157
+ let analyzeDiagnostics: Diagnostic[];
158
+ try {
159
+ const r = isApp
160
+ ? await buildPrimaryAppModel(opts.workspace)
161
+ : await analyzeWorkspace({ workspaceRoot: opts.workspace, noRootsConfig });
162
+ model = r.model;
163
+ analyzeDiagnostics = r.diagnostics;
164
+ } catch (err) {
165
+ process.stderr.write(
166
+ `fingerprint: failed to load ${isApp ? ".app" : "workspace"}: ${(err as Error).message}\n`,
167
+ );
168
+ if (opts.debug) process.stderr.write(`${(err as Error).stack ?? ""}\n`);
169
+ return 1;
170
+ }
171
+
172
+ if (opts.strict === true) {
173
+ const fatal = analyzeDiagnostics.find((d) => d.severity === "error");
174
+ if (fatal !== undefined) {
175
+ for (const d of analyzeDiagnostics) {
176
+ process.stderr.write(`${d.severity}: ${d.message}\n`);
177
+ }
178
+ return 1;
179
+ }
180
+ }
181
+
182
+ const rootsConfigIgnored =
183
+ !isApp && noRootsConfig && existsSync(resolve(opts.workspace, "roots.config.json"));
184
+ const snap = composeSnapshot(model, {
185
+ workspaceDir: opts.workspace,
186
+ alsemVersion: opts.alsemVersion ?? "0.0.0",
187
+ deterministic: opts.deterministic ?? false,
188
+ rootsConfigIgnored,
189
+ });
190
+
191
+ if (isSerializerFormat(format)) {
192
+ // Existing JSON/CBOR/shard paths — byte-identical to Phase 0c.
193
+ if (opts.shard !== undefined) {
194
+ if (opts.out === undefined) {
195
+ process.stderr.write("--shard requires --out <directory>\n");
196
+ return 1;
197
+ }
198
+ const files = serializeSharded(snap, { format, primaryOnly: opts.shard === "primary-only" });
199
+ try {
200
+ for (const [name, bytes] of files) {
201
+ writeFileSync(join(opts.out, name), bytes);
202
+ }
203
+ } catch (err) {
204
+ process.stderr.write(`failed to write shard files: ${(err as Error).message}\n`);
205
+ return 1;
206
+ }
207
+ return 0;
208
+ }
209
+ let bytes: Uint8Array;
210
+ if (format === "json") bytes = new TextEncoder().encode(serializeJson(snap));
211
+ else if (format === "cbor") bytes = serializeCbor(snap);
212
+ else bytes = serializeCborGz(snap);
213
+ try {
214
+ if (opts.out !== undefined) writeFileSync(opts.out, bytes);
215
+ else process.stdout.write(bytes);
216
+ } catch (err) {
217
+ process.stderr.write(`failed to write: ${(err as Error).message}\n`);
218
+ return 1;
219
+ }
220
+ for (const d of analyzeDiagnostics) {
221
+ process.stderr.write(`${d.severity}: ${d.message}\n`);
222
+ }
223
+ return 0;
224
+ }
225
+
226
+ // --- human format ---
227
+ let validatedRoots: RootKind[] = [];
228
+ try {
229
+ validatedRoots = opts.roots !== undefined ? validateRoots(opts.roots) : [];
230
+ } catch (err) {
231
+ if (err instanceof FingerprintCliError) {
232
+ process.stderr.write(`${err.message}\n`);
233
+ return err.exitCode;
234
+ }
235
+ throw err;
236
+ }
237
+ let witness: false | number | "all";
238
+ try {
239
+ witness = normalizeWitness(opts.witness);
240
+ } catch (err) {
241
+ if (err instanceof FingerprintCliError) {
242
+ process.stderr.write(`${err.message}\n`);
243
+ return err.exitCode;
244
+ }
245
+ throw err;
246
+ }
247
+ const filters: FingerprintFilters = {
248
+ roots: validatedRoots.length > 0 ? new Set(validatedRoots) : undefined,
249
+ routineSelectors: opts.routineSelectors,
250
+ includeInherited: opts.includeInherited ?? true,
251
+ witnessLimit: witness,
252
+ };
253
+ const result = fingerprintQuery(snap, filters);
254
+
255
+ const selectorErrors = result.diagnostics.filter(
256
+ (d) => d.kind === "selector-unresolved" || d.kind === "selector-ambiguous",
257
+ );
258
+ if (selectorErrors.length > 0) {
259
+ for (const d of selectorErrors) {
260
+ if (d.kind === "selector-unresolved") {
261
+ process.stderr.write(
262
+ `error: --routine '${d.selector}' did not match any routine (tried: ${d.triedForms.join(", ")})\n`,
263
+ );
264
+ } else if (d.kind === "selector-ambiguous") {
265
+ process.stderr.write(
266
+ `error: --routine '${d.selector}' is ambiguous (matched via ${d.matchedForm}); candidates:\n`,
267
+ );
268
+ for (const c of d.candidates) {
269
+ process.stderr.write(` - ${c.display} (${c.stableId})\n`);
270
+ }
271
+ }
272
+ }
273
+ return 2;
274
+ }
275
+
276
+ const text = formatFingerprint(result, {
277
+ deterministic: opts.deterministic,
278
+ color: opts.color ?? false,
279
+ verbosity: opts.verbosity ?? "compact",
280
+ });
281
+ try {
282
+ if (opts.out !== undefined) writeFileSync(opts.out, text);
283
+ else process.stdout.write(text);
284
+ } catch (err) {
285
+ process.stderr.write(`failed to write: ${(err as Error).message}\n`);
286
+ return 1;
287
+ }
288
+ for (const d of analyzeDiagnostics) {
289
+ process.stderr.write(`${d.severity}: ${d.message}\n`);
290
+ }
291
+ return 0;
292
+ }
@@ -0,0 +1,45 @@
1
+ import type { AnalyzeWorkspaceResult } from "../index.ts";
2
+ import type { DetectorStats } from "../model/finding.ts";
3
+ import { type FindingSummary, projectFinding } from "../projection/finding-summary.ts";
4
+
5
+ export interface CompactReport {
6
+ summary: {
7
+ totalFindings: number;
8
+ bySeverity: Record<string, number>;
9
+ byDetector: Record<string, number>;
10
+ routinesAnalyzed: number;
11
+ sourceUnitsParsed: number;
12
+ opaqueApps: string[];
13
+ detectorStats: DetectorStats[];
14
+ };
15
+ findings: FindingSummary[];
16
+ diagnostics: AnalyzeWorkspaceResult["diagnostics"];
17
+ }
18
+
19
+ export function buildCompactReport(result: AnalyzeWorkspaceResult): CompactReport {
20
+ const findings = result.findings.map((f) => projectFinding(f, result.model));
21
+ const bySeverity: Record<string, number> = {};
22
+ const byDetector: Record<string, number> = {};
23
+ for (const f of findings) {
24
+ bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
25
+ byDetector[f.detector] = (byDetector[f.detector] ?? 0) + 1;
26
+ }
27
+ const coverage = result.model.coverage;
28
+ return {
29
+ summary: {
30
+ totalFindings: findings.length,
31
+ bySeverity,
32
+ byDetector,
33
+ routinesAnalyzed: coverage.routinesTotal,
34
+ sourceUnitsParsed: coverage.sourceUnitsParsed,
35
+ opaqueApps: coverage.opaqueApps,
36
+ detectorStats: result.detectorStats,
37
+ },
38
+ findings,
39
+ diagnostics: result.diagnostics,
40
+ };
41
+ }
42
+
43
+ export function formatCompactJson(result: AnalyzeWorkspaceResult): string {
44
+ return JSON.stringify(buildCompactReport(result), null, 2);
45
+ }
@@ -0,0 +1,77 @@
1
+ import type { ChainNode, ChainReport, FanoutReport } from "../engine/event-flow.ts";
2
+
3
+ export interface EventsFormatOptions {
4
+ format: "human" | "json";
5
+ coveragePolicy?: "warn" | "strict" | "ignore";
6
+ deterministic?: boolean;
7
+ alsemVersion?: string;
8
+ }
9
+
10
+ function coverageGlyph(s: "complete" | "partial" | "unknown"): string {
11
+ if (s === "complete") return "✓";
12
+ if (s === "partial") return "≈";
13
+ return "?";
14
+ }
15
+
16
+ export function formatFanout(report: FanoutReport, opts: EventsFormatOptions): string {
17
+ if (opts.format === "json") {
18
+ const payload = {
19
+ al_sem_version: opts.alsemVersion ?? "0.0.0",
20
+ generated_at: opts.deterministic ? "0" : new Date().toISOString(),
21
+ kind: "events.fanout",
22
+ summary: report.summary,
23
+ entries: report.entries,
24
+ };
25
+ return JSON.stringify(payload, undefined, 2);
26
+ }
27
+ const lines: string[] = [];
28
+ lines.push(
29
+ `Event fanout report (${report.summary.totalPublishers} publishers, ${report.summary.totalEvents} events, ${report.summary.hotEvents} hot)`,
30
+ );
31
+ lines.push("");
32
+ for (const e of report.entries) {
33
+ const cov = `[${coverageGlyph(e.coverage.dispatchEdges)}${coverageGlyph(e.coverage.subscriberDiscovery)}${coverageGlyph(e.coverage.capabilityComposition)}]`;
34
+ lines.push(
35
+ ` ${e.eventName} (${e.eventKind}) → ${e.directSubscriberCount} subscriber(s) ${cov}`,
36
+ );
37
+ }
38
+ return `${lines.join("\n")}\n`;
39
+ }
40
+
41
+ function renderChain(node: ChainNode, depth: number, lines: string[]): void {
42
+ const indent = " ".repeat(depth);
43
+ if (node.kind === "root") {
44
+ lines.push(`${indent}root ${node.routineId ?? "?"}`);
45
+ } else if (node.kind === "event-dispatch") {
46
+ const tail = node.depthTruncated ? " (depth truncated)" : "";
47
+ lines.push(`${indent}↪ ${node.eventName ?? node.eventId}${tail}`);
48
+ } else if (node.kind === "subscriber") {
49
+ const marker = node.cycleDetected
50
+ ? " (cycle)"
51
+ : node.depthTruncated
52
+ ? " (depth truncated)"
53
+ : "";
54
+ lines.push(`${indent}• ${node.routineId}${marker}`);
55
+ }
56
+ for (const c of node.children) renderChain(c, depth + 1, lines);
57
+ }
58
+
59
+ export function formatChains(report: ChainReport, opts: EventsFormatOptions): string {
60
+ if (opts.format === "json") {
61
+ const payload = {
62
+ al_sem_version: opts.alsemVersion ?? "0.0.0",
63
+ generated_at: opts.deterministic ? "0" : new Date().toISOString(),
64
+ kind: "events.chains",
65
+ summary: report.summary,
66
+ chains: report.chains,
67
+ };
68
+ return JSON.stringify(payload, undefined, 2);
69
+ }
70
+ const lines: string[] = [];
71
+ lines.push(
72
+ `Event chains report (${report.summary.totalRoots} roots, max depth ${report.summary.maxChainDepth}, ${report.summary.cyclesDetected} cycles, ${report.summary.depthTruncatedNodes} depth-truncated)`,
73
+ );
74
+ lines.push("");
75
+ for (const c of report.chains) renderChain(c, 0, lines);
76
+ return `${lines.join("\n")}\n`;
77
+ }