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,154 @@
1
+ import type { Diagnostic } from "../model/finding.ts";
2
+ import { profilingActive, recordPhase } from "../perf/profiler.ts";
3
+ import type { ManifestDependency } from "../symbols/app-manifest.ts";
4
+ import { packageSemanticHash } from "../symbols/package-hash.ts";
5
+ import type { DependencyArtifact, DirectDependencyRef } from "./dependency-artifact.ts";
6
+ import {
7
+ computeArtifactKey,
8
+ readCachedArtifact,
9
+ resolveCacheDir,
10
+ writeCachedArtifact,
11
+ } from "./dependency-cache.ts";
12
+ import { resolveDependencyDag } from "./dependency-dag.ts";
13
+ import { discoverDependencyPackages } from "./dependency-package-discovery.ts";
14
+ import { ingestDependencyApp } from "./dependency-pipeline.ts";
15
+
16
+ export interface ResolveDependencyArtifactsOptions {
17
+ /** Cache directory override. Default: ~/.al-sem/cache/. */
18
+ cacheDir?: string;
19
+ /** Skip the behavioral cold run — structural ABI only, conservative effects. */
20
+ noDepSummaries?: boolean;
21
+ }
22
+
23
+ export interface ResolveDependencyArtifactsResult {
24
+ /** Artifacts in dependency order (a dependency precedes its dependents). */
25
+ artifacts: DependencyArtifact[];
26
+ diagnostics: Diagnostic[];
27
+ }
28
+
29
+ /**
30
+ * The L1.5 dependency stage. Discover → resolve DAG → per package in topo order: compute the
31
+ * artifactKey from packageSemanticHash + resolved direct-dependency keys, then cache-hit or
32
+ * cold-run+write. Never throws — every failure is a diagnostic. Cold-run progress is written
33
+ * to stderr (operational), never to `Diagnostic[]` (which must be cache-presence-independent).
34
+ */
35
+ export async function resolveDependencyArtifacts(
36
+ alpackagesDir: string,
37
+ workspaceDependencies: ManifestDependency[],
38
+ options: ResolveDependencyArtifactsOptions,
39
+ ): Promise<ResolveDependencyArtifactsResult> {
40
+ const diagnostics: Diagnostic[] = [];
41
+ const cacheDir = resolveCacheDir(options.cacheDir);
42
+
43
+ const discovered = discoverDependencyPackages(alpackagesDir);
44
+ diagnostics.push(...discovered.diagnostics);
45
+
46
+ const dag = resolveDependencyDag(discovered.packages, workspaceDependencies);
47
+ diagnostics.push(...dag.diagnostics);
48
+
49
+ if (options.noDepSummaries) {
50
+ diagnostics.push({
51
+ severity: "info",
52
+ stage: "symbol-read",
53
+ message:
54
+ "[DEP022] dependency effect summaries skipped (--no-dep-summaries) — calls into dependencies treated as conservative",
55
+ });
56
+ }
57
+
58
+ const artifacts: DependencyArtifact[] = [];
59
+ const keyByGuid = new Map<string, string>();
60
+ const artifactByGuid = new Map<string, DependencyArtifact>();
61
+
62
+ for (const pkg of dag.ordered) {
63
+ // direct-dependency refs, resolved from already-processed lower artifacts
64
+ const directDependencies: DirectDependencyRef[] = [];
65
+ const lowerArtifacts: DependencyArtifact[] = [];
66
+ for (const md of pkg.manifestDependencies) {
67
+ const depKey = keyByGuid.get(md.appGuid);
68
+ const depArtifact = artifactByGuid.get(md.appGuid);
69
+ if (depKey === undefined || depArtifact === undefined) continue; // dropped by DAG resolution
70
+ directDependencies.push({
71
+ appGuid: depArtifact.header.appIdentity.appGuid,
72
+ publisher: depArtifact.header.appIdentity.publisher,
73
+ name: depArtifact.header.appIdentity.name,
74
+ version: depArtifact.header.appIdentity.version,
75
+ artifactKey: depKey,
76
+ });
77
+ lowerArtifacts.push(depArtifact);
78
+ }
79
+
80
+ let semanticHash: string;
81
+ try {
82
+ semanticHash = packageSemanticHash(pkg.appPath);
83
+ } catch (err) {
84
+ diagnostics.push({
85
+ severity: "warning",
86
+ stage: "symbol-read",
87
+ message: `[DEP002] ${pkg.name}: could not compute semantic hash: ${(err as Error).message}`,
88
+ sourceRef: pkg.appPath,
89
+ });
90
+ continue;
91
+ }
92
+ const artifactMode = options.noDepSummaries ? "structural-only" : "full";
93
+ const artifactKey = computeArtifactKey(semanticHash, directDependencies, artifactMode);
94
+
95
+ // Cache is now namespaced by mode (full vs structural-only), so a `--no-dep-summaries`
96
+ // run hits its own cache and a flag-flipped run never picks up the wrong shape.
97
+ let artifact: DependencyArtifact | null = readCachedArtifact(artifactKey, cacheDir);
98
+
99
+ if (artifact === null) {
100
+ // cold run
101
+ const coldMessage = options.noDepSummaries
102
+ ? `al-sem: building dependency cache for ${pkg.name} ${pkg.version} (one-time, structural-only)\n`
103
+ : `al-sem: building dependency cache for ${pkg.name} ${pkg.version} (one-time)\n`;
104
+ process.stderr.write(coldMessage);
105
+ const _tIngest = profilingActive() ? performance.now() : 0;
106
+ try {
107
+ artifact = await ingestDependencyApp(pkg, lowerArtifacts, artifactKey, {
108
+ structuralOnly: options.noDepSummaries ?? false,
109
+ });
110
+ } catch (err) {
111
+ diagnostics.push({
112
+ severity: "warning",
113
+ stage: "symbol-read",
114
+ message: `[DEP030] ${pkg.name}: dependency ingestion failed, app left opaque: ${(err as Error).message}`,
115
+ sourceRef: pkg.appPath,
116
+ });
117
+ continue;
118
+ }
119
+ // fill orchestrator-owned header fields
120
+ artifact.header.packageSemanticHash = semanticHash;
121
+ artifact.header.directDependencies = directDependencies;
122
+ if (artifact.header.summaryMode === "structural-only-parser-unavailable") {
123
+ // Don't persist a parser-unavailable degradation — it's an environmental
124
+ // failure (missing native lib), not a property of the dep package. Caching
125
+ // it would pin the degradation across runs even after the user reinstalls.
126
+ } else {
127
+ if (profilingActive())
128
+ recordPhase(`ingest:${pkg.name}:ingest-total`, performance.now() - _tIngest);
129
+ const _tWrite = profilingActive() ? performance.now() : 0;
130
+ try {
131
+ // writeCachedArtifact returns the canonical in-memory form (parse of the exact
132
+ // bytes a future cache hit reads), so cold-write output matches a warm hit
133
+ // without re-reading + re-validating the just-written multi-hundred-MB file.
134
+ artifact = writeCachedArtifact(artifact, cacheDir);
135
+ if (profilingActive())
136
+ recordPhase(`ingest:${pkg.name}:write`, performance.now() - _tWrite);
137
+ } catch (err) {
138
+ process.stderr.write(
139
+ `al-sem: could not persist dependency cache for ${pkg.name}: ${(err as Error).message}\n`,
140
+ );
141
+ }
142
+ }
143
+ }
144
+
145
+ // replay the artifact's own degradation diagnostics (skip behavioral ones in no-summaries mode)
146
+ if (!options.noDepSummaries) diagnostics.push(...artifact.diagnostics);
147
+
148
+ artifacts.push(artifact);
149
+ keyByGuid.set(pkg.appGuid, artifactKey);
150
+ artifactByGuid.set(pkg.appGuid, artifact);
151
+ }
152
+
153
+ return { artifacts, diagnostics };
154
+ }
@@ -0,0 +1,114 @@
1
+ import type { ManifestDependency } from "../symbols/app-manifest.ts";
2
+
3
+ interface AppJsonDependency {
4
+ id?: string;
5
+ name?: string;
6
+ publisher?: string;
7
+ version?: string;
8
+ }
9
+
10
+ interface AppJsonInternalsVisibleEntry {
11
+ id?: string;
12
+ name?: string;
13
+ publisher?: string;
14
+ }
15
+
16
+ interface AppJsonFull {
17
+ dependencies?: AppJsonDependency[];
18
+ /** Microsoft Application tier minimum (workspace's `application: "X.Y.Z.W"`). */
19
+ application?: string;
20
+ /** Microsoft Platform tier minimum (workspace's `platform: "X.Y.Z.W"`). */
21
+ platform?: string;
22
+ /** AL `internalsVisibleTo` — apps allowed to call this app's `internal` procedures. */
23
+ internalsVisibleTo?: AppJsonInternalsVisibleEntry[];
24
+ }
25
+
26
+ /**
27
+ * Well-known stable Microsoft platform app identities. BC `.app`s never list these in
28
+ * `<Dependencies>` — they are implicit, declared via `application` / `platform` in app.json.
29
+ * The compiler treats them as required platform-tier dependencies; al-sem mirrors that so
30
+ * symbols like `Customer`, `Sales Header`, etc. resolve through the dependency stage.
31
+ */
32
+ const MS_APPLICATION_TIER: readonly { appGuid: string; name: string; publisher: string }[] = [
33
+ { appGuid: "c1335042-3002-4257-bf8a-75c898ccb1b8", name: "Application", publisher: "Microsoft" },
34
+ {
35
+ appGuid: "437dbf0e-84ff-417a-965d-ed2bb9650972",
36
+ name: "Base Application",
37
+ publisher: "Microsoft",
38
+ },
39
+ {
40
+ appGuid: "f3552374-a1f2-4356-848e-196002525837",
41
+ name: "Business Foundation",
42
+ publisher: "Microsoft",
43
+ },
44
+ ];
45
+
46
+ const MS_PLATFORM_TIER: readonly { appGuid: string; name: string; publisher: string }[] = [
47
+ {
48
+ appGuid: "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
49
+ name: "System Application",
50
+ publisher: "Microsoft",
51
+ },
52
+ { appGuid: "8874ed3a-0643-4247-9ced-7a7002f7135d", name: "System", publisher: "Microsoft" },
53
+ ];
54
+
55
+ /**
56
+ * Parse a workspace app.json's dependency declarations into ManifestDependency[]. Includes
57
+ * both:
58
+ * - **explicit** `dependencies[]` entries (other extensions the workspace depends on);
59
+ * - **implicit** Microsoft platform-tier deps derived from the `application` / `platform`
60
+ * fields (BC compilation convention — these are never listed in `dependencies`).
61
+ *
62
+ * Implicit deps are tagged with well-known Microsoft appGuids; the DAG resolver matches by
63
+ * appGuid, so the Microsoft `.app`s present in `.alpackages` are selected automatically.
64
+ * Never throws.
65
+ */
66
+ export function parseWorkspaceDependencies(appJsonText: string): ManifestDependency[] {
67
+ let parsed: AppJsonFull;
68
+ try {
69
+ parsed = JSON.parse(appJsonText) as AppJsonFull;
70
+ } catch {
71
+ return [];
72
+ }
73
+
74
+ const explicit: ManifestDependency[] = (parsed.dependencies ?? []).map((d) => ({
75
+ appGuid: d.id ?? "",
76
+ name: d.name ?? "",
77
+ publisher: d.publisher ?? "",
78
+ minVersion: d.version ?? "0.0.0.0",
79
+ }));
80
+
81
+ const implicit: ManifestDependency[] = [];
82
+ if (typeof parsed.application === "string" && parsed.application !== "") {
83
+ for (const a of MS_APPLICATION_TIER) implicit.push({ ...a, minVersion: parsed.application });
84
+ }
85
+ if (typeof parsed.platform === "string" && parsed.platform !== "") {
86
+ for (const a of MS_PLATFORM_TIER) implicit.push({ ...a, minVersion: parsed.platform });
87
+ }
88
+
89
+ return [...explicit, ...implicit];
90
+ }
91
+
92
+ /**
93
+ * Parse the workspace app.json's `internalsVisibleTo` array — the AL feature that names
94
+ * apps allowed to call this app's `internal` procedures. Returns the lowercased app GUIDs
95
+ * (entries are deduplicated; malformed/missing entries are skipped). Empty array means
96
+ * no apps are granted internal access → `internal` procedures are effectively app-scoped
97
+ * and the dead-routine detector treats them like `local procedure` for reachability.
98
+ *
99
+ * Never throws.
100
+ */
101
+ export function parseWorkspaceInternalsVisibleTo(appJsonText: string): string[] {
102
+ let parsed: AppJsonFull;
103
+ try {
104
+ parsed = JSON.parse(appJsonText) as AppJsonFull;
105
+ } catch {
106
+ return [];
107
+ }
108
+ const guids = new Set<string>();
109
+ for (const e of parsed.internalsVisibleTo ?? []) {
110
+ const id = typeof e.id === "string" ? e.id.trim().toLowerCase() : "";
111
+ if (id !== "") guids.add(id);
112
+ }
113
+ return [...guids].sort();
114
+ }
@@ -0,0 +1,145 @@
1
+ import type { CapabilityFact, CapabilityOp, CapabilityResourceKind } from "../model/capability.ts";
2
+ import type { CoverageStatus } from "../model/coverage.ts";
3
+ import type { EventId, TableId } from "../model/ids.ts";
4
+ import type { EffectPresence, RoutineSummary } from "../model/summary.ts";
5
+
6
+ /**
7
+ * Phase 1a capability-query helpers — pure functions over RoutineSummary.
8
+ *
9
+ * Every helper reads `capabilityFactsDirect ∪ capabilityFactsInherited` and
10
+ * returns a derived view. Helpers honour G6 coverage semantics: when a fact
11
+ * is absent and the cone is not "complete", tri-state helpers return
12
+ * "unknown" rather than "no".
13
+ *
14
+ * These helpers do NOT (in Phase 1a) replace the legacy boolean lattice
15
+ * (touchesDb / commits / writesTables / publishesEvents); they sit ALONGSIDE
16
+ * it, with a parity test (Phase 1a Task 7) asserting mechanical equivalence.
17
+ * Phase 1b migrates detectors over per-domain; Phase 1c deletes the legacy
18
+ * fields.
19
+ */
20
+
21
+ function reachable(s: RoutineSummary): readonly CapabilityFact[] {
22
+ const direct = s.capabilityFactsDirect ?? [];
23
+ const inherited = s.capabilityFactsInherited ?? [];
24
+ if (direct.length === 0 && inherited.length === 0) return [];
25
+ return [...direct, ...inherited];
26
+ }
27
+
28
+ /**
29
+ * Filter reachable facts (direct + inherited) by an arbitrary predicate.
30
+ * Returns a fresh array; never mutates the summary.
31
+ */
32
+ export function findCapabilities(
33
+ s: RoutineSummary,
34
+ predicate: (f: CapabilityFact) => boolean,
35
+ ): CapabilityFact[] {
36
+ return reachable(s).filter(predicate);
37
+ }
38
+
39
+ /**
40
+ * True when at least one reachable fact has the given (op, resourceKind).
41
+ * Strict discrimination — no fuzzy/substring matching.
42
+ */
43
+ export function hasCapability(
44
+ s: RoutineSummary,
45
+ op: CapabilityOp,
46
+ kind: CapabilityResourceKind,
47
+ ): boolean {
48
+ for (const f of reachable(s)) {
49
+ if (f.op === op && f.resourceKind === kind) return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ const TABLE_WRITE_OPS = new Set<CapabilityOp>(["insert", "modify", "delete"]);
55
+
56
+ /**
57
+ * Sorted + deduped TableIds targeted by any reachable insert/modify/delete
58
+ * fact on `resourceKind === "table"` with a known `resourceId`. Facts whose
59
+ * table identity is unresolved (no `resourceId`) are dropped — callers that
60
+ * need to know whether the cone is partial should consult `reachableCoverage`
61
+ * separately.
62
+ *
63
+ * Read facts are NOT included. Phase 4 framework detectors that care about
64
+ * full read+write surface read `findCapabilities` directly.
65
+ */
66
+ export function writesTablesOf(s: RoutineSummary): TableId[] {
67
+ const ids = new Set<string>();
68
+ for (const f of reachable(s)) {
69
+ if (f.resourceKind !== "table") continue;
70
+ if (!TABLE_WRITE_OPS.has(f.op)) continue;
71
+ if (typeof f.resourceId !== "string") continue;
72
+ ids.add(f.resourceId);
73
+ }
74
+ return Array.from(ids).sort() as TableId[];
75
+ }
76
+
77
+ /**
78
+ * Returns "yes" when any reachable fact is a commit on the transaction
79
+ * resource; "no" when no commit fact AND the inherited cone is "complete";
80
+ * "unknown" otherwise (G6 honesty — absence of evidence is not evidence of
81
+ * absence when the cone is partial or coverage data is missing).
82
+ */
83
+ export function mayCommit(s: RoutineSummary): EffectPresence {
84
+ for (const f of reachable(s)) {
85
+ if (f.op === "commit" && f.resourceKind === "transaction") return "yes";
86
+ }
87
+ const status = s.coverage?.inheritedStatus ?? "unknown";
88
+ return status === "complete" ? "no" : "unknown";
89
+ }
90
+
91
+ /**
92
+ * Returns "yes" when any reachable fact has resourceKind === "table"
93
+ * (regardless of op — read or write); "no" when no such fact AND the
94
+ * inherited cone is "complete"; "unknown" otherwise.
95
+ *
96
+ * Note: state-only ops (SetRange, SetFilter, Init) are NOT in the
97
+ * capability fact stream per spec §3.1.1 substrate discipline — they
98
+ * stay on RecordOperation. So touchesDbOf reflects only "did the routine
99
+ * actually read or write a row," not "did it touch table state."
100
+ */
101
+ export function touchesDbOf(s: RoutineSummary): EffectPresence {
102
+ for (const f of reachable(s)) {
103
+ if (f.resourceKind === "table") return "yes";
104
+ }
105
+ const status = s.coverage?.inheritedStatus ?? "unknown";
106
+ return status === "complete" ? "no" : "unknown";
107
+ }
108
+
109
+ /**
110
+ * Sorted + deduped EventIds (as plain strings — matches legacy
111
+ * RoutineSummary.publishesEvents shape) from reachable op="publish" facts
112
+ * on resourceKind="event" with a known resourceId. Facts whose event
113
+ * identity is unresolved are dropped.
114
+ */
115
+ export function publishesEventsOf(s: RoutineSummary): string[] {
116
+ const ids = new Set<string>();
117
+ for (const f of reachable(s)) {
118
+ if (f.op !== "publish") continue;
119
+ if (f.resourceKind !== "event") continue;
120
+ if (typeof f.resourceId !== "string") continue;
121
+ ids.add(f.resourceId);
122
+ }
123
+ return Array.from(ids).sort();
124
+ }
125
+
126
+ /**
127
+ * Returns the routine's inherited coverage status, optionally narrowed to
128
+ * a single resourceKind.
129
+ *
130
+ * Phase 1a: the per-routine `coverage.inheritedStatus` is the only roll-up
131
+ * the composer maintains — there is no per-family status table yet. So when
132
+ * a `kind` is supplied, the function returns the same overall status,
133
+ * NOT a kind-specific narrowing. Per-family roll-up arrives with the
134
+ * fingerprint dump CLI's coverage rendering (§4.4 / spec §3.7
135
+ * FingerprintCoverage.perFamily) once that field lands.
136
+ *
137
+ * Returns "unknown" when the summary has no coverage record at all
138
+ * (pre-Phase-0b routines).
139
+ */
140
+ export function reachableCoverage(
141
+ s: RoutineSummary,
142
+ _kind?: CapabilityResourceKind,
143
+ ): CoverageStatus {
144
+ return s.coverage?.inheritedStatus ?? "unknown";
145
+ }
@@ -0,0 +1,52 @@
1
+ import type { FindingConfidence } from "../model/finding.ts";
2
+ import type { Uncertainty } from "../model/summary.ts";
3
+
4
+ type CappedByKind = NonNullable<FindingConfidence["cappedBy"]>[number];
5
+
6
+ // The Uncertainty kinds that are also valid FindingConfidence.cappedBy values.
7
+ const VALID_CAPPED_BY = new Set<CappedByKind>([
8
+ "unresolved-call",
9
+ "opaque-callee",
10
+ "dynamic-dispatch",
11
+ "parse-incomplete",
12
+ "version-mismatch",
13
+ ]);
14
+
15
+ function describe(u: Uncertainty): string {
16
+ if ("callsiteId" in u) return `${u.kind} at ${u.callsiteId}`;
17
+ if ("operationId" in u) return `${u.kind} at ${u.operationId}`;
18
+ return `${u.kind} at ${u.routineId}`;
19
+ }
20
+
21
+ function isCappedByKind(kind: string): kind is CappedByKind {
22
+ return (VALID_CAPPED_BY as ReadonlySet<string>).has(kind);
23
+ }
24
+
25
+ /**
26
+ * Map a list of uncertainties to a FindingConfidence. Any uncertainty caps `level` at
27
+ * `possible`. Uncertainty kinds that are valid `cappedBy` values are listed there; the
28
+ * others (`interface-dispatch`, `recordref-or-variant`) still cap the level but are recorded
29
+ * only in `evidence` — never as an invalid `cappedBy` string. `baseLevel` is never raised.
30
+ */
31
+ export function toConfidence(
32
+ uncertainties: Uncertainty[],
33
+ baseLevel: FindingConfidence["level"],
34
+ ): FindingConfidence {
35
+ if (uncertainties.length === 0) {
36
+ return { level: baseLevel, evidence: [] };
37
+ }
38
+ const cappedBySet = new Set(uncertainties.map((u) => u.kind).filter(isCappedByKind));
39
+ const cappedBy = cappedBySet.size > 0 ? ([...cappedBySet].sort() as CappedByKind[]) : undefined;
40
+ const evidence = uncertainties.map((u) => ({
41
+ source: "tree-sitter" as const,
42
+ note: describe(u),
43
+ }));
44
+ const result: FindingConfidence = {
45
+ level: "possible",
46
+ evidence,
47
+ };
48
+ if (cappedBy) {
49
+ result.cappedBy = cappedBy;
50
+ }
51
+ return result;
52
+ }