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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enumerateDispatchSites — bridges the event-flow substrate with D43's dispatch-site
|
|
3
|
+
* guard analysis.
|
|
4
|
+
*
|
|
5
|
+
* For each call-site in the model where a non-publisher routine calls an event-publisher
|
|
6
|
+
* routine, this function produces a `DispatchSite` record capturing:
|
|
7
|
+
* - Which publisher and event are involved.
|
|
8
|
+
* - Which caller routine + call-site makes the dispatch.
|
|
9
|
+
* - The caller's actual argument bound to the publisher's IsHandled formal (if any).
|
|
10
|
+
* - Any post-call `conditionReference` entries in the caller body that reference the
|
|
11
|
+
* IsHandled actual — these are the guards D43 reasons about.
|
|
12
|
+
* - The set of tables the caller writes (transitively) when a post-call guard exists.
|
|
13
|
+
*
|
|
14
|
+
* ## Argument-binding strategy (fallback documented here)
|
|
15
|
+
*
|
|
16
|
+
* Phase 4's `CallArgumentBinding` carries a `sourceVariableName` for record-typed
|
|
17
|
+
* arguments, but Boolean arguments resolve as `bindingResolution: "non-record-arg"` with
|
|
18
|
+
* no `sourceVariableName`. Consequently, for a Boolean IsHandled formal we fall back to
|
|
19
|
+
* the raw `argumentTexts[handledFormalIndex]` value, lowercased. This is sound for
|
|
20
|
+
* canonical AL where the caller passes a declared local or parameter by its bare
|
|
21
|
+
* identifier — `OnBeforePost(Rec, IsHandled)` → text `"IsHandled"` → lowercased
|
|
22
|
+
* `"ishandled"`. The fallback is documented in `DispatchSiteHandledActual.variableName`.
|
|
23
|
+
*/
|
|
24
|
+
import { writesTablesOf } from "../detectors/capability-query.ts";
|
|
25
|
+
import type { ConditionReference } from "../model/entities.ts";
|
|
26
|
+
import type { CallsiteId, EventId, RoutineId, TableId } from "../model/ids.ts";
|
|
27
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
28
|
+
import { type EventFlowIndexes, IS_HANDLED_RE } from "./event-flow.ts";
|
|
29
|
+
import { compareStrings } from "./uncertainty-util.ts";
|
|
30
|
+
|
|
31
|
+
export interface DispatchSiteHandledActual {
|
|
32
|
+
/**
|
|
33
|
+
* Lowercased name of the caller's local/global variable passed by-var into the
|
|
34
|
+
* publisher's IsHandled formal. Derived from the raw `argumentTexts` entry when
|
|
35
|
+
* `argumentBindings` cannot carry Boolean-scalar identity (the common fallback path).
|
|
36
|
+
*/
|
|
37
|
+
variableName: string;
|
|
38
|
+
/** 0-based index of the publisher's IsHandled formal among its parameters. */
|
|
39
|
+
formalIndex: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DispatchSite {
|
|
43
|
+
publisherRoutine: RoutineId;
|
|
44
|
+
eventId: EventId;
|
|
45
|
+
callerRoutine: RoutineId;
|
|
46
|
+
callsiteId: CallsiteId;
|
|
47
|
+
/**
|
|
48
|
+
* Set when the publisher has a `var Boolean` formal matching IS_HANDLED_RE AND the
|
|
49
|
+
* caller binds a recognizable identifier to that formal.
|
|
50
|
+
*/
|
|
51
|
+
handledActual?: DispatchSiteHandledActual;
|
|
52
|
+
/**
|
|
53
|
+
* Caller's `conditionReferences` whose identifier matches `handledActual.variableName`
|
|
54
|
+
* and whose `referenceAnchor` is AFTER the callsite's start position (line / column).
|
|
55
|
+
*/
|
|
56
|
+
postCallGuards: readonly ConditionReference[];
|
|
57
|
+
/**
|
|
58
|
+
* Tables written by the caller's reachable cone when a post-call guard exists.
|
|
59
|
+
* Initial cut: full caller-summary writes — branch-level slice is deferred to T3+.
|
|
60
|
+
* Empty when there is no post-call guard or when the caller has no summary.
|
|
61
|
+
*/
|
|
62
|
+
guardedTablesWritten: readonly TableId[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function enumerateDispatchSites(
|
|
66
|
+
model: SemanticModel,
|
|
67
|
+
_ix: EventFlowIndexes,
|
|
68
|
+
): readonly DispatchSite[] {
|
|
69
|
+
const out: DispatchSite[] = [];
|
|
70
|
+
|
|
71
|
+
// Index routines by id for O(1) lookup.
|
|
72
|
+
const routineById = new Map(model.routines.map((r) => [r.id, r] as const));
|
|
73
|
+
|
|
74
|
+
// Collect the set of event-publisher routine ids together with their associated
|
|
75
|
+
// event id (from the event graph). We prefer the event graph as the authoritative
|
|
76
|
+
// source because it carries the stable EventId.
|
|
77
|
+
const publisherEventId = new Map<RoutineId, EventId>();
|
|
78
|
+
for (const ev of model.eventGraph.events) {
|
|
79
|
+
if (ev.publisherRoutineId !== undefined) {
|
|
80
|
+
publisherEventId.set(ev.publisherRoutineId, ev.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Walk typed edges. We look for edges whose target is an event-publisher routine.
|
|
85
|
+
// Relevant edge kinds: "direct-call", "object-run-resolved", "dependency-export".
|
|
86
|
+
// "event-dispatch" edges are publisher→subscriber (not caller→publisher), so they
|
|
87
|
+
// are explicitly skipped — they don't carry a callsiteId.
|
|
88
|
+
const typedEdges = model.typedEdges ?? [];
|
|
89
|
+
for (const edge of typedEdges) {
|
|
90
|
+
if (
|
|
91
|
+
edge.kind !== "direct-call" &&
|
|
92
|
+
edge.kind !== "object-run-resolved" &&
|
|
93
|
+
edge.kind !== "dependency-export"
|
|
94
|
+
) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const publisherEventEntry = publisherEventId.get(edge.to);
|
|
99
|
+
if (publisherEventEntry === undefined) continue; // target is not a publisher
|
|
100
|
+
|
|
101
|
+
const caller = routineById.get(edge.from);
|
|
102
|
+
const publisher = routineById.get(edge.to);
|
|
103
|
+
if (!caller || !publisher) continue;
|
|
104
|
+
|
|
105
|
+
// Find the callsite in the caller's features that matches the edge's callsiteId.
|
|
106
|
+
const callSite = caller.features.callSites.find((cs) => cs.id === edge.callsiteId);
|
|
107
|
+
if (!callSite) continue;
|
|
108
|
+
|
|
109
|
+
// Find the publisher's IsHandled-shaped formal: var Boolean matching IS_HANDLED_RE.
|
|
110
|
+
// ParameterSymbol uses `isVar` (boolean) and `typeText` (raw type string).
|
|
111
|
+
let handledFormalIndex = -1;
|
|
112
|
+
for (let i = 0; i < publisher.parameters.length; i++) {
|
|
113
|
+
const p = publisher.parameters[i];
|
|
114
|
+
if (p === undefined) continue;
|
|
115
|
+
if (!p.isVar) continue;
|
|
116
|
+
if (!/^boolean$/i.test(p.typeText)) continue;
|
|
117
|
+
if (!IS_HANDLED_RE.test(p.name)) continue;
|
|
118
|
+
handledFormalIndex = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Identify the caller's actual variable bound to the IsHandled formal.
|
|
123
|
+
//
|
|
124
|
+
// Primary path: check argumentBindings for a sourceVariableName (works when the
|
|
125
|
+
// binding resolution succeeded and the argument is not a record). For Boolean
|
|
126
|
+
// formals, `bindingResolution` is typically "non-record-arg" with no
|
|
127
|
+
// `sourceVariableName`, so this path usually returns nothing.
|
|
128
|
+
//
|
|
129
|
+
// Fallback path: read `argumentTexts[handledFormalIndex]` directly. The raw text
|
|
130
|
+
// is the bare identifier the caller passed (e.g. "IsHandled"), and lowercasing it
|
|
131
|
+
// gives the canonical name for conditionReference matching.
|
|
132
|
+
let handledActual: DispatchSiteHandledActual | undefined;
|
|
133
|
+
if (handledFormalIndex >= 0) {
|
|
134
|
+
// Primary: try argumentBindings.sourceVariableName.
|
|
135
|
+
const binding = callSite.argumentBindings[handledFormalIndex];
|
|
136
|
+
const nameFromBinding =
|
|
137
|
+
binding !== undefined &&
|
|
138
|
+
binding.sourceKind !== "unknown" &&
|
|
139
|
+
typeof binding.sourceVariableName === "string" &&
|
|
140
|
+
binding.sourceVariableName.length > 0
|
|
141
|
+
? binding.sourceVariableName
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
// Fallback: raw argumentTexts entry, lowercased.
|
|
145
|
+
const nameFromText =
|
|
146
|
+
nameFromBinding === undefined
|
|
147
|
+
? (callSite.argumentTexts[handledFormalIndex]?.trim().toLowerCase() ?? undefined)
|
|
148
|
+
: undefined;
|
|
149
|
+
|
|
150
|
+
const varName = nameFromBinding ?? nameFromText;
|
|
151
|
+
if (varName !== undefined && varName.length > 0) {
|
|
152
|
+
handledActual = { variableName: varName, formalIndex: handledFormalIndex };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Collect post-call conditionReferences from the CALLER's features.
|
|
157
|
+
// A reference is "post-call" when its referenceAnchor is strictly after the
|
|
158
|
+
// callsite's start position (same source unit, later line OR same line later column).
|
|
159
|
+
const postCallGuards: ConditionReference[] = [];
|
|
160
|
+
if (handledActual) {
|
|
161
|
+
const callRow = callSite.sourceAnchor.range.startLine;
|
|
162
|
+
const callCol = callSite.sourceAnchor.range.startColumn;
|
|
163
|
+
const actualLower = handledActual.variableName.toLowerCase();
|
|
164
|
+
for (const cref of caller.features.conditionReferences ?? []) {
|
|
165
|
+
if (cref.identifier !== actualLower) continue;
|
|
166
|
+
const r = cref.referenceAnchor.range;
|
|
167
|
+
if (r.startLine < callRow) continue;
|
|
168
|
+
if (r.startLine === callRow && r.startColumn <= callCol) continue;
|
|
169
|
+
postCallGuards.push(cref);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Guarded writes: full caller transitive writes when at least one post-call guard
|
|
174
|
+
// exists. Branch-level slicing (then vs else) is deferred to T3.
|
|
175
|
+
let guardedTablesWritten: TableId[] = [];
|
|
176
|
+
if (postCallGuards.length > 0 && caller.summary !== undefined) {
|
|
177
|
+
guardedTablesWritten = writesTablesOf(caller.summary);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
out.push({
|
|
181
|
+
publisherRoutine: publisher.id,
|
|
182
|
+
eventId: publisherEventEntry,
|
|
183
|
+
callerRoutine: caller.id,
|
|
184
|
+
callsiteId: edge.callsiteId,
|
|
185
|
+
handledActual,
|
|
186
|
+
postCallGuards,
|
|
187
|
+
guardedTablesWritten,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
out.sort(
|
|
192
|
+
(a, b) =>
|
|
193
|
+
compareStrings(a.eventId, b.eventId) ||
|
|
194
|
+
compareStrings(a.callerRoutine, b.callerRoutine) ||
|
|
195
|
+
compareStrings(a.callsiteId, b.callsiteId),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { TempState } from "../model/entities.ts";
|
|
2
|
+
import type { TableId } from "../model/ids.ts";
|
|
3
|
+
import type { DbEffect, EffectPresence } from "../model/summary.ts";
|
|
4
|
+
import { compareStrings } from "./uncertainty-util.ts";
|
|
5
|
+
|
|
6
|
+
// --- tri-state presence: no < unknown < yes ---
|
|
7
|
+
const PRESENCE_RANK: Record<EffectPresence, number> = { no: 0, unknown: 1, yes: 2 };
|
|
8
|
+
const PRESENCE_BY_RANK: EffectPresence[] = ["no", "unknown", "yes"];
|
|
9
|
+
|
|
10
|
+
/** Lattice join: the more-informative presence wins (yes > unknown > no). Monotone. */
|
|
11
|
+
export function joinPresence(a: EffectPresence, b: EffectPresence): EffectPresence {
|
|
12
|
+
const rank = Math.max(PRESENCE_RANK[a], PRESENCE_RANK[b]);
|
|
13
|
+
return PRESENCE_BY_RANK[rank] ?? "unknown";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Set union of two `writesTables` values; `"unknown"` absorbs. Result is sorted. */
|
|
17
|
+
export function unionTables(
|
|
18
|
+
a: TableId[] | "unknown",
|
|
19
|
+
b: TableId[] | "unknown",
|
|
20
|
+
): TableId[] | "unknown" {
|
|
21
|
+
if (a === "unknown" || b === "unknown") return "unknown";
|
|
22
|
+
return [...new Set([...a, ...b])].sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Normalise a TempState to a short stable key fragment. */
|
|
26
|
+
function tempStateKey(t: TempState): string {
|
|
27
|
+
if (t.kind === "known") return t.value ? "t" : "f";
|
|
28
|
+
if (t.kind === "parameter-dependent") return `p${t.parameterIndex}`;
|
|
29
|
+
return "u";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stable, path-insensitive effect key. Deliberately EXCLUDES `via` — two DbEffects for the
|
|
34
|
+
* same operation are the same fact regardless of how they propagated. Used to de-dupe.
|
|
35
|
+
*/
|
|
36
|
+
export function effectKeyOf(
|
|
37
|
+
e: Pick<DbEffect, "op" | "tableId" | "operationId" | "tempState">,
|
|
38
|
+
): string {
|
|
39
|
+
return `${e.op}|${e.tableId}|${e.operationId}|${tempStateKey(e.tempState)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- via precedence: most specific wins ---
|
|
43
|
+
const VIA_RANK: Record<DbEffect["via"], number> = {
|
|
44
|
+
direct: 4,
|
|
45
|
+
"implicit-trigger": 3,
|
|
46
|
+
"event-subscriber": 2,
|
|
47
|
+
dynamic: 1,
|
|
48
|
+
inherited: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Merge two `via` tags, keeping the most specific (direct > implicit-trigger > event-subscriber > dynamic > inherited). */
|
|
52
|
+
export function mergeVia(a: DbEffect["via"], b: DbEffect["via"]): DbEffect["via"] {
|
|
53
|
+
return VIA_RANK[a] >= VIA_RANK[b] ? a : b;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Concatenate and de-dupe DbEffects by `effectKey`. The key is always recomputed via
|
|
58
|
+
* `effectKeyOf` — a caller-supplied `effectKey` is treated as a write target only, never
|
|
59
|
+
* trusted as input, so a divergent caller scheme can't cause silent dedupe failures. When
|
|
60
|
+
* two effects share a key, `via` is merged by precedence. Result is sorted by
|
|
61
|
+
* `(effectKey, operationId)` for determinism.
|
|
62
|
+
*/
|
|
63
|
+
export function mergeDbEffects(...lists: DbEffect[][]): DbEffect[] {
|
|
64
|
+
const byKey = new Map<string, DbEffect>();
|
|
65
|
+
for (const list of lists) {
|
|
66
|
+
for (const e of list) {
|
|
67
|
+
const key = effectKeyOf(e);
|
|
68
|
+
const normalized: DbEffect = { ...e, effectKey: key };
|
|
69
|
+
const existing = byKey.get(key);
|
|
70
|
+
if (existing) {
|
|
71
|
+
byKey.set(key, { ...existing, via: mergeVia(existing.via, normalized.via) });
|
|
72
|
+
} else {
|
|
73
|
+
byKey.set(key, normalized);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...byKey.values()].sort((a, b) => {
|
|
78
|
+
if (a.effectKey !== b.effectKey) return compareStrings(a.effectKey, b.effectKey);
|
|
79
|
+
return compareStrings(a.operationId, b.operationId);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { roleOf } from "../model/entities.ts";
|
|
2
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
3
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Identify primary-app routines that BC dispatches to without an in-app caller — the root
|
|
7
|
+
* set for reachability analysis. An entry point is a primary-app routine whose `kind` is
|
|
8
|
+
* `trigger` (OnRun, OnOpenPage, OnValidate, OnInsert, etc.) or `event-subscriber`.
|
|
9
|
+
*
|
|
10
|
+
* Used by D8 (transaction-span roots). For D14's forward-reachability question, use
|
|
11
|
+
* `findReachableRoots` instead — it adds non-local procedures as roots, because they
|
|
12
|
+
* may be called from outside the app.
|
|
13
|
+
*/
|
|
14
|
+
export function findEntryPoints(model: SemanticModel): RoutineId[] {
|
|
15
|
+
const out: RoutineId[] = [];
|
|
16
|
+
for (const r of model.routines) {
|
|
17
|
+
if (roleOf(r) !== "primary") continue;
|
|
18
|
+
if (r.kind === "event-subscriber" || r.kind === "trigger") {
|
|
19
|
+
out.push(r.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out.sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Root set for "is this routine ever invoked?" reachability analysis (D14 dead-routine).
|
|
27
|
+
* Adds to `findEntryPoints` the procedures we cannot prove are app-scoped:
|
|
28
|
+
* - default-access (public): callable from any app that declares us as a dependency;
|
|
29
|
+
* - `protected`: callable from overriding codeunits;
|
|
30
|
+
* - `internal`: callable only from apps named in this workspace's `internalsVisibleTo`.
|
|
31
|
+
*
|
|
32
|
+
* `internal` is added as a root only when `internalReachableExternally` is true (some
|
|
33
|
+
* app is granted access). With no `internalsVisibleTo` entries, `internal` is
|
|
34
|
+
* effectively app-scoped, so its dead-routine reachability collapses to the `local`
|
|
35
|
+
* case — D14 then flags `internal` procedures with no in-app caller.
|
|
36
|
+
*
|
|
37
|
+
* D8's transaction-span computation deliberately does NOT use this — its roots are
|
|
38
|
+
* trigger/event boundaries (the natural unit of work), not every exported procedure.
|
|
39
|
+
*/
|
|
40
|
+
export function findReachableRoots(
|
|
41
|
+
model: SemanticModel,
|
|
42
|
+
opts: { internalReachableExternally: boolean } = { internalReachableExternally: false },
|
|
43
|
+
): RoutineId[] {
|
|
44
|
+
const out: RoutineId[] = [];
|
|
45
|
+
for (const r of model.routines) {
|
|
46
|
+
if (roleOf(r) !== "primary") continue;
|
|
47
|
+
if (r.kind === "event-subscriber" || r.kind === "trigger") {
|
|
48
|
+
out.push(r.id);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (r.kind !== "procedure") continue;
|
|
52
|
+
if (r.accessModifier === "local") continue;
|
|
53
|
+
if (r.accessModifier === "internal" && !opts.internalReachableExternally) continue;
|
|
54
|
+
out.push(r.id);
|
|
55
|
+
}
|
|
56
|
+
return out.sort();
|
|
57
|
+
}
|