@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
@@ -1,450 +0,0 @@
1
- export const id = 3653;
2
- export const ids = [3653];
3
- export const modules = {
4
-
5
- /***/ 63653:
6
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
-
8
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
- /* harmony export */ computeGraphDiff: () => (/* binding */ computeGraphDiff)
10
- /* harmony export */ });
11
- /* unused harmony exports WATCHED_FLOW_FIELDS, validateGraphDiff */
12
- /* harmony import */ var _graph_snapshot_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(14870);
13
- /* harmony import */ var _ids_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(19051);
14
- /* harmony import */ var _dataflow_privacy_governance_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(24701);
15
- // graph-diff.js — M4 deliverable #9 (FR-503 §14, DFG-022), sub-project
16
- // 8b, Task 1: the GraphDiff extension contract + computeGraphDiff + honest
17
- // change-cause classification.
18
- //
19
- // Mirrors graph-snapshot.js's/obligation-mapping.js's/decision-story.js's
20
- // own shape exactly: a record is explicitly NOT a DataFlowGraph v1 entity
21
- // (§10.10 — "associated with, but not required inside, the immutable base
22
- // graph"), never added to dataflow-graph.schema.json, never routed
23
- // through validate.js's validateGraph(). See ids.js's diffId() for the id
24
- // scheme.
25
- //
26
- // This module ONLY computes a diff between two already-persisted
27
- // GraphSnapshot records and classifies why each removed flow disappeared,
28
- // as far as real, checkable signals allow. It does NOT decide what a
29
- // caller should DO with a GraphDiff (no CLI, no drift-policy matching —
30
- // see this sub-project's own task brief: those are Tasks 2/3).
31
- //
32
- // ── Design judgment calls made in this file (disclosed, not hidden) ──
33
- //
34
- // 1. WATCHED_FLOW_FIELDS includes `governanceRefs`, not just the four
35
- // scalar fields the task brief names literally
36
- // (protectionSummary/policyVerdict/handling/coverageStatus). Read
37
- // directly against dataflow-graph.schema.json's own flow $def (§145 of
38
- // that file) and graph-builder.js's real flow-construction loop:
39
- // `flow.governanceRefs` is a real, populated field (coverage.js's
40
- // `resolveGovernanceRefs` default, keyed by
41
- // dataflow/privacy-governance.js's own GOVERNANCE_FIELDS —
42
- // purpose/lawfulBasis/subject/retention/residency/recipient/transfer/
43
- // minimization/consent/access/deletion), never a fabricated shape. A
44
- // "retention/deletion or lawful-basis change" — AC-27's own bullet
45
- // list — can ONLY show up as a governanceRefs difference, since none
46
- // of the other four watched fields carry that information at all. The
47
- // diff compares it value-by-value over GOVERNANCE_FIELDS (never a
48
- // bare object-identity/JSON-string check), emitting one changed-field
49
- // entry per differing governance field (`governanceRefs.<field>`), so
50
- // a caller learns WHICH governance fact changed, not just "something
51
- // in governance changed."
52
- //
53
- // 2. Non-flow removed entities (nodes/edges/dataElements) always get
54
- // causeClassification: 'application_change'. The task brief's Global
55
- // Constraint says the coverage-regression check applies "per removed
56
- // flow entry ONLY" — read literally, that leaves open what a removed
57
- // node/edge/dataElement gets. The same reasoning the brief gives for
58
- // added/changed entries applies here too ("no other real signal
59
- // exists... do not invent one") — a removed node/edge/dataElement has
60
- // no flow-level protection/policy verdict to have regressed, and this
61
- // module has no coverage-completeness signal scoped to a bare node or
62
- // edge (coverage.js's own ledger is source/sink/language-level, not
63
- // node/edge-level) — so 'application_change' is the only honest
64
- // default for those three entity kinds.
65
- //
66
- // 3. Change-cause classification for flows.changed entries is always
67
- // 'application_change', per the brief's own literal instruction — a
68
- // CHANGED flow (same id, different watched field) can never be a
69
- // coverage regression by definition: coverage regression means the
70
- // engine stopped seeing something, and a flow whose id is still
71
- // present in both snapshots was, by construction, seen in both.
72
- //
73
- // 4. Reidentification (fix round 1, Important 1). flowId's own
74
- // discriminator (graph-builder.js's `ids.flowId(src.id, snk.id,
75
- // [de.id], [p.shape, g.grade, sortedT.join(',')])` call site) includes
76
- // `evidenceGrade`/`transformationIds` — neither is an application-level
77
- // fact; `evidenceGrade` in particular is the analyzer's own confidence
78
- // in the flow. When either changes between two scans of literally the
79
- // same real-world (source, sink, dataElementIds) flow — the commonest
80
- // cause is a refactor the analyzer resolves less/more precisely — the
81
- // flow mints a NEW flowId, and a naive id-set diff would report it as
82
- // an unrelated remove+add pair: a fabricated `firstSeen` on the added
83
- // entry, and a `removed` entry reading as "this flow went away" when it
84
- // did not. `_flowCorrelationKey` (below) correlates a removed/added
85
- // pair by REAL application identity — (source, sink, sorted
86
- // dataElementIds) — instead, and computeGraphDiff marks a correlated
87
- // pair `causeClassification: 'reidentified'` with a pointer
88
- // (`reidentifiedFrom`/`reidentifiedTo`) at its counterpart, rather than
89
- // silently treating them as independent. This never substitutes for
90
- // flowId identity anywhere else in this file — it exists ONLY to
91
- // detect this one reidentification case.
92
- //
93
- // A direct, disclosed consequence: `WATCHED_FLOW_FIELDS`'s `handling`
94
- // entry is near-unreachable in the `changed.flows` bucket for the same
95
- // root cause — a `handling` change almost always also changes
96
- // `transformationIds`, which is itself part of flowId's own
97
- // discriminator, so it manifests as a reidentification (remove+add,
98
- // correlated) rather than surviving as a same-id `changed.flows` entry.
99
- // This is a real, disclosed limitation of the current flowId shape, not
100
- // a bug in this file.
101
-
102
-
103
-
104
-
105
-
106
- const DIFF_VERSION = '1.0.0';
107
-
108
- const ENTITY_ARRAYS = Object.freeze(['nodes', 'edges', 'dataElements', 'flows']);
109
-
110
- // See judgment call #1 above for why `governanceRefs` is included
111
- // alongside the brief's own literal four.
112
- const WATCHED_FLOW_FIELDS = Object.freeze([
113
- 'protectionSummary', 'policyVerdict', 'handling', 'coverageStatus', 'governanceRefs',
114
- ]);
115
-
116
- function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
117
- function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
118
-
119
- function _idMap(arr) {
120
- const m = new Map();
121
- for (const e of arr ?? []) {
122
- if (e && typeof e.id === 'string') m.set(e.id, e);
123
- }
124
- return m;
125
- }
126
-
127
- /** Sorted id-set diff between two id->entity Maps. Deterministic order. */
128
- function _idSetDiff(beforeMap, afterMap) {
129
- const added = [];
130
- const removed = [];
131
- for (const id of afterMap.keys()) if (!beforeMap.has(id)) added.push(id);
132
- for (const id of beforeMap.keys()) if (!afterMap.has(id)) removed.push(id);
133
- added.sort();
134
- removed.sort();
135
- return { added, removed };
136
- }
137
-
138
- // Correlates a flow across a commit boundary by APPLICATION identity
139
- // (source, sink, dataElementIds) — deliberately NOT by flowId, since
140
- // flowId's own discriminator includes evidenceGrade/transformationIds
141
- // (engine-confidence/shape signals, not application facts — see
142
- // graph-builder.js's ids.flowId call site), so the SAME real-world flow
143
- // can legitimately mint two different ids across two scans. Never a
144
- // substitute for flowId identity elsewhere in this file — used ONLY to
145
- // detect this one specific reidentification case.
146
- function _flowCorrelationKey(flow) {
147
- const des = Array.isArray(flow.dataElementIds) ? [...flow.dataElementIds].sort() : [];
148
- return `${flow.source}|${flow.sink}|${des.join(',')}`;
149
- }
150
-
151
- function _addedEntry(id, afterSnapshot) {
152
- return {
153
- id,
154
- causeClassification: 'application_change',
155
- firstSeen: { commit: afterSnapshot.commit, capturedAt: afterSnapshot.capturedAt },
156
- };
157
- }
158
-
159
- function _removedEntry(id, beforeSnapshot, causeClassification, extra = {}) {
160
- return {
161
- id,
162
- causeClassification,
163
- lastSeen: { commit: beforeSnapshot.commit, capturedAt: beforeSnapshot.capturedAt },
164
- ...extra,
165
- };
166
- }
167
-
168
- /**
169
- * Diffs one flow's WATCHED_FLOW_FIELDS between two snapshots of the SAME
170
- * flow id. Returns an array of {field, before, after} — empty when
171
- * nothing watched differs. Never a bare "something changed" — every
172
- * entry names the exact field and its real before/after value.
173
- */
174
- function _diffFlowFields(before, after) {
175
- const changes = [];
176
- for (const field of WATCHED_FLOW_FIELDS) {
177
- if (field === 'governanceRefs') {
178
- const b = before.governanceRefs ?? {};
179
- const a = after.governanceRefs ?? {};
180
- for (const gField of _dataflow_privacy_governance_js__WEBPACK_IMPORTED_MODULE_2__/* .GOVERNANCE_FIELDS */ .n4) {
181
- const bv = b[gField] ?? null;
182
- const av = a[gField] ?? null;
183
- if (JSON.stringify(bv) !== JSON.stringify(av)) {
184
- changes.push({ field: `governanceRefs.${gField}`, before: bv, after: av });
185
- }
186
- }
187
- continue;
188
- }
189
- const bv = before[field] ?? null;
190
- const av = after[field] ?? null;
191
- if (bv !== av) changes.push({ field, before: bv, after: av });
192
- }
193
- return changes;
194
- }
195
-
196
- /**
197
- * Real, checkable coverage-completeness signals — read directly against
198
- * coverage.js's own buildCoverageLedger output shape: `sources.matched`,
199
- * `sinks.callStatementSites`, `languages[].filesAnalyzed`. Returns an
200
- * array of reason strings naming exactly which completeness field
201
- * regressed (empty when none did — never a guess in the regression
202
- * direction).
203
- */
204
- function _coverageRegressionReasons(coverageBefore, coverageAfter) {
205
- const b = coverageBefore ?? {};
206
- const a = coverageAfter ?? {};
207
- const reasons = [];
208
-
209
- const beforeMatched = b.sources?.matched ?? 0;
210
- const afterMatched = a.sources?.matched ?? 0;
211
- if (afterMatched < beforeMatched) {
212
- reasons.push(`sources.matched decreased (${beforeMatched} -> ${afterMatched})`);
213
- }
214
-
215
- const beforeSites = b.sinks?.callStatementSites ?? 0;
216
- const afterSites = a.sinks?.callStatementSites ?? 0;
217
- if (afterSites < beforeSites) {
218
- reasons.push(`sinks.callStatementSites decreased (${beforeSites} -> ${afterSites})`);
219
- }
220
-
221
- const beforeLangs = new Map((b.languages ?? []).map((l) => [l.language, l.filesAnalyzed]));
222
- for (const l of a.languages ?? []) {
223
- const prevAnalyzed = beforeLangs.get(l.language);
224
- if (prevAnalyzed != null && l.filesAnalyzed < prevAnalyzed) {
225
- reasons.push(`languages.${l.language}.filesAnalyzed decreased (${prevAnalyzed} -> ${l.filesAnalyzed})`);
226
- }
227
- }
228
-
229
- return reasons;
230
- }
231
-
232
- /**
233
- * Structural validation only — mirrors graph-snapshot.js's own
234
- * validateGraphSnapshot {valid, errors} shape and "never throws"
235
- * contract. Checks the record's own top-level shape plus each
236
- * added/removed/changed entry's own minimal shape; never cross-references
237
- * a real graph (this module's validator, like every §10.10 extension
238
- * contract's own validator, is a pure shape check).
239
- */
240
- function validateGraphDiff(record) {
241
- const errors = [];
242
- const err = (p, message) => errors.push({ path: p, message });
243
-
244
- if (!record || typeof record !== 'object' || Array.isArray(record)) {
245
- err('$', 'GraphDiff record must be an object');
246
- return { valid: false, errors };
247
- }
248
-
249
- if (!_isNonEmptyString(record.id) || !record.id.startsWith('diff:')) {
250
- err('$.id', 'id is required and must start with "diff:"');
251
- }
252
- if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
253
- if (!_isNonEmptyString(record.beforeSnapshotId)) err('$.beforeSnapshotId', 'beforeSnapshotId is required');
254
- if (!_isNonEmptyString(record.afterSnapshotId)) err('$.afterSnapshotId', 'afterSnapshotId is required');
255
- if (!_isNonEmptyString(record.generatedAt)) err('$.generatedAt', 'generatedAt is required');
256
-
257
- if (!_isPlainObject(record.comparability)) {
258
- err('$.comparability', 'comparability is required and must be an object');
259
- } else {
260
- if (typeof record.comparability.comparable !== 'boolean') {
261
- err('$.comparability.comparable', 'comparability.comparable must be a boolean');
262
- }
263
- if (!Array.isArray(record.comparability.reasons)) {
264
- err('$.comparability.reasons', 'comparability.reasons must be an array');
265
- }
266
- }
267
-
268
- const _checkEntryArray = (bucketPath, arr, requireFirstSeen, requireLastSeen) => {
269
- if (!Array.isArray(arr)) {
270
- err(bucketPath, `${bucketPath} must be an array`);
271
- return;
272
- }
273
- arr.forEach((entry, i) => {
274
- const p = `${bucketPath}[${i}]`;
275
- if (!_isPlainObject(entry)) { err(p, `${p} must be an object`); return; }
276
- if (!_isNonEmptyString(entry.id)) err(`${p}.id`, `${p}.id is required`);
277
- if (!_isNonEmptyString(entry.causeClassification)) err(`${p}.causeClassification`, `${p}.causeClassification is required`);
278
- if (requireFirstSeen && !_isPlainObject(entry.firstSeen)) err(`${p}.firstSeen`, `${p}.firstSeen is required and must be an object`);
279
- if (requireLastSeen && !_isPlainObject(entry.lastSeen)) err(`${p}.lastSeen`, `${p}.lastSeen is required and must be an object`);
280
- });
281
- };
282
-
283
- if (!_isPlainObject(record.added)) {
284
- err('$.added', 'added is required and must be an object');
285
- } else {
286
- for (const key of ENTITY_ARRAYS) _checkEntryArray(`$.added.${key}`, record.added[key], true, false);
287
- }
288
-
289
- if (!_isPlainObject(record.removed)) {
290
- err('$.removed', 'removed is required and must be an object');
291
- } else {
292
- for (const key of ENTITY_ARRAYS) _checkEntryArray(`$.removed.${key}`, record.removed[key], false, true);
293
- }
294
-
295
- if (!_isPlainObject(record.changed)) {
296
- err('$.changed', 'changed is required and must be an object');
297
- } else if (!Array.isArray(record.changed.flows)) {
298
- err('$.changed.flows', 'changed.flows must be an array');
299
- } else {
300
- record.changed.flows.forEach((entry, i) => {
301
- const p = `$.changed.flows[${i}]`;
302
- if (!_isPlainObject(entry)) { err(p, `${p} must be an object`); return; }
303
- if (!_isNonEmptyString(entry.id)) err(`${p}.id`, `${p}.id is required`);
304
- if (!_isNonEmptyString(entry.causeClassification)) err(`${p}.causeClassification`, `${p}.causeClassification is required`);
305
- if (!Array.isArray(entry.changes) || entry.changes.length === 0) {
306
- err(`${p}.changes`, `${p}.changes must be a non-empty array naming which field(s) changed`);
307
- }
308
- });
309
- }
310
-
311
- return { valid: errors.length === 0, errors };
312
- }
313
-
314
- /**
315
- * Diffs two GraphSnapshot records. Refuses (throws, naming the real
316
- * reasons) on an incomparable pair — never silently diffs across one.
317
- *
318
- * added/removed: an ID-set diff across nodes/edges/dataElements/flows.
319
- * changed: flows ONLY — every flow id present in both snapshots whose
320
- * WATCHED_FLOW_FIELDS differ.
321
- *
322
- * Change-cause classification: every added/changed entry gets
323
- * 'application_change' (no other real signal exists for those — see this
324
- * file's own header). Every removed FLOW entry gets
325
- * 'possible_coverage_regression' when the AFTER snapshot's real coverage
326
- * is measurably lower on a real completeness field, otherwise
327
- * 'application_change'; every removed node/edge/dataElement entry gets
328
- * 'application_change' unconditionally (see judgment call #2 above).
329
- */
330
- function computeGraphDiff(snapshotBefore, snapshotAfter, opts = {}) {
331
- const { comparable, reasons } = (0,_graph_snapshot_js__WEBPACK_IMPORTED_MODULE_0__/* .snapshotsComparable */ .mx)(snapshotBefore, snapshotAfter);
332
- if (!comparable) {
333
- throw new Error(`computeGraphDiff: snapshots are not comparable — ${reasons.join('; ')}`);
334
- }
335
-
336
- const graphBefore = snapshotBefore.graph ?? {};
337
- const graphAfter = snapshotAfter.graph ?? {};
338
-
339
- const coverageRegressionReasons = _coverageRegressionReasons(snapshotBefore.coverage, snapshotAfter.coverage);
340
- const flowRemovalCause = coverageRegressionReasons.length ? 'possible_coverage_regression' : 'application_change';
341
-
342
- const added = {};
343
- const removed = {};
344
- const flowMaps = {};
345
-
346
- for (const key of ENTITY_ARRAYS) {
347
- const beforeMap = _idMap(graphBefore[key]);
348
- const afterMap = _idMap(graphAfter[key]);
349
- if (key === 'flows') {
350
- flowMaps.before = beforeMap;
351
- flowMaps.after = afterMap;
352
- }
353
-
354
- const { added: addedIds, removed: removedIds } = _idSetDiff(beforeMap, afterMap);
355
-
356
- // Reidentification pairing (see judgment call #4 in this file's own
357
- // header for the full reasoning): group removed/added flow ids by
358
- // _flowCorrelationKey, pair them 1:1 (sorted by id, index-aligned)
359
- // within each matching key. A pair is a re-identification, not a real
360
- // remove+add. Any surplus on either side (uncommon — would need >1
361
- // flow sharing the same correlation key on one side) falls through to
362
- // the existing add/remove logic unchanged.
363
- const reidentifiedFrom = new Map(); // addedId -> removedId
364
- const reidentifiedTo = new Map(); // removedId -> addedId
365
- if (key === 'flows') {
366
- const removedByKey = new Map();
367
- for (const id of removedIds) {
368
- const f = beforeMap.get(id);
369
- const k = _flowCorrelationKey(f);
370
- if (!removedByKey.has(k)) removedByKey.set(k, []);
371
- removedByKey.get(k).push(id);
372
- }
373
- const addedByKey = new Map();
374
- for (const id of addedIds) {
375
- const f = afterMap.get(id);
376
- const k = _flowCorrelationKey(f);
377
- if (!addedByKey.has(k)) addedByKey.set(k, []);
378
- addedByKey.get(k).push(id);
379
- }
380
- for (const [k, removedGroup] of removedByKey) {
381
- const addedGroup = addedByKey.get(k);
382
- if (!addedGroup) continue;
383
- removedGroup.sort();
384
- addedGroup.sort();
385
- const n = Math.min(removedGroup.length, addedGroup.length);
386
- for (let i = 0; i < n; i++) {
387
- reidentifiedTo.set(removedGroup[i], addedGroup[i]);
388
- reidentifiedFrom.set(addedGroup[i], removedGroup[i]);
389
- }
390
- }
391
- }
392
-
393
- added[key] = addedIds.map((id) => {
394
- const entry = _addedEntry(id, snapshotAfter);
395
- if (key === 'flows' && reidentifiedFrom.has(id)) {
396
- entry.causeClassification = 'reidentified';
397
- entry.reidentifiedFrom = reidentifiedFrom.get(id);
398
- }
399
- return entry;
400
- });
401
-
402
- if (key === 'flows') {
403
- removed[key] = removedIds.map((id) => {
404
- const extra = coverageRegressionReasons.length ? { coverageRegressionReasons } : {};
405
- const entry = _removedEntry(id, snapshotBefore, flowRemovalCause, extra);
406
- if (reidentifiedTo.has(id)) {
407
- entry.causeClassification = 'reidentified';
408
- entry.reidentifiedTo = reidentifiedTo.get(id);
409
- }
410
- return entry;
411
- });
412
- } else {
413
- removed[key] = removedIds.map((id) => _removedEntry(id, snapshotBefore, 'application_change'));
414
- }
415
- }
416
-
417
- const changedFlows = [];
418
- for (const [id, beforeFlow] of flowMaps.before) {
419
- const afterFlow = flowMaps.after.get(id);
420
- if (!afterFlow) continue; // removed — already handled above
421
- const changes = _diffFlowFields(beforeFlow, afterFlow);
422
- if (changes.length > 0) {
423
- changedFlows.push({ id, causeClassification: 'application_change', changes });
424
- }
425
- }
426
- changedFlows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
427
-
428
- const record = {
429
- id: (0,_ids_js__WEBPACK_IMPORTED_MODULE_1__/* .diffId */ .Jd)({ beforeSnapshotId: snapshotBefore.id, afterSnapshotId: snapshotAfter.id }),
430
- version: DIFF_VERSION,
431
- beforeSnapshotId: snapshotBefore.id,
432
- afterSnapshotId: snapshotAfter.id,
433
- comparability: { comparable: true, reasons: [] },
434
- added,
435
- removed,
436
- changed: { flows: changedFlows },
437
- generatedAt: opts.generatedAt ?? new Date().toISOString(),
438
- };
439
-
440
- const { valid, errors } = validateGraphDiff(record);
441
- if (!valid) {
442
- throw new Error(`computeGraphDiff: internal error — produced an invalid GraphDiff: ${JSON.stringify(errors)}`);
443
- }
444
- return record;
445
- }
446
-
447
-
448
- /***/ })
449
-
450
- };