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,186 @@
1
+ import type { AnalyzeWorkspaceResult } from "../index.ts";
2
+ import type { EvidenceStep } from "../model/finding.ts";
3
+ import { type FindingSummary, projectFinding } from "../projection/finding-summary.ts";
4
+
5
+ const SARIF_LEVEL: Record<FindingSummary["severity"], "error" | "warning" | "note"> = {
6
+ critical: "error",
7
+ high: "error",
8
+ medium: "warning",
9
+ low: "note",
10
+ info: "note",
11
+ };
12
+
13
+ interface SarifRule {
14
+ id: string;
15
+ name?: string;
16
+ shortDescription: { text: string };
17
+ helpUri?: string;
18
+ }
19
+
20
+ const RULES: SarifRule[] = [
21
+ {
22
+ id: "d1-db-op-in-loop",
23
+ name: "DbOpInLoop",
24
+ shortDescription: { text: "Database operation inside a loop" },
25
+ },
26
+ {
27
+ id: "d2-event-fanout-in-loop",
28
+ name: "EventFanoutInLoop",
29
+ shortDescription: { text: "Event raised inside a loop with DB-touching subscribers" },
30
+ },
31
+ {
32
+ id: "d3-missing-setloadfields",
33
+ name: "MissingSetLoadFields",
34
+ shortDescription: { text: "Missing SetLoadFields before record retrieval" },
35
+ },
36
+ {
37
+ id: "d4-repeated-lookup-in-loop",
38
+ name: "RepeatedLookupInLoop",
39
+ shortDescription: { text: "Repeated identical lookup inside a loop" },
40
+ },
41
+ {
42
+ id: "d5-set-based-opportunity",
43
+ name: "SetBasedOpportunity",
44
+ shortDescription: { text: "Loop-and-Modify candidate for ModifyAll" },
45
+ },
46
+ {
47
+ id: "d7-recursive-event-expansion",
48
+ name: "RecursiveEventExpansion",
49
+ shortDescription: { text: "Event subscriber chain forms a cycle" },
50
+ },
51
+ {
52
+ id: "d8-commit-in-transaction",
53
+ name: "CommitInTransaction",
54
+ shortDescription: { text: "Commit inside a posting transaction span" },
55
+ },
56
+ {
57
+ id: "d9-transaction-span-summary",
58
+ name: "TransactionSpanSummary",
59
+ shortDescription: { text: "Transaction span summary (info)" },
60
+ },
61
+ {
62
+ id: "d10-self-modifying-loop",
63
+ name: "SelfModifyingLoop",
64
+ shortDescription: { text: "Self-modifying loop" },
65
+ },
66
+ {
67
+ id: "d11-modify-without-get",
68
+ name: "ModifyWithoutGet",
69
+ shortDescription: { text: "Modify without prior Get" },
70
+ },
71
+ {
72
+ id: "d12-dead-integration-event",
73
+ name: "DeadIntegrationEvent",
74
+ shortDescription: { text: "Integration event has no subscribers" },
75
+ },
76
+ {
77
+ id: "d13-cross-app-internal-call",
78
+ name: "CrossAppInternalCall",
79
+ shortDescription: { text: "Cross-extension call into an internal procedure" },
80
+ },
81
+ {
82
+ id: "d14-dead-routine",
83
+ name: "DeadRoutine",
84
+ shortDescription: { text: "Routine unreachable from any entry point" },
85
+ },
86
+ {
87
+ id: "d16-obsolete-routine-call",
88
+ name: "ObsoleteRoutineCall",
89
+ shortDescription: { text: "Call to an obsolete routine" },
90
+ },
91
+ {
92
+ id: "d17-min-version-drift",
93
+ name: "MinVersionDrift",
94
+ shortDescription: { text: "Call into API newer than declared MinVersion" },
95
+ },
96
+ ];
97
+
98
+ /**
99
+ * Convert one Finding evidence path to a SARIF threadFlow. Each EvidenceStep
100
+ * becomes one threadFlowLocation; the step's note carries to the location's
101
+ * message. SARIF consumers (GitHub code-scanning, Sonar, etc.) render
102
+ * threadFlows as a navigable trace.
103
+ */
104
+ function pathToThreadFlow(path: EvidenceStep[]): {
105
+ locations: { location: { physicalLocation: object; message: { text: string } } }[];
106
+ } {
107
+ return {
108
+ locations: path.map((step) => ({
109
+ location: {
110
+ physicalLocation: {
111
+ artifactLocation: { uri: step.sourceAnchor.sourceUnitId },
112
+ region: {
113
+ startLine: step.sourceAnchor.range.startLine + 1,
114
+ startColumn: step.sourceAnchor.range.startColumn + 1,
115
+ },
116
+ },
117
+ message: { text: step.note },
118
+ },
119
+ })),
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Format an analysis result as SARIF 2.1.0 JSON. Suitable for `gh code-scanning upload-sarif`.
125
+ * Per finding emits ruleId, severity-mapped level, message, fingerprint, and one physical
126
+ * location. Optional logical location encodes object :: routine. When a Finding has
127
+ * `additionalPaths` (D1/D2 multi-caller cases), each path becomes a SARIF `codeFlow`
128
+ * (a `threadFlows[]` wrapper) so consumers can render every reaching trace.
129
+ */
130
+ export function formatSarif(result: AnalyzeWorkspaceResult): string {
131
+ const findings = result.findings.map((f) => projectFinding(f, result.model));
132
+ const sarif = {
133
+ $schema:
134
+ "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
135
+ version: "2.1.0",
136
+ runs: [
137
+ {
138
+ tool: {
139
+ driver: {
140
+ name: "al-sem",
141
+ version: "0.0.1",
142
+ informationUri: "https://github.com/SShadowS/al-sem",
143
+ rules: RULES,
144
+ },
145
+ },
146
+ results: findings.map((f, idx) => {
147
+ const raw = result.findings[idx];
148
+ const allPaths = raw ? [raw.evidencePath, ...(raw.additionalPaths ?? [])] : [];
149
+ const result_: Record<string, unknown> = {
150
+ ruleId: f.detector,
151
+ level: SARIF_LEVEL[f.severity] ?? "warning",
152
+ message: { text: `${f.title} — ${f.rootCause}` },
153
+ fingerprints: { "al-sem/v1": f.fingerprint },
154
+ locations: [
155
+ {
156
+ physicalLocation: {
157
+ artifactLocation: { uri: f.primaryLocation.file },
158
+ region: {
159
+ startLine: f.primaryLocation.line,
160
+ startColumn: f.primaryLocation.column,
161
+ },
162
+ },
163
+ logicalLocations:
164
+ f.primaryLocation.objectName !== undefined ||
165
+ f.primaryLocation.routineName !== undefined
166
+ ? [
167
+ {
168
+ name: `${f.primaryLocation.objectName ?? ""} :: ${f.primaryLocation.routineName ?? ""}`,
169
+ },
170
+ ]
171
+ : undefined,
172
+ },
173
+ ],
174
+ };
175
+ if (allPaths.length > 0 && allPaths.some((p) => p.length > 0)) {
176
+ result_.codeFlows = allPaths
177
+ .filter((p) => p.length > 0)
178
+ .map((p) => ({ threadFlows: [pathToThreadFlow(p)] }));
179
+ }
180
+ return result_;
181
+ }),
182
+ },
183
+ ],
184
+ };
185
+ return JSON.stringify(sarif, null, 2);
186
+ }
@@ -0,0 +1,153 @@
1
+ import type { AnalyzeWorkspaceResult } from "../index.ts";
2
+ import {
3
+ type FindingLocation,
4
+ type FindingSummary,
5
+ projectFinding,
6
+ } from "../projection/finding-summary.ts";
7
+ import { type RolledOrSingle, rollupFindings } from "../projection/rollup-findings.ts";
8
+ import type { RootClassificationSlot } from "../snapshot/types.ts";
9
+
10
+ const SEV_ORDER = ["critical", "high", "medium", "low", "info"] as const;
11
+ const SAFETY_RANK = { high: 3, medium: 2, low: 1 } as const;
12
+
13
+ function locStr(loc: FindingLocation): string {
14
+ const where =
15
+ loc.objectName && loc.routineName ? ` in ${loc.objectName} :: ${loc.routineName}` : "";
16
+ return `${loc.file}:${loc.line}:${loc.column}${where}`;
17
+ }
18
+
19
+ function renderSingle(f: FindingSummary, lines: string[]): void {
20
+ lines.push(` [${f.detector}] ${f.title} — ${f.rootCause}`);
21
+ lines.push(` ${locStr(f.primaryLocation)}`);
22
+ if (f.terminalLocation) lines.push(` terminal: ${locStr(f.terminalLocation)}`);
23
+ lines.push(
24
+ ` confidence: ${f.confidence.level}${f.confidence.cappedBy ? ` (capped by ${f.confidence.cappedBy.join(", ")})` : ""}`,
25
+ );
26
+ const pc = f.pathCount ?? 1;
27
+ if (pc > 1) {
28
+ const noun = pc - 1 === 1 ? "other path" : "other paths";
29
+ lines.push(
30
+ ` also reached from ${pc - 1} ${noun} (full traces in SARIF output / explain_path MCP tool)`,
31
+ );
32
+ }
33
+ if (f.fixHint) lines.push(` fix (${f.fixHint.safety}): ${f.fixHint.description}`);
34
+ }
35
+
36
+ function renderRolled(r: Extract<RolledOrSingle, { kind: "rolled" }>, lines: string[]): void {
37
+ const n = r.contributors.length;
38
+ lines.push(` ${locStr(r.primaryLocation)} — ${n} detectors agree:`);
39
+ for (const c of r.contributors) {
40
+ lines.push(` [${c.detector}] ${c.title} (${c.severity})`);
41
+ }
42
+ // Union of confidence levels — show the worst-case (lowest) so the user
43
+ // knows the rolled-up bundle's overall confidence.
44
+ const confs = r.contributors.map((c) => c.confidence.level);
45
+ const order = ["confirmed", "likely", "possible"] as const;
46
+ const worstConf =
47
+ order.find((lvl) => confs.includes(lvl as (typeof confs)[number])) ?? "possible";
48
+ const cappedBys = new Set<string>();
49
+ for (const c of r.contributors) for (const cb of c.confidence.cappedBy ?? []) cappedBys.add(cb);
50
+ const capStr = cappedBys.size > 0 ? ` (capped by ${[...cappedBys].sort().join(", ")})` : "";
51
+ lines.push(` confidence: ${worstConf}${capStr}`);
52
+ // Aggregate fix recommendations sorted by safety (high → low) — let the
53
+ // user see the safest replacement first.
54
+ const fixes = r.contributors
55
+ .filter((c) => c.fixHint !== undefined)
56
+ .map((c) => ({ from: c.detector, hint: c.fixHint }))
57
+ .filter((x) => x.hint !== undefined)
58
+ .sort((a, b) => {
59
+ const sa = a.hint ? SAFETY_RANK[a.hint.safety] : 0;
60
+ const sb = b.hint ? SAFETY_RANK[b.hint.safety] : 0;
61
+ return sb - sa;
62
+ });
63
+ if (fixes.length > 0) {
64
+ lines.push(" fix options (safest first):");
65
+ for (const f of fixes) {
66
+ if (f.hint === undefined) continue;
67
+ lines.push(` • (${f.hint.safety}) ${f.hint.description} [${f.from}]`);
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Human-readable terminal output: coverage summary, then findings grouped by
74
+ * severity. Findings that coincide at the same `(file, line, column, tables)`
75
+ * are rolled up (typically D1/D5/D10 on a single iterating-Modify pattern) —
76
+ * see `rollupFindings`. The roll-up is purely presentational; underlying
77
+ * Findings remain per-detector in JSON / SARIF output.
78
+ */
79
+ /**
80
+ * Render one RootClassificationSlot as a single line for terminal output
81
+ * (Phase 1 §4.3, consumed by §4.4 fingerprint CLI).
82
+ *
83
+ * Format: `<objectName>::<routineName> [<kind>, <kind>, ...]<marker>`
84
+ *
85
+ * `marker` reflects the classification's `source`:
86
+ * - `""` (empty) for `source: "ast"` — pure AST classification
87
+ * - ` [config-asserted]` for `source: "ast+config"` — AST + roots.config.json agreement (or partial agreement)
88
+ * - ` [config-root]` for `source: "config"` — declared only in roots.config.json
89
+ *
90
+ * `objectName` and `routineName` are passed by the caller because the
91
+ * snapshot stores StableRoutineId opaquely — the caller has the display
92
+ * names available via the snapshot's identityTable.
93
+ */
94
+ export function formatRootClassification(
95
+ slot: RootClassificationSlot,
96
+ objectName: string,
97
+ routineName: string,
98
+ ): string {
99
+ const kindList = slot.kinds.join(", ");
100
+ const marker =
101
+ slot.source === "config"
102
+ ? " [config-root]"
103
+ : slot.source === "ast+config"
104
+ ? " [config-asserted]"
105
+ : "";
106
+ return `${objectName}::${routineName} [${kindList}]${marker}`;
107
+ }
108
+
109
+ export function formatTerminal(result: AnalyzeWorkspaceResult): string {
110
+ const findings = result.findings.map((f) => projectFinding(f, result.model));
111
+ const rolled = rollupFindings(findings);
112
+ const cov = result.model.coverage;
113
+ const lines: string[] = [];
114
+
115
+ const rollupCount = rolled.filter((r) => r.kind === "rolled").length;
116
+ const rollupNote =
117
+ rollupCount > 0
118
+ ? `; ${rollupCount} location(s) flagged by multiple detectors (rolled up below).`
119
+ : ".";
120
+ lines.push(
121
+ `Analysed ${cov.routinesTotal} routines (${cov.routinesBodyAvailable} with bodies, ${cov.routinesParseIncomplete.length} parse-incomplete); ${cov.sourceUnitsParsed}/${cov.sourceUnitsTotal} source units parsed; ${cov.opaqueApps.length} opaque app(s)${rollupNote}`,
122
+ );
123
+
124
+ if (findings.length === 0) {
125
+ lines.push("");
126
+ lines.push(
127
+ "No findings. (Absence of a finding is not absence of a problem — see coverage above.)",
128
+ );
129
+ }
130
+
131
+ for (const sev of SEV_ORDER) {
132
+ const group = rolled.filter((r) =>
133
+ r.kind === "rolled" ? r.severity === sev : r.finding.severity === sev,
134
+ );
135
+ if (group.length === 0) continue;
136
+ lines.push("");
137
+ lines.push(`${sev.toUpperCase()} (${group.length}):`);
138
+ for (const item of group) {
139
+ if (item.kind === "single") renderSingle(item.finding, lines);
140
+ else renderRolled(item, lines);
141
+ }
142
+ }
143
+
144
+ if (result.diagnostics.length > 0) {
145
+ lines.push("");
146
+ lines.push(`Diagnostics (${result.diagnostics.length}):`);
147
+ for (const d of result.diagnostics) {
148
+ lines.push(` [${d.severity}/${d.stage}] ${d.message}`);
149
+ }
150
+ }
151
+
152
+ return lines.join("\n");
153
+ }