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,206 @@
|
|
|
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 { OperationSite } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { LoopId } from "../model/ids.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
9
|
+
import { mayCommit } from "./capability-query.ts";
|
|
10
|
+
import { toConfidence } from "./confidence.ts";
|
|
11
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* D34 — `Commit` inside a loop, either directly or reached via an in-loop call.
|
|
15
|
+
*
|
|
16
|
+
* Per-iteration commits are both a perf smell (chunked SQL round-trips kill long-running
|
|
17
|
+
* batch jobs) and a correctness smell (partial-state failures aren't atomic; retries are
|
|
18
|
+
* unsafe). D8 catches commits *inside a posting-transaction span*; D34 catches commits
|
|
19
|
+
* inside *any* loop — including upgrade scripts, integrations, and validation routines
|
|
20
|
+
* that aren't inside a known transaction span.
|
|
21
|
+
*
|
|
22
|
+
* Direct case: `operationSites[].kind === "commit"` with non-empty `loopStack`.
|
|
23
|
+
* Transitive case: an in-loop callsite reaches a callee whose summary reports
|
|
24
|
+
* `commits === "yes"`. Confidence is lowered for transitive cases — the call chain
|
|
25
|
+
* is real but the commit isn't visible at the loop site.
|
|
26
|
+
*
|
|
27
|
+
* Severity:
|
|
28
|
+
* - direct commit in loop: `high`
|
|
29
|
+
* - direct commit in nested loop (depth >= 2): `critical`
|
|
30
|
+
* - transitive in-loop commit: `medium` (lowered confidence)
|
|
31
|
+
*
|
|
32
|
+
* Skipped:
|
|
33
|
+
* - nothing structural — every commit in a loop deserves attention. Intentional
|
|
34
|
+
* chunked-batch patterns can suppress via baseline.
|
|
35
|
+
*/
|
|
36
|
+
export function detectD34(
|
|
37
|
+
model: SemanticModel,
|
|
38
|
+
graph: CombinedGraph,
|
|
39
|
+
ctx: DetectorContext,
|
|
40
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
41
|
+
const findings: Finding[] = [];
|
|
42
|
+
const { routineById } = ctx;
|
|
43
|
+
let candidatesConsidered = 0;
|
|
44
|
+
let skippedParseIncomplete = 0;
|
|
45
|
+
let skippedSuppressedByDirect = 0;
|
|
46
|
+
|
|
47
|
+
for (const routine of model.routines) {
|
|
48
|
+
if (roleOf(routine) !== "primary") continue;
|
|
49
|
+
if (!routine.bodyAvailable) continue;
|
|
50
|
+
if (routine.parseIncomplete) {
|
|
51
|
+
skippedParseIncomplete++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
candidatesConsidered++;
|
|
55
|
+
|
|
56
|
+
const loopById = new Map(routine.features.loops.map((l) => [l.id, l]));
|
|
57
|
+
|
|
58
|
+
// (a) direct commit inside a loop
|
|
59
|
+
for (const site of routine.features.operationSites) {
|
|
60
|
+
if (site.kind !== "commit") continue;
|
|
61
|
+
if (site.loopStack.length === 0) continue;
|
|
62
|
+
const repId = site.loopStack.at(-1);
|
|
63
|
+
if (repId === undefined) continue;
|
|
64
|
+
const loop = loopById.get(repId);
|
|
65
|
+
if (loop === undefined) continue;
|
|
66
|
+
emitDirect(routine, loop, site, findings, model);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// (b) in-loop call reaches a routine whose summary commits
|
|
70
|
+
for (const cs of routine.features.callSites) {
|
|
71
|
+
if (cs.loopStack.length === 0) continue;
|
|
72
|
+
const repId = cs.loopStack.at(-1);
|
|
73
|
+
if (repId === undefined) continue;
|
|
74
|
+
const loop = loopById.get(repId);
|
|
75
|
+
if (loop === undefined) continue;
|
|
76
|
+
const edge = (graph.edgesByFrom.get(routine.id) ?? []).find((e) => e.callsiteId === cs.id);
|
|
77
|
+
if (edge === undefined) continue;
|
|
78
|
+
if (edge.kind === "event-dispatch") continue; // D2 lives here
|
|
79
|
+
const callee = routineById.get(edge.to);
|
|
80
|
+
if (callee?.summary === undefined || mayCommit(callee.summary) !== "yes") continue;
|
|
81
|
+
|
|
82
|
+
// Suppress if THIS routine also has a direct in-loop commit on the same loop — the
|
|
83
|
+
// direct finding is already the strongest evidence; the transitive one would be
|
|
84
|
+
// noise on top.
|
|
85
|
+
const hasDirectOnSameLoop = routine.features.operationSites.some(
|
|
86
|
+
(s) => s.kind === "commit" && s.loopStack.includes(loop.id),
|
|
87
|
+
);
|
|
88
|
+
if (hasDirectOnSameLoop) {
|
|
89
|
+
skippedSuppressedByDirect++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
emitTransitive(routine, loop, cs.id, cs.sourceAnchor, callee.name, findings, model);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
98
|
+
return {
|
|
99
|
+
findings: sorted,
|
|
100
|
+
stats: {
|
|
101
|
+
detector: "d34-commit-in-loop",
|
|
102
|
+
candidatesConsidered,
|
|
103
|
+
findingsEmitted: sorted.length,
|
|
104
|
+
skipped: {
|
|
105
|
+
...(skippedParseIncomplete > 0 ? { parseIncomplete: skippedParseIncomplete } : {}),
|
|
106
|
+
...(skippedSuppressedByDirect > 0 ? { suppressedByDirect: skippedSuppressedByDirect } : {}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function emitDirect(
|
|
113
|
+
routine: { id: string; objectId: string; name: string },
|
|
114
|
+
loop: { id: LoopId; type: string; sourceAnchor: OperationSite["sourceAnchor"] },
|
|
115
|
+
site: OperationSite,
|
|
116
|
+
findings: Finding[],
|
|
117
|
+
model: SemanticModel,
|
|
118
|
+
): void {
|
|
119
|
+
const depth = site.loopStack.length;
|
|
120
|
+
const severity: Finding["severity"] = depth >= 2 ? "critical" : "high";
|
|
121
|
+
const path: EvidenceStep[] = [
|
|
122
|
+
{
|
|
123
|
+
routineId: routine.id,
|
|
124
|
+
loopId: loop.id,
|
|
125
|
+
sourceAnchor: loop.sourceAnchor,
|
|
126
|
+
note: `${loop.type} loop`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
routineId: routine.id,
|
|
130
|
+
operationId: site.id,
|
|
131
|
+
sourceAnchor: site.sourceAnchor,
|
|
132
|
+
note: depth >= 2 ? `Commit (loop depth ${depth})` : "Commit",
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
const finding: Finding = {
|
|
136
|
+
id: `d34/${routine.id}/${loop.id}/${site.id}`,
|
|
137
|
+
rootCauseKey: `d34/${routine.id}/${loop.id}/${site.id}`,
|
|
138
|
+
detector: "d34-commit-in-loop",
|
|
139
|
+
title: depth >= 2 ? "Commit inside a nested loop" : "Commit inside a loop",
|
|
140
|
+
rootCause: `${routine.name} calls Commit inside a ${loop.type} loop — per-iteration commits break atomicity and prevent the job from being retried safely.`,
|
|
141
|
+
severity,
|
|
142
|
+
confidence: toConfidence([], "likely"),
|
|
143
|
+
primaryLocation: site.sourceAnchor,
|
|
144
|
+
evidencePath: path,
|
|
145
|
+
affectedObjects: [routine.objectId],
|
|
146
|
+
affectedTables: [],
|
|
147
|
+
fixOptions: [
|
|
148
|
+
{
|
|
149
|
+
description:
|
|
150
|
+
"Move the Commit outside the loop. If progress-saving is genuinely required, document a chunking strategy and consider a job queue.",
|
|
151
|
+
safety: "medium",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
provenance: [{ source: "tree-sitter" }],
|
|
155
|
+
};
|
|
156
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
157
|
+
findings.push(finding);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function emitTransitive(
|
|
161
|
+
routine: { id: string; objectId: string; name: string },
|
|
162
|
+
loop: { id: LoopId; type: string; sourceAnchor: OperationSite["sourceAnchor"] },
|
|
163
|
+
callsiteId: string,
|
|
164
|
+
callsiteAnchor: OperationSite["sourceAnchor"],
|
|
165
|
+
calleeName: string,
|
|
166
|
+
findings: Finding[],
|
|
167
|
+
model: SemanticModel,
|
|
168
|
+
): void {
|
|
169
|
+
const path: EvidenceStep[] = [
|
|
170
|
+
{
|
|
171
|
+
routineId: routine.id,
|
|
172
|
+
loopId: loop.id,
|
|
173
|
+
sourceAnchor: loop.sourceAnchor,
|
|
174
|
+
note: `${loop.type} loop`,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
routineId: routine.id,
|
|
178
|
+
callsiteId,
|
|
179
|
+
sourceAnchor: callsiteAnchor,
|
|
180
|
+
note: `calls ${calleeName} (transitively commits)`,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const finding: Finding = {
|
|
184
|
+
id: `d34/${routine.id}/${loop.id}/${callsiteId}`,
|
|
185
|
+
rootCauseKey: `d34/${routine.id}/${loop.id}/${callsiteId}`,
|
|
186
|
+
detector: "d34-commit-in-loop",
|
|
187
|
+
title: "Loop reaches a Commit through a callee",
|
|
188
|
+
rootCause: `${routine.name}'s ${loop.type} loop calls ${calleeName}, which commits — per-iteration commits break atomicity even when the Commit isn't visible at the loop site.`,
|
|
189
|
+
severity: "medium",
|
|
190
|
+
confidence: toConfidence([], "possible"),
|
|
191
|
+
primaryLocation: callsiteAnchor,
|
|
192
|
+
evidencePath: path,
|
|
193
|
+
affectedObjects: [routine.objectId],
|
|
194
|
+
affectedTables: [],
|
|
195
|
+
fixOptions: [
|
|
196
|
+
{
|
|
197
|
+
description:
|
|
198
|
+
"Verify the callee really needs to Commit. If the loop is correct as written, hoist the work that requires a Commit (or the Commit itself) outside the loop.",
|
|
199
|
+
safety: "medium",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
provenance: [{ source: "tree-sitter" }],
|
|
203
|
+
};
|
|
204
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
205
|
+
findings.push(finding);
|
|
206
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { mayCommit } from "./capability-query.ts";
|
|
9
|
+
import { toConfidence } from "./confidence.ts";
|
|
10
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* D35 — `Commit` reachable from an `[EventSubscriber]` routine, directly or via the
|
|
14
|
+
* call graph.
|
|
15
|
+
*
|
|
16
|
+
* Event subscribers run inside the publisher's execution context. A commit inside a
|
|
17
|
+
* subscriber pins the publisher's in-flight work and leaves orphaned partial state
|
|
18
|
+
* if the publisher later fails — the publisher cannot roll back what the subscriber
|
|
19
|
+
* already committed.
|
|
20
|
+
*
|
|
21
|
+
* Direct case (high, likely): subscriber routine has an `operationSites[].kind ===
|
|
22
|
+
* "commit"` directly in its body.
|
|
23
|
+
*
|
|
24
|
+
* Transitive case (high, possible): `mayCommit(subscriber.summary) === "yes"`. The
|
|
25
|
+
* summary engine has already flowed commits through the call graph for us, so the
|
|
26
|
+
* subscriber's own summary captures both direct and reached commits. We mark
|
|
27
|
+
* `confidence: possible` when the direct site isn't in this routine — the call
|
|
28
|
+
* chain is real but the commit might be a few hops away.
|
|
29
|
+
*
|
|
30
|
+
* Severity: `high` in both cases. Subscribers are a published extension surface;
|
|
31
|
+
* publishers shouldn't have to defensively re-validate their transaction after a
|
|
32
|
+
* subscriber runs.
|
|
33
|
+
*
|
|
34
|
+
* Skipped:
|
|
35
|
+
* - subscribers where `mayCommit(summary) === "no"` (the safe case).
|
|
36
|
+
* - non-event-subscriber routines (out of scope).
|
|
37
|
+
*/
|
|
38
|
+
export function detectD35(
|
|
39
|
+
model: SemanticModel,
|
|
40
|
+
_graph: CombinedGraph,
|
|
41
|
+
_ctx: DetectorContext,
|
|
42
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
43
|
+
const findings: Finding[] = [];
|
|
44
|
+
let candidatesConsidered = 0;
|
|
45
|
+
let safeCount = 0;
|
|
46
|
+
|
|
47
|
+
for (const routine of model.routines) {
|
|
48
|
+
if (roleOf(routine) !== "primary") continue;
|
|
49
|
+
if (routine.kind !== "event-subscriber") continue;
|
|
50
|
+
if (!routine.bodyAvailable) continue;
|
|
51
|
+
if (routine.parseIncomplete) continue;
|
|
52
|
+
candidatesConsidered++;
|
|
53
|
+
|
|
54
|
+
const summaryCommits = routine.summary === undefined ? "unknown" : mayCommit(routine.summary);
|
|
55
|
+
if (summaryCommits === "no") {
|
|
56
|
+
safeCount++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find a concrete commit anchor for the evidence path: prefer a direct one in
|
|
61
|
+
// this routine; fall back to the routine's own anchor.
|
|
62
|
+
const directCommit = routine.features.operationSites.find((s) => s.kind === "commit");
|
|
63
|
+
const isDirect = directCommit !== undefined;
|
|
64
|
+
|
|
65
|
+
emit(routine, directCommit, isDirect, summaryCommits, findings, model);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
69
|
+
return {
|
|
70
|
+
findings: sorted,
|
|
71
|
+
stats: {
|
|
72
|
+
detector: "d35-commit-in-event-subscriber",
|
|
73
|
+
candidatesConsidered,
|
|
74
|
+
findingsEmitted: sorted.length,
|
|
75
|
+
skipped: {
|
|
76
|
+
...(safeCount > 0 ? { commitFreeSubscriber: safeCount } : {}),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function emit(
|
|
83
|
+
routine: Routine,
|
|
84
|
+
directCommit: { id: string; sourceAnchor: EvidenceStep["sourceAnchor"] } | undefined,
|
|
85
|
+
isDirect: boolean,
|
|
86
|
+
summaryCommits: "yes" | "unknown",
|
|
87
|
+
findings: Finding[],
|
|
88
|
+
model: SemanticModel,
|
|
89
|
+
): void {
|
|
90
|
+
const anchor = directCommit?.sourceAnchor ?? routine.sourceAnchor;
|
|
91
|
+
const path: EvidenceStep[] = isDirect
|
|
92
|
+
? [
|
|
93
|
+
{
|
|
94
|
+
routineId: routine.id,
|
|
95
|
+
sourceAnchor: routine.sourceAnchor,
|
|
96
|
+
note: `[EventSubscriber] ${routine.name}`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
routineId: routine.id,
|
|
100
|
+
operationId: directCommit?.id,
|
|
101
|
+
sourceAnchor: anchor,
|
|
102
|
+
note: "Commit",
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
: [
|
|
106
|
+
{
|
|
107
|
+
routineId: routine.id,
|
|
108
|
+
sourceAnchor: routine.sourceAnchor,
|
|
109
|
+
note: `[EventSubscriber] ${routine.name} transitively commits (mayCommit(summary) == "${summaryCommits}")`,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const confidence = isDirect ? "likely" : summaryCommits === "yes" ? "possible" : "possible";
|
|
113
|
+
const titleSuffix = isDirect ? "" : " (via callee)";
|
|
114
|
+
|
|
115
|
+
const finding: Finding = {
|
|
116
|
+
id: `d35/${routine.id}/${isDirect ? (directCommit?.id ?? "x") : "transitive"}`,
|
|
117
|
+
rootCauseKey: `d35/${routine.id}`,
|
|
118
|
+
detector: "d35-commit-in-event-subscriber",
|
|
119
|
+
title: `Commit reachable from event subscriber${titleSuffix}`,
|
|
120
|
+
rootCause: `${routine.name} is an event subscriber that ${isDirect ? "calls Commit directly" : "transitively reaches Commit through its callees"} — the publisher cannot roll back the committed state if its work later fails.`,
|
|
121
|
+
severity: "high",
|
|
122
|
+
confidence: toConfidence([], confidence),
|
|
123
|
+
primaryLocation: anchor,
|
|
124
|
+
evidencePath: path,
|
|
125
|
+
affectedObjects: [routine.objectId],
|
|
126
|
+
affectedTables: [],
|
|
127
|
+
fixOptions: [
|
|
128
|
+
{
|
|
129
|
+
description:
|
|
130
|
+
"Remove the Commit from the subscriber path. If durable side effects are required, schedule them outside the publisher's transaction (e.g. a job-queue entry written without Commit, processed later).",
|
|
131
|
+
safety: "medium",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
provenance: [{ source: "tree-sitter" }],
|
|
135
|
+
};
|
|
136
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
137
|
+
findings.push(finding);
|
|
138
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { beforeAnchor } from "../engine/source-anchor.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { RecordOperation } from "../model/entities.ts";
|
|
6
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
9
|
+
import { toConfidence } from "./confidence.ts";
|
|
10
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
11
|
+
|
|
12
|
+
const LOAD_OPS: ReadonlySet<string> = new Set([
|
|
13
|
+
"Get",
|
|
14
|
+
"FindFirst",
|
|
15
|
+
"FindLast",
|
|
16
|
+
"FindSet",
|
|
17
|
+
"Find",
|
|
18
|
+
"Next",
|
|
19
|
+
]);
|
|
20
|
+
const LOAD_FIELDS_OPS: ReadonlySet<string> = new Set(["SetLoadFields", "AddLoadFields"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* D36 — `SetLoadFields` / `AddLoadFields` placed AFTER a load (`Get`/`Find*`/`Next`)
|
|
24
|
+
* with no later load on the same record variable. The partial-record optimisation
|
|
25
|
+
* doesn't apply to the row that was already loaded; the call has no effect on the
|
|
26
|
+
* data the routine then reads.
|
|
27
|
+
*
|
|
28
|
+
* Complements D3 (which flags missing SetLoadFields when a partial-load opportunity
|
|
29
|
+
* exists). D36 catches the inverse mistake: the developer added SetLoadFields but
|
|
30
|
+
* placed it where it can't matter.
|
|
31
|
+
*
|
|
32
|
+
* Detection (intra-routine, source-ordered):
|
|
33
|
+
* - for each SetLoadFields/AddLoadFields op O on record-var R;
|
|
34
|
+
* - find every LOAD_OP L on the same R with `before(L, O)` (anchor sort);
|
|
35
|
+
* - if there is at least one such L AND there is no subsequent LOAD_OP after O on
|
|
36
|
+
* the same R, then O is late — flag it.
|
|
37
|
+
* - if there IS a later LOAD_OP on R, the SetLoadFields applies to that next load
|
|
38
|
+
* (legitimate "prepare for next iteration" pattern); skip.
|
|
39
|
+
*
|
|
40
|
+
* Skipped:
|
|
41
|
+
* - temporary records (loadfields semantics don't apply);
|
|
42
|
+
* - by-var parameter records (caller may issue the next load);
|
|
43
|
+
* - record-vars with no preceding load (D3's domain — too-early is not D36).
|
|
44
|
+
*
|
|
45
|
+
* Severity: `low` by default; the cost is wasted intent, not a correctness break.
|
|
46
|
+
*/
|
|
47
|
+
export function detectD36(
|
|
48
|
+
model: SemanticModel,
|
|
49
|
+
_graph: CombinedGraph,
|
|
50
|
+
_ctx: DetectorContext,
|
|
51
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
52
|
+
const findings: Finding[] = [];
|
|
53
|
+
let candidatesConsidered = 0;
|
|
54
|
+
let skippedHasLaterLoad = 0;
|
|
55
|
+
let skippedNoPriorLoad = 0;
|
|
56
|
+
let skippedTempRecord = 0;
|
|
57
|
+
let skippedParameter = 0;
|
|
58
|
+
|
|
59
|
+
for (const routine of model.routines) {
|
|
60
|
+
if (roleOf(routine) !== "primary") continue;
|
|
61
|
+
if (!routine.bodyAvailable) continue;
|
|
62
|
+
if (routine.parseIncomplete) continue;
|
|
63
|
+
|
|
64
|
+
const paramRecordNames = new Set(
|
|
65
|
+
routine.features.recordVariables
|
|
66
|
+
.filter((rv) => rv.isParameter)
|
|
67
|
+
.map((rv) => rv.name.toLowerCase()),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
for (const op of routine.features.recordOperations) {
|
|
71
|
+
if (!LOAD_FIELDS_OPS.has(op.op)) continue;
|
|
72
|
+
candidatesConsidered++;
|
|
73
|
+
const varKey = op.recordVariableName.toLowerCase();
|
|
74
|
+
if (op.tempState.kind === "known" && op.tempState.value === true) {
|
|
75
|
+
skippedTempRecord++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (paramRecordNames.has(varKey)) {
|
|
79
|
+
skippedParameter++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hasPriorLoad = routine.features.recordOperations.some(
|
|
84
|
+
(other) =>
|
|
85
|
+
LOAD_OPS.has(other.op) &&
|
|
86
|
+
other.recordVariableName.toLowerCase() === varKey &&
|
|
87
|
+
beforeAnchor(other.sourceAnchor, op.sourceAnchor),
|
|
88
|
+
);
|
|
89
|
+
if (!hasPriorLoad) {
|
|
90
|
+
skippedNoPriorLoad++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hasLaterLoad = routine.features.recordOperations.some(
|
|
95
|
+
(other) =>
|
|
96
|
+
LOAD_OPS.has(other.op) &&
|
|
97
|
+
other.recordVariableName.toLowerCase() === varKey &&
|
|
98
|
+
beforeAnchor(op.sourceAnchor, other.sourceAnchor),
|
|
99
|
+
);
|
|
100
|
+
if (hasLaterLoad) {
|
|
101
|
+
skippedHasLaterLoad++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emit(routine, op, findings, model);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
110
|
+
return {
|
|
111
|
+
findings: sorted,
|
|
112
|
+
stats: {
|
|
113
|
+
detector: "d36-late-setloadfields",
|
|
114
|
+
candidatesConsidered,
|
|
115
|
+
findingsEmitted: sorted.length,
|
|
116
|
+
skipped: {
|
|
117
|
+
...(skippedHasLaterLoad > 0 ? { hasLaterLoad: skippedHasLaterLoad } : {}),
|
|
118
|
+
...(skippedNoPriorLoad > 0 ? { noPriorLoad: skippedNoPriorLoad } : {}),
|
|
119
|
+
...(skippedTempRecord > 0 ? { tempRecord: skippedTempRecord } : {}),
|
|
120
|
+
...(skippedParameter > 0 ? { parameter: skippedParameter } : {}),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function emit(
|
|
127
|
+
routine: { id: string; objectId: string; name: string },
|
|
128
|
+
op: RecordOperation,
|
|
129
|
+
findings: Finding[],
|
|
130
|
+
model: SemanticModel,
|
|
131
|
+
): void {
|
|
132
|
+
const path: EvidenceStep[] = [
|
|
133
|
+
{
|
|
134
|
+
routineId: routine.id,
|
|
135
|
+
operationId: op.id,
|
|
136
|
+
sourceAnchor: op.sourceAnchor,
|
|
137
|
+
note: `${op.op} on ${op.recordVariableName} after the record was already loaded — the call has no effect`,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
const finding: Finding = {
|
|
141
|
+
id: `d36/${routine.id}/${op.id}`,
|
|
142
|
+
rootCauseKey: `d36/${routine.id}/${op.id}`,
|
|
143
|
+
detector: "d36-late-setloadfields",
|
|
144
|
+
title: "SetLoadFields placed after the load",
|
|
145
|
+
rootCause: `${routine.name} calls ${op.op} on ${op.recordVariableName} after the record was already loaded and never loads it again — the partial-record optimisation cannot apply.`,
|
|
146
|
+
severity: "low",
|
|
147
|
+
confidence: toConfidence([], "likely"),
|
|
148
|
+
primaryLocation: op.sourceAnchor,
|
|
149
|
+
evidencePath: path,
|
|
150
|
+
affectedObjects: [routine.objectId],
|
|
151
|
+
affectedTables: op.tableId !== undefined ? [op.tableId] : [],
|
|
152
|
+
fixOptions: [
|
|
153
|
+
{
|
|
154
|
+
description: `Move the ${op.op} call to BEFORE the preceding Get / Find on ${op.recordVariableName}, so the loader can fetch only the listed fields.`,
|
|
155
|
+
safety: "high",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
provenance: [{ source: "tree-sitter" }],
|
|
159
|
+
};
|
|
160
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
161
|
+
findings.push(finding);
|
|
162
|
+
}
|