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,146 @@
1
+ import type { CapabilitySnapshot } from "../snapshot/types.ts";
2
+ import { type AbiFinding, diffAbi } from "./diff-abi.ts";
3
+ import { type CapabilityFinding, diffCapabilities } from "./diff-capabilities.ts";
4
+ import { type EventFinding, diffEvents } from "./diff-events.ts";
5
+ import { DiffCategory, type DiffKind } from "./diff-identity.ts";
6
+ import { buildDiffIndexes } from "./diff-indexes.ts";
7
+ import { type PermissionFinding, diffPermissions } from "./diff-permissions.ts";
8
+ import { type PolicyDiagnostic, applyCoveragePolicy } from "./diff-policy.ts";
9
+ import { type PreflightDiagnostic, runPreflight } from "./diff-preflight.ts";
10
+ import { type RenameDiagnostic, type RenameOverlay, buildRenameTable } from "./diff-renames.ts";
11
+ import { type SchemaFinding, diffSchema } from "./diff-schema.ts";
12
+
13
+ export type DiffFinding =
14
+ | AbiFinding
15
+ | SchemaFinding
16
+ | EventFinding
17
+ | CapabilityFinding
18
+ | PermissionFinding;
19
+
20
+ export type DiffDiagnostic = PreflightDiagnostic | RenameDiagnostic | PolicyDiagnostic;
21
+
22
+ export interface DiffEngineOptions {
23
+ coveragePolicy: "loose" | "strict";
24
+ deterministic: boolean;
25
+ renameOverlay?: RenameOverlay;
26
+ }
27
+
28
+ export interface DiffEngineResult {
29
+ findings: readonly DiffFinding[];
30
+ diagnostics: readonly DiffDiagnostic[];
31
+ summary: {
32
+ findingsByCategory: Record<DiffCategory, number>;
33
+ findingsBySeverity: Record<DiffFinding["severity"], number>;
34
+ coverageIncompleteCones: number;
35
+ renamesApplied: number;
36
+ };
37
+ }
38
+
39
+ const SEVERITY_RANK: Record<DiffFinding["severity"], number> = {
40
+ critical: 0,
41
+ high: 1,
42
+ medium: 2,
43
+ low: 3,
44
+ info: 4,
45
+ };
46
+
47
+ export function runDiffEngine(
48
+ oldSnap: CapabilitySnapshot,
49
+ newSnap: CapabilitySnapshot,
50
+ opts: DiffEngineOptions,
51
+ ): DiffEngineResult {
52
+ const diagnostics: DiffDiagnostic[] = [];
53
+
54
+ const preflight = runPreflight(oldSnap, newSnap, { coveragePolicy: opts.coveragePolicy });
55
+ diagnostics.push(...preflight.diagnostics);
56
+ if (preflight.fatal) {
57
+ return {
58
+ findings: [],
59
+ diagnostics,
60
+ summary: emptySummary(),
61
+ };
62
+ }
63
+
64
+ const { table: renameTable, diagnostics: renameDiagnostics } = buildRenameTable(
65
+ opts.renameOverlay ?? {},
66
+ );
67
+ diagnostics.push(...renameDiagnostics);
68
+
69
+ const indexes = buildDiffIndexes(oldSnap, newSnap, renameTable);
70
+ diagnostics.push(...indexes.renameDiagnostics);
71
+
72
+ const findings: DiffFinding[] = [];
73
+ findings.push(...diffAbi(oldSnap, newSnap, indexes, opts));
74
+ findings.push(...diffSchema(oldSnap, newSnap, indexes, opts));
75
+ findings.push(...diffEvents(oldSnap, newSnap, indexes, opts));
76
+ findings.push(...diffCapabilities(oldSnap, newSnap, indexes, opts));
77
+ findings.push(...diffPermissions(oldSnap, newSnap, indexes, opts));
78
+
79
+ // PolicyFinding requires an index signature; cast through unknown to satisfy the constraint.
80
+ const { findings: policyFindings, diagnostics: policyDiagnostics } = applyCoveragePolicy(
81
+ findings as unknown as import("./diff-policy.ts").PolicyFinding[],
82
+ indexes,
83
+ { coveragePolicy: opts.coveragePolicy },
84
+ );
85
+ diagnostics.push(...policyDiagnostics);
86
+
87
+ const sorted = (policyFindings as unknown as DiffFinding[]).slice().sort((a, b) => {
88
+ const sevDelta =
89
+ SEVERITY_RANK[a.severity as DiffFinding["severity"]] -
90
+ SEVERITY_RANK[b.severity as DiffFinding["severity"]];
91
+ if (sevDelta !== 0) return sevDelta;
92
+ const ac = a.category as string;
93
+ const bc = b.category as string;
94
+ if (ac !== bc) return ac < bc ? -1 : 1;
95
+ const ak = a.kind as string;
96
+ const bk = b.kind as string;
97
+ if (ak !== bk) return ak < bk ? -1 : 1;
98
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
99
+ });
100
+
101
+ return {
102
+ findings: sorted,
103
+ diagnostics,
104
+ summary: computeSummary(sorted, policyDiagnostics, renameTable.size),
105
+ };
106
+ }
107
+
108
+ function emptySummary(): DiffEngineResult["summary"] {
109
+ const byCategory: Record<DiffCategory, number> = {
110
+ [DiffCategory.ABI]: 0,
111
+ [DiffCategory.Schema]: 0,
112
+ [DiffCategory.Events]: 0,
113
+ [DiffCategory.Capabilities]: 0,
114
+ [DiffCategory.Permissions]: 0,
115
+ };
116
+ const bySeverity: Record<DiffFinding["severity"], number> = {
117
+ critical: 0,
118
+ high: 0,
119
+ medium: 0,
120
+ low: 0,
121
+ info: 0,
122
+ };
123
+ return {
124
+ findingsByCategory: byCategory,
125
+ findingsBySeverity: bySeverity,
126
+ coverageIncompleteCones: 0,
127
+ renamesApplied: 0,
128
+ };
129
+ }
130
+
131
+ function computeSummary(
132
+ findings: readonly DiffFinding[],
133
+ policyDiagnostics: readonly PolicyDiagnostic[],
134
+ renamesApplied: number,
135
+ ): DiffEngineResult["summary"] {
136
+ const summary = emptySummary();
137
+ for (const f of findings) {
138
+ summary.findingsByCategory[f.category]++;
139
+ summary.findingsBySeverity[f.severity]++;
140
+ }
141
+ summary.coverageIncompleteCones = policyDiagnostics.filter(
142
+ (d) => d.kind === "coverage-incomplete",
143
+ ).length;
144
+ summary.renamesApplied = renamesApplied;
145
+ return summary;
146
+ }
@@ -0,0 +1,323 @@
1
+ import type { EventDeclaration } 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 EventDetails {
7
+ kind:
8
+ | DiffKind.EventPublisherRemoved
9
+ | DiffKind.EventPublisherSignatureChanged
10
+ | DiffKind.EventPublisherAdded
11
+ | DiffKind.EventSubscriberInducedCapabilityGained
12
+ | DiffKind.EventSubscriberInducedCapabilityLost
13
+ | DiffKind.EventContractChangedWithAffectedSubscribers;
14
+ publisherObject: string;
15
+ eventName: string;
16
+ oldEventId?: string;
17
+ newEventId?: string;
18
+ }
19
+
20
+ export interface EventFinding {
21
+ id: string;
22
+ category: DiffCategory.Events;
23
+ kind: DiffKind;
24
+ severity: "critical" | "high" | "medium" | "low" | "info";
25
+ subject: {
26
+ normalizedStableId: string;
27
+ oldOriginalStableId?: string;
28
+ newStableId?: string;
29
+ displayName: string;
30
+ };
31
+ comparisonCone: readonly string[];
32
+ details: EventDetails;
33
+ }
34
+
35
+ const SEVERITY: Partial<Record<DiffKind, EventFinding["severity"]>> = {
36
+ [DiffKind.EventPublisherSignatureChanged]: "critical",
37
+ [DiffKind.EventPublisherRemoved]: "medium",
38
+ [DiffKind.EventPublisherAdded]: "info",
39
+ [DiffKind.EventContractChangedWithAffectedSubscribers]: "critical",
40
+ [DiffKind.EventSubscriberInducedCapabilityGained]: "medium",
41
+ [DiffKind.EventSubscriberInducedCapabilityLost]: "low",
42
+ };
43
+
44
+ /** Parse `${publisherObject}::${eventName}::${shapeHash}` → first two parts as
45
+ * identity (publisher + event name). The shape hash is the part that changes
46
+ * on signature drift. */
47
+ function parseEventIdentity(eventId: string): {
48
+ publisherObject: string;
49
+ eventName: string;
50
+ shapeHash: string;
51
+ } {
52
+ // Split on `::`; if the publisher object name contains `::` (rare), the last
53
+ // two segments are eventName + shapeHash and everything before is publisher.
54
+ const parts = eventId.split("::");
55
+ if (parts.length < 3) {
56
+ return { publisherObject: parts[0] ?? "", eventName: parts[1] ?? "", shapeHash: "" };
57
+ }
58
+ const shapeHash = parts[parts.length - 1] ?? "";
59
+ const eventName = parts[parts.length - 2] ?? "";
60
+ const publisherObject = parts.slice(0, parts.length - 2).join("::");
61
+ return { publisherObject, eventName, shapeHash };
62
+ }
63
+
64
+ /** Identity key for matching event publishers across snapshots: publisher +
65
+ * event name, ignoring shape hash. Two declarations with the same (object,
66
+ * name) but different shape are signature-changed, not remove+add. */
67
+ function publisherIdentityKey(decl: EventDeclaration): string {
68
+ const parsed = parseEventIdentity(decl.eventId);
69
+ return `${parsed.publisherObject}::${parsed.eventName}`;
70
+ }
71
+
72
+ function makeFinding(
73
+ kind: EventDetails["kind"],
74
+ subjectId: string,
75
+ indexes: DiffIndexes,
76
+ details: EventDetails,
77
+ secondaryKey: string,
78
+ ): EventFinding {
79
+ const origin = indexes.originByNormalized.get(subjectId);
80
+ const display =
81
+ indexes.newDisplayByStableId.get(subjectId) ??
82
+ indexes.oldDisplayByStableId.get(subjectId) ??
83
+ subjectId;
84
+ return {
85
+ id: computeDiffFingerprint({
86
+ category: DiffCategory.Events,
87
+ kind,
88
+ normalizedStableId: subjectId,
89
+ secondaryKey,
90
+ }),
91
+ category: DiffCategory.Events,
92
+ kind,
93
+ severity: SEVERITY[kind] ?? "medium",
94
+ subject: {
95
+ normalizedStableId: subjectId,
96
+ oldOriginalStableId: origin?.oldOriginalStableId,
97
+ newStableId: origin?.newStableId,
98
+ displayName: display,
99
+ },
100
+ comparisonCone: [subjectId],
101
+ details,
102
+ };
103
+ }
104
+
105
+ export function diffEvents(
106
+ _oldSnap: unknown,
107
+ _newSnap: unknown,
108
+ indexes: DiffIndexes,
109
+ _opts: DiffEngineOptions,
110
+ ): EventFinding[] {
111
+ const out: EventFinding[] = [];
112
+
113
+ const oldPublishers = new Map<string, EventDeclaration>();
114
+ const newPublishers = new Map<string, EventDeclaration>();
115
+
116
+ for (const decls of indexes.oldEventsBySubject.values()) {
117
+ for (const decl of decls) {
118
+ if (decl.kind !== "publisher") continue;
119
+ oldPublishers.set(publisherIdentityKey(decl), decl);
120
+ }
121
+ }
122
+ for (const decls of indexes.newEventsBySubject.values()) {
123
+ for (const decl of decls) {
124
+ if (decl.kind !== "publisher") continue;
125
+ newPublishers.set(publisherIdentityKey(decl), decl);
126
+ }
127
+ }
128
+
129
+ for (const [key, oldDecl] of oldPublishers) {
130
+ const newDecl = newPublishers.get(key);
131
+ const parsed = parseEventIdentity(oldDecl.eventId);
132
+ if (newDecl === undefined) {
133
+ out.push(
134
+ makeFinding(
135
+ DiffKind.EventPublisherRemoved,
136
+ oldDecl.routine,
137
+ indexes,
138
+ {
139
+ kind: DiffKind.EventPublisherRemoved,
140
+ publisherObject: parsed.publisherObject,
141
+ eventName: parsed.eventName,
142
+ oldEventId: oldDecl.eventId,
143
+ },
144
+ parsed.eventName,
145
+ ),
146
+ );
147
+ continue;
148
+ }
149
+ if (oldDecl.eventId !== newDecl.eventId) {
150
+ out.push(
151
+ makeFinding(
152
+ DiffKind.EventPublisherSignatureChanged,
153
+ newDecl.routine,
154
+ indexes,
155
+ {
156
+ kind: DiffKind.EventPublisherSignatureChanged,
157
+ publisherObject: parsed.publisherObject,
158
+ eventName: parsed.eventName,
159
+ oldEventId: oldDecl.eventId,
160
+ newEventId: newDecl.eventId,
161
+ },
162
+ parsed.eventName,
163
+ ),
164
+ );
165
+ }
166
+ }
167
+
168
+ for (const [key, newDecl] of newPublishers) {
169
+ if (oldPublishers.has(key)) continue;
170
+ const parsed = parseEventIdentity(newDecl.eventId);
171
+ out.push(
172
+ makeFinding(
173
+ DiffKind.EventPublisherAdded,
174
+ newDecl.routine,
175
+ indexes,
176
+ {
177
+ kind: DiffKind.EventPublisherAdded,
178
+ publisherObject: parsed.publisherObject,
179
+ eventName: parsed.eventName,
180
+ newEventId: newDecl.eventId,
181
+ },
182
+ parsed.eventName,
183
+ ),
184
+ );
185
+ }
186
+
187
+ // Phase 3: build per-event subscriber lookups (key = `${publisherObject}::${eventName}`)
188
+ const oldSubsByEvent = new Map<string, EventDeclaration[]>();
189
+ const newSubsByEvent = new Map<string, EventDeclaration[]>();
190
+ for (const decls of indexes.oldEventsBySubject.values()) {
191
+ for (const d of decls) {
192
+ if (d.kind !== "subscriber") continue;
193
+ const parsed = parseEventIdentity(d.eventId);
194
+ const k = `${parsed.publisherObject}::${parsed.eventName}`;
195
+ const bag = oldSubsByEvent.get(k) ?? [];
196
+ bag.push(d);
197
+ oldSubsByEvent.set(k, bag);
198
+ }
199
+ }
200
+ for (const decls of indexes.newEventsBySubject.values()) {
201
+ for (const d of decls) {
202
+ if (d.kind !== "subscriber") continue;
203
+ const parsed = parseEventIdentity(d.eventId);
204
+ const k = `${parsed.publisherObject}::${parsed.eventName}`;
205
+ const bag = newSubsByEvent.get(k) ?? [];
206
+ bag.push(d);
207
+ newSubsByEvent.set(k, bag);
208
+ }
209
+ }
210
+
211
+ // Specialization: signature-change WITH subscribers → ContractChangedWithAffectedSubscribers,
212
+ // suppress the generic EventPublisherSignatureChanged for the same event key.
213
+ const suppressSignatureKeys = new Set<string>();
214
+ const contractFindings: EventFinding[] = [];
215
+ for (const [key, oldDecl] of oldPublishers) {
216
+ const newDecl = newPublishers.get(key);
217
+ if (newDecl === undefined) continue;
218
+ if (oldDecl.eventId === newDecl.eventId) continue;
219
+ const hasSubs =
220
+ (oldSubsByEvent.get(key)?.length ?? 0) + (newSubsByEvent.get(key)?.length ?? 0) > 0;
221
+ if (!hasSubs) continue;
222
+ suppressSignatureKeys.add(key);
223
+ const parsed = parseEventIdentity(newDecl.eventId);
224
+ contractFindings.push(
225
+ makeFinding(
226
+ DiffKind.EventContractChangedWithAffectedSubscribers,
227
+ newDecl.routine,
228
+ indexes,
229
+ {
230
+ kind: DiffKind.EventContractChangedWithAffectedSubscribers,
231
+ publisherObject: parsed.publisherObject,
232
+ eventName: parsed.eventName,
233
+ oldEventId: oldDecl.eventId,
234
+ newEventId: newDecl.eventId,
235
+ },
236
+ parsed.eventName,
237
+ ),
238
+ );
239
+ }
240
+
241
+ // Filter out signature findings for suppressed keys.
242
+ const filtered = out.filter((f) => {
243
+ if (f.kind !== DiffKind.EventPublisherSignatureChanged) return true;
244
+ const det = f.details;
245
+ const k = `${det.publisherObject}::${det.eventName}`;
246
+ return !suppressSignatureKeys.has(k);
247
+ });
248
+ out.length = 0;
249
+ out.push(...filtered, ...contractFindings);
250
+
251
+ // Subscriber-induced capability delta — emit gained/lost per (event, table, op).
252
+ const oldCapBySubject = indexes.oldCapabilityFactsBySubject;
253
+ const newCapBySubject = indexes.newCapabilityFactsBySubject;
254
+
255
+ function writesOf(
256
+ subs: EventDeclaration[] | undefined,
257
+ capMap: typeof oldCapBySubject,
258
+ ): Map<string, Set<string>> {
259
+ // tableId → set of ops
260
+ const m = new Map<string, Set<string>>();
261
+ for (const s of subs ?? []) {
262
+ const facts = capMap.get(s.routine) ?? [];
263
+ for (const f of facts) {
264
+ if (f.resourceKind !== "table") continue;
265
+ if (!(f.op === "insert" || f.op === "modify" || f.op === "delete")) continue;
266
+ if (typeof f.resourceId !== "string") continue;
267
+ const set = m.get(f.resourceId) ?? new Set<string>();
268
+ set.add(f.op);
269
+ m.set(f.resourceId, set);
270
+ }
271
+ }
272
+ return m;
273
+ }
274
+
275
+ const matchedEventKeys = new Set([...oldSubsByEvent.keys(), ...newSubsByEvent.keys()]);
276
+ for (const key of matchedEventKeys) {
277
+ const oldWrites = writesOf(oldSubsByEvent.get(key), oldCapBySubject);
278
+ const newWrites = writesOf(newSubsByEvent.get(key), newCapBySubject);
279
+ const [publisherObject, eventName] = key.split("::", 2);
280
+ const subject = (newPublishers.get(key) ?? oldPublishers.get(key))?.routine;
281
+ if (subject === undefined) continue;
282
+ for (const [table, ops] of newWrites) {
283
+ const oldOps = oldWrites.get(table) ?? new Set();
284
+ for (const op of ops) {
285
+ if (oldOps.has(op)) continue;
286
+ out.push(
287
+ makeFinding(
288
+ DiffKind.EventSubscriberInducedCapabilityGained,
289
+ subject,
290
+ indexes,
291
+ {
292
+ kind: DiffKind.EventSubscriberInducedCapabilityGained,
293
+ publisherObject: publisherObject ?? "",
294
+ eventName: eventName ?? "",
295
+ },
296
+ `${eventName}|${table}|${op}`,
297
+ ),
298
+ );
299
+ }
300
+ }
301
+ for (const [table, ops] of oldWrites) {
302
+ const newOps = newWrites.get(table) ?? new Set();
303
+ for (const op of ops) {
304
+ if (newOps.has(op)) continue;
305
+ out.push(
306
+ makeFinding(
307
+ DiffKind.EventSubscriberInducedCapabilityLost,
308
+ subject,
309
+ indexes,
310
+ {
311
+ kind: DiffKind.EventSubscriberInducedCapabilityLost,
312
+ publisherObject: publisherObject ?? "",
313
+ eventName: eventName ?? "",
314
+ },
315
+ `${eventName}|${table}|${op}`,
316
+ ),
317
+ );
318
+ }
319
+ }
320
+ }
321
+
322
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
323
+ }
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export enum DiffCategory {
4
+ ABI = "abi",
5
+ Schema = "schema",
6
+ Events = "events",
7
+ Capabilities = "capabilities",
8
+ Permissions = "permissions",
9
+ }
10
+
11
+ export enum DiffKind {
12
+ // ABI
13
+ ObjectRemoved = "object-removed",
14
+ ObjectAccessibilityNarrowed = "object-accessibility-narrowed",
15
+ ProcedureRemoved = "procedure-removed",
16
+ ProcedureSignatureChanged = "procedure-signature-changed",
17
+ ProcedureVarDirectionChanged = "procedure-var-direction-changed",
18
+ ProcedureObsoletionRegressed = "procedure-obsoletion-regressed",
19
+ ProcedureObsoletionProgressed = "procedure-obsoletion-progressed",
20
+ ObjectAdded = "object-added",
21
+ ProcedureAdded = "procedure-added",
22
+ // Schema
23
+ TableFieldRemoved = "table-field-removed",
24
+ TableFieldTypeNarrowed = "table-field-type-narrowed",
25
+ TableFieldTypeWidened = "table-field-type-widened",
26
+ TableFieldDataClassificationTightened = "table-field-data-classification-tightened",
27
+ TableFieldDataClassificationRelaxed = "table-field-data-classification-relaxed",
28
+ EnumValueRemoved = "enum-value-removed",
29
+ EnumValueRenumbered = "enum-value-renumbered",
30
+ TableFieldAdded = "table-field-added",
31
+ EnumValueAdded = "enum-value-added",
32
+ // Events
33
+ EventPublisherRemoved = "event-publisher-removed",
34
+ EventPublisherSignatureChanged = "event-publisher-signature-changed",
35
+ EventPublisherAdded = "event-publisher-added",
36
+ EventSubscriberInducedCapabilityGained = "event-subscriber-induced-capability-gained",
37
+ EventSubscriberInducedCapabilityLost = "event-subscriber-induced-capability-lost",
38
+ EventContractChangedWithAffectedSubscribers = "event-contract-changed-with-affected-subscribers",
39
+ // Capabilities
40
+ CapabilityGainedWrite = "capability-gained-write",
41
+ CapabilityGainedRead = "capability-gained-read",
42
+ CapabilityGainedCommit = "capability-gained-commit",
43
+ CapabilityGainedHttp = "capability-gained-http",
44
+ CapabilityGainedTelemetry = "capability-gained-telemetry",
45
+ CapabilityGainedIsolatedStorage = "capability-gained-isolated-storage",
46
+ CapabilityGainedFile = "capability-gained-file",
47
+ CapabilityGainedDynamicDispatch = "capability-gained-dynamic-dispatch",
48
+ CapabilityGainedEventPublish = "capability-gained-event-publish",
49
+ CapabilityLost = "capability-lost",
50
+ CapabilityLostUnderCoverage = "capability-lost-under-coverage",
51
+ // Permissions
52
+ PermissionRightsExpanded = "permission-rights-expanded",
53
+ PermissionRightsContracted = "permission-rights-contracted",
54
+ PermissionTargetAdded = "permission-target-added",
55
+ PermissionTargetRemoved = "permission-target-removed",
56
+ }
57
+
58
+ export interface DiffFingerprintInput {
59
+ category: DiffCategory;
60
+ kind: DiffKind;
61
+ normalizedStableId: string;
62
+ secondaryKey?: string;
63
+ }
64
+
65
+ /**
66
+ * SHA-256(category|kind|normalizedStableId|secondaryKey) truncated to 16 hex.
67
+ * Matches the al-sem analyze fingerprintOf algorithm in spirit; pipe separators
68
+ * avoid collision when fields contain the same characters.
69
+ */
70
+ export function computeDiffFingerprint(input: DiffFingerprintInput): string {
71
+ const payload = `${input.category}|${input.kind}|${input.normalizedStableId}|${input.secondaryKey ?? ""}`;
72
+ return createHash("sha256").update(payload, "utf8").digest("hex").slice(0, 16);
73
+ }