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,104 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ export interface RenameOverlay {
5
+ readonly [oldId: string]: string;
6
+ }
7
+
8
+ export interface RenameEntry {
9
+ newId: string;
10
+ applied: boolean;
11
+ }
12
+
13
+ export type RenameTable = Map<string, RenameEntry>;
14
+
15
+ export type RenameDiagnostic =
16
+ | { kind: "rename-overlay-chain"; from: string; via: string; to: string }
17
+ | { kind: "rename-overlay-overlap"; targets: readonly string[] }
18
+ | { kind: "rename-overlay-stale"; staleId: string; reason: "not-in-old" | "not-in-new" };
19
+
20
+ /** Load + parse the overlay file. Engine-never-throws: parse failures surface
21
+ * as a thrown CliError in the orchestrator, not here. */
22
+ export async function loadRenameOverlay(
23
+ path: string | undefined,
24
+ ): Promise<{ overlay?: RenameOverlay; fingerprint?: string }> {
25
+ if (path === undefined) return { overlay: undefined, fingerprint: undefined };
26
+ const text = await readFile(path, "utf8");
27
+ const overlay = JSON.parse(text) as RenameOverlay;
28
+ return { overlay, fingerprint: overlayFingerprint(overlay) };
29
+ }
30
+
31
+ /** Build a normalized rename table from a raw overlay map. Detects chains and
32
+ * overlaps; stale-id detection (vs actual snapshots) lives in diff-indexes
33
+ * since that's where snapshot membership is known. */
34
+ export function buildRenameTable(overlay: RenameOverlay): {
35
+ table: RenameTable;
36
+ diagnostics: RenameDiagnostic[];
37
+ } {
38
+ const table: RenameTable = new Map();
39
+ const diagnostics: RenameDiagnostic[] = [];
40
+ const newToOlds = new Map<string, string[]>();
41
+
42
+ for (const [oldId, newId] of Object.entries(overlay)) {
43
+ if (oldId === newId) continue; // identity mapping
44
+ table.set(oldId, { newId, applied: true });
45
+ const olds = newToOlds.get(newId) ?? [];
46
+ olds.push(oldId);
47
+ newToOlds.set(newId, olds);
48
+ }
49
+
50
+ // Detect chains: any newId that also appears as an oldId in the overlay.
51
+ for (const [oldId, entry] of table) {
52
+ if (table.has(entry.newId)) {
53
+ const next = table.get(entry.newId);
54
+ if (next !== undefined) {
55
+ diagnostics.push({
56
+ kind: "rename-overlay-chain",
57
+ from: oldId,
58
+ via: entry.newId,
59
+ to: next.newId,
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ // Detect overlaps: any newId with multiple oldIds mapping to it.
66
+ for (const [newId, olds] of newToOlds) {
67
+ if (olds.length > 1) {
68
+ diagnostics.push({
69
+ kind: "rename-overlay-overlap",
70
+ targets: [...olds, newId],
71
+ });
72
+ }
73
+ }
74
+
75
+ return { table, diagnostics };
76
+ }
77
+
78
+ /** Validate the overlay against actual snapshot stable-id sets and emit
79
+ * stale-reference diagnostics. Called by diff-indexes once both snapshots
80
+ * are loaded. */
81
+ export function validateOverlayAgainstSnapshots(
82
+ overlay: RenameOverlay,
83
+ oldStableIds: ReadonlySet<string>,
84
+ newStableIds: ReadonlySet<string>,
85
+ ): RenameDiagnostic[] {
86
+ const diagnostics: RenameDiagnostic[] = [];
87
+ for (const [oldId, newId] of Object.entries(overlay)) {
88
+ if (oldId === newId) continue;
89
+ if (!oldStableIds.has(oldId)) {
90
+ diagnostics.push({ kind: "rename-overlay-stale", staleId: oldId, reason: "not-in-old" });
91
+ }
92
+ if (!newStableIds.has(newId)) {
93
+ diagnostics.push({ kind: "rename-overlay-stale", staleId: newId, reason: "not-in-new" });
94
+ }
95
+ }
96
+ return diagnostics;
97
+ }
98
+
99
+ /** Stable SHA-256(16-hex) fingerprint over a key-sorted JSON serialization. */
100
+ export function overlayFingerprint(overlay: RenameOverlay): string {
101
+ const keys = Object.keys(overlay).sort();
102
+ const canonical = JSON.stringify(keys.map((k) => [k, overlay[k]]));
103
+ return createHash("sha256").update(canonical, "utf8").digest("hex").slice(0, 16);
104
+ }
@@ -0,0 +1,232 @@
1
+ import type { SchemaFact } from "../snapshot/types.ts";
2
+ import type { DiffEngineOptions } from "./diff-abi.ts";
3
+ import { DiffCategory, DiffKind, computeDiffFingerprint } from "./diff-identity.ts";
4
+ import type { DiffIndexes } from "./diff-indexes.ts";
5
+
6
+ export interface SchemaDetails {
7
+ kind:
8
+ | DiffKind.TableFieldRemoved
9
+ | DiffKind.TableFieldTypeNarrowed
10
+ | DiffKind.TableFieldTypeWidened
11
+ | DiffKind.TableFieldDataClassificationTightened
12
+ | DiffKind.TableFieldDataClassificationRelaxed
13
+ | DiffKind.EnumValueRemoved
14
+ | DiffKind.EnumValueRenumbered
15
+ | DiffKind.TableFieldAdded
16
+ | DiffKind.EnumValueAdded;
17
+ oldShapeFingerprint?: string;
18
+ newShapeFingerprint?: string;
19
+ oldDataClassification?: string;
20
+ newDataClassification?: string;
21
+ }
22
+
23
+ export interface SchemaFinding {
24
+ id: string;
25
+ category: DiffCategory.Schema;
26
+ kind: DiffKind;
27
+ severity: "critical" | "high" | "medium" | "low" | "info";
28
+ subject: {
29
+ normalizedStableId: string;
30
+ oldOriginalStableId?: string;
31
+ newStableId?: string;
32
+ displayName: string;
33
+ };
34
+ comparisonCone: readonly string[];
35
+ details: SchemaDetails;
36
+ }
37
+
38
+ const SEVERITY: Partial<Record<DiffKind, SchemaFinding["severity"]>> = {
39
+ [DiffKind.TableFieldRemoved]: "critical",
40
+ [DiffKind.TableFieldTypeNarrowed]: "critical",
41
+ [DiffKind.EnumValueRemoved]: "critical",
42
+ [DiffKind.EnumValueRenumbered]: "critical",
43
+ [DiffKind.TableFieldDataClassificationTightened]: "high",
44
+ [DiffKind.TableFieldTypeWidened]: "low",
45
+ [DiffKind.TableFieldDataClassificationRelaxed]: "info",
46
+ [DiffKind.TableFieldAdded]: "info",
47
+ [DiffKind.EnumValueAdded]: "info",
48
+ };
49
+
50
+ /**
51
+ * DataClassification rank — higher rank = more sensitive.
52
+ * Tightened: new rank > old rank (more sensitive = restrict more access).
53
+ * Relaxed: new rank < old rank (less sensitive = expose more data).
54
+ *
55
+ * AL's DataClassification values in ascending sensitivity order:
56
+ * SystemMetadata < AccountData < OrganizationIdentifiableInformation <
57
+ * EndUserPseudonymousIdentifiers < EndUserIdentifiableInformation < CustomerContent
58
+ */
59
+ const DATA_CLASS_RANK: Record<string, number> = {
60
+ SystemMetadata: 0,
61
+ AccountData: 1,
62
+ OrganizationIdentifiableInformation: 2,
63
+ EndUserPseudonymousIdentifiers: 3,
64
+ EndUserIdentifiableInformation: 4,
65
+ CustomerContent: 5,
66
+ };
67
+
68
+ function makeFinding(
69
+ kind: SchemaDetails["kind"],
70
+ stableId: string,
71
+ indexes: DiffIndexes,
72
+ details: SchemaDetails,
73
+ ): SchemaFinding {
74
+ const origin = indexes.originByNormalized.get(stableId);
75
+ const display =
76
+ indexes.newDisplayByStableId.get(stableId) ??
77
+ indexes.oldDisplayByStableId.get(stableId) ??
78
+ stableId;
79
+ return {
80
+ id: computeDiffFingerprint({
81
+ category: DiffCategory.Schema,
82
+ kind,
83
+ normalizedStableId: stableId,
84
+ }),
85
+ category: DiffCategory.Schema,
86
+ kind,
87
+ severity: SEVERITY[kind] ?? "medium",
88
+ subject: {
89
+ normalizedStableId: stableId,
90
+ oldOriginalStableId: origin?.oldOriginalStableId,
91
+ newStableId: origin?.newStableId,
92
+ displayName: display,
93
+ },
94
+ comparisonCone: [stableId],
95
+ details,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Diff schema facts between two snapshots.
101
+ *
102
+ * SchemaFact shape uses `stableId` as the primary key and `shapeFingerprint`
103
+ * (SHA-256 of the shape) for change detection. Individual field names, types,
104
+ * and ordinals are not stored separately — only the fingerprint is available.
105
+ *
106
+ * As a result:
107
+ * - TableField removed/added: detected by stableId presence.
108
+ * - TableField shape changed: fingerprint differs → emits TableFieldTypeNarrowed
109
+ * (conservative for consumers; we cannot distinguish narrow vs widen without
110
+ * parsing the stored type, which is in the fingerprint hash).
111
+ * - TableFieldDataClassificationTightened/Relaxed: detected via the optional
112
+ * `dataClassification` field when present on both sides.
113
+ * - EnumValue removed/added: detected by stableId presence.
114
+ * - EnumValue renumbered: fingerprint differs (ordinal is part of the shape).
115
+ */
116
+ export function diffSchema(
117
+ _oldSnap: unknown,
118
+ _newSnap: unknown,
119
+ indexes: DiffIndexes,
120
+ _opts: DiffEngineOptions,
121
+ ): SchemaFinding[] {
122
+ const out: SchemaFinding[] = [];
123
+
124
+ // Walk OLD schema facts — detect removals and in-place changes.
125
+ for (const [stableId, oldFacts] of indexes.oldSchemaBySubject) {
126
+ // Each stableId maps to exactly one fact (schema facts are per-entity).
127
+ const oldFact = oldFacts[0] as SchemaFact | undefined;
128
+ if (oldFact === undefined) continue;
129
+
130
+ const newFacts = indexes.newSchemaBySubject.get(stableId);
131
+ const newFact = newFacts?.[0] as SchemaFact | undefined;
132
+
133
+ const oldKind = oldFact.kind;
134
+
135
+ if (newFact === undefined) {
136
+ // Removal.
137
+ if (oldKind === "field") {
138
+ out.push(
139
+ makeFinding(DiffKind.TableFieldRemoved, stableId, indexes, {
140
+ kind: DiffKind.TableFieldRemoved,
141
+ }),
142
+ );
143
+ } else if (oldKind === "enum-value") {
144
+ out.push(
145
+ makeFinding(DiffKind.EnumValueRemoved, stableId, indexes, {
146
+ kind: DiffKind.EnumValueRemoved,
147
+ }),
148
+ );
149
+ }
150
+ // "table", "enum", "key" removals are not tracked by this module.
151
+ continue;
152
+ }
153
+
154
+ // In-place change detection.
155
+ const oldFp = oldFact.shapeFingerprint;
156
+ const newFp = newFact.shapeFingerprint;
157
+
158
+ if (oldKind === "field") {
159
+ if (oldFp !== newFp) {
160
+ // Shape changed — emit conservative TableFieldTypeNarrowed since we
161
+ // cannot distinguish narrow vs widen from the fingerprint alone.
162
+ out.push(
163
+ makeFinding(DiffKind.TableFieldTypeNarrowed, stableId, indexes, {
164
+ kind: DiffKind.TableFieldTypeNarrowed,
165
+ oldShapeFingerprint: oldFp,
166
+ newShapeFingerprint: newFp,
167
+ }),
168
+ );
169
+ }
170
+
171
+ // Data classification change — detectable when the optional field is present.
172
+ const oldDc = oldFact.dataClassification;
173
+ const newDc = newFact.dataClassification;
174
+ if (oldDc !== undefined && newDc !== undefined && oldDc !== newDc) {
175
+ const oldRank = DATA_CLASS_RANK[oldDc] ?? 0;
176
+ const newRank = DATA_CLASS_RANK[newDc] ?? 0;
177
+ if (newRank > oldRank) {
178
+ out.push(
179
+ makeFinding(DiffKind.TableFieldDataClassificationTightened, stableId, indexes, {
180
+ kind: DiffKind.TableFieldDataClassificationTightened,
181
+ oldDataClassification: oldDc,
182
+ newDataClassification: newDc,
183
+ }),
184
+ );
185
+ } else if (newRank < oldRank) {
186
+ out.push(
187
+ makeFinding(DiffKind.TableFieldDataClassificationRelaxed, stableId, indexes, {
188
+ kind: DiffKind.TableFieldDataClassificationRelaxed,
189
+ oldDataClassification: oldDc,
190
+ newDataClassification: newDc,
191
+ }),
192
+ );
193
+ }
194
+ }
195
+ } else if (oldKind === "enum-value") {
196
+ if (oldFp !== newFp) {
197
+ // Enum value shape changed — most likely a renumbering.
198
+ out.push(
199
+ makeFinding(DiffKind.EnumValueRenumbered, stableId, indexes, {
200
+ kind: DiffKind.EnumValueRenumbered,
201
+ oldShapeFingerprint: oldFp,
202
+ newShapeFingerprint: newFp,
203
+ }),
204
+ );
205
+ }
206
+ }
207
+ }
208
+
209
+ // Walk NEW schema facts to find additions.
210
+ for (const [stableId, newFacts] of indexes.newSchemaBySubject) {
211
+ if (indexes.oldSchemaBySubject.has(stableId)) continue;
212
+ const newFact = newFacts[0] as SchemaFact | undefined;
213
+ if (newFact === undefined) continue;
214
+
215
+ const newKind = newFact.kind;
216
+ if (newKind === "field") {
217
+ out.push(
218
+ makeFinding(DiffKind.TableFieldAdded, stableId, indexes, {
219
+ kind: DiffKind.TableFieldAdded,
220
+ }),
221
+ );
222
+ } else if (newKind === "enum-value") {
223
+ out.push(
224
+ makeFinding(DiffKind.EnumValueAdded, stableId, indexes, {
225
+ kind: DiffKind.EnumValueAdded,
226
+ }),
227
+ );
228
+ }
229
+ }
230
+
231
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
232
+ }
@@ -0,0 +1,148 @@
1
+ import type { DiffEngineResult, DiffFinding } from "./diff-engine.ts";
2
+
3
+ export interface DiffRenderOptions {
4
+ format: "human" | "json" | "sarif";
5
+ deterministic: boolean;
6
+ /** Reserved for a future ANSI rendering pass. Currently ignored. */
7
+ color?: boolean;
8
+ verbosity?: "compact" | "full";
9
+ }
10
+
11
+ export function formatDiff(result: DiffEngineResult, opts: DiffRenderOptions): string {
12
+ if (opts.format === "json") return renderJson(result);
13
+ if (opts.format === "sarif") return renderSarif(result);
14
+ return renderHuman(result, opts);
15
+ }
16
+
17
+ function renderHuman(result: DiffEngineResult, opts: DiffRenderOptions): string {
18
+ const lines: string[] = [];
19
+ if (result.findings.length === 0 && result.diagnostics.length === 0) {
20
+ lines.push("No diff findings.");
21
+ return `${lines.join("\n")}\n`;
22
+ }
23
+ if (result.diagnostics.length > 0) {
24
+ lines.push("diagnostics:");
25
+ for (const d of result.diagnostics) {
26
+ lines.push(` ${d.kind}: ${JSON.stringify(d)}`);
27
+ }
28
+ lines.push("");
29
+ }
30
+ if (result.findings.length === 0) {
31
+ return `${lines.join("\n")}\n`;
32
+ }
33
+ const s = result.summary;
34
+ lines.push(
35
+ `Diff: ${result.findings.length} finding(s). critical=${s.findingsBySeverity.critical} high=${s.findingsBySeverity.high} medium=${s.findingsBySeverity.medium} low=${s.findingsBySeverity.low} info=${s.findingsBySeverity.info}`,
36
+ );
37
+ lines.push("");
38
+
39
+ // Group: Contract (abi/schema/events) then Effects (capabilities/permissions).
40
+ const contract = result.findings.filter(
41
+ (f) => f.category === "abi" || f.category === "schema" || f.category === "events",
42
+ );
43
+ const effects = result.findings.filter(
44
+ (f) => f.category === "capabilities" || f.category === "permissions",
45
+ );
46
+ if (contract.length > 0) {
47
+ lines.push("Contract:");
48
+ for (const f of contract) lines.push(formatFinding(f));
49
+ lines.push("");
50
+ }
51
+ if (effects.length > 0) {
52
+ lines.push("Effects:");
53
+ for (const f of effects) lines.push(formatFinding(f));
54
+ lines.push("");
55
+ }
56
+ return `${lines.join("\n")}\n`;
57
+ }
58
+
59
+ function formatFinding(f: DiffFinding): string {
60
+ // coverageState is attached dynamically by the policy pass (not present on the static type).
61
+ const coverageState = (f as { coverageState?: { old: string; new: string } }).coverageState;
62
+ const renameNote =
63
+ f.subject.oldOriginalStableId !== undefined &&
64
+ f.subject.oldOriginalStableId !== f.subject.normalizedStableId
65
+ ? ` (renamed from ${f.subject.oldOriginalStableId})`
66
+ : "";
67
+ const coverageNote =
68
+ coverageState !== undefined &&
69
+ (coverageState.old !== "complete" || coverageState.new !== "complete")
70
+ ? ` [cov old=${coverageState.old} new=${coverageState.new}]`
71
+ : "";
72
+ return ` [${f.severity}] ${f.kind}: ${f.subject.displayName}${renameNote}${coverageNote}`;
73
+ }
74
+
75
+ function renderJson(result: DiffEngineResult): string {
76
+ // Use a stable key order.
77
+ const payload = {
78
+ findings: result.findings,
79
+ diagnostics: result.diagnostics,
80
+ summary: result.summary,
81
+ };
82
+ return `${JSON.stringify(payload, sortReplacer, 2)}\n`;
83
+ }
84
+
85
+ function sortReplacer(_key: string, value: unknown): unknown {
86
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
87
+ const obj = value as Record<string, unknown>;
88
+ const sorted: Record<string, unknown> = {};
89
+ for (const k of Object.keys(obj).sort()) sorted[k] = obj[k];
90
+ return sorted;
91
+ }
92
+ return value;
93
+ }
94
+
95
+ function renderSarif(result: DiffEngineResult): string {
96
+ const rules: Array<{ id: string; name: string; shortDescription: { text: string } }> = [];
97
+ const rulesSeen = new Set<string>();
98
+ const results: Array<Record<string, unknown>> = [];
99
+
100
+ for (const f of result.findings) {
101
+ const ruleId = `${f.category}.${f.kind}`;
102
+ if (!rulesSeen.has(ruleId)) {
103
+ rulesSeen.add(ruleId);
104
+ rules.push({
105
+ id: ruleId,
106
+ name: f.kind,
107
+ shortDescription: { text: `${f.category} ${f.kind}` },
108
+ });
109
+ }
110
+ results.push({
111
+ ruleId,
112
+ level: severityToSarifLevel(f.severity),
113
+ message: { text: `${f.kind}: ${f.subject.displayName}` },
114
+ fingerprints: { default: f.id },
115
+ });
116
+ }
117
+
118
+ const sarif = {
119
+ $schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
120
+ version: "2.1.0",
121
+ runs: [
122
+ {
123
+ tool: {
124
+ driver: {
125
+ name: "al-sem-diff",
126
+ informationUri: "https://github.com/anthropics/al-sem",
127
+ rules,
128
+ },
129
+ },
130
+ results,
131
+ },
132
+ ],
133
+ };
134
+ return `${JSON.stringify(sarif, sortReplacer, 2)}\n`;
135
+ }
136
+
137
+ function severityToSarifLevel(severity: DiffFinding["severity"]): string {
138
+ switch (severity) {
139
+ case "critical":
140
+ case "high":
141
+ return "error";
142
+ case "medium":
143
+ return "warning";
144
+ case "low":
145
+ case "info":
146
+ return "note";
147
+ }
148
+ }
@@ -0,0 +1,50 @@
1
+ import { findAttribute, hasAttribute, stringArg } from "../model/attributes.ts";
2
+ import type { Routine } from "../model/entities.ts";
3
+
4
+ export interface RoutineAttributes {
5
+ obsoleteState?: "Pending" | "Removed";
6
+ obsoleteReason?: string;
7
+ internalProc: boolean;
8
+ }
9
+
10
+ /**
11
+ * Parse structured information out of a routine's `attributesParsed`. Recognises:
12
+ * - `[Obsolete(reason, version[, ObsoleteState::Removed])]` — reason from arg 0,
13
+ * state from arg 2 (defaults to Pending when omitted, Removed only when the
14
+ * third argument is the qualified enum value `ObsoleteState::Removed`).
15
+ * - `[InternalProc]` — presence flag.
16
+ *
17
+ * Used by D13 (cross-app Internal call, alongside `Routine.accessModifier`) and
18
+ * D16 / D38 (obsolete routine call, transitive). All arguments come from the
19
+ * tree-sitter-built `AttributeInfo` model — no `[…]` text shredding.
20
+ */
21
+ export function parseRoutineAttributes(
22
+ routine: Pick<Routine, "attributesParsed">,
23
+ ): RoutineAttributes {
24
+ let obsoleteState: RoutineAttributes["obsoleteState"];
25
+ let obsoleteReason: string | undefined;
26
+
27
+ const obsolete = findAttribute(routine.attributesParsed, "Obsolete");
28
+ if (obsolete !== undefined) {
29
+ obsoleteReason = stringArg(obsolete, 0);
30
+ // State is Removed only when the third arg is a qualified enum value whose
31
+ // member is "Removed" (e.g. `ObsoleteState::Removed`). Anything else — absent,
32
+ // `ObsoleteState::Pending`, or any other shape — means Pending, which is also
33
+ // AL's default Obsolete severity.
34
+ const stateArg = obsolete.args[2];
35
+ obsoleteState =
36
+ stateArg !== undefined &&
37
+ stateArg.kind === "qualified_enum_value" &&
38
+ (stateArg.member ?? "").toLowerCase() === "removed"
39
+ ? "Removed"
40
+ : "Pending";
41
+ }
42
+
43
+ const internalProc = hasAttribute(routine.attributesParsed, "InternalProc");
44
+
45
+ return {
46
+ ...(obsoleteState !== undefined ? { obsoleteState } : {}),
47
+ ...(obsoleteReason !== undefined ? { obsoleteReason } : {}),
48
+ internalProc,
49
+ };
50
+ }