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,132 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
3
|
+
import { roleOf } from "../model/entities.ts";
|
|
4
|
+
import type { Routine } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
9
|
+
import { writesTablesOf } from "./capability-query.ts";
|
|
10
|
+
import { toConfidence } from "./confidence.ts";
|
|
11
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
12
|
+
|
|
13
|
+
const POSTING_NAME_RE = /^(Post|Apply|Release)[A-Z]/;
|
|
14
|
+
const TRANSACTION_THRESHOLD_TABLES = 3;
|
|
15
|
+
|
|
16
|
+
function isTransactionManaging(
|
|
17
|
+
routineId: RoutineId,
|
|
18
|
+
routineById: Map<RoutineId, Routine>,
|
|
19
|
+
): boolean {
|
|
20
|
+
const r = routineById.get(routineId);
|
|
21
|
+
if (!r) return false;
|
|
22
|
+
if (POSTING_NAME_RE.test(r.name)) return true;
|
|
23
|
+
if (!r.summary) return false;
|
|
24
|
+
return writesTablesOf(r.summary).length >= TRANSACTION_THRESHOLD_TABLES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* D8 — marquee transaction-correctness detector. For each transaction span (Commit
|
|
29
|
+
* reachable from some root), if the span includes a transaction-managing routine
|
|
30
|
+
* (Post.../Apply.../Release... name OR writes >= 3 tables) AND the Commit is in a different
|
|
31
|
+
* routine — emit high-severity finding.
|
|
32
|
+
*
|
|
33
|
+
* Catches:
|
|
34
|
+
* - Event subscribers that Commit inside a posting handler.
|
|
35
|
+
* - Post-* helpers that Commit mid-transaction.
|
|
36
|
+
*
|
|
37
|
+
* Cannot be expressed by per-file analyzers: requires reverse call graph + transaction
|
|
38
|
+
* span aggregation.
|
|
39
|
+
*/
|
|
40
|
+
export function detectD8(
|
|
41
|
+
model: SemanticModel,
|
|
42
|
+
graph: CombinedGraph,
|
|
43
|
+
ctx: DetectorContext,
|
|
44
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
45
|
+
const { transactionSpans: spans, routineById } = ctx;
|
|
46
|
+
const findings: Finding[] = [];
|
|
47
|
+
let candidatesConsidered = 0;
|
|
48
|
+
let skippedOther = 0;
|
|
49
|
+
|
|
50
|
+
for (const span of spans) {
|
|
51
|
+
const commitRoutine = routineById.get(span.commitRoutineId);
|
|
52
|
+
if (!commitRoutine) continue;
|
|
53
|
+
if (roleOf(commitRoutine) !== "primary") continue;
|
|
54
|
+
candidatesConsidered++;
|
|
55
|
+
|
|
56
|
+
const managers = span.routinesInSpan
|
|
57
|
+
.filter((id) => id !== span.commitRoutineId)
|
|
58
|
+
.filter((id) => isTransactionManaging(id, routineById));
|
|
59
|
+
if (managers.length === 0) {
|
|
60
|
+
skippedOther++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const managerId = managers[0];
|
|
65
|
+
if (managerId === undefined) continue;
|
|
66
|
+
const manager = routineById.get(managerId);
|
|
67
|
+
if (!manager) continue;
|
|
68
|
+
|
|
69
|
+
const path: EvidenceStep[] = [
|
|
70
|
+
{
|
|
71
|
+
routineId: managerId,
|
|
72
|
+
sourceAnchor: manager.sourceAnchor,
|
|
73
|
+
note: `transaction-managing routine: ${manager.name}`,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const commitOpSite = commitRoutine.features.operationSites.find(
|
|
77
|
+
(os) => os.id === span.commitOperationId,
|
|
78
|
+
);
|
|
79
|
+
const commitAnchor = commitOpSite?.sourceAnchor ?? commitRoutine.sourceAnchor;
|
|
80
|
+
path.push({
|
|
81
|
+
routineId: commitRoutine.id,
|
|
82
|
+
operationId: span.commitOperationId,
|
|
83
|
+
sourceAnchor: commitAnchor,
|
|
84
|
+
note: `Commit inside ${manager.name}'s transaction span`,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const writeCount = manager.summary ? writesTablesOf(manager.summary).length : 0;
|
|
88
|
+
|
|
89
|
+
const finding: Finding = {
|
|
90
|
+
id: `d8/${span.commitOperationId}`,
|
|
91
|
+
rootCauseKey: `d8/${span.commitOperationId}`,
|
|
92
|
+
detector: "d8-commit-in-transaction",
|
|
93
|
+
title: "Commit inside a posting transaction span",
|
|
94
|
+
rootCause: `${commitRoutine.name} calls Commit while reachable from ${manager.name}, which writes ${writeCount} tables. A mid-transaction Commit breaks rollback semantics — if the surrounding operation later fails, the data is left half-written.`,
|
|
95
|
+
severity: "high",
|
|
96
|
+
confidence: toConfidence([], "likely"),
|
|
97
|
+
primaryLocation: commitAnchor,
|
|
98
|
+
evidencePath: path,
|
|
99
|
+
affectedObjects: [commitRoutine.objectId, manager.objectId].sort(),
|
|
100
|
+
affectedTables: span.writesTables,
|
|
101
|
+
fixOptions: [
|
|
102
|
+
{
|
|
103
|
+
description:
|
|
104
|
+
"Remove the Commit, or restructure so the surrounding transaction completes (returns control to its caller) before this code runs.",
|
|
105
|
+
safety: "low",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
provenance: [{ source: "tree-sitter" }],
|
|
109
|
+
};
|
|
110
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
111
|
+
findings.push(finding);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Dedupe by id (same Commit operation flagged from multiple manager routines → one finding)
|
|
115
|
+
const seen = new Set<string>();
|
|
116
|
+
const deduped: Finding[] = [];
|
|
117
|
+
for (const f of findings) {
|
|
118
|
+
if (seen.has(f.id)) continue;
|
|
119
|
+
seen.add(f.id);
|
|
120
|
+
deduped.push(f);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
findings: deduped.sort((a, b) => compareStrings(a.id, b.id)),
|
|
125
|
+
stats: {
|
|
126
|
+
detector: "d8-commit-in-transaction",
|
|
127
|
+
candidatesConsidered,
|
|
128
|
+
findingsEmitted: deduped.length,
|
|
129
|
+
skipped: { other: skippedOther > 0 ? skippedOther : undefined },
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
3
|
+
import { roleOf } from "../model/entities.ts";
|
|
4
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
5
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
6
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
7
|
+
import { toConfidence } from "./confidence.ts";
|
|
8
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
9
|
+
|
|
10
|
+
const MIN_INTERESTING_ROUTINES = 2;
|
|
11
|
+
const MIN_INTERESTING_TABLES = 2;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* D9 — for each non-trivial transaction span (>=2 routines AND >=2 tables), emit an
|
|
15
|
+
* info-level finding describing what the span covers. Aimed at code review / agent
|
|
16
|
+
* context, not a bug to fix.
|
|
17
|
+
*/
|
|
18
|
+
export function detectD9(
|
|
19
|
+
model: SemanticModel,
|
|
20
|
+
graph: CombinedGraph,
|
|
21
|
+
ctx: DetectorContext,
|
|
22
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
23
|
+
const { transactionSpans: spans, routineById } = ctx;
|
|
24
|
+
const findings: Finding[] = [];
|
|
25
|
+
let candidatesConsidered = 0;
|
|
26
|
+
let skippedOther = 0;
|
|
27
|
+
|
|
28
|
+
for (const span of spans) {
|
|
29
|
+
const commitRoutine = routineById.get(span.commitRoutineId);
|
|
30
|
+
if (!commitRoutine) continue;
|
|
31
|
+
if (roleOf(commitRoutine) !== "primary") continue;
|
|
32
|
+
candidatesConsidered++;
|
|
33
|
+
if (span.routinesInSpan.length < MIN_INTERESTING_ROUTINES) {
|
|
34
|
+
skippedOther++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Count concrete tables + presence of uncertain effects.
|
|
39
|
+
// !span.coverageComplete fires when ANY routine in the span has
|
|
40
|
+
// an undefined summary OR less-than-complete inherited coverage
|
|
41
|
+
// (Wave 3 substrate signal). Note: this is broader than the legacy
|
|
42
|
+
// writesTables-specific probe — any incomplete capability family
|
|
43
|
+
// (events, http, etc.) sets the flag, not just incomplete writes.
|
|
44
|
+
// Acceptable at info severity; the signal narrows back to writes-
|
|
45
|
+
// only when per-family coverage roll-up lands (see
|
|
46
|
+
// TransactionSpan.coverageComplete JSDoc).
|
|
47
|
+
const tableCount = span.writesTables.length;
|
|
48
|
+
const effectsAreInteresting = tableCount >= MIN_INTERESTING_TABLES || !span.coverageComplete;
|
|
49
|
+
if (!effectsAreInteresting) {
|
|
50
|
+
skippedOther++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const path: EvidenceStep[] = [
|
|
55
|
+
{
|
|
56
|
+
routineId: commitRoutine.id,
|
|
57
|
+
operationId: span.commitOperationId,
|
|
58
|
+
sourceAnchor: commitRoutine.sourceAnchor,
|
|
59
|
+
note: "Commit at end of span",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Describe table effects—concrete or unknown.
|
|
64
|
+
// !span.coverageComplete is the substrate's coverage roll-up
|
|
65
|
+
// (replaces the legacy per-routine writesTables === "unknown" probe).
|
|
66
|
+
const tableDesc =
|
|
67
|
+
span.writesTables.length > 0
|
|
68
|
+
? `writes ${span.writesTables.length} known table(s)`
|
|
69
|
+
: !span.coverageComplete
|
|
70
|
+
? "writes tables (effect scope unknown)"
|
|
71
|
+
: "writes tables";
|
|
72
|
+
|
|
73
|
+
const finding: Finding = {
|
|
74
|
+
id: `d9/${span.commitOperationId}`,
|
|
75
|
+
rootCauseKey: `d9/${span.commitOperationId}`,
|
|
76
|
+
detector: "d9-transaction-span-summary",
|
|
77
|
+
title: "Transaction span summary",
|
|
78
|
+
rootCause: `Transaction ending at ${commitRoutine.name}'s Commit spans ${span.routinesInSpan.length} routines, ${tableDesc}, publishes ${span.publishesEvents.length} event(s). Consider whether all of this needs to be atomic.`,
|
|
79
|
+
severity: "info",
|
|
80
|
+
confidence: toConfidence([], "possible"),
|
|
81
|
+
primaryLocation: commitRoutine.sourceAnchor,
|
|
82
|
+
evidencePath: path,
|
|
83
|
+
affectedObjects: [commitRoutine.objectId],
|
|
84
|
+
affectedTables: span.writesTables,
|
|
85
|
+
fixOptions: [
|
|
86
|
+
{
|
|
87
|
+
description:
|
|
88
|
+
"If the span includes operations that are logically independent, split them into separate transactions with their own Commit boundaries.",
|
|
89
|
+
safety: "low",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
provenance: [{ source: "tree-sitter" }],
|
|
93
|
+
};
|
|
94
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
95
|
+
findings.push(finding);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
findings: findings.sort((a, b) => compareStrings(a.id, b.id)),
|
|
100
|
+
stats: {
|
|
101
|
+
detector: "d9-transaction-span-summary",
|
|
102
|
+
candidatesConsidered,
|
|
103
|
+
findingsEmitted: findings.length,
|
|
104
|
+
skipped: { other: skippedOther > 0 ? skippedOther : undefined },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { findEntryPoints, findReachableRoots } from "../engine/entry-points.ts";
|
|
3
|
+
import type { EventFlowIndexes } from "../engine/event-flow.ts";
|
|
4
|
+
import { buildEventFlowIndexes } from "../engine/event-flow.ts";
|
|
5
|
+
import { type ReverseCallGraph, buildReverseCallGraph } from "../engine/reverse-call-graph.ts";
|
|
6
|
+
import { type TransactionSpan, computeTransactionSpans } from "../engine/transaction-spans.ts";
|
|
7
|
+
import type { CallSite, ObjectDecl, Routine, Table } from "../model/entities.ts";
|
|
8
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
9
|
+
import type { CallEdge } from "../model/graph.ts";
|
|
10
|
+
import type { CallsiteId, ObjectId, RoutineId, TableId } from "../model/ids.ts";
|
|
11
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
12
|
+
import type { Uncertainty } from "../model/summary.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shared, eager indexes + derived graphs detectors read from. Built once at the top of
|
|
16
|
+
* `runDetectors` so 15 detectors don't each rebuild `new Map(model.routines.map(...))`
|
|
17
|
+
* and D8/D9 don't recompute transaction spans twice.
|
|
18
|
+
*/
|
|
19
|
+
export interface DetectorContext {
|
|
20
|
+
routineById: Map<RoutineId, Routine>;
|
|
21
|
+
objectsById: Map<ObjectId, ObjectDecl>;
|
|
22
|
+
tableById: Map<TableId, Table>;
|
|
23
|
+
reverseCallGraph: ReverseCallGraph;
|
|
24
|
+
/** Trigger + event-subscriber roots — D8 transaction-span boundaries. */
|
|
25
|
+
entryPoints: Set<RoutineId>;
|
|
26
|
+
/**
|
|
27
|
+
* Reachability roots for "is this routine ever invoked?" — entry points plus the
|
|
28
|
+
* procedures we cannot prove are app-scoped (public/protected always; `internal`
|
|
29
|
+
* only when `internalReachableExternally` is true). Used by D14 only.
|
|
30
|
+
*/
|
|
31
|
+
reachableRoots: Set<RoutineId>;
|
|
32
|
+
/**
|
|
33
|
+
* True when the workspace's app.json `internalsVisibleTo` names at least one app —
|
|
34
|
+
* `internal` procedures are then treated as a potential external API surface and
|
|
35
|
+
* are not flagged as dead. False (the common case) means `internal` is app-scoped
|
|
36
|
+
* for reachability purposes and D14 flags it like a `local procedure`.
|
|
37
|
+
*/
|
|
38
|
+
internalReachableExternally: boolean;
|
|
39
|
+
transactionSpans: TransactionSpan[];
|
|
40
|
+
/**
|
|
41
|
+
* Resolved CallEdges keyed by `callsiteId`. Mirrors SummaryContext's index of the same
|
|
42
|
+
* name and lets detectors do O(1) callsite → resolved edge lookup instead of the
|
|
43
|
+
* O(N) `graph.edgesByFrom.get(routine.id).find(e => e.callsiteId === cs.id)` scan
|
|
44
|
+
* that several pre-Phase-4 detectors still use. Built from the same source — only
|
|
45
|
+
* edges with `to !== undefined` are indexed; the first edge per callsiteId wins
|
|
46
|
+
* (matches SummaryContext's behaviour for the pathological multi-target case).
|
|
47
|
+
*/
|
|
48
|
+
resolvedCallEdgeByCallsite: Map<CallsiteId, CallEdge>;
|
|
49
|
+
/**
|
|
50
|
+
* Uncertainty edges grouped by source routine + every call site indexed by id. The
|
|
51
|
+
* interprocedural path-walker (`walkEvidence`, used by D1/D2) is invoked once per in-loop
|
|
52
|
+
* call site; without these it rebuilds the routine/edge/callsite indexes from scratch on
|
|
53
|
+
* every call — O(routines + edges) per call. Built once here and threaded into `walkEvidence`.
|
|
54
|
+
*/
|
|
55
|
+
uncertaintyEdgesByFrom: Map<RoutineId, Uncertainty[]>;
|
|
56
|
+
callSiteById: Map<CallsiteId, CallSite>;
|
|
57
|
+
/**
|
|
58
|
+
* Lazy memoized event-flow indexes. Built on first call; subsequent
|
|
59
|
+
* calls return the same object. Used by D43/D44/D45 + the events
|
|
60
|
+
* fanout/chains reports.
|
|
61
|
+
*/
|
|
62
|
+
getEventFlowIndexes(): EventFlowIndexes;
|
|
63
|
+
/**
|
|
64
|
+
* Detector-side diagnostic bus. Detectors push warnings here for
|
|
65
|
+
* substrate-missing or other run-time signals; `runDetectors` merges
|
|
66
|
+
* this into its output diagnostics list.
|
|
67
|
+
*/
|
|
68
|
+
diagnostics: Diagnostic[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the shared context. Order matters: `transactionSpans` consumes the reverse graph.
|
|
73
|
+
* Called once per `runDetectors` invocation.
|
|
74
|
+
*/
|
|
75
|
+
export function buildDetectorContext(model: SemanticModel, graph: CombinedGraph): DetectorContext {
|
|
76
|
+
const routineById = new Map<RoutineId, Routine>(model.routines.map((r) => [r.id, r]));
|
|
77
|
+
const objectsById = new Map<ObjectId, ObjectDecl>(model.objects.map((o) => [o.id, o]));
|
|
78
|
+
const tableById = new Map<TableId, Table>(model.tables.map((t) => [t.id, t]));
|
|
79
|
+
const reverseCallGraph = buildReverseCallGraph(graph);
|
|
80
|
+
const entryPoints = new Set(findEntryPoints(model));
|
|
81
|
+
const internalReachableExternally = (model.identity.primaryInternalsVisibleTo?.length ?? 0) > 0;
|
|
82
|
+
const reachableRoots = new Set(findReachableRoots(model, { internalReachableExternally }));
|
|
83
|
+
const transactionSpans = computeTransactionSpans(model, graph, reverseCallGraph);
|
|
84
|
+
const resolvedCallEdgeByCallsite = new Map<CallsiteId, CallEdge>();
|
|
85
|
+
for (const ce of model.callGraph) {
|
|
86
|
+
if (ce.to === undefined) continue;
|
|
87
|
+
if (!resolvedCallEdgeByCallsite.has(ce.callsiteId)) {
|
|
88
|
+
resolvedCallEdgeByCallsite.set(ce.callsiteId, ce);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const uncertaintyEdgesByFrom = new Map<RoutineId, Uncertainty[]>();
|
|
92
|
+
for (const ue of graph.uncertaintyEdges ?? []) {
|
|
93
|
+
const list = uncertaintyEdgesByFrom.get(ue.from);
|
|
94
|
+
if (list) list.push(ue.uncertainty);
|
|
95
|
+
else uncertaintyEdgesByFrom.set(ue.from, [ue.uncertainty]);
|
|
96
|
+
}
|
|
97
|
+
const callSiteById = new Map<CallsiteId, CallSite>();
|
|
98
|
+
for (const r of model.routines) {
|
|
99
|
+
for (const cs of r.features.callSites) callSiteById.set(cs.id, cs);
|
|
100
|
+
}
|
|
101
|
+
let memoIndexes: EventFlowIndexes | undefined;
|
|
102
|
+
const diagnostics: Diagnostic[] = [];
|
|
103
|
+
return {
|
|
104
|
+
routineById,
|
|
105
|
+
objectsById,
|
|
106
|
+
tableById,
|
|
107
|
+
reverseCallGraph,
|
|
108
|
+
entryPoints,
|
|
109
|
+
reachableRoots,
|
|
110
|
+
internalReachableExternally,
|
|
111
|
+
transactionSpans,
|
|
112
|
+
resolvedCallEdgeByCallsite,
|
|
113
|
+
uncertaintyEdgesByFrom,
|
|
114
|
+
callSiteById,
|
|
115
|
+
getEventFlowIndexes() {
|
|
116
|
+
if (memoIndexes === undefined) memoIndexes = buildEventFlowIndexes(model);
|
|
117
|
+
return memoIndexes;
|
|
118
|
+
},
|
|
119
|
+
diagnostics,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
2
|
+
import type { Diagnostic, Finding } from "../model/finding.ts";
|
|
3
|
+
|
|
4
|
+
export interface GroupAndCapResult {
|
|
5
|
+
/** Findings kept after grouping + capping. Sorted by (groupKey, id). */
|
|
6
|
+
kept: readonly Finding[];
|
|
7
|
+
/** Findings dropped because their group exceeded the cap. Sorted by (groupKey, id). */
|
|
8
|
+
truncated: readonly Finding[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Group findings by `keyOf(finding)` then cap each group to `maxPerKey`.
|
|
13
|
+
* Within each group, findings are sorted by id (deterministic). Group
|
|
14
|
+
* iteration order is sorted by group key (deterministic).
|
|
15
|
+
*
|
|
16
|
+
* Use this to prevent output explosion in detectors that may emit many
|
|
17
|
+
* findings per resource (D44 read-after-write, D45 N-hop transitive).
|
|
18
|
+
*/
|
|
19
|
+
export function groupAndCap(
|
|
20
|
+
findings: readonly Finding[],
|
|
21
|
+
keyOf: (f: Finding) => string,
|
|
22
|
+
maxPerKey: number,
|
|
23
|
+
): GroupAndCapResult {
|
|
24
|
+
const groups = new Map<string, Finding[]>();
|
|
25
|
+
for (const f of findings) {
|
|
26
|
+
const k = keyOf(f);
|
|
27
|
+
const bag = groups.get(k) ?? [];
|
|
28
|
+
bag.push(f);
|
|
29
|
+
groups.set(k, bag);
|
|
30
|
+
}
|
|
31
|
+
const kept: Finding[] = [];
|
|
32
|
+
const truncated: Finding[] = [];
|
|
33
|
+
const sortedKeys = [...groups.keys()].sort(compareStrings);
|
|
34
|
+
for (const k of sortedKeys) {
|
|
35
|
+
const bag = groups.get(k) ?? [];
|
|
36
|
+
bag.sort((a, b) => compareStrings(a.id, b.id));
|
|
37
|
+
if (bag.length <= maxPerKey) {
|
|
38
|
+
kept.push(...bag);
|
|
39
|
+
} else {
|
|
40
|
+
kept.push(...bag.slice(0, maxPerKey));
|
|
41
|
+
truncated.push(...bag.slice(maxPerKey));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { kept, truncated };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a single warning diagnostic for a truncation event. Push into
|
|
49
|
+
* `ctx.diagnostics` when truncation actually happened (truncated.length > 0).
|
|
50
|
+
*/
|
|
51
|
+
export function truncationDiagnostic(
|
|
52
|
+
detector: string,
|
|
53
|
+
groupKeyKind: string,
|
|
54
|
+
truncatedCount: number,
|
|
55
|
+
): Diagnostic {
|
|
56
|
+
return {
|
|
57
|
+
severity: "warning",
|
|
58
|
+
stage: "detect",
|
|
59
|
+
message: `${detector}: truncated ${truncatedCount} finding(s) per ${groupKeyKind} cap`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
2
|
+
import type { EvidenceStep, Finding, FindingConfidence } from "../model/finding.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collapse N per-path findings that share a terminal anchor into ONE finding per
|
|
6
|
+
* anchor, with the other paths attached as `additionalPaths`.
|
|
7
|
+
*
|
|
8
|
+
* The shape problem this fixes: D1 and D2 walk the call graph and emit a finding
|
|
9
|
+
* per `(loop-containing routine, terminal op)` pair. When the same terminal op is
|
|
10
|
+
* reachable from M different in-loop ancestors, the user sees M findings on the
|
|
11
|
+
* same source location — same fix, same bug. The right entity is the terminal
|
|
12
|
+
* op; the multiple call chains are supporting traces.
|
|
13
|
+
*
|
|
14
|
+
* Contract for callers:
|
|
15
|
+
* - Pre-merge findings must already share the SAME `id`/`rootCauseKey` when they
|
|
16
|
+
* represent the same logical bug. Callers set those keys to omit any
|
|
17
|
+
* loop-identity component (so e.g. `d1/{terminalRoutine}/{terminalOp}`, not
|
|
18
|
+
* `d1/{loop}/{terminalOp}`).
|
|
19
|
+
* - Each input finding has exactly one `evidencePath`; `additionalPaths` is
|
|
20
|
+
* expected absent (this helper ignores it if set).
|
|
21
|
+
*
|
|
22
|
+
* Canonical-pick rules (deterministic — both `runDetectors` sort and `--baseline`
|
|
23
|
+
* fingerprints depend on stable order):
|
|
24
|
+
* 1. Highest severity wins (critical > high > medium > low > info). The
|
|
25
|
+
* canonical finding's `evidencePath` is the worst path so the user sees the
|
|
26
|
+
* most severe trace first.
|
|
27
|
+
* 2. Tie on severity → earliest `primaryLocation` (file, then line, then
|
|
28
|
+
* column). The pre-merge `primaryLocation` is always the same anchor
|
|
29
|
+
* across grouped findings, so this rarely matters, but it's stable.
|
|
30
|
+
* 3. Tie on location → smaller `id` lexicographically. Final stability anchor.
|
|
31
|
+
*
|
|
32
|
+
* Merge math:
|
|
33
|
+
* - `severity` = max across the group (canonical already holds this).
|
|
34
|
+
* - `confidence.level` = best level (`confirmed` > `likely` > `possible`).
|
|
35
|
+
* - `confidence.cappedBy` / `evidence` = union (deduped, sorted).
|
|
36
|
+
* - `affectedObjects` / `affectedTables` = union (deduped, sorted).
|
|
37
|
+
* - `additionalPaths` = the non-canonical paths sorted by
|
|
38
|
+
* (sourceUnitId, line, column, routineId) of the first evidence step — the
|
|
39
|
+
* loop step for D1, the call step for D2.
|
|
40
|
+
* - `rootCause` text is annotated with the path count when M > 1 (see
|
|
41
|
+
* `annotateRootCause` below). Callers control the canonical text.
|
|
42
|
+
*/
|
|
43
|
+
const SEV_RANK = { critical: 5, high: 4, medium: 3, low: 2, info: 1 } as const;
|
|
44
|
+
type Severity = keyof typeof SEV_RANK;
|
|
45
|
+
|
|
46
|
+
const CONF_RANK = { confirmed: 3, likely: 2, possible: 1 } as const;
|
|
47
|
+
type ConfLevel = keyof typeof CONF_RANK;
|
|
48
|
+
|
|
49
|
+
/** Sort key for a path's first step — used to order `additionalPaths` deterministically. */
|
|
50
|
+
function pathSortKey(path: EvidenceStep[]): string {
|
|
51
|
+
const step = path[0];
|
|
52
|
+
if (step === undefined) return "";
|
|
53
|
+
const a = step.sourceAnchor;
|
|
54
|
+
return `${a.sourceUnitId}|${String(a.range.startLine).padStart(8, "0")}|${String(a.range.startColumn).padStart(8, "0")}|${step.routineId}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pick the canonical (worst, then earliest, then smallest-id) finding from a group. */
|
|
58
|
+
function pickCanonical(group: Finding[]): Finding {
|
|
59
|
+
let best = group[0];
|
|
60
|
+
if (best === undefined) throw new Error("pickCanonical: empty group");
|
|
61
|
+
for (let i = 1; i < group.length; i++) {
|
|
62
|
+
const candidate = group[i];
|
|
63
|
+
if (candidate === undefined) continue;
|
|
64
|
+
if (SEV_RANK[candidate.severity] > SEV_RANK[best.severity]) {
|
|
65
|
+
best = candidate;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (SEV_RANK[candidate.severity] < SEV_RANK[best.severity]) continue;
|
|
69
|
+
const a = candidate.primaryLocation;
|
|
70
|
+
const b = best.primaryLocation;
|
|
71
|
+
if (a.sourceUnitId !== b.sourceUnitId) {
|
|
72
|
+
if (compareStrings(a.sourceUnitId, b.sourceUnitId) < 0) best = candidate;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (a.range.startLine !== b.range.startLine) {
|
|
76
|
+
if (a.range.startLine < b.range.startLine) best = candidate;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (a.range.startColumn !== b.range.startColumn) {
|
|
80
|
+
if (a.range.startColumn < b.range.startColumn) best = candidate;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (compareStrings(candidate.id, best.id) < 0) best = candidate;
|
|
84
|
+
}
|
|
85
|
+
return best;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mergeConfidence(group: Finding[]): FindingConfidence {
|
|
89
|
+
let bestLevel: ConfLevel = "possible";
|
|
90
|
+
const cappedBy = new Set<string>();
|
|
91
|
+
const evidence: FindingConfidence["evidence"] = [];
|
|
92
|
+
const seenEvidence = new Set<string>();
|
|
93
|
+
for (const f of group) {
|
|
94
|
+
if (CONF_RANK[f.confidence.level] > CONF_RANK[bestLevel]) bestLevel = f.confidence.level;
|
|
95
|
+
for (const c of f.confidence.cappedBy ?? []) cappedBy.add(c);
|
|
96
|
+
for (const e of f.confidence.evidence ?? []) {
|
|
97
|
+
const key = JSON.stringify(e);
|
|
98
|
+
if (!seenEvidence.has(key)) {
|
|
99
|
+
seenEvidence.add(key);
|
|
100
|
+
evidence.push(e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const result: FindingConfidence = { level: bestLevel, evidence };
|
|
105
|
+
const capArr = [...cappedBy].sort();
|
|
106
|
+
if (capArr.length > 0) {
|
|
107
|
+
result.cappedBy = capArr as NonNullable<FindingConfidence["cappedBy"]>;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unionSorted(...arrs: string[][]): string[] {
|
|
113
|
+
const set = new Set<string>();
|
|
114
|
+
for (const arr of arrs) for (const v of arr) set.add(v);
|
|
115
|
+
return [...set].sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Annotate a finding's `rootCause` with the path count when there are multiple
|
|
120
|
+
* reaching traces. Keeps the canonical text first (so existing tests / consumers
|
|
121
|
+
* that match the lead clause keep working), appends a separator + path-count
|
|
122
|
+
* note. When `pathCount === 1`, returns the input unchanged.
|
|
123
|
+
*/
|
|
124
|
+
export function annotateRootCause(rootCause: string, pathCount: number): string {
|
|
125
|
+
if (pathCount <= 1) return rootCause;
|
|
126
|
+
const others = pathCount - 1;
|
|
127
|
+
const noun = others === 1 ? "ancestor" : "ancestors";
|
|
128
|
+
return `${rootCause} (Also reached from ${others} other in-loop ${noun}.)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Group `findings` by `rootCauseKey` and collapse each group to one Finding with
|
|
133
|
+
* additionalPaths populated. Singleton groups pass through untouched.
|
|
134
|
+
*
|
|
135
|
+
* Output is sorted by canonical finding `id` for determinism. Callers run this
|
|
136
|
+
* AFTER their own per-path dedup (Set-keyed by id) so a single (loop, op) pair
|
|
137
|
+
* doesn't get its evidence duplicated.
|
|
138
|
+
*/
|
|
139
|
+
export function mergeByTerminal(findings: Finding[]): Finding[] {
|
|
140
|
+
const groups = new Map<string, Finding[]>();
|
|
141
|
+
for (const f of findings) {
|
|
142
|
+
const list = groups.get(f.rootCauseKey);
|
|
143
|
+
if (list === undefined) groups.set(f.rootCauseKey, [f]);
|
|
144
|
+
else list.push(f);
|
|
145
|
+
}
|
|
146
|
+
const out: Finding[] = [];
|
|
147
|
+
for (const group of groups.values()) {
|
|
148
|
+
if (group.length === 1) {
|
|
149
|
+
const only = group[0];
|
|
150
|
+
if (only !== undefined) out.push(only);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const canonical = pickCanonical(group);
|
|
154
|
+
const otherPaths = group
|
|
155
|
+
.filter((f) => f !== canonical)
|
|
156
|
+
.map((f) => f.evidencePath)
|
|
157
|
+
.sort((a, b) => compareStrings(pathSortKey(a), pathSortKey(b)));
|
|
158
|
+
const merged: Finding = {
|
|
159
|
+
...canonical,
|
|
160
|
+
severity: canonical.severity,
|
|
161
|
+
confidence: mergeConfidence(group),
|
|
162
|
+
affectedObjects: unionSorted(
|
|
163
|
+
...group.map((f) => f.affectedObjects.map((id) => id as unknown as string)),
|
|
164
|
+
) as typeof canonical.affectedObjects,
|
|
165
|
+
affectedTables: unionSorted(
|
|
166
|
+
...group.map((f) => f.affectedTables.map((id) => id as unknown as string)),
|
|
167
|
+
) as typeof canonical.affectedTables,
|
|
168
|
+
rootCause: annotateRootCause(canonical.rootCause, group.length),
|
|
169
|
+
additionalPaths: otherPaths,
|
|
170
|
+
};
|
|
171
|
+
out.push(merged);
|
|
172
|
+
}
|
|
173
|
+
return out.sort((a, b) => compareStrings(a.id, b.id));
|
|
174
|
+
}
|