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,92 @@
|
|
|
1
|
+
import type { RecordOpType } from "../model/entities.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The effect class of a record operation. `touchesDb` is driven only by db-read /
|
|
5
|
+
* db-write / db-lock; state-only ops feed D3's load-field analysis and parameterRoles;
|
|
6
|
+
* `trigger` (Validate) has no direct DB effect — its effects arrive via the Phase 2a
|
|
7
|
+
* implicit-trigger edge.
|
|
8
|
+
*/
|
|
9
|
+
export type OpEffectClass = "db-read" | "db-write" | "db-lock" | "state-only" | "trigger";
|
|
10
|
+
|
|
11
|
+
const CLASS_BY_OP: Record<RecordOpType, OpEffectClass> = {
|
|
12
|
+
FindSet: "db-read",
|
|
13
|
+
FindFirst: "db-read",
|
|
14
|
+
FindLast: "db-read",
|
|
15
|
+
Find: "db-read",
|
|
16
|
+
Get: "db-read",
|
|
17
|
+
Next: "db-read",
|
|
18
|
+
Count: "db-read",
|
|
19
|
+
CountApprox: "db-read",
|
|
20
|
+
IsEmpty: "db-read",
|
|
21
|
+
CalcFields: "db-read",
|
|
22
|
+
CalcSums: "db-read",
|
|
23
|
+
TestField: "state-only",
|
|
24
|
+
Modify: "db-write",
|
|
25
|
+
ModifyAll: "db-write",
|
|
26
|
+
Insert: "db-write",
|
|
27
|
+
Delete: "db-write",
|
|
28
|
+
DeleteAll: "db-write",
|
|
29
|
+
LockTable: "db-lock",
|
|
30
|
+
SetLoadFields: "state-only",
|
|
31
|
+
AddLoadFields: "state-only",
|
|
32
|
+
SetRange: "state-only",
|
|
33
|
+
SetFilter: "state-only",
|
|
34
|
+
SetCurrentKey: "state-only",
|
|
35
|
+
Reset: "state-only",
|
|
36
|
+
Copy: "state-only",
|
|
37
|
+
TransferFields: "state-only",
|
|
38
|
+
Init: "state-only",
|
|
39
|
+
Validate: "trigger",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Classify a record operation by its database effect. Pure, total over RecordOpType. */
|
|
43
|
+
export function classifyOp(op: RecordOpType): OpEffectClass {
|
|
44
|
+
return CLASS_BY_OP[op];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when this op class contributes to `touchesDb`. */
|
|
48
|
+
export function isDbTouchingClass(cls: OpEffectClass): boolean {
|
|
49
|
+
return cls === "db-read" || cls === "db-write" || cls === "db-lock";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-op record-flow role used by the record-flow framework's may-fact
|
|
54
|
+
* bootstrap (spec §(a)). Each op classifies into at most one of these
|
|
55
|
+
* categories for the purposes of state-flow tracking. Field-level facts
|
|
56
|
+
* (readsFields/writesFields) are computed independently by D3 already.
|
|
57
|
+
*/
|
|
58
|
+
export type RecordFlowOpRole =
|
|
59
|
+
| "loadsFromDb" // Get / FindFirst / FindLast / FindSet / Find / Next
|
|
60
|
+
| "initialises" // Init
|
|
61
|
+
| "persistsCurrent" // Modify / Insert / Rename
|
|
62
|
+
| "setBasedWrite" // ModifyAll / DeleteAll
|
|
63
|
+
| "validates" // Validate
|
|
64
|
+
| "copiesInto" // Copy / TransferFields (target side)
|
|
65
|
+
| "resetsFilter" // Reset
|
|
66
|
+
| "neutral"; // SetRange / SetFilter / SetLoadFields / AddLoadFields / TestField / etc.
|
|
67
|
+
|
|
68
|
+
// Partial so most ops fall through to "neutral"; tightening the key to RecordOpType
|
|
69
|
+
// gives exhaustiveness — adding a new op-type will surface here at the compiler if
|
|
70
|
+
// the new op should map to a non-neutral role. (Note: "Rename" is not yet in
|
|
71
|
+
// RecordOpType — when it lands, decide whether to add it here as "persistsCurrent".)
|
|
72
|
+
const ROLE_BY_OP: Partial<Record<RecordOpType, RecordFlowOpRole>> = {
|
|
73
|
+
Get: "loadsFromDb",
|
|
74
|
+
FindFirst: "loadsFromDb",
|
|
75
|
+
FindLast: "loadsFromDb",
|
|
76
|
+
FindSet: "loadsFromDb",
|
|
77
|
+
Find: "loadsFromDb",
|
|
78
|
+
Next: "loadsFromDb",
|
|
79
|
+
Init: "initialises",
|
|
80
|
+
Modify: "persistsCurrent",
|
|
81
|
+
Insert: "persistsCurrent",
|
|
82
|
+
ModifyAll: "setBasedWrite",
|
|
83
|
+
DeleteAll: "setBasedWrite",
|
|
84
|
+
Validate: "validates",
|
|
85
|
+
Copy: "copiesInto",
|
|
86
|
+
TransferFields: "copiesInto",
|
|
87
|
+
Reset: "resetsFilter",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function recordFlowRoleOf(op: RecordOpType): RecordFlowOpRole {
|
|
91
|
+
return ROLE_BY_OP[op] ?? "neutral";
|
|
92
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { CallSite, Routine } from "../model/entities.ts";
|
|
2
|
+
import type { EvidenceStep } from "../model/finding.ts";
|
|
3
|
+
import type { CallsiteId, RoutineId } from "../model/ids.ts";
|
|
4
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
5
|
+
import type { Uncertainty } from "../model/summary.ts";
|
|
6
|
+
import type { CombinedEdge, CombinedGraph } from "./combined-graph.ts";
|
|
7
|
+
import { dedupeUncertainties } from "./uncertainty-util.ts";
|
|
8
|
+
|
|
9
|
+
/** A real op site the walk can terminate at. Policies may return a richer subtype. */
|
|
10
|
+
export interface Terminal {
|
|
11
|
+
routineId: RoutineId;
|
|
12
|
+
/** Loop nesting depth of the op site within its OWN routine. */
|
|
13
|
+
localLoopDepth: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Why a walk branch stopped. Detectors emit findings only from `complete` results. */
|
|
17
|
+
export type WalkStop = "complete" | "cycle-cut" | "depth-cut" | "node-budget-cut" | "dead-end";
|
|
18
|
+
|
|
19
|
+
export interface WalkResult {
|
|
20
|
+
path: EvidenceStep[];
|
|
21
|
+
effectiveLoopDepth: number;
|
|
22
|
+
uncertainties: Uncertainty[];
|
|
23
|
+
stop: WalkStop;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The mutable context threaded through one walk branch. */
|
|
27
|
+
export interface PathCtx {
|
|
28
|
+
routinePath: RoutineId[];
|
|
29
|
+
inheritedLoopDepth: number;
|
|
30
|
+
steps: EvidenceStep[];
|
|
31
|
+
uncertainties: Uncertainty[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WalkBounds {
|
|
35
|
+
maxDepth: number; // max routine-path length
|
|
36
|
+
maxNodes: number; // max nodes visited across the whole walk
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Detector-supplied policy: which edges to follow, what counts as a terminal, how to build steps. */
|
|
40
|
+
export interface WalkPolicy<T extends Terminal = Terminal> {
|
|
41
|
+
terminalsAt(node: RoutineId, ctx: PathCtx): T[];
|
|
42
|
+
expand(node: RoutineId, ctx: PathCtx): CombinedEdge[];
|
|
43
|
+
buildHopStep(edge: CombinedEdge, ctx: PathCtx): EvidenceStep;
|
|
44
|
+
buildTerminalStep(terminal: T, ctx: PathCtx): EvidenceStep;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WalkOpts {
|
|
48
|
+
/** Loop depth already established by the detector (e.g. the loop D1 started from). */
|
|
49
|
+
initialLoopDepth?: number;
|
|
50
|
+
/** Evidence steps the detector wants prepended (e.g. the loop step). */
|
|
51
|
+
initialSteps?: EvidenceStep[];
|
|
52
|
+
/**
|
|
53
|
+
* Prebuilt indexes. `walkEvidence` is called once per in-loop call site by D1/D2, so
|
|
54
|
+
* rebuilding these from `model`/`graph` on every call is O(routines + edges) per call —
|
|
55
|
+
* the dominant cost on large workspaces. Callers that hold the shared DetectorContext
|
|
56
|
+
* pass its maps; when omitted, the walker builds them itself (unchanged behaviour for
|
|
57
|
+
* one-off callers / tests). All three are read-only here.
|
|
58
|
+
*/
|
|
59
|
+
routineById?: Map<RoutineId, Routine>;
|
|
60
|
+
uncertaintyEdgesByFrom?: Map<RoutineId, Uncertainty[]>;
|
|
61
|
+
callSiteById?: Map<CallsiteId, CallSite>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bounded depth-first evidence walk. Returns one WalkResult per branch that reached a
|
|
66
|
+
* terminal (`complete`) or stopped (`cycle-cut` / `depth-cut` / `node-budget-cut` /
|
|
67
|
+
* `dead-end`). Pure — no I/O. Cycle detection is per-path; bounds cap depth and total nodes.
|
|
68
|
+
*/
|
|
69
|
+
export function walkEvidence<T extends Terminal>(
|
|
70
|
+
start: RoutineId,
|
|
71
|
+
policy: WalkPolicy<T>,
|
|
72
|
+
bounds: WalkBounds,
|
|
73
|
+
graph: CombinedGraph,
|
|
74
|
+
model: SemanticModel,
|
|
75
|
+
opts: WalkOpts = {},
|
|
76
|
+
): WalkResult[] {
|
|
77
|
+
const results: WalkResult[] = [];
|
|
78
|
+
let nodesVisited = 0;
|
|
79
|
+
const routineById = opts.routineById ?? new Map(model.routines.map((r) => [r.id, r]));
|
|
80
|
+
|
|
81
|
+
const uncertaintyEdgesByFrom =
|
|
82
|
+
opts.uncertaintyEdgesByFrom ??
|
|
83
|
+
(() => {
|
|
84
|
+
const m = new Map<RoutineId, Uncertainty[]>();
|
|
85
|
+
for (const ue of graph.uncertaintyEdges) {
|
|
86
|
+
const list = m.get(ue.from);
|
|
87
|
+
if (list) list.push(ue.uncertainty);
|
|
88
|
+
else m.set(ue.from, [ue.uncertainty]);
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
const callSiteById =
|
|
94
|
+
opts.callSiteById ??
|
|
95
|
+
(() => {
|
|
96
|
+
const m = new Map<CallsiteId, CallSite>();
|
|
97
|
+
for (const r of model.routines) {
|
|
98
|
+
for (const cs of r.features.callSites) m.set(cs.id, cs);
|
|
99
|
+
}
|
|
100
|
+
return m;
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
const uncertaintiesAt = (node: RoutineId): Uncertainty[] => {
|
|
104
|
+
const fromSummary = routineById.get(node)?.summary?.uncertainties ?? [];
|
|
105
|
+
const fromEdges = uncertaintyEdgesByFrom.get(node) ?? [];
|
|
106
|
+
return [...fromSummary, ...fromEdges];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const loopDepthOfEdge = (edge: CombinedEdge): number => {
|
|
110
|
+
if (edge.callsiteId === undefined) return 0;
|
|
111
|
+
const cs = callSiteById.get(edge.callsiteId);
|
|
112
|
+
return cs?.loopStack.length ?? 0;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const visit = (node: RoutineId, ctx: PathCtx): void => {
|
|
116
|
+
nodesVisited++;
|
|
117
|
+
const ctxHere: PathCtx = {
|
|
118
|
+
...ctx,
|
|
119
|
+
uncertainties: dedupeUncertainties([...ctx.uncertainties, ...uncertaintiesAt(node)]),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const terminals = policy.terminalsAt(node, ctxHere);
|
|
123
|
+
for (const t of terminals) {
|
|
124
|
+
results.push({
|
|
125
|
+
path: [...ctxHere.steps, policy.buildTerminalStep(t, ctxHere)],
|
|
126
|
+
effectiveLoopDepth: ctxHere.inheritedLoopDepth + t.localLoopDepth,
|
|
127
|
+
uncertainties: ctxHere.uncertainties,
|
|
128
|
+
stop: "complete",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const edges = policy.expand(node, ctxHere);
|
|
133
|
+
if (edges.length === 0 && terminals.length === 0) {
|
|
134
|
+
results.push({
|
|
135
|
+
path: ctxHere.steps,
|
|
136
|
+
effectiveLoopDepth: ctxHere.inheritedLoopDepth,
|
|
137
|
+
uncertainties: ctxHere.uncertainties,
|
|
138
|
+
stop: "dead-end",
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const edge of edges) {
|
|
144
|
+
if (nodesVisited >= bounds.maxNodes) {
|
|
145
|
+
results.push({
|
|
146
|
+
path: ctxHere.steps,
|
|
147
|
+
effectiveLoopDepth: ctxHere.inheritedLoopDepth,
|
|
148
|
+
uncertainties: ctxHere.uncertainties,
|
|
149
|
+
stop: "node-budget-cut",
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ctxHere.routinePath.includes(edge.to)) {
|
|
154
|
+
results.push({
|
|
155
|
+
path: ctxHere.steps,
|
|
156
|
+
effectiveLoopDepth: ctxHere.inheritedLoopDepth,
|
|
157
|
+
uncertainties: ctxHere.uncertainties,
|
|
158
|
+
stop: "cycle-cut",
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ctxHere.routinePath.length >= bounds.maxDepth) {
|
|
163
|
+
results.push({
|
|
164
|
+
path: ctxHere.steps,
|
|
165
|
+
effectiveLoopDepth: ctxHere.inheritedLoopDepth,
|
|
166
|
+
uncertainties: ctxHere.uncertainties,
|
|
167
|
+
stop: "depth-cut",
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const childCtx: PathCtx = {
|
|
172
|
+
routinePath: [...ctxHere.routinePath, edge.to],
|
|
173
|
+
inheritedLoopDepth: ctxHere.inheritedLoopDepth + loopDepthOfEdge(edge),
|
|
174
|
+
steps: [...ctxHere.steps, policy.buildHopStep(edge, ctxHere)],
|
|
175
|
+
uncertainties: ctxHere.uncertainties,
|
|
176
|
+
};
|
|
177
|
+
visit(edge.to, childCtx);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
visit(start, {
|
|
182
|
+
routinePath: [start],
|
|
183
|
+
inheritedLoopDepth: opts.initialLoopDepth ?? 0,
|
|
184
|
+
steps: opts.initialSteps ?? [],
|
|
185
|
+
uncertainties: [],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
2
|
+
import type { CombinedEdge, CombinedGraph } from "./combined-graph.ts";
|
|
3
|
+
|
|
4
|
+
/** Map of routineId → edges where that routine is the callee. */
|
|
5
|
+
export type ReverseCallGraph = Map<RoutineId, CombinedEdge[]>;
|
|
6
|
+
|
|
7
|
+
/** Invert `graph.edgesByFrom` so each routine knows who calls it. */
|
|
8
|
+
export function buildReverseCallGraph(graph: CombinedGraph): ReverseCallGraph {
|
|
9
|
+
const reverse: ReverseCallGraph = new Map();
|
|
10
|
+
for (const edges of graph.edgesByFrom.values()) {
|
|
11
|
+
for (const e of edges) {
|
|
12
|
+
const list = reverse.get(e.to);
|
|
13
|
+
if (list) list.push(e);
|
|
14
|
+
else reverse.set(e.to, [e]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return reverse;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Return the resolved callers of a routine; empty list when none. */
|
|
21
|
+
export function callersOf(reverse: ReverseCallGraph, routineId: RoutineId): CombinedEdge[] {
|
|
22
|
+
return reverse.get(routineId) ?? [];
|
|
23
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { RootsConfig, RootsConfigTarget } from "../config/roots-config.ts";
|
|
2
|
+
import type { Routine } from "../model/entities.ts";
|
|
3
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
4
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
5
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
6
|
+
import {
|
|
7
|
+
type RootClassification,
|
|
8
|
+
type RootKind,
|
|
9
|
+
isExternallyReachableKind,
|
|
10
|
+
} from "../model/root-classification.ts";
|
|
11
|
+
import { ROOT_KIND_ORDER } from "./root-classifier.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Merge a `RootsConfig` overlay on top of the AST classification result
|
|
15
|
+
* (Phase 1 §4.3 Task 6).
|
|
16
|
+
*
|
|
17
|
+
* Provenance discipline:
|
|
18
|
+
* - AST classifications are the base layer (no config = output equals input).
|
|
19
|
+
* - Each config entry: resolve target → routine. On success, merge into
|
|
20
|
+
* an existing AST classification OR create a new one.
|
|
21
|
+
* - When AST + config agree on kinds → `source: "ast+config"`,
|
|
22
|
+
* `confidence: "static"`.
|
|
23
|
+
* - When AST + config disagree → emit `kinds-mismatch` diagnostic; union the
|
|
24
|
+
* kinds, still `source: "ast+config"`, `confidence: "static"` (AST
|
|
25
|
+
* corroboration upgrades user-asserted → static).
|
|
26
|
+
* - Config-only routines (no AST signal) → `source: "config"`,
|
|
27
|
+
* `confidence: "user-asserted"`.
|
|
28
|
+
* - Two config entries on the same routine (no AST signal) →
|
|
29
|
+
* `[roots-config/duplicate-target]` diagnostic; last-write-wins on the
|
|
30
|
+
* stored entry, both ids named in the diagnostic.
|
|
31
|
+
* - Unresolved targets → `[roots-config/unresolved]` diagnostic; entry
|
|
32
|
+
* dropped.
|
|
33
|
+
* - Ambiguous targets (multiple matches) → `[roots-config/ambiguous]`
|
|
34
|
+
* diagnostic; classification recorded on the FIRST routine by canonical
|
|
35
|
+
* id sort, with `resolutionStatus: "ambiguous"`.
|
|
36
|
+
*
|
|
37
|
+
* Pure: never throws, never does I/O. Output is sorted by `routineId` for
|
|
38
|
+
* determinism, matching the AST classifier's invariant.
|
|
39
|
+
*/
|
|
40
|
+
export function overlayConfigRoots(
|
|
41
|
+
astRoots: RootClassification[],
|
|
42
|
+
config: RootsConfig | undefined,
|
|
43
|
+
model: SemanticModel,
|
|
44
|
+
): { roots: RootClassification[]; diagnostics: Diagnostic[] } {
|
|
45
|
+
if (config === undefined) {
|
|
46
|
+
return { roots: astRoots, diagnostics: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const diagnostics: Diagnostic[] = [];
|
|
50
|
+
// `astRoots` already has at most one entry per RoutineId (Task 3 invariant).
|
|
51
|
+
// `astByRoutine` is a FROZEN snapshot of the AST baseline — read-only.
|
|
52
|
+
// `byRoutine` is the accumulator (starts from AST, then overlay writes win).
|
|
53
|
+
// Keeping them separate guarantees a second config entry on the same routine
|
|
54
|
+
// sees the ORIGINAL AST kind set, not the first entry's merged result.
|
|
55
|
+
const astByRoutine = new Map<RoutineId, RootClassification>(
|
|
56
|
+
astRoots.map((r) => [r.routineId, r]),
|
|
57
|
+
);
|
|
58
|
+
const byRoutine = new Map<RoutineId, RootClassification>(astByRoutine);
|
|
59
|
+
// Tracks which routines have already been written by a config entry in
|
|
60
|
+
// this pass, so we can emit `[roots-config/duplicate-target]` when two
|
|
61
|
+
// entries target the same routine.
|
|
62
|
+
const configWriters = new Map<RoutineId, string>();
|
|
63
|
+
|
|
64
|
+
for (const entry of config.roots) {
|
|
65
|
+
const matches = resolveTarget(entry.target, model).sort((a, b) =>
|
|
66
|
+
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (matches.length === 0) {
|
|
70
|
+
diagnostics.push(
|
|
71
|
+
diag(
|
|
72
|
+
"warning",
|
|
73
|
+
`[roots-config/unresolved] roots.config.json entry "${entry.id}" did not match any routine; skipping.`,
|
|
74
|
+
entry.id,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ambiguous = matches.length > 1;
|
|
81
|
+
if (ambiguous) {
|
|
82
|
+
diagnostics.push(
|
|
83
|
+
diag(
|
|
84
|
+
"warning",
|
|
85
|
+
`[roots-config/ambiguous] roots.config.json entry "${entry.id}" matched ${matches.length} routines; using first by id sort.`,
|
|
86
|
+
entry.id,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// matches.length >= 1 — guarded above.
|
|
92
|
+
// biome-ignore lint/style/noNonNullAssertion: length checked above.
|
|
93
|
+
const winner = matches[0]!;
|
|
94
|
+
const existingAst = astByRoutine.get(winner.id);
|
|
95
|
+
const hasAst = existingAst !== undefined;
|
|
96
|
+
// Loader already canonicalized the kind set (deduped + sorted in
|
|
97
|
+
// ROOT_KIND_ORDER); Set for O(k) lookup.
|
|
98
|
+
const cfgKinds = new Set<RootKind>(entry.kinds);
|
|
99
|
+
const cfgExternally = entry.externallyReachable;
|
|
100
|
+
|
|
101
|
+
// Duplicate-target check fires independently of AST status — two
|
|
102
|
+
// config entries pointing at the same routine is a config-author
|
|
103
|
+
// mistake either way.
|
|
104
|
+
const priorWriter = configWriters.get(winner.id);
|
|
105
|
+
if (priorWriter !== undefined) {
|
|
106
|
+
diagnostics.push(
|
|
107
|
+
diag(
|
|
108
|
+
"warning",
|
|
109
|
+
`[roots-config/duplicate-target] roots.config.json entries "${priorWriter}" and "${entry.id}" both target the same routine; last entry wins.`,
|
|
110
|
+
entry.id,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!hasAst) {
|
|
116
|
+
// Config-only root: no AST signal, "user-asserted" confidence.
|
|
117
|
+
// Last-write-wins when multiple config entries target the same
|
|
118
|
+
// routine — the duplicate-target diagnostic was already emitted
|
|
119
|
+
// above naming both ids.
|
|
120
|
+
const kinds: RootKind[] = ROOT_KIND_ORDER.filter((k) => cfgKinds.has(k));
|
|
121
|
+
// Defensive: loader rejects entries with kinds.length === 0,
|
|
122
|
+
// so this guard should be unreachable. Keep it so a future
|
|
123
|
+
// loader change can't silently emit empty-kinds classifications.
|
|
124
|
+
if (kinds.length === 0) continue;
|
|
125
|
+
byRoutine.set(winner.id, {
|
|
126
|
+
routineId: winner.id,
|
|
127
|
+
kinds,
|
|
128
|
+
externallyReachable: cfgExternally ?? kinds.some(isExternallyReachableKind),
|
|
129
|
+
source: "config",
|
|
130
|
+
confidence: "user-asserted",
|
|
131
|
+
sourceAnchor: winner.sourceAnchor,
|
|
132
|
+
configEntryId: entry.id,
|
|
133
|
+
resolutionStatus: ambiguous ? "ambiguous" : "resolved",
|
|
134
|
+
});
|
|
135
|
+
configWriters.set(winner.id, entry.id);
|
|
136
|
+
} else {
|
|
137
|
+
// AST + config corroboration: union kinds, upgrade to "static".
|
|
138
|
+
// `existingAst` is the ORIGINAL AST entry from the frozen snapshot,
|
|
139
|
+
// so a second config entry on the same routine still unions
|
|
140
|
+
// against AST's kinds (not entry 1's merged result).
|
|
141
|
+
//
|
|
142
|
+
// biome-ignore lint/style/noNonNullAssertion: hasAst === true implies astByRoutine has the entry.
|
|
143
|
+
const existing = existingAst!;
|
|
144
|
+
const astKindSet = new Set<RootKind>(existing.kinds);
|
|
145
|
+
const onlyAstSet = new Set<RootKind>([...astKindSet].filter((k) => !cfgKinds.has(k)));
|
|
146
|
+
const onlyCfgSet = new Set<RootKind>([...cfgKinds].filter((k) => !astKindSet.has(k)));
|
|
147
|
+
if (onlyAstSet.size > 0 || onlyCfgSet.size > 0) {
|
|
148
|
+
// Order diff kind lists by ROOT_KIND_ORDER for stable
|
|
149
|
+
// reading and to match the union-output ordering.
|
|
150
|
+
const onlyAst = ROOT_KIND_ORDER.filter((k) => onlyAstSet.has(k));
|
|
151
|
+
const onlyCfg = ROOT_KIND_ORDER.filter((k) => onlyCfgSet.has(k));
|
|
152
|
+
diagnostics.push(
|
|
153
|
+
diag(
|
|
154
|
+
"warning",
|
|
155
|
+
`[roots-config/kinds-mismatch] roots.config.json entry "${entry.id}" disagrees with AST: ast-only=${JSON.stringify(onlyAst)}, config-only=${JSON.stringify(onlyCfg)}.`,
|
|
156
|
+
entry.id,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const unionedSet = new Set<RootKind>([...existing.kinds, ...entry.kinds]);
|
|
161
|
+
const unionedKinds: RootKind[] = ROOT_KIND_ORDER.filter((k) => unionedSet.has(k));
|
|
162
|
+
byRoutine.set(winner.id, {
|
|
163
|
+
...existing,
|
|
164
|
+
kinds: unionedKinds,
|
|
165
|
+
externallyReachable: cfgExternally ?? unionedKinds.some(isExternallyReachableKind),
|
|
166
|
+
source: "ast+config",
|
|
167
|
+
confidence: "static",
|
|
168
|
+
configEntryId: entry.id,
|
|
169
|
+
resolutionStatus: ambiguous ? "ambiguous" : "resolved",
|
|
170
|
+
});
|
|
171
|
+
configWriters.set(winner.id, entry.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const roots = [...byRoutine.values()].sort((a, b) =>
|
|
176
|
+
a.routineId < b.routineId ? -1 : a.routineId > b.routineId ? 1 : 0,
|
|
177
|
+
);
|
|
178
|
+
return { roots, diagnostics };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveTarget(target: RootsConfigTarget, model: SemanticModel): Routine[] {
|
|
182
|
+
if ("routineId" in target) {
|
|
183
|
+
const r = model.routines.find((rr) => rr.id === target.routineId);
|
|
184
|
+
return r === undefined ? [] : [r];
|
|
185
|
+
}
|
|
186
|
+
const lcName = target.routineName.toLowerCase();
|
|
187
|
+
return model.routines.filter(
|
|
188
|
+
(rr) => rr.objectId === target.objectId && rr.name.toLowerCase() === lcName,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function diag(severity: Diagnostic["severity"], message: string, sourceRef: string): Diagnostic {
|
|
193
|
+
return { severity, stage: "discover", message, sourceRef };
|
|
194
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ObjectDecl, Routine } from "../model/entities.ts";
|
|
2
|
+
import type { ObjectId } from "../model/ids.ts";
|
|
3
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
4
|
+
import {
|
|
5
|
+
ROOT_KIND_VALUES,
|
|
6
|
+
type RootClassification,
|
|
7
|
+
type RootKind,
|
|
8
|
+
isExternallyReachableKind,
|
|
9
|
+
} from "../model/root-classification.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Canonical RootKind declaration order. Re-exported from the model layer
|
|
13
|
+
* (`ROOT_KIND_VALUES`) so tests and future formatters can reference the
|
|
14
|
+
* source-of-truth rather than redeclaring. Typed as `readonly RootKind[]`
|
|
15
|
+
* (widening away the tuple's literal `.length`) to preserve the original
|
|
16
|
+
* signature for existing consumers.
|
|
17
|
+
*/
|
|
18
|
+
export const ROOT_KIND_ORDER: readonly RootKind[] = ROOT_KIND_VALUES;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Phase 1 §4.3 AST-only root-classifier. Produces `RootClassification[]` for every
|
|
22
|
+
* routine that qualifies as one or more `RootKind`. Routines with no qualifying kind
|
|
23
|
+
* are not in the result.
|
|
24
|
+
*
|
|
25
|
+
* Pure transform over the `SemanticModel`; never throws. Routines whose declaring
|
|
26
|
+
* object is missing from `model.objects` (should never happen, but `objectId` is a
|
|
27
|
+
* string alias) are silently skipped rather than crashing — the engine never throws.
|
|
28
|
+
*
|
|
29
|
+
* Deferred kinds (not produced by this implementation):
|
|
30
|
+
* - `page-action`: needs Page action AST indexing (not yet in routine-indexer).
|
|
31
|
+
* - `web-service-exposed`: needs a cross-object WebService scan.
|
|
32
|
+
* - `job-queue-entrypoint`: no static signal — depends on runtime registration.
|
|
33
|
+
*
|
|
34
|
+
* Output is sorted by `routineId` (canonical lexicographic) for determinism.
|
|
35
|
+
*/
|
|
36
|
+
export function classifyRoots(model: SemanticModel): RootClassification[] {
|
|
37
|
+
const objectsById = new Map<ObjectId, ObjectDecl>(model.objects.map((o) => [o.id, o]));
|
|
38
|
+
const result: RootClassification[] = [];
|
|
39
|
+
|
|
40
|
+
for (const routine of model.routines) {
|
|
41
|
+
const object = objectsById.get(routine.objectId);
|
|
42
|
+
if (object === undefined) continue;
|
|
43
|
+
const kinds = kindsFor(routine, object);
|
|
44
|
+
if (kinds.length === 0) continue;
|
|
45
|
+
const externallyReachable = kinds.some(isExternallyReachableKind);
|
|
46
|
+
result.push({
|
|
47
|
+
routineId: routine.id,
|
|
48
|
+
kinds,
|
|
49
|
+
externallyReachable,
|
|
50
|
+
source: "ast",
|
|
51
|
+
confidence: "static",
|
|
52
|
+
sourceAnchor: routine.sourceAnchor,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Canonical sort for determinism — RoutineId is a string.
|
|
57
|
+
result.sort((a, b) => (a.routineId < b.routineId ? -1 : a.routineId > b.routineId ? 1 : 0));
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compute the set of `RootKind`s a routine qualifies for, based purely on its
|
|
63
|
+
* structural shape + the host object's declared metadata. Returns the empty
|
|
64
|
+
* array when no kind applies.
|
|
65
|
+
*
|
|
66
|
+
* `public-procedure` is a catch-all and is only added when no more specific
|
|
67
|
+
* kind applied — otherwise routines on an Install/Upgrade/API host would be
|
|
68
|
+
* double-classified.
|
|
69
|
+
*/
|
|
70
|
+
function kindsFor(routine: Routine, object: ObjectDecl): RootKind[] {
|
|
71
|
+
const kinds: RootKind[] = [];
|
|
72
|
+
|
|
73
|
+
// Trigger kinds — gated on routine.kind === "trigger". Codeunit OnRun is not
|
|
74
|
+
// a separate kind here; it falls through to the Subtype-based classification.
|
|
75
|
+
if (routine.kind === "trigger") {
|
|
76
|
+
switch (object.objectType) {
|
|
77
|
+
case "Table":
|
|
78
|
+
case "TableExtension":
|
|
79
|
+
kinds.push("trigger-table");
|
|
80
|
+
break;
|
|
81
|
+
case "Page":
|
|
82
|
+
case "PageExtension":
|
|
83
|
+
kinds.push("trigger-page");
|
|
84
|
+
break;
|
|
85
|
+
case "Report":
|
|
86
|
+
kinds.push("report-trigger");
|
|
87
|
+
break;
|
|
88
|
+
// Other object types: codeunit triggers (OnRun) get classified via
|
|
89
|
+
// Subtype below; the trigger alone is not an entry-point kind today.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Event-subscriber — direct from routine.kind (set by routine-indexer when
|
|
94
|
+
// the [EventSubscriber(...)] attribute is present).
|
|
95
|
+
if (routine.kind === "event-subscriber") {
|
|
96
|
+
kinds.push("event-subscriber");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Codeunit Subtype-based kinds. Applies to ALL routines on a Codeunit with
|
|
100
|
+
// the matching Subtype — Install/Upgrade codeunits run their OnRun (and any
|
|
101
|
+
// helper procedures invoked from it) as part of app install/upgrade flow.
|
|
102
|
+
if (object.objectType === "Codeunit") {
|
|
103
|
+
const subtype = object.objectSubtype?.toLowerCase();
|
|
104
|
+
if (subtype === "install") kinds.push("install-codeunit");
|
|
105
|
+
if (subtype === "upgrade") kinds.push("upgrade-codeunit");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Page with PageType=API — every routine on the page is HTTP-exposed.
|
|
109
|
+
if (
|
|
110
|
+
(object.objectType === "Page" || object.objectType === "PageExtension") &&
|
|
111
|
+
object.pageType?.toLowerCase() === "api"
|
|
112
|
+
) {
|
|
113
|
+
kinds.push("api-page");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Test procedures — via [Test] attribute on the routine itself.
|
|
117
|
+
if (routine.attributesParsed.some((a) => a.name.toLowerCase() === "test")) {
|
|
118
|
+
kinds.push("test-procedure");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Public procedures — non-trigger, non-event-subscriber procedures with
|
|
122
|
+
// default access (undefined accessModifier = AL's "public" default). This
|
|
123
|
+
// is the catch-all callable-surface kind: only added when nothing more
|
|
124
|
+
// specific applied, so a default-access procedure on an Install codeunit
|
|
125
|
+
// stays `["install-codeunit"]`, not `["install-codeunit","public-procedure"]`.
|
|
126
|
+
if (routine.kind === "procedure" && routine.accessModifier === undefined && kinds.length === 0) {
|
|
127
|
+
kinds.push("public-procedure");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Normalize to the documented invariant: deduplicated, sorted in RootKind
|
|
131
|
+
// declaration order. Insertion order above happens to match — this pass
|
|
132
|
+
// makes it defensive against future reorderings.
|
|
133
|
+
const seen = new Set<RootKind>(kinds);
|
|
134
|
+
return ROOT_KIND_ORDER.filter((k) => seen.has(k));
|
|
135
|
+
}
|