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,493 @@
|
|
|
1
|
+
import type { CapabilityFact, CapabilityVia } from "../model/capability.ts";
|
|
2
|
+
import type { CoverageRecord } from "../model/coverage.ts";
|
|
3
|
+
import type { GraphEdge } from "../model/graph-edge.ts";
|
|
4
|
+
import type { CallsiteId, OperationId } from "../model/ids.ts";
|
|
5
|
+
import type { StableObjectId, StableRoutineId } from "../model/stable-identity.ts";
|
|
6
|
+
import type { CallsiteEvidence, OperationEvidence } from "../snapshot/types.ts";
|
|
7
|
+
|
|
8
|
+
export interface WitnessRequest {
|
|
9
|
+
rootId: StableRoutineId;
|
|
10
|
+
fact: CapabilityFact;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum witness paths to return.
|
|
13
|
+
* - `false` — suppress all witness reconstruction; returns
|
|
14
|
+
* `{ paths: [], truncated: false, incomplete: false }`. Caller
|
|
15
|
+
* knows it disabled witnesses; the outcome does NOT signal
|
|
16
|
+
* "no evidence" — it signals "evidence not collected".
|
|
17
|
+
* - `number` — clamped to `[0, HARD_PATH_CAP=256]`.
|
|
18
|
+
* - `"all"` — equivalent to `HARD_PATH_CAP`.
|
|
19
|
+
*/
|
|
20
|
+
limit: false | number | "all";
|
|
21
|
+
indexes: WitnessIndexes;
|
|
22
|
+
maxDepth?: number;
|
|
23
|
+
maxExpandedStates?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WitnessIndexes {
|
|
27
|
+
outgoingEdges: Map<StableRoutineId, readonly GraphEdge[]>;
|
|
28
|
+
directFactsByRoutine: Map<StableRoutineId, readonly CapabilityFact[]>;
|
|
29
|
+
callsiteById: Map<CallsiteId, CallsiteEvidence>;
|
|
30
|
+
operationById: Map<OperationId, OperationEvidence>;
|
|
31
|
+
routineDisplayById: Map<StableRoutineId, string>;
|
|
32
|
+
stableIdToDisplay: Map<string, string>;
|
|
33
|
+
eventDisplayById?: Map<string, string>;
|
|
34
|
+
coverageByRoutine?: Map<StableRoutineId, CoverageRecord>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WitnessOutcome {
|
|
38
|
+
paths: readonly WitnessPath[];
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
incomplete: boolean;
|
|
41
|
+
diagnostics: readonly WitnessDiagnostic[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WitnessPath {
|
|
45
|
+
hops: readonly WitnessHop[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type WitnessDiagnostic =
|
|
49
|
+
| { kind: "path-limit-reached"; cap: number }
|
|
50
|
+
| { kind: "depth-exceeded"; maxDepth: number }
|
|
51
|
+
| { kind: "state-limit-exceeded"; maxExpandedStates: number }
|
|
52
|
+
| { kind: "first-hop-not-found"; callsiteId?: CallsiteId; via: CapabilityVia }
|
|
53
|
+
| { kind: "terminal-not-found" }
|
|
54
|
+
| { kind: "missing-callsite-evidence"; callsiteId: CallsiteId }
|
|
55
|
+
| { kind: "missing-operation-evidence"; operationId: OperationId }
|
|
56
|
+
| { kind: "missing-witness-anchor"; subject: StableRoutineId }
|
|
57
|
+
| { kind: "unresolved-dispatch"; callsiteId?: CallsiteId }
|
|
58
|
+
| { kind: "opaque-or-unresolved-boundary"; routineId?: StableRoutineId; target?: string };
|
|
59
|
+
|
|
60
|
+
export type WitnessHop =
|
|
61
|
+
| {
|
|
62
|
+
kind: "call";
|
|
63
|
+
routineId: StableRoutineId;
|
|
64
|
+
routineDisplay: string;
|
|
65
|
+
calleeDisplay: string;
|
|
66
|
+
callsiteId: CallsiteId;
|
|
67
|
+
sourceFile?: string;
|
|
68
|
+
line?: number;
|
|
69
|
+
column?: number;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
kind: "object-run";
|
|
73
|
+
routineId: StableRoutineId;
|
|
74
|
+
routineDisplay: string;
|
|
75
|
+
targetObjectId?: StableObjectId;
|
|
76
|
+
targetDisplay?: string;
|
|
77
|
+
resolved: boolean;
|
|
78
|
+
callsiteId?: CallsiteId;
|
|
79
|
+
sourceFile?: string;
|
|
80
|
+
line?: number;
|
|
81
|
+
column?: number;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
kind: "event-dispatch";
|
|
85
|
+
routineId: StableRoutineId;
|
|
86
|
+
routineDisplay: string;
|
|
87
|
+
eventId: string;
|
|
88
|
+
eventDisplay: string;
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
kind: "implicit-trigger";
|
|
92
|
+
routineId: StableRoutineId;
|
|
93
|
+
routineDisplay: string;
|
|
94
|
+
triggerKind: string;
|
|
95
|
+
operationId?: OperationId;
|
|
96
|
+
sourceFile?: string;
|
|
97
|
+
line?: number;
|
|
98
|
+
column?: number;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
kind: "dependency-export";
|
|
102
|
+
routineId: StableRoutineId;
|
|
103
|
+
routineDisplay: string;
|
|
104
|
+
targetAppGuid: string;
|
|
105
|
+
callsiteId: CallsiteId;
|
|
106
|
+
calleeDisplay?: string;
|
|
107
|
+
sourceFile?: string;
|
|
108
|
+
line?: number;
|
|
109
|
+
column?: number;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
kind: "terminal";
|
|
113
|
+
evidenceKind: "operation" | "callsite" | "synthetic";
|
|
114
|
+
operationId?: OperationId;
|
|
115
|
+
callsiteId?: CallsiteId;
|
|
116
|
+
displayText: string;
|
|
117
|
+
sourceFile?: string;
|
|
118
|
+
line?: number;
|
|
119
|
+
column?: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const HARD_PATH_CAP = 256;
|
|
123
|
+
|
|
124
|
+
function effectiveLimit(limit: WitnessRequest["limit"]): number {
|
|
125
|
+
if (limit === false) return 0;
|
|
126
|
+
if (limit === "all") return HARD_PATH_CAP;
|
|
127
|
+
return Math.max(0, Math.min(limit, HARD_PATH_CAP));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildDirectTerminal(
|
|
131
|
+
evidenceKind: "operation" | "callsite",
|
|
132
|
+
witnessId: OperationId | CallsiteId,
|
|
133
|
+
evidence:
|
|
134
|
+
| {
|
|
135
|
+
displayText?: string;
|
|
136
|
+
calleeDisplay?: string;
|
|
137
|
+
sourceFile?: string;
|
|
138
|
+
startLine?: number;
|
|
139
|
+
startColumn?: number;
|
|
140
|
+
}
|
|
141
|
+
| undefined,
|
|
142
|
+
): WitnessHop & { kind: "terminal" } {
|
|
143
|
+
const displayText =
|
|
144
|
+
evidenceKind === "operation"
|
|
145
|
+
? (evidence?.displayText ?? String(witnessId))
|
|
146
|
+
: (evidence?.calleeDisplay ?? String(witnessId));
|
|
147
|
+
return {
|
|
148
|
+
kind: "terminal",
|
|
149
|
+
evidenceKind,
|
|
150
|
+
operationId: evidenceKind === "operation" ? (witnessId as OperationId) : undefined,
|
|
151
|
+
callsiteId: evidenceKind === "callsite" ? (witnessId as CallsiteId) : undefined,
|
|
152
|
+
displayText,
|
|
153
|
+
sourceFile: evidence?.sourceFile,
|
|
154
|
+
line: evidence?.startLine,
|
|
155
|
+
column: evidence?.startColumn,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function reconstructWitnessPaths(req: WitnessRequest): WitnessOutcome {
|
|
160
|
+
const cap = effectiveLimit(req.limit);
|
|
161
|
+
if (cap === 0) return { paths: [], truncated: false, incomplete: false, diagnostics: [] };
|
|
162
|
+
|
|
163
|
+
const diagnostics: WitnessDiagnostic[] = [];
|
|
164
|
+
const { fact, indexes } = req;
|
|
165
|
+
|
|
166
|
+
if (fact.provenance === "direct") {
|
|
167
|
+
// Case A: witnessOperationId present → terminal "operation" evidence.
|
|
168
|
+
if (fact.witnessOperationId !== undefined) {
|
|
169
|
+
const ev = indexes.operationById.get(fact.witnessOperationId);
|
|
170
|
+
const hop = buildDirectTerminal("operation", fact.witnessOperationId, ev);
|
|
171
|
+
if (ev === undefined) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
kind: "missing-operation-evidence",
|
|
174
|
+
operationId: fact.witnessOperationId,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const path: WitnessPath = { hops: [hop] };
|
|
178
|
+
return { paths: [path], truncated: false, incomplete: ev === undefined, diagnostics };
|
|
179
|
+
}
|
|
180
|
+
// Case B: only witnessCallsiteId → terminal "callsite" evidence.
|
|
181
|
+
if (fact.witnessCallsiteId !== undefined) {
|
|
182
|
+
const ev = indexes.callsiteById.get(fact.witnessCallsiteId);
|
|
183
|
+
const hop = buildDirectTerminal("callsite", fact.witnessCallsiteId, ev);
|
|
184
|
+
if (ev === undefined) {
|
|
185
|
+
diagnostics.push({ kind: "missing-callsite-evidence", callsiteId: fact.witnessCallsiteId });
|
|
186
|
+
}
|
|
187
|
+
const path: WitnessPath = { hops: [hop] };
|
|
188
|
+
return { paths: [path], truncated: false, incomplete: ev === undefined, diagnostics };
|
|
189
|
+
}
|
|
190
|
+
// Direct with no witness anchor → synthetic.
|
|
191
|
+
diagnostics.push({ kind: "missing-witness-anchor", subject: fact.subject as StableRoutineId });
|
|
192
|
+
return {
|
|
193
|
+
paths: [
|
|
194
|
+
{
|
|
195
|
+
hops: [
|
|
196
|
+
{
|
|
197
|
+
kind: "terminal",
|
|
198
|
+
evidenceKind: "synthetic",
|
|
199
|
+
displayText: `${fact.op} ${fact.resourceKind}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
truncated: false,
|
|
205
|
+
incomplete: true,
|
|
206
|
+
diagnostics,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- Case C: inherited fact ---
|
|
211
|
+
const maxDepth = req.maxDepth ?? 64;
|
|
212
|
+
const maxStates = req.maxExpandedStates ?? 25_000;
|
|
213
|
+
let stateCount = 0;
|
|
214
|
+
let truncated = false;
|
|
215
|
+
let incomplete = false;
|
|
216
|
+
let depthExceeded = false;
|
|
217
|
+
|
|
218
|
+
const paths: WitnessPath[] = [];
|
|
219
|
+
|
|
220
|
+
// First-hop constraint
|
|
221
|
+
if (fact.witnessCallsiteId === undefined) {
|
|
222
|
+
diagnostics.push({ kind: "first-hop-not-found", via: fact.via });
|
|
223
|
+
return { paths: [], truncated: false, incomplete: true, diagnostics };
|
|
224
|
+
}
|
|
225
|
+
const outFromRoot = indexes.outgoingEdges.get(req.rootId) ?? [];
|
|
226
|
+
const firstEdges = outFromRoot.filter(
|
|
227
|
+
(e) =>
|
|
228
|
+
"callsiteId" in e && (e as { callsiteId?: CallsiteId }).callsiteId === fact.witnessCallsiteId,
|
|
229
|
+
);
|
|
230
|
+
if (firstEdges.length === 0) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
kind: "first-hop-not-found",
|
|
233
|
+
callsiteId: fact.witnessCallsiteId,
|
|
234
|
+
via: fact.via,
|
|
235
|
+
});
|
|
236
|
+
return { paths: [], truncated: false, incomplete: true, diagnostics };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type State = { routine: StableRoutineId; hops: WitnessHop[]; visited: Set<StableRoutineId> };
|
|
240
|
+
const queue: State[] = [];
|
|
241
|
+
for (const edge of firstEdges) {
|
|
242
|
+
const hop = edgeToHop(edge, indexes);
|
|
243
|
+
if (hop === undefined) continue;
|
|
244
|
+
const to = (edge as { to?: StableRoutineId }).to;
|
|
245
|
+
if (to === undefined) continue;
|
|
246
|
+
const visited = new Set<StableRoutineId>([req.rootId, to]);
|
|
247
|
+
queue.push({ routine: to, hops: [hop], visited });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
queue.sort((a, b) => (a.routine < b.routine ? -1 : 1));
|
|
251
|
+
|
|
252
|
+
while (queue.length > 0 && paths.length < cap) {
|
|
253
|
+
const state = queue.shift();
|
|
254
|
+
if (state === undefined) break;
|
|
255
|
+
stateCount++;
|
|
256
|
+
if (stateCount > maxStates) {
|
|
257
|
+
diagnostics.push({ kind: "state-limit-exceeded", maxExpandedStates: maxStates });
|
|
258
|
+
incomplete = true;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if (state.hops.length > maxDepth) {
|
|
262
|
+
depthExceeded = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Terminal check
|
|
266
|
+
const directs = indexes.directFactsByRoutine.get(state.routine) ?? [];
|
|
267
|
+
const equivalent = directs.find((d) => factEquivalent(d, fact));
|
|
268
|
+
if (equivalent !== undefined) {
|
|
269
|
+
const terminalHop = terminalHopFromFact(equivalent, indexes);
|
|
270
|
+
paths.push({ hops: [...state.hops, terminalHop] });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Opaque-or-unresolved-boundary detection at the current routine: no
|
|
274
|
+
// outgoing edges, no direct facts, and coverage marks it unknown. The
|
|
275
|
+
// path terminates here — the last hop (already in state.hops) is the
|
|
276
|
+
// boundary; no terminal hop is appended.
|
|
277
|
+
const routineOut = indexes.outgoingEdges.get(state.routine);
|
|
278
|
+
const routineCov = indexes.coverageByRoutine?.get(state.routine);
|
|
279
|
+
if (
|
|
280
|
+
(routineOut?.length ?? 0) === 0 &&
|
|
281
|
+
directs.length === 0 &&
|
|
282
|
+
routineCov?.directStatus === "unknown"
|
|
283
|
+
) {
|
|
284
|
+
diagnostics.push({ kind: "opaque-or-unresolved-boundary", routineId: state.routine });
|
|
285
|
+
// Emit the path up to (and including) the last hop already accumulated;
|
|
286
|
+
// no terminal is appended because we have no evidence at this boundary.
|
|
287
|
+
if (state.hops.length > 0) {
|
|
288
|
+
paths.push({ hops: [...state.hops] });
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
// Expand
|
|
293
|
+
const out = indexes.outgoingEdges.get(state.routine) ?? [];
|
|
294
|
+
const sorted = [...out].sort(edgeCompare);
|
|
295
|
+
for (const edge of sorted) {
|
|
296
|
+
const to = (edge as { to?: StableRoutineId }).to;
|
|
297
|
+
if (to === undefined) continue;
|
|
298
|
+
if (state.visited.has(to)) continue;
|
|
299
|
+
const hop = edgeToHop(edge, indexes);
|
|
300
|
+
if (hop === undefined) continue;
|
|
301
|
+
// Per-path visited (cloned per expansion). Allows the same routine to
|
|
302
|
+
// participate in different alternative paths; bounded by maxDepth=64 and
|
|
303
|
+
// maxStates=25_000.
|
|
304
|
+
const newVisited = new Set(state.visited);
|
|
305
|
+
newVisited.add(to);
|
|
306
|
+
queue.push({ routine: to, hops: [...state.hops, hop], visited: newVisited });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (paths.length >= cap && queue.length > 0) {
|
|
311
|
+
truncated = true;
|
|
312
|
+
diagnostics.push({ kind: "path-limit-reached", cap });
|
|
313
|
+
}
|
|
314
|
+
if (depthExceeded) {
|
|
315
|
+
diagnostics.push({ kind: "depth-exceeded", maxDepth });
|
|
316
|
+
incomplete = true;
|
|
317
|
+
}
|
|
318
|
+
if (paths.length === 0 && !incomplete) {
|
|
319
|
+
diagnostics.push({ kind: "terminal-not-found" });
|
|
320
|
+
incomplete = true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Stability over perf: JSON.stringify gives byte-stable tiebreak across
|
|
324
|
+
// path enumeration order. Cost is O(cap^2 * serialize); pre-compute per
|
|
325
|
+
// path if cap grows beyond a few hundred.
|
|
326
|
+
paths.sort((a, b) => {
|
|
327
|
+
if (a.hops.length !== b.hops.length) return a.hops.length - b.hops.length;
|
|
328
|
+
return JSON.stringify(a.hops) < JSON.stringify(b.hops) ? -1 : 1;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return { paths, truncated, incomplete, diagnostics };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Best-effort equivalence check for "is this direct fact the terminal evidence
|
|
336
|
+
* for an inherited fact?". The deriver pipeline preserves (op, resourceKind,
|
|
337
|
+
* resourceId, resourceArgSource, extra) when propagating direct facts upward
|
|
338
|
+
* into inherited facts, so a matching direct fact at a BFS terminal should
|
|
339
|
+
* agree on these dimensions.
|
|
340
|
+
*
|
|
341
|
+
* The resourceId guard is asymmetric on purpose: if one side has a resourceId
|
|
342
|
+
* and the other doesn't, the comparison is skipped (treated as equivalent).
|
|
343
|
+
* This handles the case where the inherited fact carries a specialized
|
|
344
|
+
* resourceId (e.g., resolved from a literal SetRange argument) while the
|
|
345
|
+
* direct fact at the terminal stays generic (resourceId undefined for a
|
|
346
|
+
* dynamic dispatch site whose record-variable target couldn't be resolved).
|
|
347
|
+
*
|
|
348
|
+
* If this leniency produces false-positive terminal matches in practice,
|
|
349
|
+
* tighten to require bilateral agreement.
|
|
350
|
+
*/
|
|
351
|
+
function factEquivalent(a: CapabilityFact, b: CapabilityFact): boolean {
|
|
352
|
+
if (a.op !== b.op) return false;
|
|
353
|
+
if (a.resourceKind !== b.resourceKind) return false;
|
|
354
|
+
if (a.resourceId !== undefined && b.resourceId !== undefined) {
|
|
355
|
+
if (String(a.resourceId) !== String(b.resourceId)) return false;
|
|
356
|
+
}
|
|
357
|
+
if (a.resourceArgSource !== undefined && b.resourceArgSource !== undefined) {
|
|
358
|
+
if (JSON.stringify(a.resourceArgSource) !== JSON.stringify(b.resourceArgSource)) return false;
|
|
359
|
+
}
|
|
360
|
+
const extraA = a.extra as { kind?: string; objectType?: string } | undefined;
|
|
361
|
+
const extraB = b.extra as { kind?: string; objectType?: string } | undefined;
|
|
362
|
+
if (extraA?.kind === "dispatch" || extraB?.kind === "dispatch") {
|
|
363
|
+
if (extraA?.objectType !== extraB?.objectType) return false;
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function terminalHopFromFact(fact: CapabilityFact, indexes: WitnessIndexes): WitnessHop {
|
|
369
|
+
if (fact.witnessOperationId !== undefined) {
|
|
370
|
+
const ev = indexes.operationById.get(fact.witnessOperationId);
|
|
371
|
+
return {
|
|
372
|
+
kind: "terminal",
|
|
373
|
+
evidenceKind: "operation",
|
|
374
|
+
operationId: fact.witnessOperationId,
|
|
375
|
+
displayText: ev?.displayText ?? String(fact.witnessOperationId),
|
|
376
|
+
sourceFile: ev?.sourceFile,
|
|
377
|
+
line: ev?.startLine,
|
|
378
|
+
column: ev?.startColumn,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (fact.witnessCallsiteId !== undefined) {
|
|
382
|
+
const ev = indexes.callsiteById.get(fact.witnessCallsiteId);
|
|
383
|
+
return {
|
|
384
|
+
kind: "terminal",
|
|
385
|
+
evidenceKind: "callsite",
|
|
386
|
+
callsiteId: fact.witnessCallsiteId,
|
|
387
|
+
displayText: ev?.calleeDisplay ?? String(fact.witnessCallsiteId),
|
|
388
|
+
sourceFile: ev?.sourceFile,
|
|
389
|
+
line: ev?.startLine,
|
|
390
|
+
column: ev?.startColumn,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
kind: "terminal",
|
|
395
|
+
evidenceKind: "synthetic",
|
|
396
|
+
displayText: `${fact.op} ${fact.resourceKind}`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function edgeToHop(edge: GraphEdge, indexes: WitnessIndexes): WitnessHop | undefined {
|
|
401
|
+
switch (edge.kind) {
|
|
402
|
+
case "direct-call": {
|
|
403
|
+
const to = edge.to as unknown as StableRoutineId;
|
|
404
|
+
const display = indexes.routineDisplayById.get(to) ?? String(to);
|
|
405
|
+
const cs = indexes.callsiteById.get(edge.callsiteId);
|
|
406
|
+
return {
|
|
407
|
+
kind: "call",
|
|
408
|
+
routineId: to,
|
|
409
|
+
routineDisplay: display,
|
|
410
|
+
calleeDisplay: cs?.calleeDisplay ?? "",
|
|
411
|
+
callsiteId: edge.callsiteId,
|
|
412
|
+
sourceFile: cs?.sourceFile,
|
|
413
|
+
line: cs?.startLine,
|
|
414
|
+
column: cs?.startColumn,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
case "object-run-resolved": {
|
|
418
|
+
const to = edge.to as unknown as StableRoutineId;
|
|
419
|
+
const display = indexes.routineDisplayById.get(to) ?? String(to);
|
|
420
|
+
const cs = indexes.callsiteById.get(edge.callsiteId);
|
|
421
|
+
const targetObject = String(edge.targetObject) as StableObjectId;
|
|
422
|
+
return {
|
|
423
|
+
kind: "object-run",
|
|
424
|
+
routineId: to,
|
|
425
|
+
routineDisplay: display,
|
|
426
|
+
targetObjectId: targetObject,
|
|
427
|
+
targetDisplay: indexes.stableIdToDisplay.get(String(edge.targetObject)),
|
|
428
|
+
resolved: true,
|
|
429
|
+
callsiteId: edge.callsiteId,
|
|
430
|
+
sourceFile: cs?.sourceFile,
|
|
431
|
+
line: cs?.startLine,
|
|
432
|
+
column: cs?.startColumn,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
case "object-run-unresolved":
|
|
436
|
+
// No `to` on unresolved dispatch — BFS cannot walk through.
|
|
437
|
+
// Boundary detection emits an `unresolved-dispatch` diagnostic (see BFS expand loop).
|
|
438
|
+
return undefined;
|
|
439
|
+
case "event-dispatch": {
|
|
440
|
+
const to = edge.to as unknown as StableRoutineId;
|
|
441
|
+
const display = indexes.routineDisplayById.get(to) ?? String(to);
|
|
442
|
+
const eid = String(edge.eventId);
|
|
443
|
+
return {
|
|
444
|
+
kind: "event-dispatch",
|
|
445
|
+
routineId: to,
|
|
446
|
+
routineDisplay: display,
|
|
447
|
+
eventId: eid,
|
|
448
|
+
eventDisplay: indexes.eventDisplayById?.get(eid) ?? eid,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
case "implicit-trigger": {
|
|
452
|
+
const to = edge.to as unknown as StableRoutineId;
|
|
453
|
+
const display = indexes.routineDisplayById.get(to) ?? String(to);
|
|
454
|
+
return {
|
|
455
|
+
kind: "implicit-trigger",
|
|
456
|
+
routineId: to,
|
|
457
|
+
routineDisplay: display,
|
|
458
|
+
triggerKind: edge.triggerKind,
|
|
459
|
+
operationId: edge.operationId,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
case "dependency-export": {
|
|
463
|
+
const to = edge.to as unknown as StableRoutineId;
|
|
464
|
+
const display = indexes.routineDisplayById.get(to) ?? String(to);
|
|
465
|
+
const cs = indexes.callsiteById.get(edge.callsiteId);
|
|
466
|
+
return {
|
|
467
|
+
kind: "dependency-export",
|
|
468
|
+
routineId: to,
|
|
469
|
+
routineDisplay: display,
|
|
470
|
+
targetAppGuid: edge.targetAppGuid,
|
|
471
|
+
callsiteId: edge.callsiteId,
|
|
472
|
+
calleeDisplay: cs?.calleeDisplay,
|
|
473
|
+
sourceFile: cs?.sourceFile,
|
|
474
|
+
line: cs?.startLine,
|
|
475
|
+
column: cs?.startColumn,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
default:
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function edgeCompare(a: GraphEdge, b: GraphEdge): number {
|
|
484
|
+
if (a.kind !== b.kind) return a.kind < b.kind ? -1 : 1;
|
|
485
|
+
const csOf = (e: GraphEdge) => String((e as { callsiteId?: unknown }).callsiteId ?? "");
|
|
486
|
+
const csA = csOf(a);
|
|
487
|
+
const csB = csOf(b);
|
|
488
|
+
if (csA !== csB) return csA < csB ? -1 : 1;
|
|
489
|
+
const toOf = (e: GraphEdge) => String((e as { to?: unknown }).to ?? "");
|
|
490
|
+
const toA = toOf(a);
|
|
491
|
+
const toB = toOf(b);
|
|
492
|
+
return toA < toB ? -1 : 1;
|
|
493
|
+
}
|