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,560 @@
|
|
|
1
|
+
import { extractCapabilities } from "../index/capability/extractor.ts";
|
|
2
|
+
import type { CapabilityFact, EventExtra } from "../model/capability.ts";
|
|
3
|
+
import type { CoverageReason } from "../model/coverage.ts";
|
|
4
|
+
import type { Routine, VariableSymbol } from "../model/entities.ts";
|
|
5
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
6
|
+
import type { EventId, RoutineId } from "../model/ids.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import type { DbEffect, RecordRoleSummary, RoutineSummary, Uncertainty } from "../model/summary.ts";
|
|
9
|
+
import { makeLap } from "../perf/profiler.ts";
|
|
10
|
+
import { composeInheritedCones } from "./capability-cone.ts";
|
|
11
|
+
import type { CombinedEdge, CombinedGraph } from "./combined-graph.ts";
|
|
12
|
+
import { walkRoutine } from "./control-flow-walker.ts";
|
|
13
|
+
import { effectKeyOf, joinPresence, mergeVia } from "./effect-lattice.ts";
|
|
14
|
+
import { tarjanScc } from "./scc.ts";
|
|
15
|
+
import { type SummaryContext, buildSummaryContext } from "./summary-context.ts";
|
|
16
|
+
import { baseIntraproceduralSummaryCtx } from "./summary-engine.ts";
|
|
17
|
+
import { compareStrings, uncertaintyKey } from "./uncertainty-util.ts";
|
|
18
|
+
|
|
19
|
+
const MAX_FIXED_POINT_ITERATIONS = 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional SCC instrumentation, gated on `AL_SEM_SCC_STATS=1`. Cheap counters useful
|
|
23
|
+
* for deciding whether further fixed-point optimizations (fingerprint caching, worklist
|
|
24
|
+
* propagation) are worth the complexity. Prints once to stderr at the end of `runSummaries`.
|
|
25
|
+
*/
|
|
26
|
+
const SCC_STATS_ENABLED = process.env.AL_SEM_SCC_STATS === "1";
|
|
27
|
+
|
|
28
|
+
export interface SummaryRunOptions {
|
|
29
|
+
/**
|
|
30
|
+
* A fixed leaf: a routine whose `summary` is authoritative and must NOT be recomputed.
|
|
31
|
+
* Default: any routine that already carries a summary. This covers both pipelines —
|
|
32
|
+
* workspace routines and a cold run's current-app routines start with `summary ===
|
|
33
|
+
* undefined`, so they are computed; merged-in artifact routines arrive with `summary`
|
|
34
|
+
* set, so they are fixed leaves.
|
|
35
|
+
*/
|
|
36
|
+
isLeaf?: (r: Routine) => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultIsLeaf(r: Routine): boolean {
|
|
40
|
+
return r.summary !== undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Map a combined-edge kind to the `via` tag callee effects inherit through it. */
|
|
44
|
+
function viaForEdge(
|
|
45
|
+
kind: CombinedEdge["kind"],
|
|
46
|
+
): "inherited" | "implicit-trigger" | "event-subscriber" | "dynamic" {
|
|
47
|
+
if (kind === "implicit-trigger") return "implicit-trigger";
|
|
48
|
+
if (kind === "event-dispatch") return "event-subscriber";
|
|
49
|
+
if (kind === "dynamic") return "dynamic";
|
|
50
|
+
return "inherited";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Map an EventSymbol.eventKind to the EventExtra.eventClass discriminant.
|
|
55
|
+
* Phase 1a Family B publisher fix — used by the publish-fact injector in runSummaries.
|
|
56
|
+
*/
|
|
57
|
+
function mapEventKindToClass(
|
|
58
|
+
k: "integration" | "business" | "trigger" | "internal" | "unknown",
|
|
59
|
+
): EventExtra["eventClass"] {
|
|
60
|
+
if (k === "business") return "Business";
|
|
61
|
+
if (k === "internal") return "Internal";
|
|
62
|
+
if (k === "trigger") return "Trigger";
|
|
63
|
+
return "Integration";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stable fingerprint for fixed-point change detection. */
|
|
67
|
+
function summaryFingerprint(s: RoutineSummary): string {
|
|
68
|
+
return JSON.stringify([
|
|
69
|
+
s.dbEffects.map((e) => `${e.effectKey}:${e.via}`),
|
|
70
|
+
s.hasUnresolvedCalls,
|
|
71
|
+
s.uncertainties.map(uncertaintyKey),
|
|
72
|
+
// Include may-fact fields so c1b changes are detected during fixed-point iteration.
|
|
73
|
+
// Entry-requirement fields (Phase 4 walker) and dirtyAtExit + currentLoadedFieldsAtExit
|
|
74
|
+
// (Phase 6) are pre-included so future phases cannot silently regress convergence by
|
|
75
|
+
// forgetting to extend the fingerprint.
|
|
76
|
+
s.parameterRoles.map((r) => [
|
|
77
|
+
r.parameterIndex,
|
|
78
|
+
r.loadsFromDbParam,
|
|
79
|
+
r.initialisesParam,
|
|
80
|
+
r.persistsCurrentRecord,
|
|
81
|
+
r.setBasedDbWrites,
|
|
82
|
+
r.validatesParam,
|
|
83
|
+
r.copiesIntoParam,
|
|
84
|
+
r.resetsFiltersOnParam,
|
|
85
|
+
r.mutatesParam,
|
|
86
|
+
r.requiresLoadedAtEntry,
|
|
87
|
+
r.mutatesBeforeLoad,
|
|
88
|
+
Array.isArray(r.requiredLoadedFieldsAtEntry)
|
|
89
|
+
? r.requiredLoadedFieldsAtEntry.join(",")
|
|
90
|
+
: r.requiredLoadedFieldsAtEntry,
|
|
91
|
+
r.dirtyAtExit,
|
|
92
|
+
typeof r.currentLoadedFieldsAtExit === "string"
|
|
93
|
+
? r.currentLoadedFieldsAtExit
|
|
94
|
+
: r.currentLoadedFieldsAtExit.join(","),
|
|
95
|
+
]),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compose a routine's full summary: start from its base intraprocedural summary, then fold
|
|
101
|
+
* in every outgoing combined edge's callee summary.
|
|
102
|
+
*
|
|
103
|
+
* O(1)-lookup + mutable-accumulator variant — the hot path inside `runSummaries`.
|
|
104
|
+
*
|
|
105
|
+
* Old hot-loop shape was:
|
|
106
|
+
* acc.dbEffects = mergeDbEffects(acc.dbEffects, calleeEffects); // new Map + sort
|
|
107
|
+
* acc.publishesEvents = [...new Set([...acc, ...callee])].sort(); // new Set + array + sort
|
|
108
|
+
* acc.uncertainties = dedupeUncertainties([...acc, ...callee]); // new Map + sort
|
|
109
|
+
* — once PER outgoing edge. On large dependency graphs that's a lot of GC pressure.
|
|
110
|
+
*
|
|
111
|
+
* New shape: seed local Maps/Sets from the base summary once, fold every callee into them
|
|
112
|
+
* in place, then materialize sorted/canonical arrays exactly once at the end.
|
|
113
|
+
*
|
|
114
|
+
* `baseLookup` optionally returns a precomputed base summary. When set, this function
|
|
115
|
+
* reuses the precomputed summary's data (skipping a fresh `baseIntraproceduralSummaryCtx`
|
|
116
|
+
* call). Required for the fixed-point loop on recursive SCCs.
|
|
117
|
+
*/
|
|
118
|
+
export function composeRoutineCtx(
|
|
119
|
+
routine: Routine,
|
|
120
|
+
lookup: (id: RoutineId) => RoutineSummary | undefined,
|
|
121
|
+
graph: CombinedGraph,
|
|
122
|
+
ctx: SummaryContext,
|
|
123
|
+
baseLookup?: (id: RoutineId) => RoutineSummary | undefined,
|
|
124
|
+
): RoutineSummary {
|
|
125
|
+
const base = baseLookup?.(routine.id) ?? baseIntraproceduralSummaryCtx(routine, ctx);
|
|
126
|
+
const calleeOpaque = (id: RoutineId): boolean => ctx.routineById.get(id)?.bodyAvailable === false;
|
|
127
|
+
|
|
128
|
+
// Scalar/lattice accumulators — copied because the base is shared and must not mutate.
|
|
129
|
+
let hasUnresolvedCalls = base.hasUnresolvedCalls;
|
|
130
|
+
|
|
131
|
+
// Set/Map accumulators seeded from the base. dbEffects key on effectKey so duplicate
|
|
132
|
+
// inherited entries merge by `via` precedence (matches the old mergeDbEffects).
|
|
133
|
+
const dbEffectsByKey = new Map<string, DbEffect>();
|
|
134
|
+
for (const e of base.dbEffects) dbEffectsByKey.set(e.effectKey, e);
|
|
135
|
+
const uncertaintiesByKey = new Map<string, Uncertainty>();
|
|
136
|
+
for (const u of base.uncertainties) uncertaintiesByKey.set(uncertaintyKey(u), u);
|
|
137
|
+
|
|
138
|
+
for (const edge of graph.edgesByFrom.get(routine.id) ?? []) {
|
|
139
|
+
const calleeSummary = lookup(edge.to);
|
|
140
|
+
if (calleeSummary === undefined) {
|
|
141
|
+
hasUnresolvedCalls = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const via = viaForEdge(edge.kind);
|
|
145
|
+
for (const e of calleeSummary.dbEffects) {
|
|
146
|
+
// Tag the inherited effect with the edge's via; recompute the key (effectKeyOf
|
|
147
|
+
// excludes via, so it's stable). Merge into the local map by precedence.
|
|
148
|
+
const key = effectKeyOf(e);
|
|
149
|
+
const existing = dbEffectsByKey.get(key);
|
|
150
|
+
if (existing) {
|
|
151
|
+
dbEffectsByKey.set(key, { ...existing, via: mergeVia(existing.via, via) });
|
|
152
|
+
} else {
|
|
153
|
+
dbEffectsByKey.set(key, { ...e, effectKey: key, via });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const u of calleeSummary.uncertainties) {
|
|
157
|
+
const k = uncertaintyKey(u);
|
|
158
|
+
if (!uncertaintiesByKey.has(k)) uncertaintiesByKey.set(k, u);
|
|
159
|
+
}
|
|
160
|
+
if (calleeSummary.hasUnresolvedCalls) hasUnresolvedCalls = true;
|
|
161
|
+
|
|
162
|
+
// interface / dynamic edges, and opaque callees, are confidence-lowering — the CALLER
|
|
163
|
+
// holds the callsiteId, so the opaque-callee uncertainty is attached here, not on the
|
|
164
|
+
// callee's own summary.
|
|
165
|
+
if (edge.kind === "interface" || edge.kind === "dynamic" || calleeOpaque(edge.to)) {
|
|
166
|
+
if (edge.callsiteId !== undefined) {
|
|
167
|
+
const u: Uncertainty = { kind: "opaque-callee", callsiteId: edge.callsiteId };
|
|
168
|
+
const k = uncertaintyKey(u);
|
|
169
|
+
if (!uncertaintiesByKey.has(k)) uncertaintiesByKey.set(k, u);
|
|
170
|
+
}
|
|
171
|
+
hasUnresolvedCalls = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Uncertainty edges (to-less call sites) on this routine — looked up by `from`
|
|
176
|
+
// instead of scanned globally, which on big graphs was O(R × U) per iteration.
|
|
177
|
+
for (const ue of ctx.uncertaintyEdgesByFrom.get(routine.id) ?? []) {
|
|
178
|
+
const k = uncertaintyKey(ue.uncertainty);
|
|
179
|
+
if (!uncertaintiesByKey.has(k)) uncertaintiesByKey.set(k, ue.uncertainty);
|
|
180
|
+
hasUnresolvedCalls = true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Materialize deterministic arrays once. dbEffects sort key matches the old
|
|
184
|
+
// mergeDbEffects: (effectKey, operationId).
|
|
185
|
+
const dbEffects = [...dbEffectsByKey.values()].sort((a, b) => {
|
|
186
|
+
if (a.effectKey !== b.effectKey) return compareStrings(a.effectKey, b.effectKey);
|
|
187
|
+
return compareStrings(a.operationId, b.operationId);
|
|
188
|
+
});
|
|
189
|
+
const uncertainties = [...uncertaintiesByKey.values()].sort((a, b) =>
|
|
190
|
+
compareStrings(uncertaintyKey(a), uncertaintyKey(b)),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Cross-call exit-effect composition (spec §(c1b)) — compose only when BOTH the
|
|
194
|
+
// caller-side source and the callee-side parameter are var.
|
|
195
|
+
// Deep-copy the base parameterRoles so we can mutate them independently each iteration.
|
|
196
|
+
// NOTE: entry-requirement fields (requiresLoadedAtEntry, mutatesBeforeLoad,
|
|
197
|
+
// requiredLoadedFieldsAtEntry) are overwritten by the walker below; base values
|
|
198
|
+
// computed in baseIntraproceduralSummaryCtx are intentionally superseded.
|
|
199
|
+
const parameterRoles: RecordRoleSummary[] = base.parameterRoles.map((r) => ({ ...r }));
|
|
200
|
+
for (const cs of routine.features.callSites) {
|
|
201
|
+
for (const binding of cs.argumentBindings) {
|
|
202
|
+
if (binding.bindingResolution !== "resolved") continue;
|
|
203
|
+
if (binding.sourceParameterIndex === undefined) continue;
|
|
204
|
+
if (!binding.callerSourceParameterIsVar) continue;
|
|
205
|
+
if (!binding.calleeParameterIsVar) continue;
|
|
206
|
+
const edge = ctx.resolvedCallEdgeByCallsite.get(cs.id);
|
|
207
|
+
if (edge?.to === undefined) continue;
|
|
208
|
+
const calleeRoutine = ctx.routineById.get(edge.to);
|
|
209
|
+
const calleeRoleSummary = lookup(edge.to);
|
|
210
|
+
const calleeRole = calleeRoleSummary?.parameterRoles.find(
|
|
211
|
+
(r) => r.parameterIndex === binding.parameterIndex,
|
|
212
|
+
);
|
|
213
|
+
const p = parameterRoles.find((r) => r.parameterIndex === binding.sourceParameterIndex);
|
|
214
|
+
if (p === undefined) continue;
|
|
215
|
+
// Opaque guard: any of the three reasons we cannot trust callee facts
|
|
216
|
+
// (no role, routine missing, body unavailable) takes the unknown branch.
|
|
217
|
+
// Phase 6's symbol-only projections may produce a callee role with
|
|
218
|
+
// bodyAvailable=false; this guard keeps such cases on the opaque path.
|
|
219
|
+
const opaque =
|
|
220
|
+
calleeRole === undefined ||
|
|
221
|
+
calleeRoutine === undefined ||
|
|
222
|
+
calleeRoutine.bodyAvailable === false;
|
|
223
|
+
if (opaque) {
|
|
224
|
+
p.persistsCurrentRecord = joinPresence(p.persistsCurrentRecord, "unknown");
|
|
225
|
+
p.setBasedDbWrites = joinPresence(p.setBasedDbWrites, "unknown");
|
|
226
|
+
p.validatesParam = joinPresence(p.validatesParam, "unknown");
|
|
227
|
+
p.copiesIntoParam = joinPresence(p.copiesIntoParam, "unknown");
|
|
228
|
+
p.resetsFiltersOnParam = joinPresence(p.resetsFiltersOnParam, "unknown");
|
|
229
|
+
} else {
|
|
230
|
+
p.persistsCurrentRecord = joinPresence(
|
|
231
|
+
p.persistsCurrentRecord,
|
|
232
|
+
calleeRole.persistsCurrentRecord,
|
|
233
|
+
);
|
|
234
|
+
p.setBasedDbWrites = joinPresence(p.setBasedDbWrites, calleeRole.setBasedDbWrites);
|
|
235
|
+
p.validatesParam = joinPresence(p.validatesParam, calleeRole.validatesParam);
|
|
236
|
+
p.copiesIntoParam = joinPresence(p.copiesIntoParam, calleeRole.copiesIntoParam);
|
|
237
|
+
p.resetsFiltersOnParam = joinPresence(
|
|
238
|
+
p.resetsFiltersOnParam,
|
|
239
|
+
calleeRole.resetsFiltersOnParam,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
p.mutatesParam = joinPresence(
|
|
243
|
+
joinPresence(p.persistsCurrentRecord, p.validatesParam),
|
|
244
|
+
p.copiesIntoParam,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Path-aware entry-requirement composition (spec §(c1a)).
|
|
250
|
+
// Run the walker with the current fixed-point `lookup` so callee summaries
|
|
251
|
+
// are from the current iteration (not the stale routine.summary).
|
|
252
|
+
// Only run on routines with a body — opaque/parse-incomplete cases stay "unknown"
|
|
253
|
+
// as set by baseIntraproceduralSummaryCtx.
|
|
254
|
+
//
|
|
255
|
+
// Memoization note (I7, deferred — see review): the walker is re-run every
|
|
256
|
+
// fixed-point iteration for routines in recursive SCCs. Measurement on DC/Cloud
|
|
257
|
+
// (AL_SEM_SCC_STATS=1: 35 recursive SCCs, maxSize=4, totalIters=65, maxIters=5)
|
|
258
|
+
// shows redundant walks are bounded at ~30 across the whole workspace — well
|
|
259
|
+
// below 1% of total walker cost. Phase 6's full statement-tree walker should
|
|
260
|
+
// re-measure; if cost grows materially, cache walkRoutine output keyed by
|
|
261
|
+
// (routineId × fingerprint of callee summaries' requires/mutates fields).
|
|
262
|
+
if (routine.bodyAvailable && !routine.parseIncomplete) {
|
|
263
|
+
const pathFacts = walkRoutine(routine, ctx, lookup);
|
|
264
|
+
const pathByIndex = new Map(pathFacts.map((p) => [p.parameterIndex, p]));
|
|
265
|
+
for (const role of parameterRoles) {
|
|
266
|
+
const pf = pathByIndex.get(role.parameterIndex);
|
|
267
|
+
if (pf === undefined) continue;
|
|
268
|
+
role.requiresLoadedAtEntry = pf.requiresLoadedAtEntry;
|
|
269
|
+
role.requiredLoadedFieldsAtEntry = pf.requiredLoadedFieldsAtEntry;
|
|
270
|
+
role.mutatesBeforeLoad = pf.mutatesBeforeLoad;
|
|
271
|
+
// Phase 6: walker now emits path-proven dirtyAtExit and
|
|
272
|
+
// currentLoadedFieldsAtExit. These override the base "unknown"
|
|
273
|
+
// placeholders set by baseIntraproceduralSummaryCtx.
|
|
274
|
+
role.dirtyAtExit = pf.dirtyAtExit;
|
|
275
|
+
role.currentLoadedFieldsAtExit = pf.currentLoadedFieldsAtExit;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
routineId: routine.id,
|
|
281
|
+
dbEffects,
|
|
282
|
+
inRecursiveCycle: base.inRecursiveCycle,
|
|
283
|
+
hasUnresolvedCalls,
|
|
284
|
+
uncertainties,
|
|
285
|
+
parameterRoles,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Public, model-based form kept for callers outside the runner (and the older spec text
|
|
291
|
+
* referenced in docs). Builds a one-shot context per call — fine for one-offs, NOT for
|
|
292
|
+
* hot loops. The runner uses `composeRoutineCtx` directly.
|
|
293
|
+
*/
|
|
294
|
+
export function composeRoutine(
|
|
295
|
+
routine: Routine,
|
|
296
|
+
lookup: (id: RoutineId) => RoutineSummary | undefined,
|
|
297
|
+
graph: CombinedGraph,
|
|
298
|
+
model: SemanticModel,
|
|
299
|
+
): RoutineSummary {
|
|
300
|
+
return composeRoutineCtx(routine, lookup, graph, buildSummaryContext(model, graph));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Compute a RoutineSummary for every NON-leaf routine and mutate `routine.summary` in place.
|
|
305
|
+
* Fixed-leaf routines (see `SummaryRunOptions.isLeaf`) keep their existing `summary` and are
|
|
306
|
+
* pre-seeded into the lookup map so callers compose against them. Walks the SCC condensation
|
|
307
|
+
* bottom-up; recursive SCCs get a finite monotone fixed-point.
|
|
308
|
+
*/
|
|
309
|
+
export function runSummaries(
|
|
310
|
+
model: SemanticModel,
|
|
311
|
+
graph: CombinedGraph,
|
|
312
|
+
diagnostics: Diagnostic[],
|
|
313
|
+
options?: SummaryRunOptions,
|
|
314
|
+
): void {
|
|
315
|
+
const isLeaf = options?.isLeaf ?? defaultIsLeaf;
|
|
316
|
+
const lap = makeLap("summary:");
|
|
317
|
+
// Build O(1) lookup indexes once. This replaces ~50G+ linear scans on Base App-sized
|
|
318
|
+
// dependencies — the cold-run dominant cost before the refactor.
|
|
319
|
+
const ctx = buildSummaryContext(model, graph);
|
|
320
|
+
lap("buildSummaryContext");
|
|
321
|
+
|
|
322
|
+
// Precompute base intraprocedural summaries ONCE per non-leaf routine. For non-recursive
|
|
323
|
+
// SCCs this is the same as before; for recursive SCCs it eliminates recomputing the base
|
|
324
|
+
// every fixed-point iteration (was a significant cost — base touches every record op,
|
|
325
|
+
// every call site, and computes parameterRoles, all of which never change between iters).
|
|
326
|
+
const baseSummaries = new Map<RoutineId, RoutineSummary>();
|
|
327
|
+
for (const r of model.routines) {
|
|
328
|
+
if (isLeaf(r)) continue;
|
|
329
|
+
baseSummaries.set(r.id, baseIntraproceduralSummaryCtx(r, ctx));
|
|
330
|
+
}
|
|
331
|
+
const baseLookup = (id: RoutineId): RoutineSummary | undefined => baseSummaries.get(id);
|
|
332
|
+
lap(`base-precompute (${baseSummaries.size} routines)`);
|
|
333
|
+
|
|
334
|
+
const final = new Map<RoutineId, RoutineSummary>();
|
|
335
|
+
// Pre-seed fixed leaves so composition can look them up; they are never recomputed.
|
|
336
|
+
for (const r of model.routines) {
|
|
337
|
+
if (isLeaf(r) && r.summary !== undefined) final.set(r.id, r.summary);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { sccs } = tarjanScc(graph);
|
|
341
|
+
lap(`tarjanScc (${sccs.length} sccs)`);
|
|
342
|
+
let nonRecursiveSccs = 0;
|
|
343
|
+
let recursiveSccs = 0;
|
|
344
|
+
let maxSccSize = 0;
|
|
345
|
+
let totalSccMembers = 0;
|
|
346
|
+
let totalIterations = 0;
|
|
347
|
+
let maxIterations = 0;
|
|
348
|
+
let fingerprintCalls = 0;
|
|
349
|
+
|
|
350
|
+
for (const scc of sccs) {
|
|
351
|
+
if (SCC_STATS_ENABLED) {
|
|
352
|
+
if (scc.recursive) recursiveSccs++;
|
|
353
|
+
else nonRecursiveSccs++;
|
|
354
|
+
if (scc.members.length > maxSccSize) maxSccSize = scc.members.length;
|
|
355
|
+
totalSccMembers += scc.members.length;
|
|
356
|
+
}
|
|
357
|
+
if (!scc.recursive) {
|
|
358
|
+
const id = scc.members[0];
|
|
359
|
+
const routine = id !== undefined ? ctx.routineById.get(id) : undefined;
|
|
360
|
+
if (id === undefined || routine === undefined) continue;
|
|
361
|
+
if (isLeaf(routine)) continue; // fixed leaf — already in `final`
|
|
362
|
+
final.set(
|
|
363
|
+
id,
|
|
364
|
+
composeRoutineCtx(routine, (x) => final.get(x), graph, ctx, baseLookup),
|
|
365
|
+
);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
// Recursive SCC — finite monotone fixed-point with snapshot iteration.
|
|
369
|
+
const inProgress = new Map<RoutineId, RoutineSummary>();
|
|
370
|
+
for (const id of scc.members) {
|
|
371
|
+
const routine = ctx.routineById.get(id);
|
|
372
|
+
if (routine === undefined) continue;
|
|
373
|
+
if (isLeaf(routine)) continue;
|
|
374
|
+
const base = baseSummaries.get(id);
|
|
375
|
+
if (base !== undefined) inProgress.set(id, base);
|
|
376
|
+
}
|
|
377
|
+
let iterations = 0;
|
|
378
|
+
let changed = true;
|
|
379
|
+
while (changed) {
|
|
380
|
+
changed = false;
|
|
381
|
+
iterations++;
|
|
382
|
+
const snapshot = new Map(inProgress);
|
|
383
|
+
for (const id of scc.members) {
|
|
384
|
+
const routine = ctx.routineById.get(id);
|
|
385
|
+
if (routine === undefined || isLeaf(routine)) continue;
|
|
386
|
+
const next = composeRoutineCtx(
|
|
387
|
+
routine,
|
|
388
|
+
(x) => snapshot.get(x) ?? final.get(x),
|
|
389
|
+
graph,
|
|
390
|
+
ctx,
|
|
391
|
+
baseLookup,
|
|
392
|
+
);
|
|
393
|
+
const prev = inProgress.get(id);
|
|
394
|
+
if (prev === undefined || summaryFingerprint(prev) !== summaryFingerprint(next)) {
|
|
395
|
+
changed = true;
|
|
396
|
+
}
|
|
397
|
+
if (SCC_STATS_ENABLED) fingerprintCalls += prev === undefined ? 1 : 2;
|
|
398
|
+
inProgress.set(id, next);
|
|
399
|
+
}
|
|
400
|
+
if (iterations >= MAX_FIXED_POINT_ITERATIONS) {
|
|
401
|
+
diagnostics.push({
|
|
402
|
+
severity: "warning",
|
|
403
|
+
stage: "summarize",
|
|
404
|
+
message: `Summary fixed-point did not converge for SCC [${scc.members.join(", ")}]`,
|
|
405
|
+
});
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (SCC_STATS_ENABLED) {
|
|
410
|
+
totalIterations += iterations;
|
|
411
|
+
if (iterations > maxIterations) maxIterations = iterations;
|
|
412
|
+
}
|
|
413
|
+
for (const id of scc.members) {
|
|
414
|
+
const summary = inProgress.get(id);
|
|
415
|
+
if (summary !== undefined) final.set(id, { ...summary, inRecursiveCycle: true });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
lap("scc-compose");
|
|
420
|
+
// Phase 0b-β: attach direct capability facts and baseline coverage to each non-leaf
|
|
421
|
+
// routine's final summary. Capability extraction is per-routine (intrinsic, not
|
|
422
|
+
// inherited) — it runs once here, after the fixed-point has converged, so every
|
|
423
|
+
// routine gets its own direct facts attached exactly once.
|
|
424
|
+
//
|
|
425
|
+
// capabilityFactsInherited stays [] and coverage.inheritedStatus stays "unknown"
|
|
426
|
+
// until Tasks 21-22 (coverage composer + inherited composition).
|
|
427
|
+
//
|
|
428
|
+
// Engine-never-throws: extractCapabilities catches its own errors; this loop adds a
|
|
429
|
+
// second outer guard so a bug in the wire-in itself cannot crash the engine.
|
|
430
|
+
//
|
|
431
|
+
// Index publisher events by routine ONCE. The per-routine publisher-fact injection below
|
|
432
|
+
// otherwise filtered the entire eventGraph.events array per routine — O(routines × events),
|
|
433
|
+
// which on Base App (84k non-leaf routines × thousands of platform events) is a multi-second
|
|
434
|
+
// quadratic. This map makes it O(routines + events). Insertion order matches eventGraph.events
|
|
435
|
+
// order, so injected-fact order is unchanged.
|
|
436
|
+
const eventsForPublisherIndex = model.eventGraph?.events ?? [];
|
|
437
|
+
const publisherEventsByRoutine = new Map<RoutineId, typeof eventsForPublisherIndex>();
|
|
438
|
+
for (const evt of eventsForPublisherIndex) {
|
|
439
|
+
if (evt.publisherRoutineId === undefined) continue;
|
|
440
|
+
const list = publisherEventsByRoutine.get(evt.publisherRoutineId);
|
|
441
|
+
if (list) list.push(evt);
|
|
442
|
+
else publisherEventsByRoutine.set(evt.publisherRoutineId, [evt]);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
for (const routine of model.routines) {
|
|
446
|
+
if (isLeaf(routine)) continue;
|
|
447
|
+
const summary = final.get(routine.id);
|
|
448
|
+
if (summary === undefined) continue;
|
|
449
|
+
|
|
450
|
+
// Build a minimal ExtractionContext — the orchestrator rebuilds variables
|
|
451
|
+
// internally, so ctx.variables here is just a safe placeholder.
|
|
452
|
+
const dispatchCtx = {
|
|
453
|
+
routine,
|
|
454
|
+
variables: new Map<string, VariableSymbol>(),
|
|
455
|
+
receiverTypeOf: (_name: string) => "unknown",
|
|
456
|
+
reportDiagnostic: (_d: Diagnostic) => {},
|
|
457
|
+
reportCoverageGap: (_r: CoverageReason, _t?: string) => {},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
let { facts, status, reasons } = extractCapabilities(routine, dispatchCtx);
|
|
461
|
+
|
|
462
|
+
// Opaque / parse-incomplete routines: skip extraction (no body to extract from).
|
|
463
|
+
// extractCapabilities already handles this internally but we reinforce intent.
|
|
464
|
+
if (!routine.bodyAvailable || routine.parseIncomplete) {
|
|
465
|
+
facts = [];
|
|
466
|
+
status = "unknown";
|
|
467
|
+
reasons = routine.parseIncomplete ? ["parse-incomplete"] : ["opaque-dependency"];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Phase 1a Family B — publisher-fact injection.
|
|
471
|
+
//
|
|
472
|
+
// The events-family extractor (src/index/capability/events.ts) runs at
|
|
473
|
+
// routine-indexing time and has no access to the resolved event graph, so it
|
|
474
|
+
// intentionally emits SUBSCRIBE facts only. Publisher routines ([IntegrationEvent]
|
|
475
|
+
// / [BusinessEvent] / [InternalEvent]) therefore have no direct publish facts, which
|
|
476
|
+
// caused publishesEventsOf to return [] for publisher routines — direct publish
|
|
477
|
+
// facts must be present so callers' inherited cones surface the event.
|
|
478
|
+
//
|
|
479
|
+
// Here, in the summary engine where model.eventGraph is already resolved, we look up
|
|
480
|
+
// every EventSymbol whose publisherRoutineId === routine.id and inject one direct
|
|
481
|
+
// publish CapabilityFact per event. The Task-22 inherited-facts BFS (below) then
|
|
482
|
+
// naturally propagates these facts to callers via direct-call edges, satisfying the
|
|
483
|
+
// publishesEventsOf invariant on publisher routines.
|
|
484
|
+
//
|
|
485
|
+
// Injection runs BEFORE final.set so the facts are present when the BFS reads
|
|
486
|
+
// capabilityFactsDirect for each routine.
|
|
487
|
+
const publisherEvents = publisherEventsByRoutine.get(routine.id) ?? [];
|
|
488
|
+
for (const evt of publisherEvents) {
|
|
489
|
+
const extra: EventExtra = {
|
|
490
|
+
kind: "event",
|
|
491
|
+
eventClass: mapEventKindToClass(evt.eventKind),
|
|
492
|
+
};
|
|
493
|
+
const publishFact: CapabilityFact = {
|
|
494
|
+
subject: routine.id,
|
|
495
|
+
op: "publish",
|
|
496
|
+
resourceKind: "event",
|
|
497
|
+
resourceId: evt.id as EventId,
|
|
498
|
+
confidence: "static",
|
|
499
|
+
provenance: "direct",
|
|
500
|
+
via: "self",
|
|
501
|
+
extra,
|
|
502
|
+
};
|
|
503
|
+
facts = [...facts, publishFact];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
final.set(routine.id, {
|
|
507
|
+
...summary,
|
|
508
|
+
capabilityFactsDirect: facts,
|
|
509
|
+
capabilityFactsInherited: [],
|
|
510
|
+
coverage: {
|
|
511
|
+
subject: routine.id,
|
|
512
|
+
directStatus: status,
|
|
513
|
+
inheritedStatus: "unknown",
|
|
514
|
+
reasons,
|
|
515
|
+
unknownTargets: [],
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
lap("task17-capability-extract");
|
|
521
|
+
const cones = composeInheritedCones(model, final, isLeaf, diagnostics);
|
|
522
|
+
for (const routine of model.routines) {
|
|
523
|
+
if (isLeaf(routine)) continue;
|
|
524
|
+
const summary = final.get(routine.id);
|
|
525
|
+
if (summary === undefined) continue;
|
|
526
|
+
const cone = cones.get(routine.id);
|
|
527
|
+
if (cone === undefined) continue; // composition failed → keep task-17 baseline coverage + []
|
|
528
|
+
final.set(routine.id, {
|
|
529
|
+
...summary,
|
|
530
|
+
capabilityFactsInherited: cone.inherited,
|
|
531
|
+
coverage: cone.coverage,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
lap("task22-inherited-bfs");
|
|
536
|
+
for (const routine of model.routines) {
|
|
537
|
+
if (isLeaf(routine)) continue; // leaves keep their authoritative summary
|
|
538
|
+
routine.summary = final.get(routine.id);
|
|
539
|
+
}
|
|
540
|
+
lap("writeback");
|
|
541
|
+
|
|
542
|
+
if (SCC_STATS_ENABLED) {
|
|
543
|
+
process.stderr.write(
|
|
544
|
+
`al-sem SCC stats: sccs=${nonRecursiveSccs + recursiveSccs}` +
|
|
545
|
+
` (recursive=${recursiveSccs} non-recursive=${nonRecursiveSccs})` +
|
|
546
|
+
` maxSize=${maxSccSize} totalMembers=${totalSccMembers}` +
|
|
547
|
+
` totalIterations=${totalIterations} maxIterations=${maxIterations}` +
|
|
548
|
+
` fingerprintCalls=${fingerprintCalls}\n`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Phase 2b-compatible entry point: run summaries with the default leaf policy. */
|
|
554
|
+
export function computeSummaries(
|
|
555
|
+
model: SemanticModel,
|
|
556
|
+
graph: CombinedGraph,
|
|
557
|
+
diagnostics: Diagnostic[],
|
|
558
|
+
): void {
|
|
559
|
+
runSummaries(model, graph, diagnostics);
|
|
560
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
publishesEventsOf,
|
|
3
|
+
reachableCoverage,
|
|
4
|
+
writesTablesOf,
|
|
5
|
+
} from "../detectors/capability-query.ts";
|
|
6
|
+
import { roleOf } from "../model/entities.ts";
|
|
7
|
+
import type { EventId, OperationId, RoutineId, TableId } from "../model/ids.ts";
|
|
8
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
9
|
+
import type { CombinedGraph } from "./combined-graph.ts";
|
|
10
|
+
import type { ReverseCallGraph } from "./reverse-call-graph.ts";
|
|
11
|
+
|
|
12
|
+
export interface TransactionSpan {
|
|
13
|
+
/** The Commit operation that bounds the span. */
|
|
14
|
+
commitOperationId: OperationId;
|
|
15
|
+
/** The routine containing the bounding Commit. */
|
|
16
|
+
commitRoutineId: RoutineId;
|
|
17
|
+
/** All routines reachable backward from commitRoutineId up to another commit or root. */
|
|
18
|
+
routinesInSpan: RoutineId[];
|
|
19
|
+
/** Union of tables written by any routine in the span (from writesTablesOf — sorted+deduped). */
|
|
20
|
+
writesTables: TableId[];
|
|
21
|
+
/** Union of events published by any routine in the span (from publishesEventsOf — sorted+deduped). */
|
|
22
|
+
publishesEvents: EventId[];
|
|
23
|
+
/** Entry roots — routines in the span with no upstream caller inside the span. */
|
|
24
|
+
spanRoots: RoutineId[];
|
|
25
|
+
/**
|
|
26
|
+
* True iff EVERY routine in `routinesInSpan` has a defined summary AND
|
|
27
|
+
* `reachableCoverage(summary) === "complete"`. False when at least
|
|
28
|
+
* one routine's inherited cone is partial / unknown — which means
|
|
29
|
+
* the aggregated `writesTables` and `publishesEvents` sets may be
|
|
30
|
+
* an under-approximation of what this span actually touches.
|
|
31
|
+
*
|
|
32
|
+
* Replaces the legacy per-routine `summary.writesTables === "unknown"`
|
|
33
|
+
* probe D9 used to do. The signal is coarser (overall coverage, not
|
|
34
|
+
* per-family) because Phase 1a's `reachableCoverage` is per-routine
|
|
35
|
+
* overall. When per-family coverage roll-up lands (spec §3.7
|
|
36
|
+
* FingerprintCoverage.perFamily), this field upgrades cleanly to a
|
|
37
|
+
* `writesCoverage: CoverageStatus`.
|
|
38
|
+
*/
|
|
39
|
+
coverageComplete: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MAX_DEPTH = 50;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compute transaction spans. For each primary-app routine that contains a Commit, walk
|
|
46
|
+
* callers backward to find every routine that participates in the transaction. The walk
|
|
47
|
+
* stops at any routine that itself commits (that's a prior span's domain) or at the depth
|
|
48
|
+
* bound. Each Commit operation yields one TransactionSpan.
|
|
49
|
+
*/
|
|
50
|
+
export function computeTransactionSpans(
|
|
51
|
+
model: SemanticModel,
|
|
52
|
+
_graph: CombinedGraph,
|
|
53
|
+
reverse: ReverseCallGraph,
|
|
54
|
+
): TransactionSpan[] {
|
|
55
|
+
const routineById = new Map(model.routines.map((r) => [r.id, r]));
|
|
56
|
+
const spans: TransactionSpan[] = [];
|
|
57
|
+
|
|
58
|
+
// Build: routineId → list of Commit operationIds in that routine (from operationSites
|
|
59
|
+
// with kind === "commit"). We only care about primary-app routines.
|
|
60
|
+
const commitsByRoutine = new Map<RoutineId, OperationId[]>();
|
|
61
|
+
for (const r of model.routines) {
|
|
62
|
+
if (roleOf(r) !== "primary") continue;
|
|
63
|
+
const commitOps = r.features.operationSites
|
|
64
|
+
.filter((os) => os.kind === "commit")
|
|
65
|
+
.map((os) => os.id);
|
|
66
|
+
if (commitOps.length > 0) commitsByRoutine.set(r.id, commitOps);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const [commitRoutineId, commitOps] of commitsByRoutine) {
|
|
70
|
+
for (const commitOperationId of commitOps) {
|
|
71
|
+
const visited = new Set<RoutineId>();
|
|
72
|
+
const queue: { id: RoutineId; depth: number }[] = [{ id: commitRoutineId, depth: 0 }];
|
|
73
|
+
while (queue.length > 0) {
|
|
74
|
+
const item = queue.shift();
|
|
75
|
+
if (!item) break;
|
|
76
|
+
const { id, depth } = item;
|
|
77
|
+
if (visited.has(id)) continue;
|
|
78
|
+
visited.add(id);
|
|
79
|
+
if (depth >= MAX_DEPTH) continue;
|
|
80
|
+
// Don't walk past another committing routine (prior span bounds the trace).
|
|
81
|
+
if (id !== commitRoutineId && commitsByRoutine.has(id)) continue;
|
|
82
|
+
for (const caller of reverse.get(id) ?? []) {
|
|
83
|
+
if (!visited.has(caller.from)) queue.push({ id: caller.from, depth: depth + 1 });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const writes = new Set<TableId>();
|
|
87
|
+
const events = new Set<EventId>();
|
|
88
|
+
let coverageComplete = true;
|
|
89
|
+
for (const rid of visited) {
|
|
90
|
+
const r = routineById.get(rid);
|
|
91
|
+
if (r?.summary === undefined) {
|
|
92
|
+
coverageComplete = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const t of writesTablesOf(r.summary)) writes.add(t);
|
|
96
|
+
for (const e of publishesEventsOf(r.summary)) events.add(e);
|
|
97
|
+
if (reachableCoverage(r.summary) !== "complete") coverageComplete = false;
|
|
98
|
+
}
|
|
99
|
+
const spanRoots = [...visited].filter((rid) => (reverse.get(rid) ?? []).length === 0);
|
|
100
|
+
spans.push({
|
|
101
|
+
commitOperationId,
|
|
102
|
+
commitRoutineId,
|
|
103
|
+
routinesInSpan: [...visited].sort(),
|
|
104
|
+
writesTables: [...writes].sort(),
|
|
105
|
+
publishesEvents: [...events].sort(),
|
|
106
|
+
spanRoots: spanRoots.sort(),
|
|
107
|
+
coverageComplete,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return spans;
|
|
112
|
+
}
|