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.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- 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
|
+
}
|