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,91 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { basename, join, relative } from "node:path";
4
+ import type { SemanticModel } from "../../model/model.ts";
5
+ import type { SnapshotInput } from "../types.ts";
6
+
7
+ /**
8
+ * Walk the workspace + .alpackages directory, fingerprint each input file
9
+ * that contributes to snapshot identity:
10
+ * - app.json → kind:"app-json"
11
+ * - .alpackages/*.app → kind:"dep-package"
12
+ * - roots.config.json (if loaded) → kind:"roots-config"
13
+ * - al-sem.coverage.yaml (if present) → kind:"policy"
14
+ *
15
+ * The `roots-config` entry is sourced from `model.identity.rootsConfig`
16
+ * rather than re-read from disk. `analyzeWorkspace` is the sole loader and
17
+ * sets that field only when the config was actually consulted — so
18
+ * `analyzeWorkspace({ noRootsConfig: true })` keeps the file out of the
19
+ * fingerprint even when it exists on disk. This is what lets
20
+ * `--no-roots-config` produce a different `workspaceFingerprint` from a
21
+ * baseline run, so diff tools can tell the two states apart.
22
+ *
23
+ * Paths relative to workspaceDir, forward-slash-normalised. Output sorted by
24
+ * (kind, path).
25
+ */
26
+ export function deriveInputs(model: SemanticModel, workspaceDir: string): SnapshotInput[] {
27
+ const out: SnapshotInput[] = [];
28
+
29
+ // Standalone .app subject (snapshot-from-.app): the .app file IS the sole input,
30
+ // fingerprinted by its raw bytes so distinct .apps get distinct workspaceFingerprints.
31
+ if (
32
+ workspaceDir.toLowerCase().endsWith(".app") &&
33
+ existsSync(workspaceDir) &&
34
+ statSync(workspaceDir).isFile()
35
+ ) {
36
+ return [
37
+ { kind: "app-package", path: basename(workspaceDir), contentHash: hashFile(workspaceDir) },
38
+ ];
39
+ }
40
+
41
+ const appJson = join(workspaceDir, "app.json");
42
+ if (existsSync(appJson)) {
43
+ out.push({
44
+ kind: "app-json",
45
+ path: rel(workspaceDir, appJson),
46
+ contentHash: hashFile(appJson),
47
+ });
48
+ }
49
+
50
+ const alpackages = join(workspaceDir, ".alpackages");
51
+ if (existsSync(alpackages) && statSync(alpackages).isDirectory()) {
52
+ for (const name of readdirSync(alpackages)) {
53
+ if (!name.endsWith(".app")) continue;
54
+ const p = join(alpackages, name);
55
+ out.push({ kind: "dep-package", path: rel(workspaceDir, p), contentHash: hashFile(p) });
56
+ }
57
+ }
58
+
59
+ // roots.config.json: read identity rather than re-hash from disk. This
60
+ // lets `analyzeWorkspace({ noRootsConfig: true })` keep the file out of
61
+ // the fingerprint even when the file exists on disk. `analyzeWorkspace`
62
+ // sets `model.identity.rootsConfig` only when the config was actually
63
+ // loaded.
64
+ if (model.identity.rootsConfig !== undefined) {
65
+ out.push({
66
+ kind: "roots-config",
67
+ path: rel(workspaceDir, model.identity.rootsConfig.path),
68
+ contentHash: model.identity.rootsConfig.contentHash,
69
+ });
70
+ }
71
+
72
+ const coveragePolicy = join(workspaceDir, "al-sem.coverage.yaml");
73
+ if (existsSync(coveragePolicy)) {
74
+ out.push({
75
+ kind: "policy",
76
+ path: rel(workspaceDir, coveragePolicy),
77
+ contentHash: hashFile(coveragePolicy),
78
+ });
79
+ }
80
+
81
+ out.sort((a, b) => `${a.kind}|${a.path}`.localeCompare(`${b.kind}|${b.path}`));
82
+ return out;
83
+ }
84
+
85
+ function hashFile(p: string): string {
86
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
87
+ }
88
+
89
+ function rel(from: string, p: string): string {
90
+ return relative(from, p).split("\\").join("/");
91
+ }
@@ -0,0 +1,70 @@
1
+ import type { SemanticModel } from "../../model/model.ts";
2
+ import { createIdentityIndex } from "../../model/stable-identity.ts";
3
+ import type { StableRoutineId } from "../../model/stable-identity.ts";
4
+ import type { OperationEvidence, SnapshotIdentityTable } from "../types.ts";
5
+
6
+ /**
7
+ * Emit OperationEvidence for every OperationSite + RecordOperation that
8
+ * appears as the witnessOperationId of a CapabilityFact (direct or inherited).
9
+ *
10
+ * Sort by operationId.
11
+ */
12
+ export function deriveOperationEvidence(
13
+ model: SemanticModel,
14
+ _idx: SnapshotIdentityTable,
15
+ ): OperationEvidence[] {
16
+ const idCvt = createIdentityIndex();
17
+ const referenced = new Set<string>();
18
+ for (const r of model.routines ?? []) {
19
+ const summary = r.summary;
20
+ if (summary === undefined) continue;
21
+ for (const f of [
22
+ ...(summary.capabilityFactsDirect ?? []),
23
+ ...(summary.capabilityFactsInherited ?? []),
24
+ ]) {
25
+ if (f.witnessOperationId !== undefined) {
26
+ referenced.add(String(f.witnessOperationId));
27
+ }
28
+ }
29
+ }
30
+
31
+ const out: OperationEvidence[] = [];
32
+ for (const r of model.routines ?? []) {
33
+ const stableObject = idCvt.toStableObjectId(r.objectId);
34
+ const stableRoutine = idCvt.toStableRoutineIdFromParts(
35
+ stableObject,
36
+ r.canonical?.normalizedSignatureHash ?? "",
37
+ ) as StableRoutineId;
38
+
39
+ for (const op of r.features?.operationSites ?? []) {
40
+ if (!referenced.has(String(op.id))) continue;
41
+ const a = op.sourceAnchor;
42
+ out.push({
43
+ operationId: op.id,
44
+ routine: stableRoutine,
45
+ sourceFile: a.sourceUnitId,
46
+ startLine: a.range.startLine,
47
+ startColumn: a.range.startColumn,
48
+ endLine: a.range.endLine,
49
+ endColumn: a.range.endColumn,
50
+ displayText: op.kind,
51
+ });
52
+ }
53
+ for (const ro of r.features?.recordOperations ?? []) {
54
+ if (!referenced.has(String(ro.id))) continue;
55
+ const a = ro.sourceAnchor;
56
+ out.push({
57
+ operationId: ro.id,
58
+ routine: stableRoutine,
59
+ sourceFile: a.sourceUnitId,
60
+ startLine: a.range.startLine,
61
+ startColumn: a.range.startColumn,
62
+ endLine: a.range.endLine,
63
+ endColumn: a.range.endColumn,
64
+ displayText: `${ro.recordVariableName ?? "?"}.${ro.op}`,
65
+ });
66
+ }
67
+ }
68
+ out.sort((a, b) => String(a.operationId).localeCompare(String(b.operationId)));
69
+ return out;
70
+ }
@@ -0,0 +1,186 @@
1
+ // src/snapshot/derive/permissions.ts
2
+ //
3
+ // Two halves merged into one PermissionFact[]:
4
+ //
5
+ // (a) DeclaredPermissionFact[] from model.permissionSets when present.
6
+ // Phase 0c ships best-effort: the model does not expose permissionSets
7
+ // yet. Phase 4 wires PermissionSet projection.
8
+ //
9
+ // (b) RequiredPermissionFact[] derived per spec §3.8:
10
+ // read|insert|modify|delete on table T → R|I|M|D on TableData T
11
+ // execute on codeunit|page|report O → X on O
12
+ // Only facts whose resourceId is resolved (not undefined or null) are emitted —
13
+ // unresolved capability facts carry no stable target and cannot produce
14
+ // a meaningful permission entry.
15
+ //
16
+ // Each required fact carries the coverage status of its source routine's
17
+ // inherited cone (G6 enforcement). Output sorted canonically.
18
+
19
+ import type { CapabilityOp } from "../../model/capability.ts";
20
+ import type { SemanticModel } from "../../model/model.ts";
21
+ import type {
22
+ PermissionFact,
23
+ PermissionRight,
24
+ RequiredPermissionFact,
25
+ } from "../../model/permission.ts";
26
+ import { createIdentityIndex } from "../../model/stable-identity.ts";
27
+ import type {
28
+ StableObjectId,
29
+ StableRoutineId,
30
+ StableTableId,
31
+ } from "../../model/stable-identity.ts";
32
+ import type { SnapshotIdentityTable } from "../types.ts";
33
+
34
+ const TABLE_OP_TO_RIGHT: Partial<Record<CapabilityOp, PermissionRight>> = {
35
+ read: "R",
36
+ insert: "I",
37
+ modify: "M",
38
+ delete: "D",
39
+ };
40
+
41
+ /**
42
+ * Derive the full PermissionFact[] for the snapshot.
43
+ *
44
+ * Two halves merged into one array:
45
+ *
46
+ * (a) DeclaredPermissionFact[] from PermissionSet objects — minimal in
47
+ * Phase 0c (the model does not project permissionSets yet; Phase 4
48
+ * enriches this branch).
49
+ *
50
+ * (b) RequiredPermissionFact[] derived per spec §3.8:
51
+ * read|insert|modify|delete on table T → R|I|M|D on TableData T
52
+ * execute on codeunit|page|report O → X on O
53
+ *
54
+ * Each required perm carries coverage status — G6 enforcement: callers
55
+ * see "may be incomplete" when the cone is partial. Output sorted
56
+ * canonically by (kind, subject/set, target, rights).
57
+ *
58
+ * `idx` is the SnapshotIdentityTable produced earlier; not consulted
59
+ * directly (id rewriting goes through createIdentityIndex), but accepting
60
+ * it keeps the deriver signature uniform with the rest of the family.
61
+ */
62
+ export function derivePermissions(
63
+ model: SemanticModel,
64
+ _idx: SnapshotIdentityTable,
65
+ ): PermissionFact[] {
66
+ const idCvt = createIdentityIndex();
67
+ const out: PermissionFact[] = [];
68
+
69
+ // (a) Declared — only emit if the model exposes permissionSets.
70
+ // Phase 4 wires PermissionSet projection; Phase 0c is best-effort.
71
+ const ps = (model as unknown as { permissionSets?: unknown[] }).permissionSets;
72
+ if (Array.isArray(ps)) {
73
+ for (const item of ps) {
74
+ const entry = item as Record<string, unknown>;
75
+ if (
76
+ typeof entry.permissionSet === "string" &&
77
+ typeof entry.target === "string" &&
78
+ typeof entry.targetKind === "string" &&
79
+ Array.isArray(entry.rights) &&
80
+ typeof entry.scope === "string"
81
+ ) {
82
+ out.push({
83
+ kind: "declared",
84
+ permissionSet: entry.permissionSet as never,
85
+ target: entry.target as never,
86
+ targetKind: entry.targetKind as never,
87
+ rights: entry.rights as PermissionRight[],
88
+ scope: entry.scope as "Inherent" | "Assignable",
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ // (b) Required — one entry per (routine, target, right) triple.
95
+ for (const r of model.routines ?? []) {
96
+ const summary = r.summary;
97
+ if (summary === undefined) continue;
98
+
99
+ const stableObject = idCvt.toStableObjectId(r.objectId);
100
+ const stableSubject = idCvt.toStableRoutineIdFromParts(
101
+ stableObject,
102
+ r.canonical.normalizedSignatureHash,
103
+ ) as StableRoutineId;
104
+
105
+ // Coverage: prefer inheritedStatus (whole cone), fall back to
106
+ // directStatus, then "unknown".
107
+ const coverage =
108
+ summary.coverage?.inheritedStatus ?? summary.coverage?.directStatus ?? "unknown";
109
+
110
+ const facts = [
111
+ ...(summary.capabilityFactsDirect ?? []),
112
+ ...(summary.capabilityFactsInherited ?? []),
113
+ ];
114
+
115
+ // Deduplicate within this routine's contribution.
116
+ const seen = new Set<string>();
117
+
118
+ for (const f of facts) {
119
+ if (f.resourceKind === "table" && f.resourceId != null) {
120
+ const right = TABLE_OP_TO_RIGHT[f.op];
121
+ if (right === undefined) continue;
122
+ const stableTable = idCvt.toStableTableId(f.resourceId as string);
123
+ const key = `${stableSubject}|${stableTable}|${right}`;
124
+ if (seen.has(key)) continue;
125
+ seen.add(key);
126
+ const req: RequiredPermissionFact = {
127
+ kind: "required",
128
+ subject: stableSubject,
129
+ target: stableTable as StableTableId,
130
+ targetKind: "TableData",
131
+ rights: [right],
132
+ derivedFromCapability: {
133
+ op: f.op,
134
+ ...(f.witnessCallsiteId !== undefined
135
+ ? { witnessCallsiteId: f.witnessCallsiteId }
136
+ : {}),
137
+ },
138
+ coverage,
139
+ };
140
+ out.push(req);
141
+ } else if (
142
+ f.op === "execute" &&
143
+ f.resourceId != null &&
144
+ (f.resourceKind === "codeunit" || f.resourceKind === "page" || f.resourceKind === "report")
145
+ ) {
146
+ const stableObj = idCvt.toStableObjectId(f.resourceId as string);
147
+ const key = `${stableSubject}|${stableObj}|X`;
148
+ if (seen.has(key)) continue;
149
+ seen.add(key);
150
+ const req: RequiredPermissionFact = {
151
+ kind: "required",
152
+ subject: stableSubject,
153
+ target: stableObj as StableObjectId,
154
+ targetKind: capitaliseObjectKind(f.resourceKind),
155
+ rights: ["X"],
156
+ derivedFromCapability: {
157
+ op: f.op,
158
+ ...(f.witnessCallsiteId !== undefined
159
+ ? { witnessCallsiteId: f.witnessCallsiteId }
160
+ : {}),
161
+ },
162
+ coverage,
163
+ };
164
+ out.push(req);
165
+ }
166
+ }
167
+ }
168
+
169
+ out.sort((a, b) => permKey(a).localeCompare(permKey(b)));
170
+ return out;
171
+ }
172
+
173
+ function capitaliseObjectKind(k: "codeunit" | "page" | "report"): "Codeunit" | "Page" | "Report" {
174
+ if (k === "codeunit") return "Codeunit";
175
+ if (k === "page") return "Page";
176
+ return "Report";
177
+ }
178
+
179
+ function permKey(p: PermissionFact): string {
180
+ if (p.kind === "declared") {
181
+ return ["D", String(p.permissionSet), String(p.target), p.targetKind, p.rights.join("")].join(
182
+ "|",
183
+ );
184
+ }
185
+ return ["R", String(p.subject), String(p.target), p.targetKind, p.rights.join("")].join("|");
186
+ }
@@ -0,0 +1,56 @@
1
+ import type { SemanticModel } from "../../model/model.ts";
2
+ import { createIdentityIndex } from "../../model/stable-identity.ts";
3
+ import type { StableRoutineId } from "../../model/stable-identity.ts";
4
+ import type { RootClassificationSlot, SnapshotIdentityTable } from "../types.ts";
5
+
6
+ /**
7
+ * Project `model.rootClassifications` (Phase 1 §4.3 classifier output) into
8
+ * the snapshot's `RootClassificationSlot[]` shape. Only the `routineId` field
9
+ * is rewritten (internal `RoutineId` → `StableRoutineId`); all provenance
10
+ * fields (`source`, `confidence`, `sourceAnchor`, `configEntryId`,
11
+ * `resolutionStatus`) pass through verbatim.
12
+ *
13
+ * Routines that don't appear in `model.routines` are silently dropped — they
14
+ * shouldn't exist in practice (the classifier only emits for known routines),
15
+ * but the engine-never-throws contract is preserved either way.
16
+ *
17
+ * Output is sorted by `StableRoutineId` for determinism, matching every other
18
+ * deriver in `src/snapshot/derive/`.
19
+ */
20
+ export function deriveRootClassifications(
21
+ model: SemanticModel,
22
+ _idx: SnapshotIdentityTable,
23
+ ): RootClassificationSlot[] {
24
+ const idCvt = createIdentityIndex();
25
+
26
+ // Build internal RoutineId → StableRoutineId lookup once.
27
+ const routineToStable = new Map<string, StableRoutineId>();
28
+ for (const r of model.routines ?? []) {
29
+ const stableObject = idCvt.toStableObjectId(r.objectId);
30
+ const stable = idCvt.toStableRoutineIdFromParts(
31
+ stableObject,
32
+ r.canonical?.normalizedSignatureHash ?? "",
33
+ ) as StableRoutineId;
34
+ routineToStable.set(String(r.id), stable);
35
+ }
36
+
37
+ const out: RootClassificationSlot[] = [];
38
+ for (const c of model.rootClassifications ?? []) {
39
+ const stable = routineToStable.get(String(c.routineId));
40
+ if (stable === undefined) continue;
41
+ const slot: RootClassificationSlot = {
42
+ routineId: stable,
43
+ kinds: [...c.kinds],
44
+ externallyReachable: c.externallyReachable,
45
+ source: c.source,
46
+ confidence: c.confidence,
47
+ };
48
+ if (c.sourceAnchor !== undefined) slot.sourceAnchor = c.sourceAnchor;
49
+ if (c.configEntryId !== undefined) slot.configEntryId = c.configEntryId;
50
+ if (c.resolutionStatus !== undefined) slot.resolutionStatus = c.resolutionStatus;
51
+ out.push(slot);
52
+ }
53
+
54
+ out.sort((a, b) => String(a.routineId).localeCompare(String(b.routineId)));
55
+ return out;
56
+ }
@@ -0,0 +1,130 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Field, Key, Table } from "../../model/entities.ts";
3
+ import type { SemanticModel } from "../../model/model.ts";
4
+ import { createIdentityIndex } from "../../model/stable-identity.ts";
5
+ import type { SchemaFact, SnapshotIdentityTable } from "../types.ts";
6
+
7
+ /**
8
+ * Project every workspace table + its fields + keys to SchemaFact[].
9
+ *
10
+ * Enum + enum-value projections are deferred to Phase 1+ schema work — the
11
+ * model does not expose enum members at L2 in the current pipeline.
12
+ *
13
+ * shapeFingerprint = SHA-256(canonical-json(shape)):
14
+ * Table shape: { number, name }
15
+ * Field shape: { dataType, fieldClass, isBlobLike }
16
+ * Key shape: { fields: StableFieldId[], isEnabled }
17
+ * where fields are the stable field ids (appGuid:Table:N#fieldNum) sorted
18
+ * to make the fingerprint position-independent within the field list.
19
+ *
20
+ * stableId formats (from createIdentityIndex):
21
+ * Table: `${appGuid}:Table:${tableNumber}`
22
+ * Field: `${stableTableId}#${fieldNumber}`
23
+ * Key: `${stableTableId}#K${keyIndex}` (index from the Key.id suffix)
24
+ *
25
+ * Output sorted by stableId for determinism.
26
+ */
27
+ export function deriveSchema(model: SemanticModel, _idx: SnapshotIdentityTable): SchemaFact[] {
28
+ const idCvt = createIdentityIndex();
29
+ const out: SchemaFact[] = [];
30
+
31
+ for (const tbl of model.tables ?? []) {
32
+ const stableTable = idCvt.toStableTableId(tbl.id);
33
+ out.push(makeTableFact(tbl, stableTable));
34
+
35
+ for (const fld of tbl.fields) {
36
+ const stableField = idCvt.toStableFieldId(fld.id);
37
+ out.push(makeFieldFact(fld, stableField, idCvt));
38
+ }
39
+
40
+ for (const key of tbl.keys) {
41
+ const stableKey = stableKeyId(key.id, stableTable);
42
+ out.push(makeKeyFact(key, stableKey, idCvt));
43
+ }
44
+ }
45
+
46
+ out.sort((a, b) => a.stableId.localeCompare(b.stableId));
47
+ return out;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Per-entity fact builders
52
+ // ---------------------------------------------------------------------------
53
+
54
+ function makeTableFact(tbl: Table, stableId: string): SchemaFact {
55
+ return {
56
+ kind: "table",
57
+ stableId,
58
+ shapeFingerprint: sha256(canonicalJson({ number: tbl.tableNumber, name: tbl.name })),
59
+ };
60
+ }
61
+
62
+ function makeFieldFact(
63
+ fld: Field,
64
+ stableId: string,
65
+ _idCvt: ReturnType<typeof createIdentityIndex>,
66
+ ): SchemaFact {
67
+ return {
68
+ kind: "field",
69
+ stableId,
70
+ shapeFingerprint: sha256(
71
+ canonicalJson({
72
+ dataType: fld.dataType,
73
+ fieldClass: fld.fieldClass,
74
+ isBlobLike: fld.isBlobLike,
75
+ }),
76
+ ),
77
+ };
78
+ }
79
+
80
+ function makeKeyFact(
81
+ key: Key,
82
+ stableId: string,
83
+ idCvt: ReturnType<typeof createIdentityIndex>,
84
+ ): SchemaFact {
85
+ // Convert internal FieldIds to stable ids for cross-version stability.
86
+ const stableFields = key.fields.map((f) => idCvt.toStableFieldId(f)).sort();
87
+ return {
88
+ kind: "key",
89
+ stableId,
90
+ shapeFingerprint: sha256(
91
+ canonicalJson({
92
+ fields: stableFields,
93
+ isEnabled: key.isEnabled ?? true,
94
+ }),
95
+ ),
96
+ };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Key stable-id derivation
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * Derive a stable key id from the raw KeyId.
105
+ * KeyId format: `${tableId}/key/${index}` (from encodeKeyId in model/ids.ts).
106
+ * We strip the table prefix and replace the `/key/` segment with `#K` so the
107
+ * result is: `${stableTableId}#K${keyIndex}`.
108
+ */
109
+ function stableKeyId(keyId: string, stableTable: string): string {
110
+ const marker = "/key/";
111
+ const pos = keyId.lastIndexOf(marker);
112
+ const keyIndex = pos >= 0 ? keyId.slice(pos + marker.length) : keyId;
113
+ return `${stableTable}#K${keyIndex}`;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Helpers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function canonicalJson(v: unknown): string {
121
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
122
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
123
+ const o = v as Record<string, unknown>;
124
+ const keys = Object.keys(o).sort();
125
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
126
+ }
127
+
128
+ function sha256(s: string): string {
129
+ return createHash("sha256").update(s, "utf8").digest("hex");
130
+ }
@@ -0,0 +1,60 @@
1
+ // src/snapshot/derive/typed-edges.ts
2
+ //
3
+ // Project model.typedEdges (populated by Phase 0b-β combined-graph) with
4
+ // from/to rewritten from internal RoutineId → StableRoutineId.
5
+ //
6
+ // Every edge in the discriminated union has a `from: RoutineId` field.
7
+ // All except `object-run-unresolved` also have a `to: RoutineId` field.
8
+ // Both are rewritten in-place on a shallow copy of the edge.
9
+ //
10
+ // Sort key: (kind, from, to).
11
+
12
+ import type { GraphEdge } from "../../model/graph-edge.ts";
13
+ import type { SemanticModel } from "../../model/model.ts";
14
+ import { createIdentityIndex } from "../../model/stable-identity.ts";
15
+ import type { SnapshotIdentityTable } from "../types.ts";
16
+
17
+ /**
18
+ * Project model.typedEdges (populated by Phase 0b-β combined-graph) with
19
+ * from/to rewritten internal RoutineId → StableRoutineId.
20
+ *
21
+ * Sort by (kind, from, to).
22
+ */
23
+ export function deriveTypedEdges(model: SemanticModel, _idx: SnapshotIdentityTable): GraphEdge[] {
24
+ const idCvt = createIdentityIndex();
25
+
26
+ // Build a RoutineId → StableRoutineId lookup for every routine in the model.
27
+ const routineToStable = new Map<string, string>();
28
+ for (const r of model.routines ?? []) {
29
+ const stableObject = idCvt.toStableObjectId(r.objectId);
30
+ const stable = idCvt.toStableRoutineIdFromParts(
31
+ stableObject,
32
+ r.canonical?.normalizedSignatureHash ?? "",
33
+ );
34
+ routineToStable.set(r.id as unknown as string, stable);
35
+ }
36
+
37
+ const remap = (id: string): string => routineToStable.get(id) ?? id;
38
+
39
+ const out: GraphEdge[] = [];
40
+ for (const e of model.typedEdges ?? []) {
41
+ // Shallow-copy and rewrite from/to where present.
42
+ const copy = { ...e } as GraphEdge;
43
+ if ("from" in copy && typeof copy.from === "string") {
44
+ (copy as { from: string }).from = remap(copy.from);
45
+ }
46
+ if ("to" in copy && typeof copy.to === "string") {
47
+ (copy as { to: string }).to = remap(copy.to);
48
+ }
49
+ out.push(copy);
50
+ }
51
+
52
+ out.sort((a, b) => edgeKey(a).localeCompare(edgeKey(b)));
53
+ return out;
54
+ }
55
+
56
+ function edgeKey(e: GraphEdge): string {
57
+ const from = "from" in e ? String(e.from ?? "") : "";
58
+ const to = "to" in e ? String((e as { to?: unknown }).to ?? "") : "";
59
+ return [e.kind, from, to].join("|");
60
+ }
@@ -0,0 +1,19 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { SnapshotInput } from "../types.ts";
3
+
4
+ /**
5
+ * SHA-256 over the sorted (kind, path, contentHash) triples + alsemVersion.
6
+ * Stable across runs given the same inputs + version; differs when ANY input
7
+ * contentHash or the alsem version changes.
8
+ */
9
+ export function computeWorkspaceFingerprint(
10
+ inputs: readonly SnapshotInput[],
11
+ alsemVersion: string,
12
+ ): string {
13
+ const sorted = [...inputs].sort((a, b) =>
14
+ `${a.kind}|${a.path}`.localeCompare(`${b.kind}|${b.path}`),
15
+ );
16
+ const lines = sorted.map((i) => `${i.kind}\t${i.path}\t${i.contentHash}`);
17
+ lines.push(`alsemVersion\t${alsemVersion}`);
18
+ return createHash("sha256").update(lines.join("\n"), "utf8").digest("hex");
19
+ }
@@ -0,0 +1,40 @@
1
+ import { decode as cborDecode } from "cbor-x";
2
+ import { type CapabilitySnapshot, SNAPSHOT_SCHEMA_VERSION, type SnapshotFormat } from "./types.ts";
3
+
4
+ /**
5
+ * Deserialize a snapshot from bytes. Auto-detects format unless `formatHint`
6
+ * is provided:
7
+ * - first byte 0x7b ('{') → JSON
8
+ * - first two bytes 0x1f 0x8b → gzip → un-gzip → CBOR
9
+ * - otherwise → CBOR
10
+ *
11
+ * Asserts schemaVersion. Phase 0c only accepts 1; future versions add
12
+ * migration shims here.
13
+ */
14
+ export function deserializeSnapshot(
15
+ bytes: Uint8Array,
16
+ formatHint?: SnapshotFormat,
17
+ ): CapabilitySnapshot {
18
+ const fmt = formatHint ?? detectFormat(bytes);
19
+ let parsed: unknown;
20
+ if (fmt === "json") {
21
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
22
+ } else if (fmt === "cbor.gz") {
23
+ parsed = cborDecode(Bun.gunzipSync(bytes as unknown as ArrayBuffer));
24
+ } else {
25
+ parsed = cborDecode(bytes);
26
+ }
27
+ const snap = parsed as CapabilitySnapshot;
28
+ if (snap.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
29
+ throw new Error(
30
+ `deserializeSnapshot: unknown schemaVersion ${snap.schemaVersion} (this build only handles ${SNAPSHOT_SCHEMA_VERSION})`,
31
+ );
32
+ }
33
+ return snap;
34
+ }
35
+
36
+ function detectFormat(bytes: Uint8Array): SnapshotFormat {
37
+ if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) return "cbor.gz";
38
+ if (bytes.length >= 1 && bytes[0] === 0x7b) return "json";
39
+ return "cbor";
40
+ }
@@ -0,0 +1,12 @@
1
+ import { serializeCbor } from "./serialize-cbor.ts";
2
+ import type { CapabilitySnapshot } from "./types.ts";
3
+
4
+ /**
5
+ * CBOR + gzip serializer. Wraps the CBOR output with Bun's built-in
6
+ * gzipSync. Byte-stable; smaller than uncompressed CBOR; starts with
7
+ * the gzip magic bytes 0x1f 0x8b used by the deserializer auto-detect.
8
+ */
9
+ export function serializeCborGz(snapshot: CapabilitySnapshot): Uint8Array {
10
+ const cbor = serializeCbor(snapshot);
11
+ return Bun.gzipSync(cbor as unknown as ArrayBuffer) as Uint8Array;
12
+ }
@@ -0,0 +1,19 @@
1
+ import { Encoder } from "cbor-x";
2
+ import type { CapabilitySnapshot } from "./types.ts";
3
+
4
+ // Pinned encoder options for deterministic output.
5
+ const encoder = new Encoder({
6
+ useRecords: false,
7
+ mapsAsObjects: true,
8
+ pack: false,
9
+ });
10
+
11
+ /**
12
+ * CBOR serializer. Returns a Uint8Array.
13
+ *
14
+ * Deterministic: encoder options pinned; cbor-x's tag tables and key order
15
+ * are stable for plain JS objects. Round-trip tested in Task 18 deserializer.
16
+ */
17
+ export function serializeCbor(snapshot: CapabilitySnapshot): Uint8Array {
18
+ return encoder.encode(snapshot);
19
+ }