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,116 @@
|
|
|
1
|
+
import type { SourceAnchor } from "./identity.ts";
|
|
2
|
+
import type { RoutineId } from "./ids.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The classification kinds a routine may receive from §4.3 root-classifier.
|
|
6
|
+
* Each value names a static AST-derivable entry-point pattern.
|
|
7
|
+
*
|
|
8
|
+
* Deferred kinds (not produced by the Phase 1 §4.3 classifier; reserved for
|
|
9
|
+
* future substrate):
|
|
10
|
+
* - page-action: needs Page action AST indexing.
|
|
11
|
+
* - web-service-exposed: needs cross-object WebService scan.
|
|
12
|
+
* - job-queue-entrypoint: no static signal; runtime-registered.
|
|
13
|
+
*
|
|
14
|
+
* "externallyReachable" routines are those whose RootKind set includes any
|
|
15
|
+
* kind that is fired from outside the AL code itself (BC runtime, HTTP,
|
|
16
|
+
* test runner, etc.). All Phase §4.3 kinds today qualify.
|
|
17
|
+
*/
|
|
18
|
+
export type RootKind =
|
|
19
|
+
| "trigger-table"
|
|
20
|
+
| "trigger-page"
|
|
21
|
+
| "page-action" // deferred: needs Page action AST indexing
|
|
22
|
+
| "report-trigger"
|
|
23
|
+
| "event-subscriber"
|
|
24
|
+
| "install-codeunit"
|
|
25
|
+
| "upgrade-codeunit"
|
|
26
|
+
| "api-page"
|
|
27
|
+
| "web-service-exposed" // deferred: needs cross-object WebService scan
|
|
28
|
+
| "job-queue-entrypoint" // deferred: no static signal (runtime-registered)
|
|
29
|
+
| "public-procedure"
|
|
30
|
+
| "test-procedure";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-routine classification with full provenance (spec §4.3). One entry per
|
|
34
|
+
* classified routine; routines with no qualifying kind are not in the array.
|
|
35
|
+
*
|
|
36
|
+
* `kinds` is deduplicated and sorted in `RootKind` declaration order — Task 3's
|
|
37
|
+
* AST classifier and Task 6's overlay both produce arrays satisfying this invariant.
|
|
38
|
+
*
|
|
39
|
+
* `source` records how the classification was produced:
|
|
40
|
+
* - "ast" — derived purely from AST + attributes (no config file).
|
|
41
|
+
* - "config" — roots.config.json asserted this routine as a root.
|
|
42
|
+
* - "ast+config" — both paths agreed (or both contributed kinds — the
|
|
43
|
+
* union is recorded; disagreement emits a diagnostic).
|
|
44
|
+
*
|
|
45
|
+
* `confidence` is "user-asserted" iff `source === "config"` (config-only
|
|
46
|
+
* with no AST corroboration); otherwise "static".
|
|
47
|
+
*
|
|
48
|
+
* `resolutionStatus` is only populated when `source` includes "config":
|
|
49
|
+
* - "resolved" — config entry mapped to exactly one routine.
|
|
50
|
+
* - "ambiguous" — config entry matched multiple routines (a diagnostic
|
|
51
|
+
* was emitted; this classification reflects the FIRST
|
|
52
|
+
* match by canonical id sort).
|
|
53
|
+
* - "unresolved" — config entry did not match any routine.
|
|
54
|
+
*/
|
|
55
|
+
export interface RootClassification {
|
|
56
|
+
routineId: RoutineId;
|
|
57
|
+
kinds: RootKind[];
|
|
58
|
+
externallyReachable: boolean;
|
|
59
|
+
source: "ast" | "config" | "ast+config";
|
|
60
|
+
confidence: "static" | "user-asserted";
|
|
61
|
+
sourceAnchor?: SourceAnchor;
|
|
62
|
+
configEntryId?: string;
|
|
63
|
+
resolutionStatus?: "resolved" | "ambiguous" | "unresolved";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the kind is externally reachable today. Spec §4.3 marks
|
|
68
|
+
* all current kinds externally reachable; this helper centralises the
|
|
69
|
+
* mapping so future internal-trigger-style kinds can opt out cleanly.
|
|
70
|
+
*
|
|
71
|
+
* Exhaustiveness-guarded: adding a RootKind variant must extend this switch.
|
|
72
|
+
*/
|
|
73
|
+
export function isExternallyReachableKind(kind: RootKind): boolean {
|
|
74
|
+
switch (kind) {
|
|
75
|
+
case "trigger-table":
|
|
76
|
+
case "trigger-page":
|
|
77
|
+
case "page-action":
|
|
78
|
+
case "report-trigger":
|
|
79
|
+
case "event-subscriber":
|
|
80
|
+
case "install-codeunit":
|
|
81
|
+
case "upgrade-codeunit":
|
|
82
|
+
case "api-page":
|
|
83
|
+
case "web-service-exposed":
|
|
84
|
+
case "job-queue-entrypoint":
|
|
85
|
+
case "public-procedure":
|
|
86
|
+
case "test-procedure":
|
|
87
|
+
return true;
|
|
88
|
+
default: {
|
|
89
|
+
const _exhaustive: never = kind;
|
|
90
|
+
return _exhaustive;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Canonical RootKind values in declaration order. The single source of truth
|
|
97
|
+
* for valid kinds, consumed by the classifier (sort/dedup invariant), the
|
|
98
|
+
* roots-config loader (validation), and the §4.4 CLI (--roots flag validation).
|
|
99
|
+
*
|
|
100
|
+
* Promoted from `src/engine/root-classifier.ts`'s `ROOT_KIND_ORDER` so the
|
|
101
|
+
* model layer owns it. `ROOT_KIND_ORDER` becomes a re-export.
|
|
102
|
+
*/
|
|
103
|
+
export const ROOT_KIND_VALUES = [
|
|
104
|
+
"trigger-table",
|
|
105
|
+
"trigger-page",
|
|
106
|
+
"page-action",
|
|
107
|
+
"report-trigger",
|
|
108
|
+
"event-subscriber",
|
|
109
|
+
"install-codeunit",
|
|
110
|
+
"upgrade-codeunit",
|
|
111
|
+
"api-page",
|
|
112
|
+
"web-service-exposed",
|
|
113
|
+
"job-queue-entrypoint",
|
|
114
|
+
"public-procedure",
|
|
115
|
+
"test-procedure",
|
|
116
|
+
] as const satisfies readonly RootKind[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { FieldId, ObjectId, TableId } from "./ids.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable identities — snapshot-comparable across analysis runs and across
|
|
5
|
+
* al-sem versions. Used at the snapshot serialization boundary; in-memory
|
|
6
|
+
* model code continues to use the internal `ObjectId` / `TableId` / etc.
|
|
7
|
+
* for index speed.
|
|
8
|
+
*
|
|
9
|
+
* Format choice (`:` and `#` separators) deliberately differs from the
|
|
10
|
+
* internal id format (`/` separator) so the two cannot be accidentally
|
|
11
|
+
* substituted at type-check time when both are `string` aliases.
|
|
12
|
+
*/
|
|
13
|
+
export type StableAppId = string; // appGuid
|
|
14
|
+
export type StableObjectId = string; // `${appGuid}:${objectType}:${objectNumber}`
|
|
15
|
+
export type StableTableId = string; // `${appGuid}:Table:${tableNumber}`
|
|
16
|
+
export type StableFieldId = string; // `${stableTableId}#${fieldNumber}`
|
|
17
|
+
export type StableEventId = string; // `${stablePublisherObjectId}::${eventName}::${parameterShapeHash}`
|
|
18
|
+
export type StableRoutineId = string; // `${stableObjectId}#${canonicalSignatureHash}`
|
|
19
|
+
export type StablePermissionSetId = string; // permissionSetGuid OR `${appGuid}:PermissionSet:${name}`
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bidirectional converter between in-memory ids and stable serialization ids.
|
|
23
|
+
*
|
|
24
|
+
* Phase 0a ships the format-only directions (Object/Table/Field/Event) plus
|
|
25
|
+
* the forward direction for routines. The reverse direction for routines
|
|
26
|
+
* requires a lookup table over `CanonicalRoutineKey` that is built in
|
|
27
|
+
* Phase 0b alongside the variable indexer.
|
|
28
|
+
*/
|
|
29
|
+
export interface IdentityIndex {
|
|
30
|
+
toStableObjectId(internal: ObjectId): StableObjectId;
|
|
31
|
+
toInternalObjectId(stable: StableObjectId): ObjectId;
|
|
32
|
+
|
|
33
|
+
toStableTableId(internal: TableId): StableTableId;
|
|
34
|
+
toInternalTableId(stable: StableTableId): TableId;
|
|
35
|
+
|
|
36
|
+
toStableFieldId(internal: FieldId): StableFieldId;
|
|
37
|
+
toInternalFieldId(stable: StableFieldId): FieldId;
|
|
38
|
+
|
|
39
|
+
toStableEventId(
|
|
40
|
+
publisher: StableObjectId,
|
|
41
|
+
eventName: string,
|
|
42
|
+
parameterShapeHash: string,
|
|
43
|
+
): StableEventId;
|
|
44
|
+
|
|
45
|
+
toStableRoutineIdFromParts(
|
|
46
|
+
stableObject: StableObjectId,
|
|
47
|
+
canonicalSignatureHash: string,
|
|
48
|
+
): StableRoutineId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Factory — Phase 0a returns a stateless converter. Phase 0b will accept a
|
|
52
|
+
* lookup-context argument so the routine reverse direction can be resolved. */
|
|
53
|
+
export function createIdentityIndex(): IdentityIndex {
|
|
54
|
+
function toStableTableId(internal: TableId): StableTableId {
|
|
55
|
+
const parts = internal.split("/");
|
|
56
|
+
if (parts.length !== 3 || parts[1] !== "table") {
|
|
57
|
+
throw new Error(`toStableTableId: malformed TableId: ${internal}`);
|
|
58
|
+
}
|
|
59
|
+
return `${parts[0]}:Table:${parts[2]}`;
|
|
60
|
+
}
|
|
61
|
+
function toInternalTableId(stable: StableTableId): TableId {
|
|
62
|
+
const parts = stable.split(":");
|
|
63
|
+
if (parts.length !== 3 || parts[1] !== "Table") {
|
|
64
|
+
throw new Error(`toInternalTableId: malformed StableTableId: ${stable}`);
|
|
65
|
+
}
|
|
66
|
+
return `${parts[0]}/table/${parts[2]}`;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
toStableObjectId(internal) {
|
|
70
|
+
return internal.replaceAll("/", ":");
|
|
71
|
+
},
|
|
72
|
+
toInternalObjectId(stable) {
|
|
73
|
+
return stable.replaceAll(":", "/");
|
|
74
|
+
},
|
|
75
|
+
toStableTableId,
|
|
76
|
+
toInternalTableId,
|
|
77
|
+
toStableFieldId(internal) {
|
|
78
|
+
const lastSlash = internal.lastIndexOf("/");
|
|
79
|
+
if (lastSlash <= 0) {
|
|
80
|
+
throw new Error(`toStableFieldId: malformed FieldId: ${internal}`);
|
|
81
|
+
}
|
|
82
|
+
const tableInternal = internal.slice(0, lastSlash);
|
|
83
|
+
const fieldNum = internal.slice(lastSlash + 1);
|
|
84
|
+
return `${toStableTableId(tableInternal)}#${fieldNum}`;
|
|
85
|
+
},
|
|
86
|
+
toInternalFieldId(stable) {
|
|
87
|
+
const hash = stable.indexOf("#");
|
|
88
|
+
if (hash <= 0) {
|
|
89
|
+
throw new Error(`toInternalFieldId: malformed StableFieldId: ${stable}`);
|
|
90
|
+
}
|
|
91
|
+
const tableStable = stable.slice(0, hash);
|
|
92
|
+
const fieldNum = stable.slice(hash + 1);
|
|
93
|
+
return `${toInternalTableId(tableStable)}/${fieldNum}`;
|
|
94
|
+
},
|
|
95
|
+
toStableEventId(publisher, eventName, parameterShapeHash) {
|
|
96
|
+
return `${publisher}::${eventName}::${parameterShapeHash}`;
|
|
97
|
+
},
|
|
98
|
+
toStableRoutineIdFromParts(stableObject, canonicalSignatureHash) {
|
|
99
|
+
return `${stableObject}#${canonicalSignatureHash}`;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CapabilityFact } from "./capability.ts";
|
|
2
|
+
import type { CoverageRecord } from "./coverage.ts";
|
|
3
|
+
import type { RecordOpType, TempState } from "./entities.ts";
|
|
4
|
+
import type { CallsiteId, FieldId, OperationId, RoutineId, TableId } from "./ids.ts";
|
|
5
|
+
|
|
6
|
+
/** Tri-state effect presence — distinct from "unresolved call". */
|
|
7
|
+
export type EffectPresence = "yes" | "no" | "unknown";
|
|
8
|
+
|
|
9
|
+
export type Uncertainty =
|
|
10
|
+
| { kind: "unresolved-call"; callsiteId: CallsiteId }
|
|
11
|
+
| { kind: "opaque-callee"; callsiteId: CallsiteId }
|
|
12
|
+
| { kind: "dynamic-dispatch"; operationId: OperationId }
|
|
13
|
+
| { kind: "recordref-or-variant"; operationId: OperationId }
|
|
14
|
+
| { kind: "interface-dispatch"; callsiteId: CallsiteId }
|
|
15
|
+
| { kind: "parse-incomplete"; routineId: RoutineId };
|
|
16
|
+
|
|
17
|
+
/** Compact, de-duped effect fact. Carries NO evidence path — the path-walker rebuilds paths. */
|
|
18
|
+
export interface DbEffect {
|
|
19
|
+
effectKey: string; // op + table + operationSite + paramDependency + uncertaintyKind
|
|
20
|
+
operationId: OperationId;
|
|
21
|
+
op: RecordOpType;
|
|
22
|
+
tableId: TableId | "unknown";
|
|
23
|
+
recordVariableId?: string;
|
|
24
|
+
tempState: TempState;
|
|
25
|
+
via: "direct" | "inherited" | "implicit-trigger" | "event-subscriber" | "dynamic";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Field effects relative to one parameter — required by detector D3. */
|
|
29
|
+
export interface RecordRoleSummary {
|
|
30
|
+
parameterIndex: number;
|
|
31
|
+
tableId: TableId | "unknown";
|
|
32
|
+
readsFields: FieldId[] | "unknown";
|
|
33
|
+
writesFields: FieldId[] | "unknown";
|
|
34
|
+
mayResetFilters: boolean;
|
|
35
|
+
mayChangeLoadFields: boolean;
|
|
36
|
+
mayAssignRecord: boolean;
|
|
37
|
+
mayUseRecordRef: boolean;
|
|
38
|
+
|
|
39
|
+
// ====================================================================
|
|
40
|
+
// Entry requirements — populated in Phase 4 (path-aware walker)
|
|
41
|
+
// ====================================================================
|
|
42
|
+
requiresLoadedAtEntry: EffectPresence;
|
|
43
|
+
requiredLoadedFieldsAtEntry: FieldId[] | "unknown";
|
|
44
|
+
mutatesBeforeLoad: EffectPresence;
|
|
45
|
+
|
|
46
|
+
// ====================================================================
|
|
47
|
+
// Exit effects — populated in this phase (may-facts) and Phase 6 (path)
|
|
48
|
+
// ====================================================================
|
|
49
|
+
persistsCurrentRecord: EffectPresence;
|
|
50
|
+
setBasedDbWrites: EffectPresence;
|
|
51
|
+
validatesParam: EffectPresence;
|
|
52
|
+
copiesIntoParam: EffectPresence;
|
|
53
|
+
resetsFiltersOnParam: EffectPresence;
|
|
54
|
+
dirtyAtExit: EffectPresence;
|
|
55
|
+
currentLoadedFieldsAtExit: FieldId[] | "full" | "unknown";
|
|
56
|
+
|
|
57
|
+
// ====================================================================
|
|
58
|
+
// Convenience derivations
|
|
59
|
+
// ====================================================================
|
|
60
|
+
mutatesParam: EffectPresence;
|
|
61
|
+
loadsFromDbParam: EffectPresence;
|
|
62
|
+
initialisesParam: EffectPresence;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FieldEffectSet {
|
|
66
|
+
readsByRecordVariable: Record<string, FieldId[]>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RoutineSummary {
|
|
70
|
+
routineId: RoutineId;
|
|
71
|
+
dbEffects: DbEffect[];
|
|
72
|
+
inRecursiveCycle: boolean;
|
|
73
|
+
hasUnresolvedCalls: boolean;
|
|
74
|
+
uncertainties: Uncertainty[];
|
|
75
|
+
parameterRoles: RecordRoleSummary[];
|
|
76
|
+
fieldEffects?: FieldEffectSet; // lazy — only when a detector needs it
|
|
77
|
+
/**
|
|
78
|
+
* Phase 0a addition (capability-stack roadmap §3.4). Direct capability
|
|
79
|
+
* facts emitted by this routine's body. `undefined` in Phase 0a
|
|
80
|
+
* (extractor is still a no-op shell); populated in Phase 0b.
|
|
81
|
+
*/
|
|
82
|
+
capabilityFactsDirect?: CapabilityFact[];
|
|
83
|
+
/**
|
|
84
|
+
* Phase 0a addition. Capability facts inherited from the routine's
|
|
85
|
+
* transitive reachable closure (via call / event-dispatch /
|
|
86
|
+
* object-run / implicit-trigger / dependency-export edges). `undefined`
|
|
87
|
+
* in Phase 0a; populated in Phase 0b.
|
|
88
|
+
*/
|
|
89
|
+
capabilityFactsInherited?: CapabilityFact[];
|
|
90
|
+
/**
|
|
91
|
+
* Phase 0a addition. Coverage status for this routine's direct
|
|
92
|
+
* extraction + inherited capability cone. `undefined` in Phase 0a;
|
|
93
|
+
* populated in Phase 0b.
|
|
94
|
+
*/
|
|
95
|
+
coverage?: CoverageRecord;
|
|
96
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SourceRange } from "../model/identity.ts";
|
|
2
|
+
import type { Node as SyntaxNode } from "./native/wrapper.ts";
|
|
3
|
+
|
|
4
|
+
/** Strip surrounding double quotes from a quoted_identifier node's text. */
|
|
5
|
+
export function stripQuotes(text: string): string {
|
|
6
|
+
if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
|
|
7
|
+
return text.slice(1, -1);
|
|
8
|
+
}
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Map a tree-sitter node's position to a model SourceRange (0-based). */
|
|
13
|
+
export function nodeToSourceRange(node: SyntaxNode): SourceRange {
|
|
14
|
+
return {
|
|
15
|
+
startLine: node.startPosition.row,
|
|
16
|
+
startColumn: node.startPosition.column,
|
|
17
|
+
endLine: node.endPosition.row,
|
|
18
|
+
endColumn: node.endPosition.column,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Depth-first collect every named descendant (and the node itself) matching a predicate.
|
|
24
|
+
* Traverses `namedChildren`, so anonymous tokens are skipped. Traversal order is not
|
|
25
|
+
* guaranteed to match document order — the stack-based DFS visits siblings in reverse.
|
|
26
|
+
*
|
|
27
|
+
* When `pruneAtMatch` is true, the traversal does not descend into a node that matched
|
|
28
|
+
* the predicate. Use for "find top-level X" patterns where descendants of an X can never
|
|
29
|
+
* themselves be the X we're looking for — e.g. AL has no nested procedures, so the
|
|
30
|
+
* routine-finder should not walk into a procedure's body looking for more procedures.
|
|
31
|
+
* On a big object that pruning collapses an O(routines × body-size) traversal into one
|
|
32
|
+
* proportional to the object skeleton only.
|
|
33
|
+
*/
|
|
34
|
+
export function collectDescendants(
|
|
35
|
+
root: SyntaxNode,
|
|
36
|
+
predicate: (node: SyntaxNode) => boolean,
|
|
37
|
+
pruneAtMatch = false,
|
|
38
|
+
): SyntaxNode[] {
|
|
39
|
+
const out: SyntaxNode[] = [];
|
|
40
|
+
const stack: SyntaxNode[] = [root];
|
|
41
|
+
while (stack.length > 0) {
|
|
42
|
+
const node = stack.pop();
|
|
43
|
+
if (!node) continue;
|
|
44
|
+
if (predicate(node)) {
|
|
45
|
+
out.push(node);
|
|
46
|
+
if (pruneAtMatch) continue; // don't descend into matched node
|
|
47
|
+
}
|
|
48
|
+
for (const child of node.namedChildren) {
|
|
49
|
+
if (child) stack.push(child);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True if `node` is a descendant of `ancestor`. */
|
|
56
|
+
export function isDescendantOf(node: SyntaxNode, ancestor: SyntaxNode): boolean {
|
|
57
|
+
let current = node.parent;
|
|
58
|
+
while (current) {
|
|
59
|
+
if (current.id === ancestor.id) return true;
|
|
60
|
+
current = current.parent;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Find the first named child matching a predicate, or null. */
|
|
66
|
+
export function findChild(
|
|
67
|
+
node: SyntaxNode,
|
|
68
|
+
predicate: (child: SyntaxNode) => boolean,
|
|
69
|
+
): SyntaxNode | null {
|
|
70
|
+
for (const child of node.namedChildren) {
|
|
71
|
+
if (child && predicate(child)) return child;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True if a node is a generic V2 `property` node with the given name (case-insensitive). */
|
|
77
|
+
export function isPropertyNamed(node: SyntaxNode, name: string): boolean {
|
|
78
|
+
return (
|
|
79
|
+
node.type === "property" &&
|
|
80
|
+
node.childForFieldName("name")?.text?.toLowerCase() === name.toLowerCase()
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/parser/native/ffi.ts
|
|
2
|
+
// bun:ffi declarations for the al_shim ABI. Two-phase load for clean
|
|
3
|
+
// ABI-mismatch diagnostics. Module-level singleton (one process = one parser).
|
|
4
|
+
|
|
5
|
+
import { FFIType, type Library, dlopen } from "bun:ffi";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { arch as nodeArch, platform as nodePlatform } from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const EXPECTED_MAJOR = 1;
|
|
12
|
+
const REQUIRED_MINOR = 0;
|
|
13
|
+
|
|
14
|
+
export class NativeParserUnavailableError extends Error {
|
|
15
|
+
readonly kind: "missing" | "abi-mismatch" | "symbol-missing";
|
|
16
|
+
constructor(kind: NativeParserUnavailableError["kind"], message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "NativeParserUnavailableError";
|
|
19
|
+
this.kind = kind;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MetaShape {
|
|
24
|
+
tag: string;
|
|
25
|
+
platform: string;
|
|
26
|
+
arch: string;
|
|
27
|
+
asset: string;
|
|
28
|
+
sha256: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveLibPath(): string {
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const metaPath = join(here, "lib.meta.json");
|
|
34
|
+
if (!existsSync(metaPath)) {
|
|
35
|
+
throw new NativeParserUnavailableError(
|
|
36
|
+
"missing",
|
|
37
|
+
`al-sem: native parser meta missing at ${metaPath}. Did 'bun install' complete? If running offline, set AL_SEM_NATIVE_PARSER_PATH to a preseeded artifact.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8")) as MetaShape;
|
|
41
|
+
if (meta.platform !== nodePlatform || meta.arch !== nodeArch) {
|
|
42
|
+
throw new NativeParserUnavailableError(
|
|
43
|
+
"abi-mismatch",
|
|
44
|
+
`al-sem: native parser meta is for ${meta.platform}-${meta.arch}, ` +
|
|
45
|
+
`but current process is ${nodePlatform}-${nodeArch}. Re-run 'bun install'.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const ext = meta.asset.split(".").pop() ?? "so";
|
|
49
|
+
const libPath = join(here, `lib-${meta.tag}-${meta.platform}-${meta.arch}.${ext}`);
|
|
50
|
+
if (!existsSync(libPath)) {
|
|
51
|
+
throw new NativeParserUnavailableError(
|
|
52
|
+
"missing",
|
|
53
|
+
`al-sem: native parser library missing at ${libPath}. Re-run 'bun install'.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return libPath;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FULL_SYMBOLS = {
|
|
60
|
+
al_shim_abi_version: { args: [], returns: FFIType.u32 },
|
|
61
|
+
al_shim_parser_new: { args: [], returns: FFIType.ptr },
|
|
62
|
+
al_shim_parser_delete: { args: [FFIType.ptr], returns: FFIType.void },
|
|
63
|
+
al_shim_language: { args: [], returns: FFIType.ptr },
|
|
64
|
+
al_shim_parser_set_language: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.u32 },
|
|
65
|
+
al_shim_parse_utf8: { args: [FFIType.ptr, FFIType.ptr, FFIType.u32], returns: FFIType.ptr },
|
|
66
|
+
al_shim_tree_delete: { args: [FFIType.ptr], returns: FFIType.void },
|
|
67
|
+
al_shim_node_size: { args: [], returns: FFIType.u32 },
|
|
68
|
+
al_shim_tree_root_node: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
69
|
+
al_shim_node_type: { args: [FFIType.ptr], returns: FFIType.cstring },
|
|
70
|
+
al_shim_node_start_byte: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
71
|
+
al_shim_node_end_byte: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
72
|
+
al_shim_node_start_row: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
73
|
+
al_shim_node_start_column: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
74
|
+
al_shim_node_end_row: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
75
|
+
al_shim_node_end_column: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
76
|
+
al_shim_node_child_count: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
77
|
+
al_shim_node_named_child_count: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
78
|
+
al_shim_node_child: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.void },
|
|
79
|
+
al_shim_node_named_child: {
|
|
80
|
+
args: [FFIType.ptr, FFIType.u32, FFIType.ptr],
|
|
81
|
+
returns: FFIType.void,
|
|
82
|
+
},
|
|
83
|
+
al_shim_node_child_by_field_name: {
|
|
84
|
+
args: [FFIType.ptr, FFIType.ptr, FFIType.u32, FFIType.ptr],
|
|
85
|
+
returns: FFIType.void,
|
|
86
|
+
},
|
|
87
|
+
al_shim_node_parent: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
88
|
+
al_shim_node_previous_sibling: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
89
|
+
al_shim_node_next_sibling: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
90
|
+
al_shim_node_is_null: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
91
|
+
al_shim_node_is_named: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
92
|
+
al_shim_node_eq: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.u32 },
|
|
93
|
+
al_shim_node_has_error: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
94
|
+
al_shim_cursor_new: { args: [FFIType.ptr], returns: FFIType.ptr },
|
|
95
|
+
al_shim_cursor_delete: { args: [FFIType.ptr], returns: FFIType.void },
|
|
96
|
+
al_shim_cursor_goto_first_child: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
97
|
+
al_shim_cursor_goto_next_sibling: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
98
|
+
al_shim_cursor_goto_parent: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
99
|
+
al_shim_cursor_current_node: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
100
|
+
al_shim_cursor_current_field_name: { args: [FFIType.ptr], returns: FFIType.cstring },
|
|
101
|
+
al_shim_tree_sitter_language_version: { args: [], returns: FFIType.u32 },
|
|
102
|
+
al_shim_tree_sitter_min_compatible_language_version: { args: [], returns: FFIType.u32 },
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
let cached: Library<typeof FULL_SYMBOLS> | null = null;
|
|
106
|
+
|
|
107
|
+
export function loadShim(): Library<typeof FULL_SYMBOLS> {
|
|
108
|
+
if (cached) return cached;
|
|
109
|
+
const libPath = resolveLibPath();
|
|
110
|
+
|
|
111
|
+
// Phase 1: load only the version function. If this fails, the lib is broken
|
|
112
|
+
// or missing — we know it's not an ABI mismatch.
|
|
113
|
+
let v: Library<{ al_shim_abi_version: { args: []; returns: typeof FFIType.u32 } }>;
|
|
114
|
+
try {
|
|
115
|
+
v = dlopen(libPath, { al_shim_abi_version: { args: [], returns: FFIType.u32 } });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new NativeParserUnavailableError(
|
|
118
|
+
"missing",
|
|
119
|
+
`al-sem: could not dlopen ${libPath}: ${(err as Error).message}. Re-run 'bun install'.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const version = v.symbols.al_shim_abi_version();
|
|
123
|
+
const major = (version >>> 16) & 0xffff;
|
|
124
|
+
const minor = version & 0xffff;
|
|
125
|
+
if (major !== EXPECTED_MAJOR || minor < REQUIRED_MINOR) {
|
|
126
|
+
throw new NativeParserUnavailableError(
|
|
127
|
+
"abi-mismatch",
|
|
128
|
+
`al-sem: native parser ABI ${major}.${minor} is incompatible with this build ` +
|
|
129
|
+
`(needs ${EXPECTED_MAJOR}.${REQUIRED_MINOR}+). Re-install al-sem.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Phase 2: load the full symbol surface. A missing symbol now raises a
|
|
134
|
+
// meaningful error (we know the ABI version is right, so the lib is corrupt
|
|
135
|
+
// or wrong-build).
|
|
136
|
+
try {
|
|
137
|
+
cached = dlopen(libPath, FULL_SYMBOLS);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new NativeParserUnavailableError(
|
|
140
|
+
"symbol-missing",
|
|
141
|
+
`al-sem: native parser is missing expected symbols: ${(err as Error).message}. Re-install al-sem.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return cached;
|
|
145
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// src/parser/native/parse-index-pool.ts
|
|
2
|
+
// Tiny fixed-size pool around the parse-index Worker. Round-robin dispatch with
|
|
3
|
+
// per-worker busy state; each `submit()` returns a Promise resolved when that
|
|
4
|
+
// specific worker completes the job.
|
|
5
|
+
//
|
|
6
|
+
// Determinism contract: the pool does NOT preserve submission order in results.
|
|
7
|
+
// Callers must collect all results, then merge in a canonical order (e.g. sorted
|
|
8
|
+
// by relativePath) before pushing into the SemanticIndex. The dependency
|
|
9
|
+
// pipeline does this explicitly.
|
|
10
|
+
|
|
11
|
+
import type { WorkerResult } from "./parse-index-worker.ts";
|
|
12
|
+
|
|
13
|
+
export interface ParseIndexJob {
|
|
14
|
+
relativePath: string;
|
|
15
|
+
content: string;
|
|
16
|
+
appGuid: string;
|
|
17
|
+
sourceUnitId: string;
|
|
18
|
+
modelInstanceId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingJob {
|
|
22
|
+
jobId: number;
|
|
23
|
+
resolve: (value: WorkerResult) => void;
|
|
24
|
+
reject: (reason: Error) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ParseIndexPool {
|
|
28
|
+
private workers: Worker[];
|
|
29
|
+
private pending: Map<number, PendingJob> = new Map();
|
|
30
|
+
private nextJobId = 0;
|
|
31
|
+
/** Round-robin pointer; the next worker to receive a job. */
|
|
32
|
+
private rr = 0;
|
|
33
|
+
private disposed = false;
|
|
34
|
+
|
|
35
|
+
constructor(size: number) {
|
|
36
|
+
if (size < 1) throw new Error("ParseIndexPool size must be >= 1");
|
|
37
|
+
this.workers = new Array(size);
|
|
38
|
+
for (let i = 0; i < size; i++) {
|
|
39
|
+
const w = new Worker(new URL("./parse-index-worker.ts", import.meta.url), {
|
|
40
|
+
type: "module",
|
|
41
|
+
});
|
|
42
|
+
w.onmessage = (event: MessageEvent<WorkerResult>) => {
|
|
43
|
+
const job = this.pending.get(event.data.jobId);
|
|
44
|
+
if (!job) return; // late message after termination
|
|
45
|
+
this.pending.delete(event.data.jobId);
|
|
46
|
+
job.resolve(event.data);
|
|
47
|
+
};
|
|
48
|
+
w.onerror = (event) => {
|
|
49
|
+
// Worker-level error — fail every in-flight job; the worker is unusable now.
|
|
50
|
+
const message = `parse-index worker crashed: ${event.message ?? "unknown"}`;
|
|
51
|
+
for (const job of this.pending.values()) job.reject(new Error(message));
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
};
|
|
54
|
+
this.workers[i] = w;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get size(): number {
|
|
59
|
+
return this.workers.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
submit(job: ParseIndexJob): Promise<WorkerResult> {
|
|
63
|
+
if (this.disposed) return Promise.reject(new Error("ParseIndexPool is disposed"));
|
|
64
|
+
const jobId = this.nextJobId++;
|
|
65
|
+
const worker = this.workers[this.rr];
|
|
66
|
+
this.rr = (this.rr + 1) % this.workers.length;
|
|
67
|
+
const promise = new Promise<WorkerResult>((resolve, reject) => {
|
|
68
|
+
this.pending.set(jobId, { jobId, resolve, reject });
|
|
69
|
+
});
|
|
70
|
+
worker?.postMessage({ jobId, ...job });
|
|
71
|
+
return promise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Drive `jobs` through the pool with bounded in-flight concurrency. At most
|
|
76
|
+
* `maxInFlight` jobs are pending at once — back-pressure that keeps the workers'
|
|
77
|
+
* mailboxes from filling up with all of `jobs[]` at once. Critical for big deps:
|
|
78
|
+
* submitting all 7,634 jobs for Base Application at once means structured-cloning
|
|
79
|
+
* 97 MB of source into mailboxes and queueing 97 k routine arrays of results back,
|
|
80
|
+
* which is enough to OOM the runtime. Returns results in `jobs[]` order (the
|
|
81
|
+
* caller sorts later if it needs a different order).
|
|
82
|
+
*/
|
|
83
|
+
async mapBounded(
|
|
84
|
+
jobs: ParseIndexJob[],
|
|
85
|
+
maxInFlight: number = this.workers.length * 2,
|
|
86
|
+
onComplete?: (result: WorkerResult) => void,
|
|
87
|
+
): Promise<WorkerResult[]> {
|
|
88
|
+
const results: WorkerResult[] = new Array(jobs.length);
|
|
89
|
+
let nextIdx = 0;
|
|
90
|
+
const limit = Math.max(1, Math.min(maxInFlight, jobs.length));
|
|
91
|
+
const runOne = async (slot: number): Promise<void> => {
|
|
92
|
+
while (true) {
|
|
93
|
+
const idx = nextIdx++;
|
|
94
|
+
if (idx >= jobs.length) return;
|
|
95
|
+
const job = jobs[idx];
|
|
96
|
+
if (!job) continue;
|
|
97
|
+
const r = await this.submit(job);
|
|
98
|
+
results[idx] = r;
|
|
99
|
+
onComplete?.(r);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const runners: Promise<void>[] = new Array(limit);
|
|
103
|
+
for (let i = 0; i < limit; i++) runners[i] = runOne(i);
|
|
104
|
+
await Promise.all(runners);
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
terminate(): void {
|
|
109
|
+
if (this.disposed) return;
|
|
110
|
+
this.disposed = true;
|
|
111
|
+
for (const w of this.workers) w.terminate();
|
|
112
|
+
for (const job of this.pending.values()) {
|
|
113
|
+
job.reject(new Error("ParseIndexPool terminated before job completed"));
|
|
114
|
+
}
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Default pool size: physical-cores-minus-one, clamped to [2, 16]. Leave one core for
|
|
121
|
+
* the main thread doing the merge/sort and the OS. Override via the
|
|
122
|
+
* `AL_SEM_PARSE_POOL_SIZE` env var if needed.
|
|
123
|
+
*
|
|
124
|
+
* The clamp ceiling is 16 (was 8): on Microsoft Base Application (8,124 embedded files,
|
|
125
|
+
* 108,848 routines) measured cold-parse wall time was 8w=10.0s, 16w=7.6s, 24w=6.4s — the
|
|
126
|
+
* knee is ~16. Past it, the serial result-merge (sorted insertion of 108k routines on the
|
|
127
|
+
* main thread) dominates, so more workers buy little. Power users on many-core boxes can
|
|
128
|
+
* push higher via `AL_SEM_PARSE_POOL_SIZE`.
|
|
129
|
+
*/
|
|
130
|
+
export function defaultPoolSize(): number {
|
|
131
|
+
const envOverride = process.env.AL_SEM_PARSE_POOL_SIZE;
|
|
132
|
+
if (envOverride !== undefined) {
|
|
133
|
+
const n = Number.parseInt(envOverride, 10);
|
|
134
|
+
if (Number.isFinite(n) && n >= 1) return n;
|
|
135
|
+
}
|
|
136
|
+
const cores = (() => {
|
|
137
|
+
try {
|
|
138
|
+
const os = require("node:os") as {
|
|
139
|
+
availableParallelism?: () => number;
|
|
140
|
+
cpus?: () => unknown[];
|
|
141
|
+
};
|
|
142
|
+
if (typeof os.availableParallelism === "function") return os.availableParallelism();
|
|
143
|
+
if (typeof os.cpus === "function") return os.cpus().length;
|
|
144
|
+
} catch {}
|
|
145
|
+
return 4;
|
|
146
|
+
})();
|
|
147
|
+
return Math.max(2, Math.min(16, cores - 1));
|
|
148
|
+
}
|