@clear-capabilities/agentic-security-scanner 0.184.0 → 0.186.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 (228) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/agentic-security.js +111 -1
  3. package/dist/1128.index.js +912 -0
  4. package/dist/1665.index.js +828 -0
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/5752.index.js +466 -0
  8. package/dist/7405.index.js +1 -1
  9. package/dist/8158.index.js +1713 -0
  10. package/dist/8798.index.js +1 -1
  11. package/dist/9382.index.js +2 -2
  12. package/dist/9579.index.js +1 -1
  13. package/dist/agentic-security.mjs +29 -29
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/dist/{dad58d1113a12c23a142.json → ca62ee93079be4c24eee.json} +29 -12
  16. package/dist/frontend/src/shell.js +12 -2
  17. package/package.json +29 -12
  18. package/src/dataflow/CLAUDE.md +46 -1
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/backward.js +42 -7
  21. package/src/dataflow/cross-service-taint.js +83 -1
  22. package/src/dataflow/demand-query.js +1168 -0
  23. package/src/dataflow/dep-fingerprint.js +718 -0
  24. package/src/dataflow/engine.js +48 -6
  25. package/src/dataflow/exploit-prover.js +6 -2
  26. package/src/dataflow/ifds-precise.js +22 -0
  27. package/src/dataflow/incremental-replay.js +118 -0
  28. package/src/dataflow/incremental-session.js +257 -0
  29. package/src/dataflow/incremental.js +237 -35
  30. package/src/dataflow/index.js +25 -94
  31. package/src/dataflow/ir-cache.js +268 -0
  32. package/src/dataflow/privacy-taint.js +7 -0
  33. package/src/dataflow/summaries.js +72 -6
  34. package/src/dataflow/tabulation.js +80 -24
  35. package/src/engine.js +113 -8
  36. package/src/graph/CLAUDE.md +31 -0
  37. package/src/graph/adapters/artifact.js +81 -0
  38. package/src/graph/adapters/crossrepo.js +75 -0
  39. package/src/graph/adapters/dataflow.js +76 -0
  40. package/src/graph/adapters/provenance.js +92 -0
  41. package/src/graph/adapters/semantic.js +89 -0
  42. package/src/graph/crosswalk-builders.js +152 -0
  43. package/src/graph/crosswalk.js +146 -0
  44. package/src/graph/index.js +15 -0
  45. package/src/graph/observation.js +57 -0
  46. package/src/graph/refs.js +105 -0
  47. package/src/graph/registry.js +129 -0
  48. package/src/graph/relations.js +81 -0
  49. package/src/graph/scc.js +241 -0
  50. package/src/graph/view.js +254 -0
  51. package/src/ir/CLAUDE.md +4 -0
  52. package/src/ir/callgraph.js +36 -8
  53. package/src/ir/index.js +18 -4
  54. package/src/ir/parser-js.js +10 -0
  55. package/src/language/CLAUDE.md +13 -0
  56. package/src/language/nix-option-types.js +170 -0
  57. package/src/language/nix-script-taint.js +46 -3
  58. package/src/language/nix-secrets.js +20 -2
  59. package/src/lineage/CLAUDE.md +72 -8
  60. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  61. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  62. package/src/lineage/boundary-contract.js +273 -0
  63. package/src/lineage/completion.js +205 -0
  64. package/src/lineage/continuation.js +140 -0
  65. package/src/lineage/control-credit.js +313 -0
  66. package/src/lineage/correlation.js +199 -0
  67. package/src/lineage/coverage.js +8 -1
  68. package/src/lineage/cross-repo-link.js +22 -0
  69. package/src/lineage/cut-planner.js +548 -0
  70. package/src/lineage/driver.js +219 -66
  71. package/src/lineage/engine.js +166 -21
  72. package/src/lineage/export-json.js +5 -0
  73. package/src/lineage/graph-builder.js +84 -7
  74. package/src/lineage/haskell-view.js +59 -0
  75. package/src/lineage/index.js +3 -0
  76. package/src/lineage/nix-view.js +35 -0
  77. package/src/lineage/owned-build.js +77 -0
  78. package/src/lineage/owned-worker-entry.js +22 -0
  79. package/src/lineage/owned-worker.js +95 -0
  80. package/src/lineage/path-evidence.js +123 -0
  81. package/src/lineage/path-predicates.js +529 -0
  82. package/src/lineage/path-query.js +189 -20
  83. package/src/lineage/path-store.js +191 -17
  84. package/src/lineage/scc-solver.js +186 -0
  85. package/src/lineage/smt-orchestrator.js +174 -0
  86. package/src/lineage/smt-z3-backend.js +294 -0
  87. package/src/lineage/source-seeding.js +36 -2
  88. package/src/lineage/summaries.js +66 -4
  89. package/src/lineage/value-versions.js +130 -0
  90. package/src/lineage/witness-selection.js +95 -0
  91. package/src/pipeline/scan-health.js +18 -0
  92. package/src/posture/CLAUDE.md +17 -0
  93. package/src/posture/business-logic.js +16 -7
  94. package/src/posture/dag-commands.js +442 -0
  95. package/src/posture/evidence-bundle.js +1 -1
  96. package/src/posture/finding-defaults.js +19 -0
  97. package/src/posture/git-history.js +91 -1
  98. package/src/posture/witness-bundle.js +573 -0
  99. package/src/posture/witness-runtime.js +276 -0
  100. package/src/report/index.js +2 -0
  101. package/src/sast/CLAUDE.md +2 -0
  102. package/src/sast/_call-args.js +70 -0
  103. package/src/sast/init-scope.js +87 -0
  104. package/src/sast/listen-bind-address.js +49 -0
  105. package/src/sast/redirect-target.js +106 -0
  106. package/src/sast/reflected-input.js +76 -0
  107. package/src/sast/send-file-root.js +27 -0
  108. package/src/sast/sensitive-data-logging.js +38 -1
  109. package/dist/119.index.js +0 -197
  110. package/dist/128.index.js +0 -129
  111. package/dist/1293.index.js +0 -5689
  112. package/dist/1336.index.js +0 -16182
  113. package/dist/1351.index.js +0 -133
  114. package/dist/1387.index.js +0 -271
  115. package/dist/1437.index.js +0 -23
  116. package/dist/1445.index.js +0 -110
  117. package/dist/1505.index.js +0 -151
  118. package/dist/1557.index.js +0 -293
  119. package/dist/1621.index.js +0 -311
  120. package/dist/1751.index.js +0 -160
  121. package/dist/1765.index.js +0 -727
  122. package/dist/1861.index.js +0 -690
  123. package/dist/1906.index.js +0 -433
  124. package/dist/208.index.js +0 -1631
  125. package/dist/2223.index.js +0 -159
  126. package/dist/2238.index.js +0 -295
  127. package/dist/2530.index.js +0 -152
  128. package/dist/2565.index.js +0 -116
  129. package/dist/2574.index.js +0 -127
  130. package/dist/2620.index.js +0 -345
  131. package/dist/2773.index.js +0 -98
  132. package/dist/2775.index.js +0 -32
  133. package/dist/2966.index.js +0 -171
  134. package/dist/2972.index.js +0 -87
  135. package/dist/2991.index.js +0 -131
  136. package/dist/3063.index.js +0 -833
  137. package/dist/3075.index.js +0 -227
  138. package/dist/3121.index.js +0 -297
  139. package/dist/3314.index.js +0 -263
  140. package/dist/3480.index.js +0 -1822
  141. package/dist/3481.index.js +0 -24
  142. package/dist/3506.index.js +0 -268
  143. package/dist/3533.index.js +0 -591
  144. package/dist/3653.index.js +0 -450
  145. package/dist/3740.index.js +0 -722
  146. package/dist/3822.index.js +0 -131
  147. package/dist/3868.index.js +0 -292
  148. package/dist/3918.index.js +0 -192
  149. package/dist/4090.index.js +0 -288
  150. package/dist/4113.index.js +0 -1286
  151. package/dist/4121.index.js +0 -1190
  152. package/dist/4142.index.js +0 -690
  153. package/dist/4168.index.js +0 -509
  154. package/dist/4281.index.js +0 -993
  155. package/dist/4336.index.js +0 -436
  156. package/dist/4387.index.js +0 -532
  157. package/dist/4413.index.js +0 -302
  158. package/dist/4476.index.js +0 -4510
  159. package/dist/4491.index.js +0 -133
  160. package/dist/4538.index.js +0 -185
  161. package/dist/4684.index.js +0 -319
  162. package/dist/4740.index.js +0 -165
  163. package/dist/4780.index.js +0 -209
  164. package/dist/4802.index.js +0 -225
  165. package/dist/4812.index.js +0 -379
  166. package/dist/4835.index.js +0 -126
  167. package/dist/494.index.js +0 -226
  168. package/dist/4991.index.js +0 -549
  169. package/dist/5199.index.js +0 -305
  170. package/dist/5216.index.js +0 -109
  171. package/dist/5439.index.js +0 -301
  172. package/dist/5443.index.js +0 -97
  173. package/dist/5496.index.js +0 -377
  174. package/dist/5738.index.js +0 -240
  175. package/dist/5801.index.js +0 -588
  176. package/dist/5817.index.js +0 -714
  177. package/dist/5858.index.js +0 -223
  178. package/dist/5933.index.js +0 -143
  179. package/dist/601.index.js +0 -23
  180. package/dist/6173.index.js +0 -273
  181. package/dist/6291.index.js +0 -1871
  182. package/dist/6312.index.js +0 -666
  183. package/dist/634.index.js +0 -16182
  184. package/dist/644.index.js +0 -87
  185. package/dist/6508.index.js +0 -283
  186. package/dist/6560.index.js +0 -645
  187. package/dist/6576.index.js +0 -422
  188. package/dist/6579.index.js +0 -271
  189. package/dist/6655.index.js +0 -3221
  190. package/dist/6745.index.js +0 -305
  191. package/dist/6818.index.js +0 -21642
  192. package/dist/6932.index.js +0 -1190
  193. package/dist/6972.index.js +0 -167
  194. package/dist/6995.index.js +0 -163
  195. package/dist/7249.index.js +0 -191
  196. package/dist/7372.index.js +0 -1197
  197. package/dist/7546.index.js +0 -541
  198. package/dist/76.index.js +0 -448
  199. package/dist/7879.index.js +0 -163
  200. package/dist/8025.index.js +0 -130
  201. package/dist/8054.index.js +0 -248
  202. package/dist/808.index.js +0 -1330
  203. package/dist/8097.index.js +0 -201
  204. package/dist/8126.index.js +0 -191
  205. package/dist/8178.index.js +0 -78
  206. package/dist/8382.index.js +0 -93
  207. package/dist/8408.index.js +0 -261
  208. package/dist/8540.index.js +0 -174
  209. package/dist/8635.index.js +0 -802
  210. package/dist/8688.index.js +0 -410
  211. package/dist/8699.index.js +0 -779
  212. package/dist/8891.index.js +0 -457
  213. package/dist/8916.index.js +0 -885
  214. package/dist/9146.index.js +0 -170
  215. package/dist/9168.index.js +0 -841
  216. package/dist/9292.index.js +0 -369
  217. package/dist/9422.index.js +0 -866
  218. package/dist/9440.index.js +0 -453
  219. package/dist/9479.index.js +0 -152
  220. package/dist/9481.index.js +0 -220
  221. package/dist/9539.index.js +0 -100
  222. package/dist/9547.index.js +0 -103
  223. package/dist/9692.index.js +0 -90
  224. package/dist/9800.index.js +0 -462
  225. package/dist/9899.index.js +0 -138
  226. package/dist/9981.index.js +0 -206
  227. package/dist/999.index.js +0 -117
  228. package/dist/9999.index.js +0 -1698
@@ -0,0 +1,1713 @@
1
+ export const id = 8158;
2
+ export const ids = [8158];
3
+ export const modules = {
4
+
5
+ /***/ 88158:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+
9
+ // EXPORTS
10
+ __webpack_require__.d(__webpack_exports__, {
11
+ GraphViewError: () => (/* reexport */ refs_GraphViewError),
12
+ dataflowView: () => (/* reexport */ dataflowView)
13
+ });
14
+
15
+ // UNUSED EXPORTS: ARTIFACT_ADAPTER_VERSION, CROSSREPO_ADAPTER_VERSION, CROSSWALK_VERSION, DATAFLOW_ADAPTER_VERSION, DEFAULT_TRUSTED_ORIGINS, EDGE_ORIGINS, FINDINGS_ADAPTER_VERSION, GRAPH_KINDS, IndexedGraphView, MAPPING_TYPES, PROVENANCE_ADAPTER_VERSION, REF_VERSION, RELATION_KINDS, SEMANTIC_ADAPTER_VERSION, SUPPORTED_ADAPTER_MAJORS, adjacencyCacheStats, artifactView, attributeObservation, canonicalJson, clearAdjacencyCache, createCrosswalk, createGraphRegistry, crossRepoLinkView, digestOf, edgeMatches, findingsView, foreign, graphIdentityKey, local, makeRef, mapCallSitesToWitnesses, mapFindingsToCallSites, mapProjectedNodesToSites, mapWitnessesToFlows, normalizeFilter, parseRef, provenanceView, refKey, sameRef, semanticView, validateRef
16
+
17
+ // EXTERNAL MODULE: external "node:crypto"
18
+ var external_node_crypto_ = __webpack_require__(77598);
19
+ ;// CONCATENATED MODULE: ./src/graph/refs.js
20
+ //
21
+ // refs.js: the graph reference format (DAG-06 R06.2).
22
+ //
23
+ // A reference names ONE entity in ONE immutable graph:
24
+ // { v, graphKind, repository, graphDigest, entityKind, localId }
25
+ //
26
+ // The local id is only unique inside its graph, so it is never used alone as
27
+ // a key. The repository identity and the graph digest are part of the
28
+ // reference, which is what keeps two repositories with identical local ids
29
+ // distinct and stops a stale reference from silently resolving against a
30
+ // different version of the graph.
31
+ //
32
+ // Pure functions, no I/O.
33
+
34
+
35
+
36
+ const refs_REF_VERSION = 1;
37
+
38
+ /** The graph layers that have an adapter. Every one keeps its own native schema. */
39
+ const GRAPH_KINDS = Object.freeze([
40
+ 'provenance', // PathStore pnode/pedge DAG
41
+ 'dataflow', // DataFlowGraph v1
42
+ 'semantic', // SemanticGraph v1
43
+ 'artifact', // artifact provenance graph
44
+ 'cross-repo-links', // declared CrossRepoLink records
45
+ 'findings', // scanner findings, as referenceable entities
46
+ ]);
47
+
48
+ class refs_GraphViewError extends Error {
49
+ constructor(code, message, detail = {}) {
50
+ super(`${code}: ${message}`);
51
+ this.name = 'GraphViewError';
52
+ this.code = code;
53
+ this.detail = detail;
54
+ }
55
+ }
56
+
57
+ const isNonEmpty = (v) => typeof v === 'string' && v.length > 0;
58
+
59
+ /** Deterministic key-sorted JSON, the canonical form every digest here hashes. */
60
+ function refs_canonicalJson(value) {
61
+ const canon = (v) => {
62
+ if (Array.isArray(v)) return v.map(canon);
63
+ if (v && typeof v === 'object') {
64
+ const o = {};
65
+ for (const k of Object.keys(v).sort()) if (v[k] !== undefined) o[k] = canon(v[k]);
66
+ return o;
67
+ }
68
+ return v;
69
+ };
70
+ return JSON.stringify(canon(value));
71
+ }
72
+
73
+ function refs_digestOf(value) {
74
+ return createHash('sha256').update(refs_canonicalJson(value)).digest('hex');
75
+ }
76
+
77
+ /**
78
+ * Build a reference object. Throws GraphViewError('BAD_REF') on a missing
79
+ * component: a reference with a blank part is never constructed.
80
+ */
81
+ function refs_makeRef({ graphKind, repository, graphDigest, entityKind, localId }) {
82
+ const ref = { v: refs_REF_VERSION, graphKind, repository, graphDigest, entityKind, localId };
83
+ const bad = refs_validateRef(ref);
84
+ if (bad) throw new refs_GraphViewError('BAD_REF', bad, { ref });
85
+ return Object.freeze(ref);
86
+ }
87
+
88
+ /** @returns {string|null} a problem description, or null when the reference is well formed */
89
+ function refs_validateRef(ref) {
90
+ if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return 'a reference is an object';
91
+ if (ref.v !== refs_REF_VERSION) return `unsupported reference version ${JSON.stringify(ref.v)} (supported: ${refs_REF_VERSION})`;
92
+ if (!GRAPH_KINDS.includes(ref.graphKind)) return `unknown graph kind ${JSON.stringify(ref.graphKind)}`;
93
+ for (const f of ['repository', 'graphDigest', 'entityKind', 'localId']) {
94
+ if (!isNonEmpty(ref[f])) return `${f} is required`;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /** The identity of the graph a reference points into (no entity part). */
100
+ function refs_graphIdentityKey(ref) {
101
+ return [ref.graphKind, ref.repository, ref.graphDigest].map(encodeURIComponent).join('|');
102
+ }
103
+
104
+ /** Stable string form; also the map key used by every index. */
105
+ function refs_refKey(ref) {
106
+ const bad = refs_validateRef(ref);
107
+ if (bad) throw new refs_GraphViewError('BAD_REF', bad, { ref });
108
+ return `gref${ref.v}|${[ref.graphKind, ref.repository, ref.graphDigest, ref.entityKind, ref.localId].map(encodeURIComponent).join('|')}`;
109
+ }
110
+
111
+ function parseRef(text) {
112
+ if (typeof text !== 'string') throw new refs_GraphViewError('BAD_REF', 'a reference string is required');
113
+ const parts = text.split('|');
114
+ const m = /^gref(\d+)$/.exec(parts[0] || '');
115
+ if (!m || parts.length !== 6) throw new refs_GraphViewError('BAD_REF', 'malformed reference string');
116
+ let decoded;
117
+ try { decoded = parts.slice(1).map(decodeURIComponent); } catch { throw new refs_GraphViewError('BAD_REF', 'malformed escape in reference string'); }
118
+ const [graphKind, repository, graphDigest, entityKind, localId] = decoded;
119
+ return refs_makeRef({ graphKind, repository, graphDigest, entityKind, localId, v: Number(m[1]) });
120
+ }
121
+
122
+ function sameRef(a, b) {
123
+ return refs_refKey(a) === refs_refKey(b);
124
+ }
125
+
126
+ ;// CONCATENATED MODULE: ./src/graph/relations.js
127
+ //
128
+ // relations.js: relation kinds and the traversal filter (DAG-06 R06.3, R06.4).
129
+ //
130
+ // Two independent axes, never collapsed into one:
131
+ // - RELATION: what the edge means (data flow, calls, ...).
132
+ // - ORIGIN: how the edge came to exist (derived from code, declared by an
133
+ // operator, observed at runtime, ...).
134
+ //
135
+ // A query chooses the relations it may traverse AND the origins it trusts.
136
+ // The default filter traverses code-derived and derived edges only, so an
137
+ // operator declaration or a runtime observation is never silently walked as
138
+ // if it were a code-derived data flow (R06.4).
139
+
140
+
141
+
142
+ const RELATION_KINDS = Object.freeze([
143
+ 'data_flow', // a value moves from one place to another
144
+ 'control_dependence', // execution of B depends on a decision at A
145
+ 'calls', // a call site invokes a callee
146
+ 'returns', // a callee result comes back to a caller
147
+ 'dependency', // A needs B (package, module, component)
148
+ 'declaration', // A declares/contains B (symbol -> call site, origin)
149
+ 'observation', // a runtime observation attached to an entity
150
+ 'artifact_derivation', // A was built into / produced B
151
+ ]);
152
+
153
+ const EDGE_ORIGINS = Object.freeze([
154
+ 'code', // read directly from analyzed source
155
+ 'derived', // computed from code-derived facts by a projection or index
156
+ 'declared', // asserted by an operator (cross-repo links, deployments)
157
+ 'observed', // seen at runtime (telemetry)
158
+ 'schema', // taken from a schema or manifest
159
+ ]);
160
+
161
+ /** Origins a default query trusts as "the program's own structure". */
162
+ const DEFAULT_TRUSTED_ORIGINS = Object.freeze(['code', 'derived', 'schema']);
163
+
164
+ /**
165
+ * Normalize a relation filter. Accepted forms:
166
+ * undefined / null -> every relation, trusted origins only
167
+ * 'calls' | ['calls', ...] -> those relations, trusted origins only
168
+ * { relations, origins } -> explicit on both axes
169
+ * Unknown relation or origin names throw, so a typo never silently widens
170
+ * or empties a query.
171
+ */
172
+ function normalizeFilter(filter) {
173
+ let relations = null;
174
+ let origins = DEFAULT_TRUSTED_ORIGINS;
175
+ if (filter === undefined || filter === null) {
176
+ // defaults
177
+ } else if (typeof filter === 'string') {
178
+ relations = [filter];
179
+ } else if (Array.isArray(filter)) {
180
+ relations = filter;
181
+ } else if (typeof filter === 'object') {
182
+ if (filter.relations !== undefined && filter.relations !== null) {
183
+ relations = typeof filter.relations === 'string' ? [filter.relations] : filter.relations;
184
+ }
185
+ if (filter.origins !== undefined && filter.origins !== null) {
186
+ origins = typeof filter.origins === 'string' ? [filter.origins] : filter.origins;
187
+ }
188
+ } else {
189
+ throw new refs_GraphViewError('BAD_FILTER', 'a relation filter is a name, a list of names or { relations, origins }');
190
+ }
191
+ if (relations !== null) {
192
+ if (!Array.isArray(relations)) throw new refs_GraphViewError('BAD_FILTER', 'relations must be a list');
193
+ for (const r of relations) {
194
+ if (!RELATION_KINDS.includes(r)) throw new refs_GraphViewError('BAD_FILTER', `unknown relation kind ${JSON.stringify(r)}`);
195
+ }
196
+ }
197
+ if (!Array.isArray(origins)) throw new refs_GraphViewError('BAD_FILTER', 'origins must be a list');
198
+ for (const o of origins) {
199
+ if (!EDGE_ORIGINS.includes(o)) throw new refs_GraphViewError('BAD_FILTER', `unknown edge origin ${JSON.stringify(o)}`);
200
+ }
201
+ return { relations: relations ? new Set(relations) : null, origins: new Set(origins) };
202
+ }
203
+
204
+ function edgeMatches(edge, nf) {
205
+ if (nf.relations && !nf.relations.has(edge.relation)) return false;
206
+ return nf.origins.has(edge.origin);
207
+ }
208
+
209
+ ;// CONCATENATED MODULE: ./src/graph/view.js
210
+ //
211
+ // view.js: the read-only typed graph view contract (DAG-06 R06.1) and the
212
+ // shared adjacency-index cache (R06.6).
213
+ //
214
+ // A GraphView is a READ-ONLY lens over one existing graph. It never replaces
215
+ // the graph's own schema: every node and edge keeps its original object in
216
+ // `native`, every original id is preserved, and `reconstruct()` hands back the
217
+ // source graph so an adapter round trip loses nothing.
218
+ //
219
+ // The contract (every adapter exposes exactly this):
220
+ // identity { graphKind, repository, graphDigest, adapterVersion }
221
+ // metadata() adapter/graph facts: counts, schema versions, sources
222
+ // completion() { status: 'complete'|'partial'|'unknown', reasons[], coverage }
223
+ // has(ref) boolean, never throws on a well-formed ref
224
+ // node(ref) the entity, or throws DANGLING_REF / WRONG_GRAPH
225
+ // incoming(ref, f) edges ending at ref, restricted by the relation filter
226
+ // outgoing(ref, f) edges starting at ref, restricted by the relation filter
227
+ // reconstruct() the original source graph (round trip)
228
+ //
229
+ // Adjacency indexes are built once per immutable (graphKind, digest, adapter
230
+ // version) and shared by every view over the same content. The index is
231
+ // keyed by LOCAL ids and references are rebuilt per view, so two repositories
232
+ // that happen to share content never share each other's identity.
233
+
234
+
235
+
236
+
237
+
238
+ const localKey = (entityKind, localId) => `${entityKind}\u0000${localId}`;
239
+
240
+ // ---- adjacency cache -------------------------------------------------------------------------------------------------------
241
+
242
+ const CACHE_LIMIT = 64;
243
+ const _cache = new Map(); // cacheKey -> { fingerprint, index }
244
+ const _stats = { builds: 0, hits: 0, evictions: 0 };
245
+
246
+ function adjacencyCacheStats() { return { ..._stats, size: _cache.size }; }
247
+ function clearAdjacencyCache() { _cache.clear(); _stats.builds = 0; _stats.hits = 0; _stats.evictions = 0; }
248
+
249
+ function buildIndex(templates) {
250
+ const out = new Map();
251
+ const inn = new Map();
252
+ const push = (m, k, e) => { const a = m.get(k); if (a) a.push(e); else m.set(k, [e]); };
253
+ const endKey = (end) => (end.local ? localKey(end.entityKind, end.localId) : refs_refKey(end.ref));
254
+ for (const t of templates) {
255
+ push(out, endKey(t.from), t);
256
+ push(inn, endKey(t.to), t);
257
+ }
258
+ return { out, in: inn };
259
+ }
260
+
261
+ function sharedIndex({ graphKind, graphDigest, adapterVersion }, templates, fingerprint) {
262
+ const key = `${graphKind}|${graphDigest}|${adapterVersion}`;
263
+ const hit = _cache.get(key);
264
+ if (hit) {
265
+ if (hit.fingerprint !== fingerprint) {
266
+ // The digest is the cache key because a graph with that digest is immutable.
267
+ // Different content under the same digest means the digest was not computed
268
+ // from the content, and serving the cached index would be a silent wrong answer.
269
+ throw new refs_GraphViewError('DIGEST_CONFLICT', `graph digest ${graphDigest} was seen with different content`, { key });
270
+ }
271
+ _stats.hits += 1;
272
+ _cache.delete(key); _cache.set(key, hit); // LRU touch
273
+ return hit.index;
274
+ }
275
+ const index = buildIndex(templates);
276
+ _stats.builds += 1;
277
+ _cache.set(key, { fingerprint, index });
278
+ if (_cache.size > CACHE_LIMIT) {
279
+ _cache.delete(_cache.keys().next().value);
280
+ _stats.evictions += 1;
281
+ }
282
+ return index;
283
+ }
284
+
285
+ // ---- edge templates --------------------------------------------------------------------------------------------------------
286
+
287
+ /**
288
+ * Adapters describe edges with `local(entityKind, localId)` endpoints for
289
+ * entities of their own graph, or `foreign(ref)` for an endpoint that lives in
290
+ * another graph (cross-repository declarations).
291
+ */
292
+ const view_local = (entityKind, localId) => ({ local: true, entityKind, localId });
293
+ const view_foreign = (ref) => {
294
+ const bad = validateRef(ref);
295
+ if (bad) throw new GraphViewError('BAD_REF', bad);
296
+ return { local: false, ref };
297
+ };
298
+
299
+ function checkTemplate(t) {
300
+ if (!t || typeof t.id !== 'string' || !t.id) throw new refs_GraphViewError('BAD_EDGE', 'an edge needs an id');
301
+ if (!RELATION_KINDS.includes(t.relation)) throw new refs_GraphViewError('BAD_EDGE', `edge ${t.id}: unknown relation ${JSON.stringify(t.relation)}`);
302
+ if (!EDGE_ORIGINS.includes(t.origin)) throw new refs_GraphViewError('BAD_EDGE', `edge ${t.id}: unknown origin ${JSON.stringify(t.origin)}`);
303
+ if (!t.from || !t.to) throw new refs_GraphViewError('BAD_EDGE', `edge ${t.id}: both endpoints are required`);
304
+ }
305
+
306
+ // ---- the view --------------------------------------------------------------------------------------------------------------
307
+
308
+ class view_IndexedGraphView {
309
+ /**
310
+ * @param {object} spec
311
+ * @param {string} spec.graphKind
312
+ * @param {string} spec.adapterVersion semver of the adapter, not of the graph schema
313
+ * @param {string} spec.repository
314
+ * @param {string} spec.graphDigest digest of the SOURCE graph, preserved as given
315
+ * @param {object} spec.metadata
316
+ * @param {object} spec.completion { status, reasons, coverage }
317
+ * @param {{entityKind:string, localId:string, native:any, summary?:object}[]} spec.nodes
318
+ * @param {object[]} spec.edges edge templates (see local()/foreign())
319
+ * @param {() => any} spec.reconstruct returns the original source graph
320
+ * @param {boolean} [spec.allowForeign] endpoints may live in other graphs
321
+ */
322
+ constructor(spec) {
323
+ if (!GRAPH_KINDS.includes(spec.graphKind)) throw new refs_GraphViewError('BAD_REF', `unknown graph kind ${spec.graphKind}`);
324
+ if (!/^\d+\.\d+\.\d+$/.test(spec.adapterVersion || '')) throw new refs_GraphViewError('BAD_VERSION', 'adapterVersion must be a semantic version');
325
+ for (const f of ['repository', 'graphDigest']) {
326
+ if (typeof spec[f] !== 'string' || !spec[f]) throw new refs_GraphViewError('BAD_REF', `${f} is required`);
327
+ }
328
+ this.graphKind = spec.graphKind;
329
+ this.adapterVersion = spec.adapterVersion;
330
+ this.repository = spec.repository;
331
+ this.graphDigest = spec.graphDigest;
332
+ this._metadata = spec.metadata ?? {};
333
+ this._completion = spec.completion ?? { status: 'unknown', reasons: ['adapter supplied no completion record'], coverage: null };
334
+ this._reconstruct = spec.reconstruct;
335
+ this._allowForeign = !!spec.allowForeign;
336
+
337
+ this._nodes = new Map();
338
+ for (const n of spec.nodes) {
339
+ const k = localKey(n.entityKind, n.localId);
340
+ if (this._nodes.has(k)) throw new refs_GraphViewError('DUPLICATE_ID', `${this.graphKind} graph has two ${n.entityKind} entities with id ${n.localId}`, { entityKind: n.entityKind, localId: n.localId });
341
+ this._nodes.set(k, n);
342
+ }
343
+ this._templates = [];
344
+ const seen = new Set();
345
+ for (const t of spec.edges) {
346
+ checkTemplate(t);
347
+ if (seen.has(t.id)) throw new refs_GraphViewError('DUPLICATE_ID', `${this.graphKind} graph has two edges with id ${t.id}`, { edgeId: t.id });
348
+ seen.add(t.id);
349
+ for (const end of [t.from, t.to]) {
350
+ if (end.local && !this._nodes.has(localKey(end.entityKind, end.localId))) {
351
+ throw new refs_GraphViewError('DANGLING_REF', `edge ${t.id} points at ${end.entityKind} ${end.localId}, which is not in the graph`, { edgeId: t.id });
352
+ }
353
+ if (!end.local && !this._allowForeign) throw new refs_GraphViewError('BAD_EDGE', `edge ${t.id}: foreign endpoints are only allowed in declaration views`);
354
+ }
355
+ this._templates.push(t);
356
+ }
357
+ // Content fingerprint over every entity key and edge id (order-insensitive). A
358
+ // digest names one immutable graph; the fingerprint is how a digest that does
359
+ // NOT match its content is caught instead of served from the shared index.
360
+ const h = (0,external_node_crypto_.createHash)('sha256');
361
+ for (const k of [...this._nodes.keys()].sort()) h.update(k).update('\u0001');
362
+ h.update('\u0002');
363
+ for (const t of [...this._templates].map((x) => x.id).sort()) h.update(t).update('\u0001');
364
+ this.fingerprint = h.digest('hex');
365
+ this._index = sharedIndex(this.identity, this._templates, this.fingerprint);
366
+ }
367
+
368
+ get identity() {
369
+ return { graphKind: this.graphKind, repository: this.repository, graphDigest: this.graphDigest, adapterVersion: this.adapterVersion };
370
+ }
371
+
372
+ /** Reference to one of this graph's own entities. Throws DANGLING_REF if it is not there. */
373
+ ref(entityKind, localId) {
374
+ if (!this._nodes.has(localKey(entityKind, localId))) {
375
+ throw new refs_GraphViewError('DANGLING_REF', `no ${entityKind} ${localId} in this ${this.graphKind} graph`, { entityKind, localId });
376
+ }
377
+ return refs_makeRef({ graphKind: this.graphKind, repository: this.repository, graphDigest: this.graphDigest, entityKind, localId });
378
+ }
379
+
380
+ owns(ref) {
381
+ return ref.graphKind === this.graphKind && ref.repository === this.repository && ref.graphDigest === this.graphDigest;
382
+ }
383
+
384
+ _checkRef(ref) {
385
+ const bad = refs_validateRef(ref);
386
+ if (bad) {
387
+ throw new refs_GraphViewError(ref && ref.v !== undefined && ref.v !== refs_REF_VERSION ? 'VERSION_INCOMPATIBLE' : 'BAD_REF', bad, { ref });
388
+ }
389
+ }
390
+
391
+ has(ref) {
392
+ this._checkRef(ref);
393
+ return this.owns(ref) && this._nodes.has(localKey(ref.entityKind, ref.localId));
394
+ }
395
+
396
+ node(ref) {
397
+ this._checkRef(ref);
398
+ if (!this.owns(ref)) {
399
+ throw new refs_GraphViewError('WRONG_GRAPH', `reference points into ${ref.graphKind}/${ref.repository}/${ref.graphDigest.slice(0, 12)}, not this view`, { ref });
400
+ }
401
+ const n = this._nodes.get(localKey(ref.entityKind, ref.localId));
402
+ if (!n) throw new refs_GraphViewError('DANGLING_REF', `no ${ref.entityKind} ${ref.localId} in this ${this.graphKind} graph`, { ref });
403
+ return { ref, entityKind: n.entityKind, localId: n.localId, summary: n.summary ?? null, native: n.native };
404
+ }
405
+
406
+ _lookup(map, ref) {
407
+ this._checkRef(ref);
408
+ let key;
409
+ if (this.owns(ref)) {
410
+ key = localKey(ref.entityKind, ref.localId);
411
+ if (!this._nodes.has(key)) throw new refs_GraphViewError('DANGLING_REF', `no ${ref.entityKind} ${ref.localId} in this ${this.graphKind} graph`, { ref });
412
+ } else if (this._allowForeign) {
413
+ key = refs_refKey(ref);
414
+ } else {
415
+ throw new refs_GraphViewError('WRONG_GRAPH', 'reference points into a different graph', { ref });
416
+ }
417
+ return map.get(key) ?? [];
418
+ }
419
+
420
+ _materialize(t) {
421
+ const end = (e) => (e.local ? this.ref(e.entityKind, e.localId) : e.ref);
422
+ return {
423
+ id: t.id, relation: t.relation, origin: t.origin,
424
+ from: end(t.from), to: end(t.to),
425
+ evidence: t.evidence ?? { grade: 'none', source: null },
426
+ unknowns: t.unknowns ?? [],
427
+ attrs: t.attrs ?? {},
428
+ native: t.native,
429
+ };
430
+ }
431
+
432
+ outgoing(ref, filter) {
433
+ const nf = normalizeFilter(filter);
434
+ return this._lookup(this._index.out, ref).filter((t) => edgeMatches(t, nf)).map((t) => this._materialize(t));
435
+ }
436
+
437
+ incoming(ref, filter) {
438
+ const nf = normalizeFilter(filter);
439
+ return this._lookup(this._index.in, ref).filter((t) => edgeMatches(t, nf)).map((t) => this._materialize(t));
440
+ }
441
+
442
+ /** Entities in insertion order, optionally of one kind. */
443
+ entities(entityKind) {
444
+ const out = [];
445
+ for (const n of this._nodes.values()) if (!entityKind || n.entityKind === entityKind) out.push(this.node(this.ref(n.entityKind, n.localId)));
446
+ return out;
447
+ }
448
+
449
+ edges() { return this._templates.map((t) => this._materialize(t)); }
450
+
451
+ metadata() {
452
+ return { ...this._metadata, graphKind: this.graphKind, repository: this.repository, graphDigest: this.graphDigest, adapterVersion: this.adapterVersion, nodeCount: this._nodes.size, edgeCount: this._templates.length };
453
+ }
454
+
455
+ completion() {
456
+ const c = this._completion;
457
+ return { status: c.status, reasons: [...(c.reasons ?? [])], coverage: c.coverage ?? null };
458
+ }
459
+
460
+ reconstruct() { return this._reconstruct(); }
461
+ }
462
+
463
+ ;// CONCATENATED MODULE: ./src/graph/adapters/provenance.js
464
+ //
465
+ // adapters/provenance.js: read-only view over a PathStore provenance DAG
466
+ // (pnode / pedge). Duck-typed on the store's read API (`nodes()`, `edges()`,
467
+ // `limitStatus()`, `diagnostics()`); it does not import path-store.js or any
468
+ // analysis engine.
469
+ //
470
+ // Entities: `pnode` (a point in the DAG) and `pedge` (one witness: a join of an
471
+ // in-half and an out-half at one program site). A pedge is BOTH an entity, so a
472
+ // crosswalk can name it as a witness, and a relation edge between two pnodes.
473
+ //
474
+ // Relation choice, from the store's own fields (nothing is guessed):
475
+ // widenReason 'control-dependence' -> control_dependence
476
+ // target path '<return>' -> returns
477
+ // crossScope (argument into a callee) -> calls
478
+ // otherwise -> data_flow
479
+ // Evidence grade reuses the flow-grade vocabulary: severed (loss reason),
480
+ // ambiguous (correlation ambiguity), widened, implicit, else explicit.
481
+
482
+
483
+
484
+
485
+ const PROVENANCE_ADAPTER_VERSION = '1.0.0';
486
+
487
+ function relationOf(e, toNode) {
488
+ if ((e.widenReasons || []).includes('control-dependence')) return 'control_dependence';
489
+ if (toNode && toNode.path === '<return>') return 'returns';
490
+ if (e.crossScope) return 'calls';
491
+ return 'data_flow';
492
+ }
493
+
494
+ function gradeOf(e) {
495
+ if ((e.lossReasons || []).length) return 'severed';
496
+ if (e.ambiguousCorrelation) return 'ambiguous';
497
+ if ((e.widenReasons || []).includes('control-dependence')) return 'implicit';
498
+ if ((e.widenReasons || []).length) return 'widened';
499
+ return 'explicit';
500
+ }
501
+
502
+ /**
503
+ * @param {{nodes:()=>object[], edges:()=>object[], limitStatus?:()=>object, diagnostics?:()=>object}} store
504
+ * @param {{repository:string, completion?:object}} opts
505
+ * `completion` may carry the driver's analysisCompletion record; it is
506
+ * folded into the view's completion() so an unfinished analysis never
507
+ * reads as complete.
508
+ */
509
+ function provenanceView(store, opts) {
510
+ if (!store || typeof store.nodes !== 'function' || typeof store.edges !== 'function') {
511
+ throw new TypeError('provenanceView requires a store with nodes() and edges()');
512
+ }
513
+ const repository = opts?.repository;
514
+ const pnodes = store.nodes();
515
+ const pedges = store.edges();
516
+ const byId = new Map(pnodes.map((n) => [n.id, n]));
517
+ const graphDigest = digestOf({ nodes: pnodes, edges: pedges });
518
+
519
+ const nodes = [
520
+ ...pnodes.map((n) => ({ entityKind: 'pnode', localId: n.id, native: n, summary: { kind: n.kind, scope: n.scope, path: n.path, truncated: !!n.truncated } })),
521
+ ...pedges.map((e) => ({ entityKind: 'pedge', localId: e.id, native: e, summary: { scope: e.scope, line: e.line ?? null, siteNodeId: e.siteNodeId ?? null, dataElementId: e.dataElementId } })),
522
+ ];
523
+ const edges = pedges.map((e) => {
524
+ const unknowns = [
525
+ ...(e.widenReasons || []).map((r) => ({ kind: 'widen', reason: r })),
526
+ ...(e.lossReasons || []).map((r) => ({ kind: 'loss', reason: r })),
527
+ ];
528
+ if (e.truncated) unknowns.push({ kind: 'truncated', reason: 'analysis-incomplete' });
529
+ return {
530
+ id: e.id, relation: relationOf(e, byId.get(e.toNodeId)), origin: 'code',
531
+ from: local('pnode', e.fromNodeId), to: local('pnode', e.toNodeId),
532
+ evidence: { grade: gradeOf(e), source: 'path-store' },
533
+ unknowns, attrs: { line: e.line ?? null, scope: e.scope, dataElementId: e.dataElementId }, native: e,
534
+ };
535
+ });
536
+
537
+ const reasons = [];
538
+ const lim = typeof store.limitStatus === 'function' ? store.limitStatus() : null;
539
+ if (lim?.hit) reasons.push('store-limit-hit');
540
+ const diag = typeof store.diagnostics === 'function' ? store.diagnostics() : null;
541
+ if (diag?.truncations?.length) reasons.push(`truncated-units:${diag.truncations.length}`);
542
+ if (diag?.orphanedPeerSources?.length) reasons.push(`orphaned-peer-sources:${diag.orphanedPeerSources.length}`);
543
+ if (diag?.malformed?.length) reasons.push(`malformed-hops:${diag.malformed.length}`);
544
+ const ac = opts?.completion ?? null;
545
+ if (ac && ac.status && ac.status !== 'complete') reasons.push(`analysis-${ac.status}`);
546
+ const status = reasons.length ? 'partial' : (lim || ac) ? 'complete' : 'unknown';
547
+
548
+ return new IndexedGraphView({
549
+ graphKind: 'provenance', adapterVersion: PROVENANCE_ADAPTER_VERSION, repository, graphDigest,
550
+ metadata: { source: 'PathStore', pnodes: pnodes.length, pedges: pedges.length, relationEdges: edges.length },
551
+ completion: { status, reasons, coverage: { limits: lim, analysis: ac } },
552
+ nodes, edges,
553
+ reconstruct: () => ({ nodes: pnodes, edges: pedges }),
554
+ });
555
+ }
556
+
557
+ // EXTERNAL MODULE: ./src/lineage/export-json.js
558
+ var export_json = __webpack_require__(40859);
559
+ ;// CONCATENATED MODULE: ./src/graph/adapters/dataflow.js
560
+ //
561
+ // adapters/dataflow.js: read-only view over a DataFlowGraph v1 document (the
562
+ // graph-builder output / signed export body).
563
+ //
564
+ // The graph keeps its own schema, validator and digest. This view:
565
+ // - uses the existing `computeGraphDigest` as the graph digest, so the
566
+ // digest in every reference equals the digest the export already signs;
567
+ // - exposes node, edge, flow, dataElement, evidence and transformation
568
+ // entities under their original ids;
569
+ // - maps `edge.provenance` to the origin axis (code -> code, schema ->
570
+ // schema, manual -> declared, runtime -> observed), so a runtime-observed
571
+ // or manually declared edge is never traversed as a code-derived one by a
572
+ // default query;
573
+ // - reports completion from `scanHealth`, `coverage` and `limitations`.
574
+ //
575
+ // Projected nodes are category-granular: one node can stand for many source
576
+ // sites. That coarsening is not hidden here; it is recorded by crosswalks
577
+ // (one-to-many) built in crosswalk-builders.js.
578
+
579
+
580
+
581
+
582
+ const DATAFLOW_ADAPTER_VERSION = '1.0.0';
583
+
584
+ const ORIGIN_OF_PROVENANCE = { code: 'code', schema: 'schema', manual: 'declared', runtime: 'observed' };
585
+
586
+ const ENTITY_SECTIONS = [
587
+ ['node', 'nodes'], ['edge', 'edges'], ['flow', 'flows'],
588
+ ['dataElement', 'dataElements'], ['evidence', 'evidence'], ['transformation', 'transformations'],
589
+ ];
590
+
591
+ /**
592
+ * @param {object} graph DataFlowGraph v1 (e.g. buildDataFlowGraph(...).graph)
593
+ * @param {{repository?:string}} [opts]
594
+ */
595
+ function dataflowView(graph, opts = {}) {
596
+ if (!graph || typeof graph !== 'object' || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
597
+ throw new TypeError('dataflowView requires a DataFlowGraph v1 object with nodes[] and edges[]');
598
+ }
599
+ const repository = opts.repository ?? graph.scope?.repository;
600
+ const graphDigest = (0,export_json.computeGraphDigest)(graph);
601
+
602
+ const nodes = [];
603
+ for (const [kind, section] of ENTITY_SECTIONS) {
604
+ for (const e of graph[section] || []) {
605
+ nodes.push({ entityKind: kind, localId: e.id, native: e, summary: kind === 'node' ? { kind: e.kind, subtype: e.subtype ?? null, label: e.label ?? null, coverageStatus: e.coverageStatus ?? null } : null });
606
+ }
607
+ }
608
+ const edges = graph.edges.map((e) => {
609
+ const unknowns = [];
610
+ if (e.protocol?.destinationResolution && e.protocol.destinationResolution !== 'literal') unknowns.push({ kind: 'destination-resolution', reason: e.protocol.destinationResolution });
611
+ if (e.coverageStatus && e.coverageStatus !== 'modeled') unknowns.push({ kind: 'coverage', reason: e.coverageStatus });
612
+ return {
613
+ id: e.id, relation: 'data_flow', origin: ORIGIN_OF_PROVENANCE[e.provenance] ?? 'schema',
614
+ from: view_local('node', e.from), to: view_local('node', e.to),
615
+ evidence: { grade: e.evidenceGrade ?? e.coverageStatus ?? 'none', source: 'dataflow-graph', evidenceRefs: e.evidenceRefs ?? [] },
616
+ unknowns, attrs: { provenance: e.provenance, relationship: e.relationship }, native: e,
617
+ };
618
+ });
619
+
620
+ const health = graph.scanHealth || {};
621
+ const reasons = [];
622
+ const status = health.status === 'complete' ? 'complete' : health.status ? 'partial' : 'unknown';
623
+ if (health.status && health.status !== 'complete') reasons.push(`scan-health:${health.status}${health.reason ? `:${health.reason}` : ''}`);
624
+ if (graph.coverage?.pathBudgetTruncation) reasons.push('path-budget-truncation');
625
+ if (health.analysisCompletion && health.analysisCompletion.status && health.analysisCompletion.status !== 'complete') reasons.push(`analysis-${health.analysisCompletion.status}`);
626
+ if ((graph.limitations || []).length) reasons.push(`limitations:${graph.limitations.length}`);
627
+
628
+ return new view_IndexedGraphView({
629
+ graphKind: 'dataflow', adapterVersion: DATAFLOW_ADAPTER_VERSION, repository, graphDigest,
630
+ metadata: { schemaVersion: graph.schemaVersion ?? null, graphId: graph.graphId ?? null, scope: graph.scope ?? null, counts: Object.fromEntries(ENTITY_SECTIONS.map(([k, s]) => [k, (graph[s] || []).length])) },
631
+ completion: { status: reasons.length && status === 'complete' ? 'partial' : status, reasons, coverage: graph.coverage ?? null },
632
+ nodes, edges,
633
+ reconstruct: () => graph,
634
+ });
635
+ }
636
+
637
+ ;// CONCATENATED MODULE: ./src/dataflow/analysis-limits.js
638
+ // Dynamic features the static analysis cannot resolve, reported instead of silently approximated (E2-03).
639
+ //
640
+ // Where the engine meets one of these it takes the sound direction (a write through an unknown key taints the whole container, a read through
641
+ // an unknown key reads the whole container) and loses precision. That is the right trade, but it must be visible: a result that depended on it is
642
+ // less precise than one that did not, and a reviewer should be able to see which functions contain such constructs.
643
+ //
644
+ // Pure: reads the Layer-1 IR only.
645
+ const LIMIT_KINDS = Object.freeze({
646
+ 'computed-write': 'a write through a key that is not a literal (`o[k] = v`): the engine taints the whole container `o`, so clean sibling fields read afterwards are over-approximated as tainted',
647
+ 'computed-read': 'a read through a key that is not a literal (`o[k]`): the engine cannot tell which field is read, so any tainted field of `o` reaches it',
648
+ 'eval-like': 'code built at run time (`eval`, `new Function`, `setTimeout` with a string): its content is not analysed',
649
+ 'dynamic-require': 'a module name computed at run time (`require(name)`, `import(name)`): the callee is unknown, so the call stays unresolved',
650
+ 'reflective-call': 'a call through reflection (`Reflect.apply`, `fn.call/apply` on a computed target, `obj[name]()`): the callee is unknown',
651
+ 'getter-or-proxy': 'property access that may run user code (`Proxy`, `Object.defineProperty` accessors): reads and writes may have effects the engine does not model',
652
+ });
653
+
654
+ const expressions = (e, acc = []) => {
655
+ if (!e || typeof e !== 'object') return acc;
656
+ acc.push(e);
657
+ for (const k of ['object', 'callee', 'left', 'right', 'value']) if (e[k] && typeof e[k] === 'object') expressions(e[k], acc);
658
+ for (const k of ['args', 'parts', 'elements', 'props', 'branches']) if (Array.isArray(e[k])) for (const x of e[k]) expressions(x && x.value && !x.kind ? x.value : x, acc);
659
+ return acc;
660
+ };
661
+ const nameOf = (c) => (typeof c === 'string' ? c : c && c.kind === 'ident' ? c.name : c && c.kind === 'member' ? `${nameOf(c.object)}.${c.prop}` : '');
662
+
663
+ /** @returns {{kind:string, line:number, detail:string, effect:string}[]} */
664
+ function limitsOfFunction(fn) {
665
+ const out = [];
666
+ const add = (kind, line, detail) => out.push({ kind, line: Number.isInteger(line) ? line : (fn.line || 0), detail, effect: LIMIT_KINDS[kind] });
667
+ for (const n of Object.values(fn.cfg?.nodes || {})) {
668
+ if (n.kind === 'assign') {
669
+ if (typeof n.target === 'string' && /\.\*$/.test(n.target)) add('computed-write', n.line, n.target);
670
+ for (const x of expressions(n.source)) inspect(x, n.line, add);
671
+ } else if (n.kind === 'call') {
672
+ inspect({ kind: 'call', callee: n.callee, args: n.args }, n.line, add);
673
+ for (const a of n.args || []) for (const x of expressions(a)) inspect(x, n.line, add);
674
+ } else if (n.kind === 'return' && n.value) for (const x of expressions(n.value)) inspect(x, n.line, add);
675
+ }
676
+ return out;
677
+ }
678
+
679
+ function inspect(x, line, add) {
680
+ if (x.kind === 'member' && x.prop === '*') add('computed-read', line, `${nameOf(x.object)}[…]`);
681
+ if (x.kind === 'call') {
682
+ const c = nameOf(x.callee);
683
+ if (/^(?:eval|Function|new Function|setTimeout|setInterval)$/.test(c)) { if (c === 'eval' || c === 'Function' || c === 'new Function' || (x.args && x.args[0] && x.args[0].kind !== 'function-value' && x.args[0].kind !== 'ident' && x.args[0].kind !== 'literal')) add('eval-like', line, c); else if ((c === 'setTimeout' || c === 'setInterval') && x.args && x.args[0] && x.args[0].kind === 'literal' && typeof x.args[0].value === 'string') add('eval-like', line, `${c}(string)`); }
684
+ if (c === 'require' && x.args && x.args[0] && x.args[0].kind !== 'literal') add('dynamic-require', line, 'require(<expression>)');
685
+ if (/^Reflect\.(?:apply|construct|get|set)$/.test(c)) add('reflective-call', line, c);
686
+ if (x.callee && x.callee.kind === 'member' && x.callee.prop === '*') add('reflective-call', line, `${nameOf(x.callee.object)}[…](…)`);
687
+ if (/^(?:Object\.defineProperty|Object\.defineProperties|Object\.setPrototypeOf)$/.test(c) || c === 'Proxy' || c === 'new Proxy') add('getter-or-proxy', line, c);
688
+ }
689
+ }
690
+
691
+ /**
692
+ * @param {{perFile:Record<string,{functions:object[]}>}} projectIR
693
+ * @returns {{file:string, qid:string, function:string, kind:string, line:number, detail:string, effect:string}[]} sorted, stable
694
+ */
695
+ function analysis_limits_analysisLimits(projectIR) {
696
+ const out = [];
697
+ for (const file of Object.keys(projectIR?.perFile || {}).sort()) {
698
+ for (const fn of projectIR.perFile[file].functions || []) {
699
+ for (const l of limitsOfFunction(fn)) out.push({ file, qid: String(fn.qid).replace(/@\d+$/, ''), function: fn.name, ...l });
700
+ }
701
+ }
702
+ return out.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || (a.kind < b.kind ? -1 : 1)));
703
+ }
704
+
705
+ ;// CONCATENATED MODULE: ./src/ir/semantic-graph.js
706
+ // SemanticGraph v1: a typed, versioned, additive view over the Layer-1 IR (E2-01).
707
+ //
708
+ // WHY. The IR (`buildProjectIR`) is a bag of per-function records consumed by the taint engine. It has no stable identifiers, no shared
709
+ // entity types and no serialization contract, so every analysis that needs "the call at this place" or "where did this value come from"
710
+ // re-derives it with its own conventions, and nothing can be compared across runs. This module names the entities once, gives each a
711
+ // STABLE id, and fixes a versioned wire format.
712
+ //
713
+ // ADDITIVE. Building the graph reads the IR and never modifies it; it is not wired into findings, `chain[]` or last-scan.json. Existing
714
+ // output is unchanged by construction (a test pins that). A consumer opts in by calling `buildSemanticGraph`.
715
+ //
716
+ // HONEST. An entity type the IR cannot yet supply (Sanitizer, Guard, ServiceBoundary, StorageEdge) is declared in the schema, emitted as an
717
+ // empty list, and reported `schema-only` in `coverage` with the reason. A location the IR does not carry (column) is `null` and the entity
718
+ // says `locationPrecision: 'line'`. Nothing is guessed to fill a field.
719
+ //
720
+ // STABLE IDS. An id is the first 16 hex of SHA-256 over the entity's kind and its identity fields, which never include a line number: adding
721
+ // a blank line above a function does not change any id in it. Ties between otherwise identical entities (two `exec(x)` in one function) are
722
+ // broken by their order of appearance in that function.
723
+
724
+
725
+
726
+ const semantic_graph_SEMANTIC_GRAPH_SCHEMA = 'agentic-security/semantic-graph';
727
+ const SEMANTIC_GRAPH_VERSION = '1.0.0';
728
+ const semantic_graph_SUPPORTED_MAJOR = 1;
729
+
730
+ const semantic_graph_ENTITY_LISTS = Object.freeze(['symbols', 'callSites', 'allocations', 'fieldAccesses', 'valueOrigins', 'serviceBoundaries', 'storageEdges', 'sanitizers', 'guards']);
731
+ const CONFIDENCE_TIERS = Object.freeze(['observed', 'inferred', 'unresolved']);
732
+ const SYMBOL_KINDS = Object.freeze(['module', 'function', 'class', 'variable', 'parameter']);
733
+ const ORIGIN_KINDS = Object.freeze(['parameter', 'call-result', 'literal', 'member', 'concat', 'object', 'array', 'function', 'identifier', 'unknown']);
734
+
735
+ const ID_PREFIX = Object.freeze({ symbols: 'sym', callSites: 'call', allocations: 'alloc', fieldAccesses: 'field', valueOrigins: 'orig', serviceBoundaries: 'svc', storageEdges: 'store', sanitizers: 'san', guards: 'guard' });
736
+
737
+ const h16 = (parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 16);
738
+ const stripLine = (qid) => String(qid).replace(/@\d+$/, '');
739
+
740
+ // ---- canonical serialization ------------------------------------------------------------------------------------------------
741
+
742
+ function canon(v) {
743
+ if (Array.isArray(v)) return v.map(canon);
744
+ if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().map((k) => [k, canon(v[k])]));
745
+ return v;
746
+ }
747
+ function serializeGraph(graph) {
748
+ const g = { ...graph };
749
+ for (const k of semantic_graph_ENTITY_LISTS) g[k] = [...(graph[k] || [])].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
750
+ return JSON.stringify(canon(g));
751
+ }
752
+
753
+ // ---- validation -------------------------------------------------------------------------------------------------------------
754
+
755
+ /** @returns {{ok:boolean, errors:{code:string,path:string,message:string}[]}} Never throws on hostile input. */
756
+ function semantic_graph_validateGraph(g) {
757
+ const errors = [];
758
+ const err = (code, path, message) => errors.push({ code, path, message });
759
+ if (!g || typeof g !== 'object' || Array.isArray(g)) return { ok: false, errors: [{ code: 'NOT_AN_OBJECT', path: '', message: 'a graph is an object' }] };
760
+ if (g.schema !== semantic_graph_SEMANTIC_GRAPH_SCHEMA) { err('SCHEMA_MISMATCH', 'schema', `expected ${semantic_graph_SEMANTIC_GRAPH_SCHEMA}`); return { ok: false, errors }; }
761
+ const m = typeof g.schemaVersion === 'string' ? /^(\d+)\.(\d+)\.(\d+)$/.exec(g.schemaVersion) : null;
762
+ if (!m) { err('BAD_VERSION', 'schemaVersion', 'must be a semantic version'); return { ok: false, errors }; }
763
+ if (Number(m[1]) !== semantic_graph_SUPPORTED_MAJOR) { err('UNSUPPORTED_MAJOR', 'schemaVersion', `major ${m[1]} is not supported (supported: ${semantic_graph_SUPPORTED_MAJOR}); migrate it first`); return { ok: false, errors }; }
764
+ const ids = new Map();
765
+ for (const list of semantic_graph_ENTITY_LISTS) {
766
+ if (!Array.isArray(g[list])) { err('MISSING_FIELD', list, 'must be a list (empty when none)'); continue; }
767
+ g[list].forEach((e, i) => {
768
+ const p = `${list}[${i}]`;
769
+ if (!e || typeof e !== 'object' || Array.isArray(e)) { err('BAD_TYPE', p, 'must be an object'); return; }
770
+ if (typeof e.id !== 'string' || !new RegExp(`^${ID_PREFIX[list]}:[0-9a-f]{16}$`).test(e.id)) err('BAD_ID', `${p}.id`, `must look like ${ID_PREFIX[list]}:<16 hex>`);
771
+ else if (ids.has(e.id)) err('DUPLICATE_ID', `${p}.id`, `duplicate id ${e.id}`); else ids.set(e.id, list);
772
+ if (typeof e.file !== 'string' || !e.file || e.file.startsWith('/') || e.file.split('/').includes('..')) err('BAD_TYPE', `${p}.file`, 'must be a repository-relative path');
773
+ if (!(Number.isInteger(e.line) && e.line >= 0)) err('BAD_TYPE', `${p}.line`, 'line must be a non-negative integer');
774
+ if (!(e.column === null || (Number.isInteger(e.column) && e.column >= 0))) err('BAD_TYPE', `${p}.column`, 'column must be null or a non-negative integer');
775
+ if (e.column === null && e.locationPrecision !== 'line') err('RULE_VIOLATION', `${p}.locationPrecision`, "a null column requires locationPrecision 'line'");
776
+ const c = e.confidence;
777
+ if (!c || typeof c !== 'object' || !CONFIDENCE_TIERS.includes(c.tier) || typeof c.basis !== 'string' || !c.basis) err('BAD_TYPE', `${p}.confidence`, 'needs { tier, basis }');
778
+ });
779
+ }
780
+ if (errors.length) return { ok: false, errors };
781
+ const need = (e, p, field, list) => { if (e[field] !== null && e[field] !== undefined && ids.get(e[field]) !== list) err('DANGLING_REF', `${p}.${field}`, `${e[field]} is not a ${list} id`); };
782
+ g.symbols.forEach((e, i) => { if (!SYMBOL_KINDS.includes(e.kind)) err('UNKNOWN_ENUM', `symbols[${i}].kind`, `unknown symbol kind ${e.kind}`); need(e, `symbols[${i}]`, 'parent', 'symbols'); });
783
+ g.callSites.forEach((e, i) => { need(e, `callSites[${i}]`, 'caller', 'symbols'); need(e, `callSites[${i}]`, 'callee', 'symbols'); if (e.callee && e.resolution !== 'resolved') err('RULE_VIOLATION', `callSites[${i}].resolution`, 'a call with a callee must be resolved'); if (!e.callee && e.resolution === 'resolved') err('RULE_VIOLATION', `callSites[${i}].resolution`, 'a resolved call needs a callee'); (e.args || []).forEach((a, j) => need(a, `callSites[${i}].args[${j}]`, 'origin', 'valueOrigins')); });
784
+ g.valueOrigins.forEach((e, i) => { if (!ORIGIN_KINDS.includes(e.kind)) err('UNKNOWN_ENUM', `valueOrigins[${i}].kind`, `unknown origin kind ${e.kind}`); need(e, `valueOrigins[${i}]`, 'symbol', 'symbols'); });
785
+ g.fieldAccesses.forEach((e, i) => { need(e, `fieldAccesses[${i}]`, 'symbol', 'symbols'); if (!['read', 'write'].includes(e.mode)) err('UNKNOWN_ENUM', `fieldAccesses[${i}].mode`, 'mode must be read or write'); });
786
+ g.allocations.forEach((e, i) => need(e, `allocations[${i}]`, 'symbol', 'symbols'));
787
+ if (!g.coverage || typeof g.coverage !== 'object') err('MISSING_FIELD', 'coverage', 'coverage is required');
788
+ else for (const list of semantic_graph_ENTITY_LISTS) { const c = g.coverage[list]; if (!c || !['populated', 'schema-only'].includes(c.state) || !c.reason) err('BAD_TYPE', `coverage.${list}`, 'needs { state, reason }'); else if (c.state === 'schema-only' && g[list].length) err('RULE_VIOLATION', `coverage.${list}`, 'schema-only but not empty'); else if (c.state === 'populated' && !g[list].length) err('RULE_VIOLATION', `coverage.${list}`, 'populated but empty'); }
789
+ return { ok: errors.length === 0, errors };
790
+ }
791
+
792
+ /** Parse and validate a serialized graph. Returns { ok, graph?, errors }. */
793
+ function parseGraph(text) {
794
+ let g; try { g = JSON.parse(text); } catch (e) { return { ok: false, errors: [{ code: 'BAD_JSON', path: '', message: e.message }] }; }
795
+ const v = semantic_graph_validateGraph(g);
796
+ return v.ok ? { ok: true, graph: g, errors: [] } : { ok: false, errors: v.errors };
797
+ }
798
+
799
+ /** Bring an older-minor graph to this version. A different major is refused: there is no silent best-effort. */
800
+ function migrateGraph(g) {
801
+ const m = typeof g?.schemaVersion === 'string' ? /^(\d+)\.(\d+)\.(\d+)$/.exec(g.schemaVersion) : null;
802
+ if (!m || g.schema !== semantic_graph_SEMANTIC_GRAPH_SCHEMA) return { ok: false, reason: 'not a semantic graph' };
803
+ if (Number(m[1]) !== semantic_graph_SUPPORTED_MAJOR) return { ok: false, reason: `major ${m[1]} has no migration path to ${SEMANTIC_GRAPH_VERSION}` };
804
+ return { ok: true, graph: { ...g, schemaVersion: SEMANTIC_GRAPH_VERSION }, migrated: g.schemaVersion !== SEMANTIC_GRAPH_VERSION };
805
+ }
806
+
807
+ // ---- builder ----------------------------------------------------------------------------------------------------------------
808
+
809
+ const exprText = (e) => {
810
+ if (!e || typeof e !== 'object') return '';
811
+ switch (e.kind) {
812
+ case 'ident': return e.name;
813
+ case 'member': return `${exprText(e.object)}.${e.prop ?? '[]'}`;
814
+ case 'call': return `${exprText(e.callee)}(...)`;
815
+ case 'literal': return JSON.stringify(e.value);
816
+ default: return `<${e.kind || 'unknown'}>`;
817
+ }
818
+ };
819
+ const originKindOf = (e, params) => {
820
+ if (!e) return 'unknown';
821
+ if (e.kind === 'ident') return params.includes(e.name) ? 'parameter' : 'identifier';
822
+ if (e.kind === 'call') return 'call-result';
823
+ if (e.kind === 'literal') return 'literal';
824
+ if (e.kind === 'member') return 'member';
825
+ if (e.kind === 'binary' || e.kind === 'tpl') return 'concat';
826
+ if (e.kind === 'object') return 'object';
827
+ if (e.kind === 'array') return 'array';
828
+ if (e.kind === 'function-value') return 'function';
829
+ return 'unknown';
830
+ };
831
+ const loc = (file, line) => ({ file, line: Number.isInteger(line) ? line : 0, column: null, locationPrecision: 'line' });
832
+ const OBSERVED = { tier: 'observed', basis: 'read directly from the Layer-1 IR' };
833
+ const UNRESOLVED = { tier: 'unresolved', basis: 'the IR holds no resolved target for this call' };
834
+
835
+ function memberChain(e) {
836
+ const path = [];
837
+ let cur = e;
838
+ while (cur && cur.kind === 'member') { path.unshift(cur.prop ?? '[]'); cur = cur.object; }
839
+ return cur && cur.kind === 'ident' ? { base: cur.name, path } : null;
840
+ }
841
+ function walkExprs(e, visit) {
842
+ if (!e || typeof e !== 'object') return;
843
+ visit(e);
844
+ for (const k of ['object', 'callee', 'left', 'right', 'test', 'cons', 'alt']) if (e[k]) walkExprs(e[k], visit);
845
+ for (const k of ['args', 'parts', 'elements', 'values']) if (Array.isArray(e[k])) e[k].forEach((x) => walkExprs(x, visit));
846
+ if (Array.isArray(e.props)) e.props.forEach((p) => walkExprs(p?.value ?? p, visit));
847
+ }
848
+
849
+ /**
850
+ * @param {{perFile:object, callGraph?:object}} projectIR the object `buildProjectIR` returns
851
+ * @param {{engineVersion?:string}} [meta]
852
+ */
853
+ function buildSemanticGraph(projectIR, meta = {}) {
854
+ const g = Object.fromEntries(semantic_graph_ENTITY_LISTS.map((k) => [k, []]));
855
+ const ordinal = new Map();
856
+ const next = (key) => { const n = ordinal.get(key) ?? 0; ordinal.set(key, n + 1); return n; };
857
+ const symByQid = new Map();
858
+ const files = Object.keys(projectIR?.perFile || {}).sort();
859
+
860
+ // pass 1: symbols
861
+ for (const file of files) {
862
+ const pf = projectIR.perFile[file];
863
+ for (const fn of pf.functions || []) {
864
+ const isModule = fn.name === '<module>';
865
+ const key = `${file}|${stripLine(fn.qid)}`;
866
+ const id = `sym:${h16(['symbol', file, stripLine(fn.qid), next(`sym|${key}`)])}`;
867
+ symByQid.set(fn.qid, id);
868
+ g.symbols.push({ id, kind: isModule ? 'module' : 'function', name: fn.name, qid: stripLine(fn.qid), parent: null, params: [...(fn.params || [])], ...loc(file, fn.line), confidence: OBSERVED });
869
+ }
870
+ }
871
+ // function-by-name index for DIRECT same-file resolution only; anything else stays unresolved (cross-file resolution is E2-02)
872
+ const byFileName = new Map();
873
+ for (const file of files) for (const fn of projectIR.perFile[file].functions || []) if (fn.name !== '<module>') { const k = `${file}|${fn.name}`; byFileName.set(k, byFileName.has(k) ? null : symByQid.get(fn.qid)); }
874
+
875
+ // pass 2: call sites, origins, field accesses, allocations
876
+ for (const file of files) {
877
+ for (const fn of projectIR.perFile[file].functions || []) {
878
+ const symId = symByQid.get(fn.qid);
879
+ const params = fn.params || [];
880
+ const originFor = (expr, variable, line) => {
881
+ const kind = originKindOf(expr, params);
882
+ const o = { id: `orig:${h16(['origin', symId, variable, kind, exprText(expr), next(`orig|${symId}|${variable}|${kind}|${exprText(expr)}`)])}`, symbol: symId, variable, kind, text: exprText(expr), ...loc(file, line), confidence: kind === 'unknown' ? { tier: 'unresolved', basis: 'the IR could not model this expression' } : OBSERVED };
883
+ g.valueOrigins.push(o);
884
+ return o.id;
885
+ };
886
+ const field = (expr, mode, line) => {
887
+ const mc = memberChain(expr);
888
+ if (!mc || !mc.path.length) return;
889
+ g.fieldAccesses.push({ id: `field:${h16(['field', symId, mc.base, mc.path, mode, next(`field|${symId}|${mc.base}|${mc.path.join('.')}|${mode}`)])}`, symbol: symId, base: mc.base, path: mc.path, mode, ...loc(file, line), confidence: OBSERVED });
890
+ };
891
+ const alloc = (expr, line) => {
892
+ const kind = expr.kind === 'object' ? 'object' : expr.kind === 'array' ? 'array' : expr.kind === 'function-value' ? 'function' : null;
893
+ if (!kind) return;
894
+ g.allocations.push({ id: `alloc:${h16(['alloc', symId, kind, next(`alloc|${symId}|${kind}`)])}`, symbol: symId, kind, ...loc(file, line), confidence: OBSERVED });
895
+ };
896
+ for (const c of fn.calls || []) {
897
+ const calleeText = c.callee;
898
+ const direct = byFileName.get(`${file}|${calleeText}`) || null;
899
+ const args = (c.args || []).map((a, index) => { walkExprs(a, (x) => { if (x.kind === 'member') field(x, 'read', c.line); alloc(x, c.line); }); return { index, origin: originFor(a, `arg:${calleeText}:${index}`, c.line) }; });
900
+ g.callSites.push({ id: `call:${h16(['call', symId, calleeText, next(`call|${symId}|${calleeText}`)])}`, caller: symId, calleeText, callee: direct, resolution: direct ? 'resolved' : 'unresolved', args, ...loc(file, c.line), confidence: direct ? { tier: 'inferred', basis: 'a function of this name exists in the same file' } : UNRESOLVED });
901
+ }
902
+ const callFromExpr = (x, line) => {
903
+ const calleeText = exprText(x.callee).replace(/\(\.\.\.\)$/, '');
904
+ const direct = x.callee?.kind === 'ident' ? (byFileName.get(`${file}|${calleeText}`) || null) : null;
905
+ const args = (x.args || []).map((a, index) => ({ index, origin: originFor(a, `arg:${calleeText}:${index}`, line) }));
906
+ g.callSites.push({ id: `call:${h16(['call', symId, calleeText, next(`call|${symId}|${calleeText}`)])}`, caller: symId, calleeText, callee: direct, resolution: direct ? 'resolved' : 'unresolved', args, ...loc(file, line), confidence: direct ? { tier: 'inferred', basis: 'a function of this name exists in the same file' } : UNRESOLVED });
907
+ };
908
+ // The CFG is the one structure every language's IR shares (`writes` exists for JavaScript only): assignment nodes carry { target, source, line }.
909
+ const assigns = Object.values(fn.cfg?.nodes || {}).filter((n) => n.kind === 'assign' && n.target).sort((x, y) => (x.line ?? 0) - (y.line ?? 0) || String(x.id ?? '').localeCompare(String(y.id ?? '')));
910
+ for (const d of assigns) {
911
+ const variable = String(d.target);
912
+ const line = d.line ?? fn.line;
913
+ originFor(d.source, variable, line);
914
+ // calls that appear INSIDE an assignment source (`const cmd = h.build(x)`) are not in fn.calls; they are call sites all the same
915
+ walkExprs(d.source, (x) => { if (x.kind === 'member') field(x, 'read', line); alloc(x, line); if (x.kind === 'call' && x.callee) callFromExpr(x, line); });
916
+ const mc = variable.includes('.') ? variable.split('.') : null;
917
+ if (mc) g.fieldAccesses.push({ id: `field:${h16(['field', symId, mc[0], mc.slice(1), 'write', next(`field|${symId}|${mc[0]}|${mc.slice(1).join('.')}|write`)])}`, symbol: symId, base: mc[0], path: mc.slice(1), mode: 'write', ...loc(file, line), confidence: OBSERVED });
918
+ }
919
+ }
920
+ }
921
+
922
+ // E2-07: service relationships are opt-in. A caller that ran dataflow/service-edges.js passes the result as `meta.serviceEdges`; HTTP and
923
+ // messaging relationships become `serviceBoundaries`, persistence relationships become `storageEdges`, and relationships with no counterpart
924
+ // are kept as boundaries with `to: null` and unresolved confidence. Without it both lists stay schema-only, as before.
925
+ const se = meta.serviceEdges && meta.serviceEdges.optedIn ? meta.serviceEdges : null;
926
+ if (se) {
927
+ const tier = (e) => (e === 'observed' ? 'observed' : e === 'inferred' ? 'inferred' : 'unresolved');
928
+ for (const e of se.edges) {
929
+ const list = e.kind === 'persistence' ? g.storageEdges : g.serviceBoundaries;
930
+ list.push({ id: `${e.kind === 'persistence' ? 'store' : 'svc'}:${e.id.slice('svcedge:'.length)}`, kind: e.kind, from: e.from, to: e.to, via: e.via, ...loc(e.from.file, e.from.line), evidence: e.evidence, confidence: { tier: tier(e.evidence), basis: e.basis } });
931
+ }
932
+ for (const u of se.unknown) g.serviceBoundaries.push({ id: `svc:${h16(['unknown', u.service, u.file, u.kind, u.reason])}`, kind: u.kind, from: { service: u.service, file: u.file, line: u.line }, to: null, via: null, ...loc(u.file, u.line), evidence: 'unknown', confidence: { tier: 'unresolved', basis: u.reason } });
933
+ }
934
+ const SCHEMA_ONLY = {
935
+ ...(se ? {} : { serviceBoundaries: 'opt-in: pass meta.serviceEdges (dataflow/service-edges.js) with two or more declared services; the IR itself carries no service-boundary facts', storageEdges: 'opt-in: pass meta.serviceEdges (dataflow/service-edges.js); the IR itself carries no persistence facts' }),
936
+ sanitizers: 'sanitizer decisions are made by the dataflow layer (sanitizer-gate.js), not recorded in the IR; they are folded in by E2-08',
937
+ guards: 'guard dominance is computed by dataflow/guard-dominance.js, not recorded in the IR; it is folded in by E2-08',
938
+ };
939
+ const REASON_POP = { serviceBoundaries: 'HTTP and messaging relationships between declared services, and the ones with no counterpart', storageEdges: 'tables one declared service writes and another reads', symbols: 'one per IR function and per file top level', callSites: 'every call recorded in an IR function, plus calls nested inside assignment sources', allocations: 'object, array and function-valued expressions in call arguments and assignments', fieldAccesses: 'member reads in arguments and assignments, and dotted writes', valueOrigins: 'every call argument and every assignment node in the CFG of a function' };
940
+ const coverage = {};
941
+ for (const k of semantic_graph_ENTITY_LISTS) coverage[k] = SCHEMA_ONLY[k] ? { state: 'schema-only', reason: SCHEMA_ONLY[k] } : g[k].length ? { state: 'populated', reason: REASON_POP[k] } : { state: 'schema-only', reason: `no ${k} in this input` };
942
+ const limits = [
943
+ 'columns are null: the IR carries line numbers only',
944
+ 'a call resolves only to a same-file function of the same name; imports, re-exports and methods are unresolved until E2-02',
945
+ 'heap, alias and closure facts are not in the IR, so field accesses are syntactic, not points-to results',
946
+ ];
947
+ // Dynamic constructs the analysis cannot resolve, per function (E2-03): the graph says where precision was given up, instead of leaving it implicit.
948
+ const dynamicFeatures = analysisLimits(projectIR).map((l) => ({ symbol: symByQid.get(`${l.qid}@${(projectIR.perFile[l.file].functions.find((f) => stripLine(f.qid) === l.qid) || {}).line}`) ?? null, file: l.file, line: l.line, kind: l.kind, detail: l.detail, effect: l.effect }));
949
+ return { schema: semantic_graph_SEMANTIC_GRAPH_SCHEMA, schemaVersion: SEMANTIC_GRAPH_VERSION, producer: { layer: 'ir-v1', engineVersion: meta.engineVersion ?? null }, ...g, coverage, limits, dynamicFeatures };
950
+ }
951
+
952
+ ;// CONCATENATED MODULE: ./src/graph/adapters/semantic.js
953
+ //
954
+ // adapters/semantic.js: read-only view over a SemanticGraph v1
955
+ // (ir/semantic-graph.js). Imports only the schema constants and validator; it
956
+ // never builds or mutates a graph.
957
+ //
958
+ // A graph that fails `validateGraph`, or whose major version is not the
959
+ // supported one, is REJECTED (INCOMPATIBLE_VERSION / INVALID_GRAPH) rather than
960
+ // partially adapted.
961
+ //
962
+ // Entities: symbol, callSite, allocation, fieldAccess, valueOrigin,
963
+ // serviceBoundary, storageEdge, sanitizer, guard (singular names of the
964
+ // graph's own lists). The semantic graph stores references as id fields, not
965
+ // an edge list, so the relation edges here are DERIVED (origin 'derived') and
966
+ // get deterministic ids; the underlying confidence tier becomes the evidence
967
+ // grade, and an unresolved call is carried as an `unknowns` entry, never as a
968
+ // fabricated callee edge.
969
+ //
970
+ // symbol --declaration--> callSite (the caller declares/contains it)
971
+ // symbol --declaration--> valueOrigin / fieldAccess / allocation
972
+ // callSite --calls--> symbol (resolved calls only)
973
+ // valueOrigin --data_flow--> callSite (an argument origin feeds the call)
974
+
975
+
976
+
977
+
978
+
979
+ const SEMANTIC_ADAPTER_VERSION = '1.0.0';
980
+
981
+ const SINGULAR = {
982
+ symbols: 'symbol', callSites: 'callSite', allocations: 'allocation', fieldAccesses: 'fieldAccess',
983
+ valueOrigins: 'valueOrigin', serviceBoundaries: 'serviceBoundary', storageEdges: 'storageEdge',
984
+ sanitizers: 'sanitizer', guards: 'guard',
985
+ };
986
+
987
+ /**
988
+ * @param {object} graph SemanticGraph v1 (buildSemanticGraph output)
989
+ * @param {{repository:string}} opts
990
+ */
991
+ function semanticView(graph, opts) {
992
+ const m = typeof graph?.schemaVersion === 'string' ? /^(\d+)\./.exec(graph.schemaVersion) : null;
993
+ if (graph?.schema === SEMANTIC_GRAPH_SCHEMA && m && Number(m[1]) !== SUPPORTED_MAJOR) {
994
+ throw new GraphViewError('INCOMPATIBLE_VERSION', `semantic graph major ${m[1]} is not supported (supported: ${SUPPORTED_MAJOR})`, { schemaVersion: graph.schemaVersion });
995
+ }
996
+ const v = validateGraph(graph);
997
+ if (!v.ok) throw new GraphViewError('INVALID_GRAPH', `semantic graph failed validation: ${v.errors[0].code} at ${v.errors[0].path}`, { errors: v.errors.slice(0, 5) });
998
+
999
+ const graphDigest = digestOf(graph);
1000
+ const nodes = [];
1001
+ for (const list of ENTITY_LISTS) {
1002
+ for (const e of graph[list]) {
1003
+ nodes.push({ entityKind: SINGULAR[list], localId: e.id, native: e, summary: { file: e.file, line: e.line, tier: e.confidence?.tier ?? null } });
1004
+ }
1005
+ }
1006
+ const edges = [];
1007
+ const edge = (relation, from, to, grade, extra = {}) => {
1008
+ edges.push({
1009
+ id: `sgedge:${relation}:${from.localId}:${to.localId}`, relation, origin: 'derived',
1010
+ from, to, evidence: { grade, source: 'semantic-graph' }, unknowns: extra.unknowns ?? [], attrs: extra.attrs ?? {}, native: null,
1011
+ });
1012
+ };
1013
+ for (const c of graph.callSites) {
1014
+ edge('declaration', local('symbol', c.caller), local('callSite', c.id), c.confidence.tier);
1015
+ if (c.callee) edge('calls', local('callSite', c.id), local('symbol', c.callee), c.confidence.tier, { attrs: { resolution: c.resolution } });
1016
+ for (const a of c.args || []) {
1017
+ if (a.origin) edge('data_flow', local('valueOrigin', a.origin), local('callSite', c.id), c.confidence.tier, { attrs: { argIndex: a.index } });
1018
+ }
1019
+ }
1020
+ for (const o of graph.valueOrigins) if (o.symbol) edge('declaration', local('symbol', o.symbol), local('valueOrigin', o.id), o.confidence.tier);
1021
+ for (const f of graph.fieldAccesses) if (f.symbol) edge('declaration', local('symbol', f.symbol), local('fieldAccess', f.id), f.confidence.tier);
1022
+ for (const a of graph.allocations) if (a.symbol) edge('declaration', local('symbol', a.symbol), local('allocation', a.id), a.confidence.tier);
1023
+
1024
+ // Unresolved calls are unknowns on the view, not edges.
1025
+ const unresolvedCalls = graph.callSites.filter((c) => !c.callee).length;
1026
+ const reasons = [];
1027
+ const schemaOnly = ENTITY_LISTS.filter((k) => graph.coverage[k].state === 'schema-only');
1028
+ if (schemaOnly.length) reasons.push(`schema-only:${schemaOnly.join(',')}`);
1029
+ if (unresolvedCalls) reasons.push(`unresolved-calls:${unresolvedCalls}`);
1030
+ if ((graph.dynamicFeatures || []).length) reasons.push(`dynamic-features:${graph.dynamicFeatures.length}`);
1031
+
1032
+ return new IndexedGraphView({
1033
+ graphKind: 'semantic', adapterVersion: SEMANTIC_ADAPTER_VERSION, repository: opts?.repository, graphDigest,
1034
+ metadata: { schema: graph.schema, schemaVersion: graph.schemaVersion, producer: graph.producer ?? null, limits: graph.limits ?? [], unresolvedCalls },
1035
+ completion: { status: reasons.length ? 'partial' : 'complete', reasons, coverage: graph.coverage },
1036
+ nodes, edges,
1037
+ reconstruct: () => graph,
1038
+ });
1039
+ }
1040
+
1041
+ ;// CONCATENATED MODULE: ./src/graph/adapters/artifact.js
1042
+ //
1043
+ // adapters/artifact.js: read-only view over the artifact provenance graph
1044
+ // (artifact/provenance-graph.js: commit -> build -> OCI digest -> SBOM
1045
+ // component -> finding, plus an operator-declared deployment).
1046
+ //
1047
+ // The artifact graph has no repository identity of its own, so the caller names
1048
+ // it (`repository`: the image repository or the source repository the artifact
1049
+ // was built from). The graph digest is a canonical hash of the graph body.
1050
+ //
1051
+ // Relations: commit->build, build->digest and digest->deployment are
1052
+ // `artifact_derivation`; digest->component and component->finding are
1053
+ // `dependency`. An operator-declared deployment edge has origin `declared`; an
1054
+ // edge with evidence 'none' (a commit that is not known) is kept, carries an
1055
+ // `unknowns` entry, and has grade 'none'.
1056
+ //
1057
+ // The generator can list the same SBOM component or finding twice (an OS and an
1058
+ // application package with one purl). Byte-identical duplicates are collapsed
1059
+ // in the view and counted in metadata; duplicates that DIFFER are rejected,
1060
+ // because choosing one would be a silent merge. `reconstruct()` returns the
1061
+ // original graph, duplicates included.
1062
+
1063
+
1064
+
1065
+
1066
+ const ARTIFACT_ADAPTER_VERSION = '1.0.0';
1067
+
1068
+ function relationFor(fromKind, toKind) {
1069
+ if (toKind === 'sbomComponent' || toKind === 'finding') return 'dependency';
1070
+ return 'artifact_derivation';
1071
+ }
1072
+
1073
+ function artifactView(graph, opts) {
1074
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
1075
+ throw new TypeError('artifactView requires a graph with nodes[] and edges[]');
1076
+ }
1077
+ const graphDigest = digestOf(graph);
1078
+ const byId = new Map();
1079
+ let duplicateNodes = 0;
1080
+ for (const n of graph.nodes) {
1081
+ const prev = byId.get(n.id);
1082
+ if (prev) {
1083
+ if (canonicalJson(prev) !== canonicalJson(n)) throw new GraphViewError('DUPLICATE_ID', `artifact graph has two different nodes with id ${n.id}`, { id: n.id });
1084
+ duplicateNodes += 1;
1085
+ } else byId.set(n.id, n);
1086
+ }
1087
+ const nodes = [...byId.values()].map((n) => ({ entityKind: n.kind, localId: n.id, native: n, summary: { kind: n.kind } }));
1088
+ const seenEdges = new Map();
1089
+ let duplicateEdges = 0;
1090
+ const edges = [];
1091
+ for (const e of graph.edges) {
1092
+ const id = `agedge:${e.from}->${e.to}`;
1093
+ const prev = seenEdges.get(id);
1094
+ if (prev) {
1095
+ if (canonicalJson(prev) !== canonicalJson(e)) throw new GraphViewError('DUPLICATE_ID', `artifact graph has two different edges ${e.from} -> ${e.to}`, { id });
1096
+ duplicateEdges += 1;
1097
+ continue;
1098
+ }
1099
+ seenEdges.set(id, e);
1100
+ const from = byId.get(e.from);
1101
+ const to = byId.get(e.to);
1102
+ if (!from || !to) throw new GraphViewError('DANGLING_REF', `artifact edge ${e.from} -> ${e.to} points at a node that is not in the graph`, { id });
1103
+ const declared = e.evidence === 'operator-declared' || to.kind === 'deployment';
1104
+ const none = e.evidence === 'none';
1105
+ edges.push({
1106
+ id, relation: relationFor(from.kind, to.kind), origin: declared ? 'declared' : 'derived',
1107
+ from: local(from.kind, from.id), to: local(to.kind, to.id),
1108
+ evidence: { grade: e.evidence ?? 'none', source: 'artifact-provenance-graph' },
1109
+ unknowns: none ? [{ kind: 'unlinked', reason: 'no evidence links these two nodes' }] : (e.digestMatches === false ? [{ kind: 'digest-mismatch', reason: 'declared digest differs from the scanned artifact' }] : []),
1110
+ attrs: e.digestMatches === undefined ? {} : { digestMatches: e.digestMatches }, native: e,
1111
+ });
1112
+ }
1113
+ const unlinked = edges.filter((e) => e.unknowns.some((u) => u.kind === 'unlinked')).length;
1114
+ const reasons = unlinked ? [`unlinked-edges:${unlinked}`] : [];
1115
+ return new IndexedGraphView({
1116
+ graphKind: 'artifact', adapterVersion: ARTIFACT_ADAPTER_VERSION, repository: opts?.repository, graphDigest,
1117
+ metadata: { nodes: graph.nodes.length, edges: graph.edges.length, duplicateNodes, duplicateEdges },
1118
+ completion: { status: reasons.length ? 'partial' : 'complete', reasons, coverage: null },
1119
+ nodes, edges,
1120
+ reconstruct: () => graph,
1121
+ });
1122
+ }
1123
+
1124
+ // EXTERNAL MODULE: ./src/lineage/cross-repo-link.js
1125
+ var cross_repo_link = __webpack_require__(24974);
1126
+ ;// CONCATENATED MODULE: ./src/graph/adapters/crossrepo.js
1127
+ //
1128
+ // adapters/crossrepo.js: read-only view over CrossRepoLink records
1129
+ // (lineage/cross-repo-link.js), plus a small findings view.
1130
+ //
1131
+ // A cross-repository link is an operator DECLARATION. In this view it is an
1132
+ // edge with origin `declared` and evidence grade `declared`, so the default
1133
+ // query filter (code-derived and derived edges only) never walks it as if it
1134
+ // were a code-derived data flow, and it can never be mistaken for observed
1135
+ // telemetry (R06.4). Its endpoints live in OTHER graphs, so they are foreign
1136
+ // references, each carrying that graph's repository and digest. An endpoint
1137
+ // that no registered graph can resolve is reported by the registry as
1138
+ // unresolved, not dropped.
1139
+ //
1140
+ // Both endpoints are `dataflow` node references: that is the only graph kind a
1141
+ // CrossRepoLink record binds today.
1142
+
1143
+
1144
+
1145
+
1146
+
1147
+ const CROSSREPO_ADAPTER_VERSION = '1.0.0';
1148
+ const FINDINGS_ADAPTER_VERSION = '1.0.0';
1149
+
1150
+ /**
1151
+ * @param {object[]} links CrossRepoLink records
1152
+ * @param {{localRepository:string}} opts the repository the `local` side belongs to
1153
+ */
1154
+ function crossRepoLinkView(links, opts) {
1155
+ if (!Array.isArray(links)) throw new TypeError('crossRepoLinkView requires an array of links');
1156
+ const localRepository = opts?.localRepository;
1157
+ const nodes = [];
1158
+ const edges = [];
1159
+ for (const l of links) {
1160
+ const v = validateCrossRepoLink(l);
1161
+ if (!v.valid) throw new GraphViewError('INVALID_LINK', `cross-repo link ${l?.id ?? '?'} is invalid: ${v.errors[0].path} ${v.errors[0].message}`, { errors: v.errors });
1162
+ nodes.push({ entityKind: 'link', localId: l.id, native: l, summary: { declaredBy: l.declaredBy, declaredAt: l.declaredAt } });
1163
+ const from = makeRef({ graphKind: 'dataflow', repository: localRepository, graphDigest: l.local.graphDigest, entityKind: 'node', localId: l.local.nodeId });
1164
+ const to = makeRef({ graphKind: 'dataflow', repository: l.remote.repository, graphDigest: l.remote.graphDigest, entityKind: 'node', localId: l.remote.nodeId });
1165
+ edges.push({
1166
+ id: l.id, relation: 'data_flow', origin: 'declared',
1167
+ from: foreign(from), to: foreign(to),
1168
+ evidence: { grade: 'declared', source: 'operator', declaredBy: l.declaredBy },
1169
+ unknowns: [{ kind: 'not-observed', reason: 'a declared link is an assertion, not an observed data flow' }],
1170
+ attrs: { rationale: l.rationale ?? null, provenance: l.provenance }, native: l,
1171
+ });
1172
+ }
1173
+ return new IndexedGraphView({
1174
+ graphKind: 'cross-repo-links', adapterVersion: CROSSREPO_ADAPTER_VERSION, repository: localRepository, graphDigest: digestOf(links),
1175
+ metadata: { links: links.length },
1176
+ completion: { status: 'complete', reasons: [], coverage: null },
1177
+ nodes, edges, allowForeign: true,
1178
+ reconstruct: () => links,
1179
+ });
1180
+ }
1181
+
1182
+ /**
1183
+ * Findings as referenceable entities (no edges): the starting point of a
1184
+ * finding -> call site -> witnesses -> flow join.
1185
+ * @param {object[]} findings scanner findings; localId is `stableId` when present, else `id`
1186
+ */
1187
+ function findingsView(findings, opts) {
1188
+ if (!Array.isArray(findings)) throw new TypeError('findingsView requires an array of findings');
1189
+ const nodes = findings.map((f) => {
1190
+ const id = f.stableId ?? f.id;
1191
+ if (typeof id !== 'string' || !id) throw new GraphViewError('BAD_REF', 'every finding needs a stableId or id');
1192
+ return { entityKind: 'finding', localId: id, native: f, summary: { file: f.file ?? null, line: Number.isInteger(f.line) ? f.line : null, severity: f.severity ?? null } };
1193
+ });
1194
+ return new IndexedGraphView({
1195
+ graphKind: 'findings', adapterVersion: FINDINGS_ADAPTER_VERSION, repository: opts?.repository, graphDigest: digestOf(findings),
1196
+ metadata: { findings: findings.length },
1197
+ completion: { status: 'unknown', reasons: ['a finding list says nothing about what was not scanned'], coverage: null },
1198
+ nodes, edges: [],
1199
+ reconstruct: () => findings,
1200
+ });
1201
+ }
1202
+
1203
+ ;// CONCATENATED MODULE: ./src/graph/registry.js
1204
+ //
1205
+ // registry.js: the set of graph views a query may span (DAG-06 R06.6).
1206
+ //
1207
+ // What registration rejects, each with a typed GraphViewError:
1208
+ // INCOMPATIBLE_VERSION an adapter whose major version this build cannot read
1209
+ // ID_COLLISION the same (graph kind, repository, digest) registered
1210
+ // twice with different content: a digest names ONE
1211
+ // immutable graph
1212
+ // BAD_REF a malformed view
1213
+ //
1214
+ // What lookups reject:
1215
+ // DANGLING_REF the graph is not registered, or the entity is not in it
1216
+ // VERSION_INCOMPATIBLE a reference written by a newer reference format
1217
+ // AMBIGUOUS_LOCAL_ID a bare local id that exists in more than one graph; a
1218
+ // local id is only meaningful inside its graph, so the
1219
+ // registry will not pick one
1220
+ //
1221
+ // A reference never resolves across repositories: the repository is part of the
1222
+ // reference, so `pnode:abc` in repository A and `pnode:abc` in repository B are
1223
+ // two different entities.
1224
+
1225
+
1226
+
1227
+
1228
+
1229
+
1230
+
1231
+
1232
+ const major = (v) => Number(String(v).split('.')[0]);
1233
+
1234
+ /** Adapter majors this build understands, per graph kind. */
1235
+ const SUPPORTED_ADAPTER_MAJORS = Object.freeze({
1236
+ provenance: [major(PROVENANCE_ADAPTER_VERSION)],
1237
+ dataflow: [major(DATAFLOW_ADAPTER_VERSION)],
1238
+ semantic: [major(SEMANTIC_ADAPTER_VERSION)],
1239
+ artifact: [major(ARTIFACT_ADAPTER_VERSION)],
1240
+ 'cross-repo-links': [major(CROSSREPO_ADAPTER_VERSION)],
1241
+ findings: [major(FINDINGS_ADAPTER_VERSION)],
1242
+ });
1243
+
1244
+ function createGraphRegistry() {
1245
+ const views = new Map(); // identity key -> view
1246
+
1247
+ function viewFor(ref) {
1248
+ const bad = validateRef(ref);
1249
+ if (bad) {
1250
+ throw new GraphViewError(ref && ref.v !== undefined && ref.v !== REF_VERSION ? 'VERSION_INCOMPATIBLE' : 'BAD_REF', bad, { ref });
1251
+ }
1252
+ const v = views.get(graphIdentityKey(ref));
1253
+ if (!v) throw new GraphViewError('DANGLING_REF', `no registered ${ref.graphKind} graph for repository ${ref.repository} with digest ${ref.graphDigest.slice(0, 12)}`, { ref });
1254
+ return v;
1255
+ }
1256
+
1257
+ const foreignViews = () => [...views.values()].filter((v) => v._allowForeign);
1258
+
1259
+ return {
1260
+ register(view) {
1261
+ const kinds = SUPPORTED_ADAPTER_MAJORS[view?.graphKind];
1262
+ if (!kinds) throw new GraphViewError('BAD_REF', `unknown graph kind ${view?.graphKind}`);
1263
+ if (!kinds.includes(major(view.adapterVersion))) {
1264
+ throw new GraphViewError('INCOMPATIBLE_VERSION', `${view.graphKind} adapter ${view.adapterVersion} is not readable by this build (supported majors: ${kinds.join(',')})`);
1265
+ }
1266
+ const key = graphIdentityKey(view);
1267
+ const prev = views.get(key);
1268
+ if (prev) {
1269
+ if (prev.fingerprint !== view.fingerprint) {
1270
+ throw new GraphViewError('ID_COLLISION', `${view.graphKind}/${view.repository} digest ${view.graphDigest.slice(0, 12)} is already registered with different content`, { key });
1271
+ }
1272
+ return prev;
1273
+ }
1274
+ views.set(key, view);
1275
+ return view;
1276
+ },
1277
+
1278
+ views() { return [...views.values()]; },
1279
+ viewFor,
1280
+
1281
+ has(ref) {
1282
+ const v = views.get(graphIdentityKey(ref));
1283
+ return !!v && v.has(ref);
1284
+ },
1285
+
1286
+ node(ref) { return viewFor(ref).node(ref); },
1287
+
1288
+ outgoing(ref, filter) {
1289
+ const out = viewFor(ref).outgoing(ref, filter);
1290
+ for (const fv of foreignViews()) if (fv.graphKind !== ref.graphKind || fv.repository !== ref.repository || fv.graphDigest !== ref.graphDigest) out.push(...fv.outgoing(ref, filter));
1291
+ return out;
1292
+ },
1293
+
1294
+ incoming(ref, filter) {
1295
+ const inn = viewFor(ref).incoming(ref, filter);
1296
+ for (const fv of foreignViews()) if (fv.graphKind !== ref.graphKind || fv.repository !== ref.repository || fv.graphDigest !== ref.graphDigest) inn.push(...fv.incoming(ref, filter));
1297
+ return inn;
1298
+ },
1299
+
1300
+ /** A bare local id is ambiguous when more than one registered graph has it. */
1301
+ resolveLocal(entityKind, localId, { graphKind } = {}) {
1302
+ const hits = [];
1303
+ for (const v of views.values()) {
1304
+ if (graphKind && v.graphKind !== graphKind) continue;
1305
+ try { hits.push(v.ref(entityKind, localId)); } catch { /* not in this graph */ }
1306
+ }
1307
+ if (hits.length === 0) throw new GraphViewError('DANGLING_REF', `no ${entityKind} ${localId} in any registered graph`);
1308
+ if (hits.length > 1) throw new GraphViewError('AMBIGUOUS_LOCAL_ID', `${entityKind} ${localId} exists in ${hits.length} registered graphs; use a full reference`, { candidates: hits });
1309
+ return hits[0];
1310
+ },
1311
+
1312
+ /**
1313
+ * Classify a foreign endpoint (for example, a cross-repository link end):
1314
+ * resolved registered, and the entity exists
1315
+ * unresolved no registered graph with that identity (not an error:
1316
+ * the remote graph simply is not loaded)
1317
+ * dangling the graph is registered but the entity is not in it
1318
+ * repository-mismatch a graph with that digest is registered, but under a
1319
+ * different repository than the reference claims
1320
+ */
1321
+ classify(ref) {
1322
+ const bad = validateRef(ref);
1323
+ if (bad) throw new GraphViewError('BAD_REF', bad, { ref });
1324
+ const v = views.get(graphIdentityKey(ref));
1325
+ if (v) return v.has(ref) ? 'resolved' : 'dangling';
1326
+ for (const o of views.values()) {
1327
+ if (o.graphKind === ref.graphKind && o.graphDigest === ref.graphDigest && o.repository !== ref.repository) return 'repository-mismatch';
1328
+ }
1329
+ return 'unresolved';
1330
+ },
1331
+ };
1332
+ }
1333
+
1334
+ ;// CONCATENATED MODULE: ./src/graph/crosswalk.js
1335
+ //
1336
+ // crosswalk.js: evidence-bearing identity mappings between graph layers
1337
+ // (DAG-06 R06.2, R06.5).
1338
+ //
1339
+ // A crosswalk never merges entities. It records "this entity in graph A
1340
+ // corresponds to these entities in graph B, by this method, with this
1341
+ // evidence". Every mapping is:
1342
+ // - versioned (CROSSWALK_VERSION);
1343
+ // - explicit about cardinality (`one-to-one`, `one-to-many`, or `none` with a
1344
+ // stated reason), so a projection that coarsens many underlying sites into
1345
+ // one projected node says so instead of pretending to be one-to-one;
1346
+ // - evidence-bearing: method, grade and basis are required;
1347
+ // - checked against the registry when one is supplied: a reference to a graph
1348
+ // or entity that is not registered is rejected as DANGLING_REF.
1349
+ //
1350
+ // Mapping types and the graph layers each connects:
1351
+ // finding-to-callsite findings -> semantic (location join)
1352
+ // callsite-to-witness semantic -> provenance (scope + line join)
1353
+ // witness-to-flow provenance -> dataflow (data element join)
1354
+ // node-to-sites dataflow node -> semantic (a projected node's underlying call sites)
1355
+ // identity same layer -> same layer (the same entity under another reference)
1356
+ //
1357
+ // `identity` mappings are only legal inside ONE repository. Declaring two
1358
+ // entities in different repositories "the same" because their local ids match
1359
+ // is exactly the collision this module exists to prevent, and is rejected as
1360
+ // CROSS_REPO_ID_COLLISION.
1361
+
1362
+
1363
+
1364
+
1365
+ const CROSSWALK_VERSION = '1.0.0';
1366
+
1367
+ const MAPPING_TYPES = Object.freeze({
1368
+ 'finding-to-callsite': { from: { graphKind: 'findings', entityKind: 'finding' }, to: { graphKind: 'semantic', entityKind: 'callSite' } },
1369
+ 'callsite-to-witness': { from: { graphKind: 'semantic', entityKind: 'callSite' }, to: { graphKind: 'provenance', entityKind: 'pedge' } },
1370
+ 'witness-to-flow': { from: { graphKind: 'provenance', entityKind: 'pedge' }, to: { graphKind: 'dataflow', entityKind: 'flow' } },
1371
+ 'node-to-sites': { from: { graphKind: 'dataflow', entityKind: 'node' }, to: { graphKind: 'semantic', entityKind: 'callSite' } },
1372
+ identity: { from: null, to: null },
1373
+ });
1374
+
1375
+ const CARDINALITY = (n) => (n === 0 ? 'none' : n === 1 ? 'one-to-one' : 'one-to-many');
1376
+
1377
+ function checkEnd(ref, want, label, type) {
1378
+ const bad = validateRef(ref);
1379
+ if (bad) throw new GraphViewError('BAD_REF', `${type} ${label}: ${bad}`, { ref });
1380
+ if (want && (ref.graphKind !== want.graphKind || ref.entityKind !== want.entityKind)) {
1381
+ throw new GraphViewError('BAD_MAPPING', `${type} ${label} must be a ${want.graphKind} ${want.entityKind}, got ${ref.graphKind} ${ref.entityKind}`, { ref });
1382
+ }
1383
+ }
1384
+
1385
+ function createCrosswalk({ registry = null } = {}) {
1386
+ const mappings = new Map(); // id -> record
1387
+ const forward = new Map(); // refKey -> record[]
1388
+ const reverse = new Map(); // refKey -> record[]
1389
+ const push = (m, k, r) => { const a = m.get(k); if (a) a.push(r); else m.set(k, [r]); };
1390
+
1391
+ function add(spec) {
1392
+ const type = spec?.type;
1393
+ const desc = MAPPING_TYPES[type];
1394
+ if (!desc) throw new GraphViewError('BAD_MAPPING', `unknown mapping type ${JSON.stringify(type)}`);
1395
+ checkEnd(spec.from, desc.from, 'from', type);
1396
+ if (!Array.isArray(spec.to)) throw new GraphViewError('BAD_MAPPING', `${type}: "to" is a list (empty with an unmapped reason when nothing matched)`);
1397
+ for (const t of spec.to) checkEnd(t, desc.to, 'to', type);
1398
+
1399
+ const ev = spec.evidence;
1400
+ if (!ev || typeof ev.method !== 'string' || !ev.method || typeof ev.grade !== 'string' || !ev.grade || typeof ev.basis !== 'string' || !ev.basis) {
1401
+ throw new GraphViewError('BAD_MAPPING', `${type}: evidence needs { method, grade, basis }`);
1402
+ }
1403
+ const keys = spec.to.map(refKey);
1404
+ if (new Set(keys).size !== keys.length) throw new GraphViewError('BAD_MAPPING', `${type}: duplicate targets in one mapping`);
1405
+ if (spec.to.length === 0 && !spec.unmapped) throw new GraphViewError('BAD_MAPPING', `${type}: an empty mapping must say why in "unmapped"`);
1406
+
1407
+ if (type === 'identity') {
1408
+ for (const t of spec.to) {
1409
+ if (t.repository !== spec.from.repository) {
1410
+ throw new GraphViewError('CROSS_REPO_ID_COLLISION', `identity mapping between repositories ${spec.from.repository} and ${t.repository} refused: matching local ids do not make two entities the same`, { from: spec.from, to: t });
1411
+ }
1412
+ if (t.entityKind !== spec.from.entityKind || t.graphKind !== spec.from.graphKind) {
1413
+ throw new GraphViewError('BAD_MAPPING', 'identity maps an entity to the same kind of entity');
1414
+ }
1415
+ }
1416
+ }
1417
+ if (registry) {
1418
+ for (const r of [spec.from, ...spec.to]) {
1419
+ if (!registry.has(r)) throw new GraphViewError('DANGLING_REF', `${type}: ${r.graphKind} ${r.entityKind} ${r.localId} (repository ${r.repository}) is not in a registered graph`, { ref: r });
1420
+ }
1421
+ }
1422
+
1423
+ const id = `xwalk:${createHash('sha256').update(canonicalJson([type, refKey(spec.from), [...keys].sort(), ev.method])).digest('hex').slice(0, 16)}`;
1424
+ if (mappings.has(id)) return mappings.get(id);
1425
+ const record = Object.freeze({
1426
+ id, version: CROSSWALK_VERSION, type, from: spec.from, to: Object.freeze([...spec.to]),
1427
+ cardinality: CARDINALITY(spec.to.length),
1428
+ coarsens: !!spec.coarsens,
1429
+ evidence: Object.freeze({ ...ev }),
1430
+ unknowns: Object.freeze([...(spec.unknowns ?? [])]),
1431
+ unmapped: spec.unmapped ?? null,
1432
+ });
1433
+ mappings.set(id, record);
1434
+ push(forward, refKey(record.from), record);
1435
+ for (const k of keys) push(reverse, k, record);
1436
+ return record;
1437
+ }
1438
+
1439
+ const ofType = (list, type) => (type ? list.filter((m) => m.type === type) : list);
1440
+
1441
+ return {
1442
+ version: CROSSWALK_VERSION,
1443
+ add,
1444
+ all: () => [...mappings.values()],
1445
+ /** Mappings that start at `ref`. */
1446
+ from: (ref, type) => ofType(forward.get(refKey(ref)) ?? [], type),
1447
+ /** Mappings that end at `ref` (the reverse direction; many sources may share a target). */
1448
+ to: (ref, type) => ofType(reverse.get(refKey(ref)) ?? [], type),
1449
+
1450
+ /**
1451
+ * Follow a chain of mapping types from `start`. Returns every chain found
1452
+ * and, for each step, which references had NO mapping (a gap is reported,
1453
+ * never skipped). `evidence` lists every mapping used, so the join is
1454
+ * traceable end to end.
1455
+ */
1456
+ trace(start, types, { maxChains = 1000 } = {}) {
1457
+ let chains = [{ refs: [start], mappings: [] }];
1458
+ const gaps = [];
1459
+ let truncated = false;
1460
+ for (const type of types) {
1461
+ const next = [];
1462
+ for (const c of chains) {
1463
+ const last = c.refs[c.refs.length - 1];
1464
+ const ms = this.from(last, type);
1465
+ if (ms.length === 0) { gaps.push({ type, ref: last }); continue; }
1466
+ for (const m of ms) {
1467
+ if (m.to.length === 0) { gaps.push({ type, ref: last, unmapped: m.unmapped, mapping: m.id }); continue; }
1468
+ for (const t of m.to) {
1469
+ if (next.length >= maxChains) { truncated = true; break; }
1470
+ next.push({ refs: [...c.refs, t], mappings: [...c.mappings, m.id] });
1471
+ }
1472
+ }
1473
+ }
1474
+ chains = next;
1475
+ }
1476
+ const used = new Set(chains.flatMap((c) => c.mappings));
1477
+ return { start, types: [...types], chains, gaps, truncated, evidence: [...used].map((id) => mappings.get(id)) };
1478
+ },
1479
+ };
1480
+ }
1481
+
1482
+ ;// CONCATENATED MODULE: ./src/graph/crosswalk-builders.js
1483
+ //
1484
+ // crosswalk-builders.js: build the standard crosswalk mappings from registered
1485
+ // views. Every builder joins on facts the two graphs both carry and records
1486
+ // the join as evidence; none invents an identity.
1487
+ //
1488
+ // finding -> semantic call site file + line (location-exact)
1489
+ // call site -> provenance witnesses enclosing function (qid) + line
1490
+ // witness -> projected flow data element id
1491
+ // projected node -> underlying sites decision kind/category/coverage/externality
1492
+ //
1493
+ // Where a join cannot pin one target, the mapping lists ALL candidates
1494
+ // (one-to-many) and carries an `unknowns` entry. It never picks one silently.
1495
+
1496
+
1497
+
1498
+ const crosswalk_builders_stripLine = (qid) => String(qid).replace(/@\d+$/, '');
1499
+ const normFile = (f) => String(f ?? '').replace(/^\.\//, '').replace(/\\/g, '/');
1500
+
1501
+ /**
1502
+ * Join findings to semantic call sites by exact file and line.
1503
+ * @returns {object[]} the mappings added
1504
+ */
1505
+ function mapFindingsToCallSites(crosswalk, findings, semantic) {
1506
+ const byLoc = new Map();
1507
+ for (const cs of semantic.entities('callSite')) {
1508
+ const k = `${normFile(cs.native.file)}:${cs.native.line}`;
1509
+ if (!byLoc.has(k)) byLoc.set(k, []);
1510
+ byLoc.get(k).push(cs);
1511
+ }
1512
+ const added = [];
1513
+ for (const f of findings.entities('finding')) {
1514
+ const { file, line } = f.native;
1515
+ if (!Number.isInteger(line) || !file) {
1516
+ added.push(crosswalk.add({
1517
+ type: 'finding-to-callsite', from: f.ref, to: [], unmapped: 'finding-has-no-file-and-line',
1518
+ evidence: { method: 'file-line', grade: 'none', basis: 'the finding carries no integer line, so it cannot be located' },
1519
+ }));
1520
+ continue;
1521
+ }
1522
+ const hits = byLoc.get(`${normFile(file)}:${line}`) ?? [];
1523
+ if (hits.length === 0) continue; // not a call-site finding; absence is visible as a trace gap
1524
+ added.push(crosswalk.add({
1525
+ type: 'finding-to-callsite', from: f.ref, to: hits.map((h) => h.ref), coarsens: hits.length > 1,
1526
+ evidence: { method: 'file-line', grade: hits.length === 1 ? 'location-exact' : 'location-ambiguous', basis: `${normFile(file)}:${line}` },
1527
+ unknowns: hits.length > 1 ? [{ kind: 'ambiguous-location', count: hits.length, reason: 'several call sites share this line; the finding names a line, not a call' }] : [],
1528
+ }));
1529
+ }
1530
+ return added;
1531
+ }
1532
+
1533
+ /**
1534
+ * Join semantic call sites to provenance witnesses (pedges): same enclosing
1535
+ * function and same line. The provenance scope is `<qid>@<line>`; the semantic
1536
+ * symbol carries the qid without the line suffix.
1537
+ */
1538
+ function mapCallSitesToWitnesses(crosswalk, semantic, provenance) {
1539
+ const witnessByKey = new Map();
1540
+ for (const w of provenance.entities('pedge')) {
1541
+ const k = `${crosswalk_builders_stripLine(w.native.scope)}\u0000${w.native.line}`;
1542
+ if (!witnessByKey.has(k)) witnessByKey.set(k, []);
1543
+ witnessByKey.get(k).push(w);
1544
+ }
1545
+ const symQid = new Map(semantic.entities('symbol').map((s) => [s.localId, s.native.qid]));
1546
+ const sitesPerKey = new Map();
1547
+ const keyOf = (cs) => `${symQid.get(cs.native.caller)}\u0000${cs.native.line}`;
1548
+ const sites = semantic.entities('callSite');
1549
+ for (const cs of sites) sitesPerKey.set(keyOf(cs), (sitesPerKey.get(keyOf(cs)) ?? 0) + 1);
1550
+ const added = [];
1551
+ for (const cs of sites) {
1552
+ const hits = witnessByKey.get(keyOf(cs)) ?? [];
1553
+ if (hits.length === 0) continue;
1554
+ const shared = sitesPerKey.get(keyOf(cs));
1555
+ added.push(crosswalk.add({
1556
+ type: 'callsite-to-witness', from: cs.ref, to: hits.map((h) => h.ref), coarsens: hits.length > 1 || shared > 1,
1557
+ evidence: { method: 'scope-line', grade: shared > 1 ? 'location-ambiguous' : 'location-exact', basis: `${symQid.get(cs.native.caller)} line ${cs.native.line}` },
1558
+ unknowns: shared > 1 ? [{ kind: 'shared-line', callSites: shared, reason: 'provenance records a line, not a call; these witnesses cannot be pinned to one of the call sites on that line' }] : [],
1559
+ }));
1560
+ }
1561
+ return added;
1562
+ }
1563
+
1564
+ /**
1565
+ * Join provenance witnesses to the projected flows that carry their data
1566
+ * element. A flow aggregates many witnesses (and many sites), so every mapping
1567
+ * is flagged `coarsens`.
1568
+ */
1569
+ function mapWitnessesToFlows(crosswalk, provenance, dataflow) {
1570
+ const flowsByDe = new Map();
1571
+ for (const fl of dataflow.entities('flow')) {
1572
+ for (const de of fl.native.dataElementIds || []) {
1573
+ if (!flowsByDe.has(de)) flowsByDe.set(de, []);
1574
+ flowsByDe.get(de).push(fl);
1575
+ }
1576
+ }
1577
+ const added = [];
1578
+ for (const w of provenance.entities('pedge')) {
1579
+ const hits = flowsByDe.get(w.native.dataElementId) ?? [];
1580
+ if (hits.length === 0) continue;
1581
+ added.push(crosswalk.add({
1582
+ type: 'witness-to-flow', from: w.ref, to: hits.map((h) => h.ref), coarsens: true,
1583
+ evidence: { method: 'data-element', grade: 'identity-shared', basis: w.native.dataElementId },
1584
+ unknowns: [{ kind: 'flow-granularity', reason: 'a projected flow aggregates every witness of its data element; the flow does not say which site this witness belongs to' }],
1585
+ }));
1586
+ }
1587
+ return added;
1588
+ }
1589
+
1590
+ const calleeText = (e) => {
1591
+ if (!e || typeof e !== 'object') return null;
1592
+ if (e.kind === 'ident') return e.name;
1593
+ if (e.kind === 'member') { const o = calleeText(e.object); return o ? `${o}.${e.prop ?? '[]'}` : null; }
1594
+ return null;
1595
+ };
1596
+
1597
+ /**
1598
+ * Map each projected (category-granular) dataflow node to the underlying call
1599
+ * sites it stands for. `sites` is the sink-site list `buildDataFlowGraph`
1600
+ * returns. A node with several sites maps one-to-many and is flagged `coarsens`;
1601
+ * a site that no semantic call site matches is listed under `unknowns`.
1602
+ */
1603
+ function mapProjectedNodesToSites(crosswalk, dataflow, semantic, sites) {
1604
+ const callSites = semantic.entities('callSite');
1605
+ const added = [];
1606
+ for (const n of dataflow.entities('node')) {
1607
+ const node = n.native;
1608
+ const matching = (sites || []).filter((s) => s.decision
1609
+ && s.decision.kind === node.kind
1610
+ && (s.decision.category ?? null) === (node.subtype ?? null)
1611
+ && s.decision.coverageStatus === node.coverageStatus
1612
+ && s.decision.externality === node.externality?.value
1613
+ && canonicalJson(s.destination ?? null) === canonicalJson(node.destination ?? null));
1614
+ if (matching.length === 0) continue;
1615
+ const targets = new Map();
1616
+ const unmapped = [];
1617
+ for (const s of matching) {
1618
+ const atLine = callSites.filter((c) => normFile(c.native.file) === normFile(s.file) && c.native.line === s.line);
1619
+ const want = calleeText(s.calleeExpr);
1620
+ const exact = want ? atLine.filter((c) => c.native.calleeText === want) : [];
1621
+ const picked = exact.length ? exact : atLine;
1622
+ if (picked.length === 0) unmapped.push({ file: s.file, line: s.line, reason: 'no semantic call site at this location' });
1623
+ for (const c of picked) targets.set(c.localId, c);
1624
+ }
1625
+ const to = [...targets.values()].map((c) => c.ref);
1626
+ const unknowns = unmapped.map((u) => ({ kind: 'unmapped-site', ...u }));
1627
+ if (to.length === 0) continue;
1628
+ added.push(crosswalk.add({
1629
+ type: 'node-to-sites', from: n.ref, to, coarsens: to.length > 1, unknowns,
1630
+ evidence: { method: 'decision-match', grade: to.length === 1 ? 'projection-exact' : 'projection-coarse', basis: `${node.kind}/${node.subtype ?? 'none'}: ${matching.length} sink site(s)` },
1631
+ }));
1632
+ }
1633
+ return added;
1634
+ }
1635
+
1636
+ ;// CONCATENATED MODULE: ./src/graph/observation.js
1637
+ //
1638
+ // observation.js: attributing a runtime observation to the underlying sites of
1639
+ // a projected node (DAG-06 acceptance: "a projected node with several
1640
+ // underlying sites cannot falsely assign one runtime observation to every
1641
+ // site").
1642
+ //
1643
+ // A runtime observation is matched to a PROJECTED node (a destination, store or
1644
+ // queue). When that node stands for several call sites (a `node-to-sites`
1645
+ // mapping with more than one target), the observation says nothing about which
1646
+ // site produced it unless it carries its own discriminator. The result is:
1647
+ //
1648
+ // attributed exactly one site is justified: the node has a single site, or
1649
+ // the observation names a site (`file` + `line`, or a call-site
1650
+ // id) that matches exactly one candidate.
1651
+ // ambiguous several candidates remain. `attributedTo` is EMPTY, every
1652
+ // candidate is listed, and no site receives the observation.
1653
+ // unmatched the observation names a site that is not one of the candidates.
1654
+ //
1655
+ // "Observed" is therefore a property of the projected node unless a site is
1656
+ // positively identified. An observation is never copied onto each site.
1657
+
1658
+
1659
+
1660
+ /**
1661
+ * @param {object} crosswalk
1662
+ * @param {object} registry
1663
+ * @param {object} nodeRef ref to the projected dataflow node the observation matched
1664
+ * @param {{file?:string, line?:number, callSiteId?:string}} [observation]
1665
+ */
1666
+ function attributeObservation(crosswalk, registry, nodeRef, observation = {}) {
1667
+ const mappings = crosswalk.from(nodeRef, 'node-to-sites');
1668
+ const candidates = [];
1669
+ const seen = new Set();
1670
+ for (const m of mappings) for (const t of m.to) if (!seen.has(refKey(t))) { seen.add(refKey(t)); candidates.push(t); }
1671
+ const base = { node: nodeRef, candidates, mappings: mappings.map((m) => m.id) };
1672
+
1673
+ if (candidates.length === 0) {
1674
+ return { ...base, status: 'unmatched', attributedTo: [], reason: 'the projected node has no recorded underlying sites' };
1675
+ }
1676
+ const normFile = (f) => String(f ?? '').replace(/^\.\//, '');
1677
+ const hasDiscriminator = observation.callSiteId || (observation.file && Number.isInteger(observation.line));
1678
+ if (hasDiscriminator) {
1679
+ const hit = candidates.filter((c) => {
1680
+ if (observation.callSiteId) return c.localId === observation.callSiteId;
1681
+ const n = registry.node(c).native;
1682
+ return normFile(n.file) === normFile(observation.file) && n.line === observation.line;
1683
+ });
1684
+ if (hit.length === 1) return { ...base, status: 'attributed', attributedTo: hit, basis: 'observation-names-site' };
1685
+ if (hit.length === 0) return { ...base, status: 'unmatched', attributedTo: [], reason: 'the observation names a site that is not one of the node\'s underlying sites' };
1686
+ return { ...base, status: 'ambiguous', attributedTo: [], candidates: hit, reason: 'several call sites share the named location' };
1687
+ }
1688
+ if (candidates.length === 1) return { ...base, status: 'attributed', attributedTo: candidates, basis: 'sole-underlying-site' };
1689
+ return {
1690
+ ...base, status: 'ambiguous', attributedTo: [],
1691
+ reason: `the projected node stands for ${candidates.length} sites and the observation does not say which one produced it`,
1692
+ };
1693
+ }
1694
+
1695
+ ;// CONCATENATED MODULE: ./src/graph/index.js
1696
+ // Typed graph views and identity crosswalks (DAG-06). See CLAUDE.md in this directory.
1697
+
1698
+
1699
+
1700
+
1701
+
1702
+
1703
+
1704
+
1705
+
1706
+
1707
+
1708
+
1709
+
1710
+
1711
+ /***/ })
1712
+
1713
+ };