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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/package.json +64 -0
  4. package/scripts/d40-diff.ts +44 -0
  5. package/scripts/fetch-native-parser.ts +179 -0
  6. package/scripts/precision-sample.ts +99 -0
  7. package/scripts/precision-study.ts +42 -0
  8. package/scripts/precision-tabulate.ts +52 -0
  9. package/src/cli/baseline.ts +31 -0
  10. package/src/cli/diff.ts +199 -0
  11. package/src/cli/events-chains.ts +56 -0
  12. package/src/cli/events-fanout.ts +87 -0
  13. package/src/cli/exit-code.ts +30 -0
  14. package/src/cli/fingerprint-indexes.ts +130 -0
  15. package/src/cli/fingerprint-query.ts +543 -0
  16. package/src/cli/fingerprint-witness.ts +493 -0
  17. package/src/cli/fingerprint.ts +292 -0
  18. package/src/cli/format-compact-json.ts +45 -0
  19. package/src/cli/format-events.ts +77 -0
  20. package/src/cli/format-fingerprint.ts +295 -0
  21. package/src/cli/format-html.ts +503 -0
  22. package/src/cli/format-json.ts +13 -0
  23. package/src/cli/format-policy.ts +95 -0
  24. package/src/cli/format-sarif.ts +186 -0
  25. package/src/cli/format-terminal.ts +153 -0
  26. package/src/cli/index.ts +566 -0
  27. package/src/cli/policy.ts +204 -0
  28. package/src/config/roots-config.ts +302 -0
  29. package/src/deps/cache-versions.ts +74 -0
  30. package/src/deps/canonical-json.ts +27 -0
  31. package/src/deps/dependency-artifact.ts +144 -0
  32. package/src/deps/dependency-cache.ts +262 -0
  33. package/src/deps/dependency-dag.ts +128 -0
  34. package/src/deps/dependency-package-discovery.ts +85 -0
  35. package/src/deps/dependency-pipeline.ts +483 -0
  36. package/src/deps/dependency-projection.ts +211 -0
  37. package/src/deps/dependency-resolver.ts +154 -0
  38. package/src/deps/workspace-dependencies.ts +114 -0
  39. package/src/detectors/capability-query.ts +145 -0
  40. package/src/detectors/confidence.ts +52 -0
  41. package/src/detectors/d1-db-op-in-loop.ts +457 -0
  42. package/src/detectors/d10-self-modifying-loop.ts +114 -0
  43. package/src/detectors/d11-modify-without-get.ts +129 -0
  44. package/src/detectors/d12-dead-integration-event.ts +81 -0
  45. package/src/detectors/d13-cross-app-internal-call.ts +105 -0
  46. package/src/detectors/d14-dead-routine.ts +151 -0
  47. package/src/detectors/d16-obsolete-routine-call.ts +94 -0
  48. package/src/detectors/d17-min-version-drift.ts +157 -0
  49. package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
  50. package/src/detectors/d19-unused-parameter.ts +116 -0
  51. package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
  52. package/src/detectors/d20-unreachable-after-exit.ts +92 -0
  53. package/src/detectors/d21-read-without-load.ts +128 -0
  54. package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
  55. package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
  56. package/src/detectors/d3-load-state.ts +72 -0
  57. package/src/detectors/d3-missing-setloadfields.ts +234 -0
  58. package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
  59. package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
  60. package/src/detectors/d34-commit-in-loop.ts +206 -0
  61. package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
  62. package/src/detectors/d36-late-setloadfields.ts +162 -0
  63. package/src/detectors/d37-validate-without-persist.ts +271 -0
  64. package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
  65. package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
  66. package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
  67. package/src/detectors/d40-transitive-load-missing.ts +217 -0
  68. package/src/detectors/d41-transitive-filter-loss.ts +200 -0
  69. package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
  70. package/src/detectors/d43-event-ishandled-skip.ts +257 -0
  71. package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
  72. package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
  73. package/src/detectors/d5-set-based-opportunity.ts +162 -0
  74. package/src/detectors/d7-recursive-event-expansion.ts +151 -0
  75. package/src/detectors/d8-commit-in-transaction.ts +132 -0
  76. package/src/detectors/d9-transaction-span-summary.ts +107 -0
  77. package/src/detectors/detector-context.ts +121 -0
  78. package/src/detectors/finding-grouping.ts +61 -0
  79. package/src/detectors/path-merge.ts +174 -0
  80. package/src/detectors/registry.ts +176 -0
  81. package/src/detectors/table-display.ts +42 -0
  82. package/src/diff/diff-abi.ts +195 -0
  83. package/src/diff/diff-capabilities.ts +179 -0
  84. package/src/diff/diff-engine.ts +146 -0
  85. package/src/diff/diff-events.ts +323 -0
  86. package/src/diff/diff-identity.ts +73 -0
  87. package/src/diff/diff-indexes.ts +199 -0
  88. package/src/diff/diff-permissions.ts +260 -0
  89. package/src/diff/diff-policy.ts +101 -0
  90. package/src/diff/diff-preflight.ts +66 -0
  91. package/src/diff/diff-renames.ts +104 -0
  92. package/src/diff/diff-schema.ts +232 -0
  93. package/src/diff/format-diff.ts +148 -0
  94. package/src/engine/attribute-parser.ts +50 -0
  95. package/src/engine/capability-cone.ts +531 -0
  96. package/src/engine/combined-graph.ts +357 -0
  97. package/src/engine/control-flow-walker.ts +1317 -0
  98. package/src/engine/dispatch-sites.ts +199 -0
  99. package/src/engine/effect-lattice.ts +81 -0
  100. package/src/engine/entry-points.ts +57 -0
  101. package/src/engine/event-flow.ts +524 -0
  102. package/src/engine/event-relay.ts +92 -0
  103. package/src/engine/op-classification.ts +92 -0
  104. package/src/engine/path-walker.ts +189 -0
  105. package/src/engine/reverse-call-graph.ts +23 -0
  106. package/src/engine/root-classifier-overlay.ts +194 -0
  107. package/src/engine/root-classifier.ts +135 -0
  108. package/src/engine/scc.ts +110 -0
  109. package/src/engine/source-anchor.ts +25 -0
  110. package/src/engine/summary-context.ts +104 -0
  111. package/src/engine/summary-engine.ts +296 -0
  112. package/src/engine/summary-runner.ts +560 -0
  113. package/src/engine/transaction-spans.ts +112 -0
  114. package/src/engine/uncertainty-util.ts +54 -0
  115. package/src/hash.ts +31 -0
  116. package/src/index/attribute-from-node.ts +141 -0
  117. package/src/index/callee-from-node.ts +181 -0
  118. package/src/index/capability/background.ts +90 -0
  119. package/src/index/capability/commit.ts +44 -0
  120. package/src/index/capability/dispatch.ts +164 -0
  121. package/src/index/capability/events.ts +65 -0
  122. package/src/index/capability/extractor.ts +124 -0
  123. package/src/index/capability/file-blob.ts +137 -0
  124. package/src/index/capability/http.ts +159 -0
  125. package/src/index/capability/hyperlink.ts +60 -0
  126. package/src/index/capability/isolated-storage.ts +179 -0
  127. package/src/index/capability/table.ts +113 -0
  128. package/src/index/capability/telemetry.ts +84 -0
  129. package/src/index/capability/ui.ts +55 -0
  130. package/src/index/capability/value-source.ts +202 -0
  131. package/src/index/expression-from-node.ts +117 -0
  132. package/src/index/indexer.ts +102 -0
  133. package/src/index/intraprocedural-body.ts +1467 -0
  134. package/src/index/intraprocedural-ops.ts +253 -0
  135. package/src/index/intraprocedural-refs.ts +188 -0
  136. package/src/index/object-indexer.ts +279 -0
  137. package/src/index/routine-indexer.ts +282 -0
  138. package/src/index/routine-signature.ts +46 -0
  139. package/src/index/variable-indexer.ts +134 -0
  140. package/src/index/variable-initializer-extractor.ts +155 -0
  141. package/src/index/variable-type-normalizer.ts +83 -0
  142. package/src/index.ts +267 -0
  143. package/src/mcp/server.ts +72 -0
  144. package/src/mcp/session.ts +49 -0
  145. package/src/mcp/tools/explain-path.ts +75 -0
  146. package/src/mcp/tools/get-analysis-health.ts +62 -0
  147. package/src/mcp/tools/get-finding.ts +47 -0
  148. package/src/mcp/tools/get-routine-summary.ts +126 -0
  149. package/src/mcp/tools/list-findings.ts +85 -0
  150. package/src/mcp/tools/list-hotspots.ts +78 -0
  151. package/src/mcp/tools/list-rollups.ts +103 -0
  152. package/src/mcp/tools/validators.ts +25 -0
  153. package/src/model/attributes.ts +120 -0
  154. package/src/model/callee.ts +45 -0
  155. package/src/model/capability.ts +187 -0
  156. package/src/model/coverage.ts +85 -0
  157. package/src/model/entities.ts +628 -0
  158. package/src/model/expression.ts +98 -0
  159. package/src/model/finding.ts +110 -0
  160. package/src/model/graph-edge.ts +93 -0
  161. package/src/model/graph.ts +62 -0
  162. package/src/model/identity.ts +81 -0
  163. package/src/model/ids.ts +90 -0
  164. package/src/model/index.ts +13 -0
  165. package/src/model/model.ts +51 -0
  166. package/src/model/permission.ts +76 -0
  167. package/src/model/root-classification.ts +116 -0
  168. package/src/model/stable-identity.ts +102 -0
  169. package/src/model/summary.ts +96 -0
  170. package/src/parser/ast.ts +82 -0
  171. package/src/parser/native/ffi.ts +145 -0
  172. package/src/parser/native/parse-index-pool.ts +148 -0
  173. package/src/parser/native/parse-index-worker.ts +94 -0
  174. package/src/parser/native/wrapper.ts +353 -0
  175. package/src/parser/parser-init.ts +43 -0
  176. package/src/perf/profiler.ts +66 -0
  177. package/src/policy/policy-default.yaml +83 -0
  178. package/src/policy/policy-engine.ts +339 -0
  179. package/src/policy/policy-loader.ts +257 -0
  180. package/src/policy/policy-schema.json +379 -0
  181. package/src/policy/policy-types.ts +81 -0
  182. package/src/policy/predicate-compiler.ts +151 -0
  183. package/src/policy/predicate-evaluator.ts +267 -0
  184. package/src/policy/predicate-fields.ts +439 -0
  185. package/src/projection/actionable-anchor.ts +48 -0
  186. package/src/projection/finding-filters.ts +44 -0
  187. package/src/projection/finding-fingerprint.ts +54 -0
  188. package/src/projection/finding-groups.ts +41 -0
  189. package/src/projection/finding-summary.ts +110 -0
  190. package/src/projection/rollup-findings.ts +105 -0
  191. package/src/providers/discover.ts +88 -0
  192. package/src/providers/external.ts +46 -0
  193. package/src/providers/types.ts +36 -0
  194. package/src/providers/workspace.ts +117 -0
  195. package/src/resolve/call-resolver.ts +117 -0
  196. package/src/resolve/coverage.ts +61 -0
  197. package/src/resolve/event-graph.ts +166 -0
  198. package/src/resolve/implicit-edges.ts +53 -0
  199. package/src/resolve/record-types.ts +36 -0
  200. package/src/resolve/resolver.ts +23 -0
  201. package/src/resolve/semantic-graph.ts +29 -0
  202. package/src/resolve/symbol-table.ts +69 -0
  203. package/src/snapshot/app-snapshot.ts +74 -0
  204. package/src/snapshot/compose.ts +100 -0
  205. package/src/snapshot/derive/callsite-evidence.ts +76 -0
  206. package/src/snapshot/derive/capability-facts.ts +70 -0
  207. package/src/snapshot/derive/contracts.ts +131 -0
  208. package/src/snapshot/derive/coverage.ts +35 -0
  209. package/src/snapshot/derive/event-declarations.ts +140 -0
  210. package/src/snapshot/derive/identity-table.ts +58 -0
  211. package/src/snapshot/derive/inputs.ts +91 -0
  212. package/src/snapshot/derive/operation-evidence.ts +70 -0
  213. package/src/snapshot/derive/permissions.ts +186 -0
  214. package/src/snapshot/derive/root-classifications.ts +56 -0
  215. package/src/snapshot/derive/schema.ts +130 -0
  216. package/src/snapshot/derive/typed-edges.ts +60 -0
  217. package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
  218. package/src/snapshot/deserialize.ts +40 -0
  219. package/src/snapshot/serialize-cbor-gz.ts +12 -0
  220. package/src/snapshot/serialize-cbor.ts +19 -0
  221. package/src/snapshot/serialize-json.ts +22 -0
  222. package/src/snapshot/shard.ts +134 -0
  223. package/src/snapshot/types.ts +181 -0
  224. package/src/symbols/app-manifest.ts +96 -0
  225. package/src/symbols/app-package-zip.ts +50 -0
  226. package/src/symbols/embedded-source-reader.ts +41 -0
  227. package/src/symbols/package-hash.ts +81 -0
  228. package/src/symbols/symbol-reader.ts +101 -0
  229. package/src/symbols/symbol-reference-parser.ts +378 -0
  230. package/src/symbols/symbol-reference-reader.ts +27 -0
  231. package/tsconfig.json +18 -0
@@ -0,0 +1,22 @@
1
+ import type { CapabilitySnapshot } from "./types.ts";
2
+
3
+ /**
4
+ * Pretty JSON serializer with recursively sorted object keys, 2-space indent,
5
+ * newline-terminated. Byte-stable for a given input.
6
+ *
7
+ * Arrays passed through as-is — derivers sort their own outputs per the
8
+ * canonical-sort contract. Sorting twice would mask deriver bugs.
9
+ */
10
+ export function serializeJson(snapshot: CapabilitySnapshot): string {
11
+ return `${JSON.stringify(snapshot, sortedReplacer(), 2)}\n`;
12
+ }
13
+
14
+ function sortedReplacer(): (key: string, value: unknown) => unknown {
15
+ return (_key, value) => {
16
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
17
+ const obj = value as Record<string, unknown>;
18
+ const sorted: Record<string, unknown> = {};
19
+ for (const k of Object.keys(obj).sort()) sorted[k] = obj[k];
20
+ return sorted;
21
+ };
22
+ }
@@ -0,0 +1,134 @@
1
+ import { serializeCborGz } from "./serialize-cbor-gz.ts";
2
+ import { serializeCbor } from "./serialize-cbor.ts";
3
+ import { serializeJson } from "./serialize-json.ts";
4
+ import type {
5
+ AppIdentity,
6
+ CapabilitySnapshot,
7
+ ShardManifest,
8
+ SnapshotFormat,
9
+ SnapshotIdentityTable,
10
+ } from "./types.ts";
11
+ import { SNAPSHOT_SCHEMA_VERSION } from "./types.ts";
12
+
13
+ export interface ShardingOptions {
14
+ format: SnapshotFormat;
15
+ /** Drop dependency shards; keep only the primary app. Default false. */
16
+ primaryOnly?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Split a monolithic snapshot into per-app shards + a manifest.
21
+ *
22
+ * Returns a Map<filename, bytes>. Caller writes each entry to disk under
23
+ * the user's --out directory.
24
+ *
25
+ * Slicing rule: a fact belongs to the shard whose appGuid is the prefix of
26
+ * its stableId (Stable*Ids start with `${appGuid}:`). The primary shard is
27
+ * the FIRST app in snapshot.apps (apps array is sorted; the primary is
28
+ * established by analyzeWorkspace and stable across runs).
29
+ *
30
+ * Each shard is a fully valid CapabilitySnapshot — schemaVersion,
31
+ * alsemVersion, workspaceFingerprint, generatedAt are copied; apps,
32
+ * identities, inputs are narrowed to the shard's app. typedEdges that cross
33
+ * apps are duplicated into BOTH shards.
34
+ */
35
+ export function serializeSharded(
36
+ snapshot: CapabilitySnapshot,
37
+ opts: ShardingOptions,
38
+ ): Map<string, Uint8Array> {
39
+ const apps = snapshot.apps;
40
+ const out = new Map<string, Uint8Array>();
41
+ if (apps.length === 0) {
42
+ out.set("manifest.json", encodeManifest(snapshot, []));
43
+ return out;
44
+ }
45
+
46
+ const primaryGuid = apps[0]?.appGuid;
47
+ if (primaryGuid === undefined) {
48
+ out.set("manifest.json", encodeManifest(snapshot, []));
49
+ return out;
50
+ }
51
+
52
+ const shardEntries: ShardManifest["shards"] = [];
53
+
54
+ for (const app of apps) {
55
+ const role = app.appGuid === primaryGuid ? "primary" : "dependency";
56
+ if (opts.primaryOnly && role !== "primary") continue;
57
+
58
+ const shard = sliceForApp(snapshot, app);
59
+ const fileBase = role === "primary" ? "primary" : `dep-${app.appGuid}`;
60
+ const ext = opts.format === "json" ? "json" : opts.format === "cbor" ? "cbor" : "cbor.gz";
61
+ const fileName = `${fileBase}.${ext}`;
62
+ out.set(fileName, serializeOne(shard, opts.format));
63
+ shardEntries.push({ appGuid: app.appGuid, role, file: fileName });
64
+ }
65
+
66
+ out.set("manifest.json", encodeManifest(snapshot, shardEntries));
67
+ return out;
68
+ }
69
+
70
+ function sliceForApp(s: CapabilitySnapshot, app: AppIdentity): CapabilitySnapshot {
71
+ const inApp = (stableId: string): boolean => stableId.startsWith(`${app.appGuid}:`);
72
+ const shard: CapabilitySnapshot = {
73
+ schemaVersion: s.schemaVersion,
74
+ alsemVersion: s.alsemVersion,
75
+ workspaceFingerprint: s.workspaceFingerprint,
76
+ generatedAt: s.generatedAt,
77
+ apps: [app],
78
+ identities: sliceIdentities(s.identities, app.appGuid),
79
+ contractFacts: s.contractFacts.filter((c) => inApp(c.stableId)),
80
+ schemaFacts: s.schemaFacts.filter((c) => inApp(c.stableId)),
81
+ permissionFacts: s.permissionFacts.filter((p) =>
82
+ inApp(p.kind === "declared" ? String(p.permissionSet) : String(p.subject)),
83
+ ),
84
+ rootClassifications: s.rootClassifications.filter((r) => inApp(String(r.routineId))),
85
+ capabilityFacts: s.capabilityFacts.filter((f) => inApp(String(f.subject))),
86
+ typedEdges: s.typedEdges.filter(
87
+ (e) =>
88
+ ("from" in e && inApp(String(e.from))) ||
89
+ ("to" in e && inApp(String((e as { to?: unknown }).to ?? ""))),
90
+ ),
91
+ operationIndex: s.operationIndex.filter((o) => inApp(String(o.routine))),
92
+ callsiteIndex: s.callsiteIndex.filter((c) => inApp(String(c.routine))),
93
+ coverage: s.coverage.filter((c) => inApp(String(c.subject))),
94
+ inputs: s.inputs,
95
+ eventDeclarations: s.eventDeclarations.filter(
96
+ (d) => inApp(String(d.routine)) || (d.binding ? inApp(d.binding.publisherObject) : false),
97
+ ),
98
+ };
99
+ // Side-band metadata is workspace-scoped, not app-scoped — mirror onto
100
+ // every shard so each is a self-contained snapshot.
101
+ if (s.inputsMetadata !== undefined) {
102
+ shard.inputsMetadata = s.inputsMetadata;
103
+ }
104
+ return shard;
105
+ }
106
+
107
+ function sliceIdentities(table: SnapshotIdentityTable, appGuid: string): SnapshotIdentityTable {
108
+ const stableIds: string[] = [];
109
+ const displayNames: string[] = [];
110
+ for (let i = 0; i < table.stableIds.length; i++) {
111
+ const id = table.stableIds[i];
112
+ if (id?.startsWith(`${appGuid}:`)) {
113
+ stableIds.push(id);
114
+ displayNames.push(table.displayNames[i] ?? "");
115
+ }
116
+ }
117
+ return { stableIds, displayNames };
118
+ }
119
+
120
+ function serializeOne(s: CapabilitySnapshot, fmt: SnapshotFormat): Uint8Array {
121
+ if (fmt === "json") return new TextEncoder().encode(serializeJson(s));
122
+ if (fmt === "cbor") return serializeCbor(s);
123
+ return serializeCborGz(s);
124
+ }
125
+
126
+ function encodeManifest(s: CapabilitySnapshot, shards: ShardManifest["shards"]): Uint8Array {
127
+ const m: ShardManifest = {
128
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
129
+ alsemVersion: s.alsemVersion,
130
+ workspaceFingerprint: s.workspaceFingerprint,
131
+ shards: [...shards].sort((a, b) => a.appGuid.localeCompare(b.appGuid)),
132
+ };
133
+ return new TextEncoder().encode(`${JSON.stringify(m, null, 2)}\n`);
134
+ }
@@ -0,0 +1,181 @@
1
+ import type { CapabilityFact } from "../model/capability.ts";
2
+ import type { CoverageRecord } from "../model/coverage.ts";
3
+ import type { GraphEdge } from "../model/graph-edge.ts";
4
+ import type { SourceAnchor } from "../model/identity.ts";
5
+ import type { CallsiteId, OperationId } from "../model/ids.ts";
6
+ import type { PermissionFact } from "../model/permission.ts";
7
+ import type {
8
+ StableAppId,
9
+ StableEventId,
10
+ StableObjectId,
11
+ StableRoutineId,
12
+ } from "../model/stable-identity.ts";
13
+
14
+ /** Snapshot schema version. Bump per CLAUDE.md "schema bumps are cheap"
15
+ * recipe; deserializer asserts and may run a migration shim. */
16
+ export const SNAPSHOT_SCHEMA_VERSION = 1 as const;
17
+ export type SnapshotSchemaVersion = typeof SNAPSHOT_SCHEMA_VERSION;
18
+
19
+ export type SnapshotFormat = "json" | "cbor" | "cbor.gz";
20
+ export type SnapshotShardingMode = "monolithic" | "sharded";
21
+
22
+ /** Stable app metadata. Sorted by `appGuid` in the snapshot. */
23
+ export interface AppIdentity {
24
+ appGuid: StableAppId;
25
+ publisher: string;
26
+ name: string;
27
+ version: string;
28
+ }
29
+
30
+ /** Interning table — stable IDs and parallel display names. Facts reference
31
+ * ids; the table provides the human-readable name once per id. Arrays are
32
+ * parallel: stableIds[i] ↔ displayNames[i]. Sorted by stableIds. */
33
+ export interface SnapshotIdentityTable {
34
+ stableIds: string[];
35
+ displayNames: string[];
36
+ }
37
+
38
+ /** Canonical fingerprint of one parsed attribute — name + SHA-256 of
39
+ * normalized argument list. */
40
+ export interface AttributeFingerprint {
41
+ name: string;
42
+ argsHash: string;
43
+ }
44
+
45
+ /** Contract surface — drives Phase 2 ABI / contract diff. */
46
+ export interface ContractFact {
47
+ kind: "object" | "routine" | "event-publisher" | "interface";
48
+ stableId: string;
49
+ visibility: "public" | "internal" | "local" | "protected";
50
+ obsoleteState?: "Pending" | "Removed";
51
+ obsoleteReason?: string;
52
+ /** Routines + events: parameter shape hash + return-type hash. */
53
+ signatureFingerprint: string;
54
+ attributes: AttributeFingerprint[];
55
+ }
56
+
57
+ /** Schema surface — drives Phase 2 schema diff. */
58
+ export interface SchemaFact {
59
+ kind: "table" | "field" | "enum" | "enum-value" | "key";
60
+ stableId: string;
61
+ /** Type/length/option/relation/dataclassification, canonicalised + SHA-256. */
62
+ shapeFingerprint: string;
63
+ dataClassification?: string;
64
+ }
65
+
66
+ /** Anchor metadata for replaying op witnesses without re-reading AL source. */
67
+ export interface OperationEvidence {
68
+ operationId: OperationId;
69
+ routine: StableRoutineId;
70
+ sourceFile: string;
71
+ startLine: number;
72
+ startColumn: number;
73
+ endLine: number;
74
+ endColumn: number;
75
+ displayText: string;
76
+ }
77
+
78
+ /** Anchor metadata for call witnesses. */
79
+ export interface CallsiteEvidence {
80
+ callsiteId: CallsiteId;
81
+ routine: StableRoutineId;
82
+ sourceFile: string;
83
+ startLine: number;
84
+ startColumn: number;
85
+ endLine: number;
86
+ endColumn: number;
87
+ calleeDisplay: string;
88
+ }
89
+
90
+ /** Reproducibility input — every file whose content contributes to the
91
+ * snapshot identity. */
92
+ export interface SnapshotInput {
93
+ kind: "app-json" | "roots-config" | "policy" | "dep-package" | "app-package";
94
+ path: string;
95
+ contentHash: string;
96
+ }
97
+
98
+ /**
99
+ * Side-band metadata about the inputs list. Tracks operator overrides
100
+ * that affect what does or doesn't appear in `inputs` (e.g.
101
+ * roots.config.json skipped via --no-roots-config). Distinct from
102
+ * `inputs` itself so diff tools can tell "config doesn't exist" (no
103
+ * inputsMetadata) from "config existed but was ignored"
104
+ * (`rootsConfigIgnored: true`).
105
+ */
106
+ export interface SnapshotInputsMetadata {
107
+ /** True if `roots.config.json` was present on disk but skipped via --no-roots-config. */
108
+ rootsConfigIgnored?: boolean;
109
+ }
110
+
111
+ /** Subscriber binding — points to publisher object + event name. */
112
+ export interface SubscriberBinding {
113
+ publisherObject: StableObjectId;
114
+ eventName: string;
115
+ }
116
+
117
+ /** Bipartite publisher / subscriber declaration. */
118
+ export interface EventDeclaration {
119
+ kind: "publisher" | "subscriber";
120
+ routine: StableRoutineId;
121
+ eventId: StableEventId;
122
+ binding?: SubscriberBinding;
123
+ sourceAnchor: SourceAnchor;
124
+ }
125
+
126
+ /** Phase 0c reserves this slot; Phase 1's root-classifier populates it. */
127
+ export interface RootClassificationSlot {
128
+ routineId: StableRoutineId;
129
+ kinds: string[];
130
+ externallyReachable: boolean;
131
+ source: "ast" | "config" | "ast+config";
132
+ confidence: "static" | "user-asserted";
133
+ sourceAnchor?: SourceAnchor;
134
+ configEntryId?: string;
135
+ resolutionStatus?: "resolved" | "ambiguous" | "unresolved";
136
+ }
137
+
138
+ /** Sharded layout manifest. References per-app shard files by stable app id. */
139
+ export interface ShardManifest {
140
+ schemaVersion: SnapshotSchemaVersion;
141
+ alsemVersion: string;
142
+ workspaceFingerprint: string;
143
+ shards: Array<{
144
+ appGuid: StableAppId;
145
+ role: "primary" | "dependency";
146
+ file: string;
147
+ }>;
148
+ }
149
+
150
+ /** The semantic snapshot — single source of truth for Phase 1+ consumers. */
151
+ export interface CapabilitySnapshot {
152
+ schemaVersion: SnapshotSchemaVersion;
153
+ alsemVersion: string;
154
+ workspaceFingerprint: string;
155
+ generatedAt: string;
156
+ apps: AppIdentity[];
157
+ identities: SnapshotIdentityTable;
158
+ contractFacts: ContractFact[];
159
+ schemaFacts: SchemaFact[];
160
+ permissionFacts: PermissionFact[];
161
+ rootClassifications: RootClassificationSlot[];
162
+ capabilityFacts: CapabilityFact[];
163
+ typedEdges: GraphEdge[];
164
+ operationIndex: OperationEvidence[];
165
+ callsiteIndex: CallsiteEvidence[];
166
+ coverage: CoverageRecord[];
167
+ inputs: SnapshotInput[];
168
+ /**
169
+ * Side-band metadata about the inputs list. Tracks operator overrides
170
+ * that affect what does or doesn't appear in `inputs` (e.g.
171
+ * roots.config.json skipped via --no-roots-config). Distinct from
172
+ * `inputs` itself so diff tools can tell "config doesn't exist" (no
173
+ * inputsMetadata) from "config existed but was ignored"
174
+ * (`rootsConfigIgnored: true`).
175
+ *
176
+ * Omitted entirely when no overrides apply — avoids noisy
177
+ * `inputsMetadata: {}` in serialized output.
178
+ */
179
+ inputsMetadata?: SnapshotInputsMetadata;
180
+ eventDeclarations: EventDeclaration[];
181
+ }
@@ -0,0 +1,96 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { filteredUnzip } from "./app-package-zip.ts";
3
+
4
+ export interface ManifestAppIdentity {
5
+ appGuid: string;
6
+ name: string;
7
+ publisher: string;
8
+ version: string;
9
+ }
10
+
11
+ export interface ManifestDependency {
12
+ appGuid: string;
13
+ name: string;
14
+ publisher: string;
15
+ minVersion: string;
16
+ }
17
+
18
+ export interface AppManifest {
19
+ identity: ManifestAppIdentity;
20
+ dependencies: ManifestDependency[];
21
+ /** ResourceExposurePolicy.IncludeSourceInSymbolFile — whether embedded .al source is present. */
22
+ includesSource: boolean;
23
+ /** Set when the manifest could not be parsed; identity is then empty. */
24
+ error?: string;
25
+ }
26
+
27
+ /** Build an empty manifest carrying an error — used for every failure path (no silent clean). */
28
+ function failManifest(error: string): AppManifest {
29
+ return {
30
+ identity: { appGuid: "", name: "", publisher: "", version: "" },
31
+ dependencies: [],
32
+ includesSource: false,
33
+ error,
34
+ };
35
+ }
36
+
37
+ /** Read an attribute value out of an opening tag, anchored on a word boundary so a
38
+ * longer attribute name (e.g. `CompatibilityId`) cannot be matched as a shorter one (`Id`). */
39
+ function readTagAttr(tag: string, attr: string): string {
40
+ return tag.match(new RegExp(`\\b${attr}\\s*=\\s*"([^"]*)"`, "i"))?.[1] ?? "";
41
+ }
42
+
43
+ /** Read one attribute from the first occurrence of `<tag ...>`, tolerant of attribute order. */
44
+ function readAttr(xml: string, tag: string, attr: string): string {
45
+ const tagMatch = xml.match(new RegExp(`<${tag}\\b[^>]*>`, "i"));
46
+ if (!tagMatch) return "";
47
+ return readTagAttr(tagMatch[0], attr);
48
+ }
49
+
50
+ /** Parse the text of a NavxManifest.xml. Never throws — failure is reported via `error`. */
51
+ export function parseAppManifestXml(xml: string): AppManifest {
52
+ if (!/<App\b/i.test(xml)) {
53
+ return failManifest("no <App> element found in manifest");
54
+ }
55
+ const identity: ManifestAppIdentity = {
56
+ appGuid: readAttr(xml, "App", "Id"),
57
+ name: readAttr(xml, "App", "Name"),
58
+ publisher: readAttr(xml, "App", "Publisher"),
59
+ version: readAttr(xml, "App", "Version"),
60
+ };
61
+ if (identity.appGuid === "") {
62
+ return failManifest("<App> element missing Id attribute");
63
+ }
64
+
65
+ const dependencies: ManifestDependency[] = [];
66
+ for (const m of xml.matchAll(/<Dependency\b[^>]*>/gi)) {
67
+ const tag = m[0];
68
+ dependencies.push({
69
+ appGuid: readTagAttr(tag, "Id"),
70
+ name: readTagAttr(tag, "Name"),
71
+ publisher: readTagAttr(tag, "Publisher"),
72
+ minVersion: readTagAttr(tag, "MinVersion"),
73
+ });
74
+ }
75
+
76
+ const includesSource = /IncludeSourceInSymbolFile\s*=\s*"true"/i.test(xml);
77
+
78
+ return { identity, dependencies, includesSource };
79
+ }
80
+
81
+ /** Read and parse NavxManifest.xml from a .app on disk. Never throws. */
82
+ export function readAppManifest(appPath: string): AppManifest {
83
+ let xml: string;
84
+ try {
85
+ const bytes = new Uint8Array(readFileSync(appPath));
86
+ const entries = filteredUnzip(bytes, (name) => name.toLowerCase().endsWith("navxmanifest.xml"));
87
+ const key = Object.keys(entries)[0];
88
+ if (key === undefined) {
89
+ return failManifest("NavxManifest.xml not found in package");
90
+ }
91
+ xml = new TextDecoder("utf-8").decode(entries[key]);
92
+ } catch (err) {
93
+ return failManifest(`could not read package: ${(err as Error).message}`);
94
+ }
95
+ return parseAppManifestXml(xml);
96
+ }
@@ -0,0 +1,50 @@
1
+ import { type Unzipped, unzipSync } from "fflate";
2
+
3
+ /** BC .app files may carry a binary header before the ZIP. The ZIP starts at PK\x03\x04. */
4
+ export function stripAppHeader(bytes: Uint8Array): Uint8Array {
5
+ const limit = Math.min(bytes.length - 4, 4096);
6
+ for (let i = 0; i < limit; i++) {
7
+ if (
8
+ bytes[i] === 0x50 &&
9
+ bytes[i + 1] === 0x4b &&
10
+ bytes[i + 2] === 0x03 &&
11
+ bytes[i + 3] === 0x04
12
+ ) {
13
+ return i === 0 ? bytes : bytes.subarray(i);
14
+ }
15
+ }
16
+ return bytes; // assume it is already a plain ZIP
17
+ }
18
+
19
+ /** Normalise a ZIP entry key: backslashes -> forward slashes. */
20
+ export function normalizeZipEntryName(key: string): string {
21
+ return key.replace(/\\/g, "/");
22
+ }
23
+
24
+ /**
25
+ * Unzip only the entries `accept` returns true for. The `.app` header is stripped first.
26
+ * Keys in the returned map are normalised to forward slashes.
27
+ */
28
+ export function filteredUnzip(appBytes: Uint8Array, accept: (name: string) => boolean): Unzipped {
29
+ const zip = stripAppHeader(appBytes);
30
+ const raw = unzipSync(zip, { filter: (f) => accept(normalizeZipEntryName(f.name)) });
31
+ const out: Unzipped = {};
32
+ for (const [k, v] of Object.entries(raw)) out[normalizeZipEntryName(k)] = v;
33
+ return out;
34
+ }
35
+
36
+ /**
37
+ * Enumerate every ZIP entry name without decompressing anything — the `filter` callback
38
+ * sees every entry's metadata and we always return false.
39
+ */
40
+ export function listZipEntryNames(appBytes: Uint8Array): string[] {
41
+ const zip = stripAppHeader(appBytes);
42
+ const names: string[] = [];
43
+ unzipSync(zip, {
44
+ filter: (f) => {
45
+ names.push(normalizeZipEntryName(f.name));
46
+ return false;
47
+ },
48
+ });
49
+ return names;
50
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { filteredUnzip } from "./app-package-zip.ts";
3
+
4
+ export interface EmbeddedSourceFile {
5
+ /** Forward-slash relative path inside the package, e.g. "src/Foo.Codeunit.al". URL-escaped form kept as-is. */
6
+ relativePath: string;
7
+ content: string;
8
+ }
9
+
10
+ /**
11
+ * Async-iterate the embedded .al files of a .app, sorted by relative path.
12
+ *
13
+ * Decompresses every .al entry in ONE pass over the ZIP central directory, then yields
14
+ * them one at a time. The previous implementation called `filteredUnzip` per file, which
15
+ * re-parsed the ZIP central directory once per entry — on Microsoft Base Application
16
+ * (7,634 .al files in a ~41 MB compressed package) that was the dominant cost of the
17
+ * cold-build loop (~40 s of pure ZIP overhead, not accounted for in any per-phase timer).
18
+ *
19
+ * Peak retained memory still tracks "one file in transit at a time": each entry is
20
+ * removed from the entries map as it is yielded, so the GC can reclaim its bytes as
21
+ * the consumer advances.
22
+ */
23
+ export async function* iterateEmbeddedSourceBytes(
24
+ appBytes: Uint8Array,
25
+ ): AsyncIterable<EmbeddedSourceFile> {
26
+ const entries = filteredUnzip(appBytes, (n) => n.toLowerCase().endsWith(".al"));
27
+ const names = Object.keys(entries).sort();
28
+ const decoder = new TextDecoder("utf-8");
29
+ for (const name of names) {
30
+ const bytes = entries[name];
31
+ if (bytes === undefined) continue; // entry vanished — defensive, should not happen
32
+ delete entries[name]; // free the buffer once we've handed it to the consumer
33
+ yield { relativePath: name, content: decoder.decode(bytes) };
34
+ }
35
+ }
36
+
37
+ /** Disk-path convenience: read the .app once, then async-iterate its embedded .al files. */
38
+ export async function* iterateEmbeddedSource(appPath: string): AsyncIterable<EmbeddedSourceFile> {
39
+ const bytes = new Uint8Array(readFileSync(appPath));
40
+ yield* iterateEmbeddedSourceBytes(bytes);
41
+ }
@@ -0,0 +1,81 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { sha256OfStrings } from "../hash.ts";
4
+ import { parseAppManifestXml } from "./app-manifest.ts";
5
+ import { filteredUnzip } from "./app-package-zip.ts";
6
+
7
+ /** Raw-byte sha256 of a .app's bytes — provenance only. */
8
+ export function packageHashOfBytes(bytes: Uint8Array): string {
9
+ return createHash("sha256").update(bytes).digest("hex");
10
+ }
11
+
12
+ /** Disk-path convenience. */
13
+ export function packageHash(appPath: string): string {
14
+ return packageHashOfBytes(new Uint8Array(readFileSync(appPath)));
15
+ }
16
+
17
+ const sha256 = (b: Uint8Array): string => createHash("sha256").update(b).digest("hex");
18
+
19
+ /**
20
+ * Semantic content hash of a .app — the cache-key input. Hashes only what affects analysis:
21
+ * normalized manifest identity + declared dependency constraints, the SymbolReference.json
22
+ * content, and every embedded .al's content hash (sorted by relative path). Deliberately
23
+ * ignores ZIP metadata, entry order, timestamps, signing, and the .app binary header — so a
24
+ * re-download of the same release reuses the cache.
25
+ *
26
+ * Single-pass ZIP extraction: every relevant entry (manifest + symbol reference + every
27
+ * .al) is decompressed in ONE `filteredUnzip` call. The previous implementation made
28
+ * one `filteredUnzip` call PER .al entry plus separate calls for manifest and symbol
29
+ * reference — on Base Application (7,634 .al files) that re-parsed the ZIP central
30
+ * directory 7,636 times per cache-key computation.
31
+ */
32
+ export function packageSemanticHashOfBytes(bytes: Uint8Array): string {
33
+ const entries = filteredUnzip(bytes, (name) => {
34
+ const lower = name.toLowerCase();
35
+ return (
36
+ lower.endsWith(".al") ||
37
+ lower.endsWith("navxmanifest.xml") ||
38
+ lower.endsWith("symbolreference.json")
39
+ );
40
+ });
41
+
42
+ // 1. manifest identity + declared dependency constraints
43
+ const manifestKey = Object.keys(entries).find((k) =>
44
+ k.toLowerCase().endsWith("navxmanifest.xml"),
45
+ );
46
+ const manifestXml =
47
+ manifestKey === undefined ? "" : new TextDecoder("utf-8").decode(entries[manifestKey]);
48
+ const manifest = parseAppManifestXml(manifestXml);
49
+ const manifestPart = sha256OfStrings([
50
+ manifest.identity.appGuid,
51
+ manifest.identity.name,
52
+ manifest.identity.publisher,
53
+ manifest.identity.version,
54
+ ...manifest.dependencies
55
+ .map((d) => `${d.appGuid}|${d.publisher}|${d.name}|${d.minVersion}`)
56
+ .sort(),
57
+ ]);
58
+
59
+ // 2. SymbolReference.json content
60
+ const symKey = Object.keys(entries).find((k) => k.toLowerCase().endsWith("symbolreference.json"));
61
+ const symPart = symKey === undefined ? "" : sha256(entries[symKey] ?? new Uint8Array());
62
+
63
+ // 3. embedded .al content hashes, sorted by relative path
64
+ const alNames = Object.keys(entries)
65
+ .filter((n) => n.toLowerCase().endsWith(".al"))
66
+ .sort();
67
+ const alParts: string[] = [];
68
+ for (const name of alNames) {
69
+ const b = entries[name];
70
+ if (b !== undefined) {
71
+ alParts.push(`${name}:${sha256(b)}`);
72
+ }
73
+ }
74
+
75
+ return sha256OfStrings(["pkgSemantic/v1", manifestPart, symPart, ...alParts]);
76
+ }
77
+
78
+ /** Disk-path convenience. */
79
+ export function packageSemanticHash(appPath: string): string {
80
+ return packageSemanticHashOfBytes(new Uint8Array(readFileSync(appPath)));
81
+ }