@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5

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 (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +379 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +841 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -0,0 +1,355 @@
1
+ # DESIGN_HANDLING_ANALYZER.md — Sub-project D's binding design record (Milestone 2, increments 1-2)
2
+
3
+ **Status:** landed as Milestone 2, Sub-project D, increments **1** (FR-403's
4
+ taxonomy label, §1-§4) and **2** (FR-307's multi-path control-credit rule,
5
+ §5) — two slices of a "Large" sub-project, per
6
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d1-plan.md`
7
+ and
8
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d2-plan.md`.
9
+ Binding on later Sub-project D increments the same way `DESIGN_
10
+ DESTINATION_RESOLVER.md` binds Sub-project A — MUCH shorter than
11
+ `DESIGN_GRAPH_BUILDER.md`/`DESIGN_PATH_PROVENANCE.md`/`DESIGN_REGISTRIES.md`
12
+ on purpose: this is two slices of a larger sub-project, not the whole thing.
13
+
14
+ ---
15
+
16
+ ## 1. What this increment actually is
17
+
18
+ FR-403 asks: for a field reaching a sink, what HANDLING was applied to it
19
+ along the way — raw, masked, hashed, tokenized, encrypted, etc.? Increment 1
20
+ produces that TAXONOMY LABEL for one already-reconstructed path (the caller
21
+ picks which path — increment 1 does not iterate multiple paths to one
22
+ sink). Increment 1 deliberately did NOT implement FR-307's multi-path
23
+ control-credit rule (AC-12: "a transform on one branch cannot make the full
24
+ flow green") — that needed comparing MULTIPLE paths to the same sink, named
25
+ here at the time as a distinct, larger follow-up (D2) rather than silently
26
+ attempted. **D2 is now done** — see §5 below.
27
+
28
+ No new detection. This increment is entirely a RECLASSIFICATION of
29
+ `transform-catalog.js`'s already-shipped `recognizeTransformation` output
30
+ (Sub-project D, increment 4) onto a new, narrower vocabulary, applied to a
31
+ single reconstructed path's hops (Sub-project C's already-shipped
32
+ `path-query.js#reconstructPaths`).
33
+
34
+ ---
35
+
36
+ ## 2. The `handling` verdict shape and the kind → HANDLING_VALUES mapping
37
+
38
+ `classifyHandling(path, callGraph)` (`handling-analyzer.js`) returns:
39
+
40
+ ```
41
+ {
42
+ handling: HandlingValue, // schema.js's HANDLING_VALUES
43
+ recognizedTransform: TransformDecision | null, // transform-catalog.js's
44
+ // own recognizeTransformation()
45
+ // return shape, unmodified
46
+ hopIndex: number | null, // index into path.hops where the
47
+ // transform was found, else null
48
+ }
49
+ ```
50
+
51
+ `schema.js`'s `HANDLING_VALUES` is FR-403's own 8-value taxonomy: `raw`,
52
+ `masked`, `redacted`, `hashed`, `tokenized`, `encrypted`, `aggregated`,
53
+ `unknown`.
54
+
55
+ The `transform-catalog.js` `kind` → `HANDLING_VALUES` mapping:
56
+
57
+ | `kind` | `handling` | Why |
58
+ |---|---|---|
59
+ | `mask` | `masked` | direct |
60
+ | `redact` | `redacted` | direct |
61
+ | `hash` | `hashed` | direct |
62
+ | `tokenize` | `tokenized` | direct |
63
+ | `encrypt` | `encrypted` | direct |
64
+ | `decrypt` | `unknown` | a decrypt immediately before a sink is actively the OPPOSITE of protection — never mapped to a protective label |
65
+ | `encode` | `unknown` | encoding is reversible obfuscation, not itself a protective handling state |
66
+ | `decode` | `unknown` | the direct counterpart of `encode` |
67
+ | `truncate` | `unknown` | general-purpose shortening, not necessarily a privacy control (`transform-catalog.js`'s own disclosed low-confidence note) |
68
+ | `normalize` | `unknown` | reversibility itself is `unknown` at the catalog level; no protective claim is possible |
69
+ | `aggregate` | `unknown` | **disclosed, not a drive-by decision**: `HANDLING_VALUES` carries its own `aggregated` value, but awarding it needs shape-level reasoning about a WHOLE collection (is EVERY record aggregated, or just this one hop's local variable?) that a single-hop, single-path classifier cannot do soundly. Deferred beyond D2, still open (§6) — `classifyHandling` never emits `'aggregated'`, the same way `transform-catalog.js` itself never emits `custom`/`unknown` as a recognized kind: an honest gap, not a guess. |
70
+
71
+ `custom`/`unknown` never reach this table because `recognizeTransformation`
72
+ never emits them (`transform-catalog.js`'s own documented contract) — a
73
+ `null` `recognizeTransformation` result (no match at that hop) is simply
74
+ skipped, not routed through this table at all.
75
+
76
+ A path with NO recognized transform on ANY hop, and a literal/direct field
77
+ reaching the sink, is `raw` — `classifyHandling`'s own fallback when the
78
+ walk exhausts every hop with no match:
79
+ `{handling: 'raw', recognizedTransform: null, hopIndex: null}`.
80
+
81
+ ### A load-bearing naming note
82
+
83
+ `protection.js`'s `PROTECTION_DIMENSIONS` already includes a dimension
84
+ literally named `'handling'` — a per-EDGE `{verdict, evidenceGrade}` object
85
+ scored from `PROTECTION_VERDICTS` (`protected`/`unprotected`/`unknown`/
86
+ `not_applicable`/`not_assessed`). That is a DIFFERENT field
87
+ (`edge.protection.handling`), a DIFFERENT vocabulary, and a DIFFERENT
88
+ question ("is this dimension protected?" vs. "what taxonomy label applies
89
+ to this hop?"). The two share a name because this taxonomy is precisely
90
+ what a later Milestone 2 analyzer will read to DECIDE that verdict — see
91
+ `transform-catalog.js`'s own header: recognizing that a transform happened
92
+ is Sub-project D's job; deciding whether it earns "protected" is a later
93
+ FR-401-405 analyzer reading this output. This increment sets only
94
+ `flow.handling` (the taxonomy string). It never writes
95
+ `edge.protection.handling` and never reads it either.
96
+
97
+ ---
98
+
99
+ ## 3. `classifyHandling(path, callGraph)` — the walk
100
+
101
+ Walks `path.hops` in the order `path-query.js` already materializes them
102
+ (source → sink — see that module's own header: "a human reads a flow
103
+ source -> sink, so the arrays are reversed here, once, at the point a
104
+ candidate is emitted"). For each hop:
105
+
106
+ 1. Resolve `callGraph.functions.get(hop.scope)?.cfg?.nodes?.[hop.siteNodeId]`
107
+ defensively — `callGraph.functions` is a real `Map` (`ir/callgraph.js`'s
108
+ own shape), never a plain object; a missing/malformed lookup at any step
109
+ is `undefined`, not a throw, and the hop is skipped.
110
+ 2. When that CFG node resolves, collect every `{kind: 'call'}` expression
111
+ reachable from it — mirroring `graph-builder.js`'s own per-hop
112
+ transformation-extraction loop (§7 of `DESIGN_GRAPH_BUILDER.md`)
113
+ exactly: `exprRoots`/`walkExpr` over the node (`source-seeding.js`,
114
+ already shipped) plus the node's OWN call when the node itself is
115
+ `kind: 'call'` (a bare call statement — `exprRoots` only ever yields a
116
+ call node's `callee`/`args`, never a `{kind:'call'}` shape for the node
117
+ itself, so `graph-builder.js` unshifts that case explicitly and this
118
+ module mirrors it). This is NOT the narrower "only literal `call`-kind
119
+ CFG nodes" reading a first pass at this plan's own wording might
120
+ suggest — the AC-02 fixture this increment must pass
121
+ (`maskCard(cardNumber)` as an assignment RHS) proves why: the hop
122
+ carrying the `call-resolved`/`call-arg-bind` production for that call is
123
+ stamped with the CALLER's `assign`-kind CFG node id (`engine.js`'s
124
+ `stepCtx` wrapper stamps every hop with the CURRENT worklist node,
125
+ whatever its kind), never a literal `call`-kind node. Restricting the
126
+ walk to `node.kind === 'call'` alone would silently never find
127
+ `maskCard` here at all — verified live before settling on this reading.
128
+ 3. For each collected call, in expression order, build a
129
+ `transform-catalog.js` descriptor and call `recognizeTransformation`.
130
+ The FIRST recognized transform found — across the whole path, hop by
131
+ hop, call by call — wins; nothing later is consulted.
132
+
133
+ ### Callee-descriptor construction: a disclosed third copy, not a cycle
134
+
135
+ `graph-builder.js` already has a private (unexported) `calleeDescriptor`
136
+ helper for this exact shape, and `registry-real-code.test.js` has its own
137
+ independent copy for a structural cross-check. This increment needs a
138
+ THIRD. Two ways to avoid tripling it were considered and rejected:
139
+
140
+ - **Export `calleeDescriptor` from `graph-builder.js` and import it here.**
141
+ Rejected: this increment ALSO wires `classifyHandling` INTO
142
+ `graph-builder.js`'s own flow-construction loop (§4 below) — so
143
+ `graph-builder.js` must import `handling-analyzer.js`. Importing
144
+ `calleeDescriptor` the other way would make the two files mutually
145
+ dependent, the exact shape `coverage.js`/`resolve-destination.js` already
146
+ carry and justify carefully (their header comments explain why it's safe
147
+ there). Introducing a SECOND such cycle for a ~10-line helper is not
148
+ worth the added reasoning burden.
149
+ - **Move `calleeDescriptor` into `source-seeding.js`**, which already hosts
150
+ the shared `exprRoots`/`walkExpr` primitives both `graph-builder.js` and
151
+ this module import. Rejected as out of this increment's stated scope —
152
+ `source-seeding.js` is not on the plan's file list, and widening its
153
+ reuse boundary is a real, separate decision this increment should not
154
+ make as a side effect.
155
+
156
+ `handling-analyzer.js` therefore carries its own small, private
157
+ `calleeDescriptorOf`, byte-for-byte the same logic as `graph-builder.js`'s
158
+ own — a disclosed, judgment-call duplicate, not an oversight.
159
+
160
+ ---
161
+
162
+ ## 4. Wiring — inside `graph-builder.js`'s flow-construction loop
163
+
164
+ `flow.handling` is set once, at FLOW-MINT time, inside
165
+ `buildDataFlowGraph`'s `groupsByFlowKey` loop — the same place
166
+ `protection.js`'s `emptyProtection()` is attached to an EDGE object
167
+ one loop up. `classifyHandling(p, callGraph)` is called on `group[0].p`
168
+ (the flow's own representative reconstructed `Path`, already in scope in
169
+ that loop) and its `.handling` string is written to `flow.handling`.
170
+
171
+ Per this increment's own simplification: EVERY flow gets a real
172
+ `flow.handling` value — never `null` — since every sink category
173
+ plausibly has SOME handling answer (even `'raw'` for a field that reached
174
+ it untransformed). A future increment MAY special-case a sink category
175
+ with no natural "handling" concept to `null` instead of a fabricated
176
+ `'raw'`; this increment does not attempt that distinction.
177
+
178
+ `classifyHandling` is computed ONCE, at graph-build time, from the flow's
179
+ OWN reconstructed path — consistent with how every other flow field is
180
+ computed today (per this plan's own "Explicitly deferred" list: no
181
+ re-derivation if a flow's underlying path later changes).
182
+
183
+ ---
184
+
185
+ ## 5. FR-307 multi-path control-credit (increment 2)
186
+
187
+ **Status:** landed as Milestone 2, Sub-project D, increment **2**, per
188
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d2-plan.md`.
189
+ Computes `transformation.appliesToAllPaths` — the field `graph-builder.js`
190
+ had stubbed at `null` on every transformation entity since increment 1 (§4's
191
+ "honest absences" note) — and nothing else.
192
+
193
+ ### Scope boundary
194
+
195
+ This increment does **NOT** compute `flow.protectionSummary` as
196
+ `mixed`/`unprotected` (AC-12's own literal end-to-end verdict wording) —
197
+ that field stays `'not_assessed'`, exactly as before. Populating it for real
198
+ needs a real protection-verdict analyzer (Sub-project B/C's transit/at-rest
199
+ analyzers, or Sub-project G's policy verdict), none of which exist yet.
200
+ `appliesToAllPaths` is the load-bearing SIGNAL those future analyzers will
201
+ consume, not the verdict itself.
202
+
203
+ ### The rule
204
+
205
+ For every **coarse key** `[src.id, snk.id, de.id].join('|')` — deliberately
206
+ coarser than `groupsByFlowKey`'s own key, since it drops `shape`/`grade`/
207
+ `sortedT`, so every flow group reaching the same sink for the same field
208
+ from the same source, regardless of which transforms were found on it,
209
+ collapses into one coarse group:
210
+
211
+ 1. Collect every DISTINCT flow group (post-dedup — `groupsByFlowKey`'s own
212
+ entries, not raw reconstructed paths) sharing that coarse key, and each
213
+ one's `sortedT` (its ordered transformation-id array).
214
+ 2. For every transformation id `tid` appearing in ANY flow group's
215
+ `sortedT` within that coarse group: `tid` "applies to all paths" for
216
+ THIS coarse group iff `sortedT.includes(tid)` is true for **every** flow
217
+ group in the coarse group — not just the ones where it appears.
218
+ 3. Write the result onto `transformsById.get(tid).appliesToAllPaths`.
219
+
220
+ Note that `groupsByFlowKey`'s own key already includes the transformation-id
221
+ set (`sortedT.join(',')`), which was Milestone 1's own flow-key design, not
222
+ something this increment needed to build — two paths to the same
223
+ `(source, sink, dataElement)` that apply DIFFERENT transforms already landed
224
+ in DIFFERENT flow groups before this increment, each with its own
225
+ `flow.handling`. This increment adds the cross-group AGGREGATION on top:
226
+ comparing every flow group in a coarse key against every other one, which
227
+ `groupsByFlowKey`'s own per-group key cannot do by itself.
228
+
229
+ ### Conjunction across coarse groups (a disclosed rare-collision case)
230
+
231
+ A transformation's own id (`ids.transformationId(anchor, ...)`) is keyed on
232
+ the SINK node id, not the data-element id — a `transformsById` entry can in
233
+ principle (rare, not observed in any shipped fixture) be referenced by flows
234
+ for more than one data element, if two different fields happen to produce
235
+ byte-identical `(file, line, fromPath, toPath)` at the same sink. If the
236
+ SAME `tid` is relevant to more than one coarse group, the final
237
+ `appliesToAllPaths` is the AND of every coarse group's own answer for that
238
+ `tid` — `true` only if every coarse group where it is relevant judges it
239
+ `true`. This is the conservative (never-overclaim) direction, consistent
240
+ with FR-307's own "insufficient" framing and with Milestone 2's own
241
+ "false-protected" exit-gate concern: a release must never assert credit it
242
+ cannot prove. This case is disclosed but not separately fixture-tested — see
243
+ §5's own "Explicitly deferred" entry below.
244
+
245
+ ### Why a truncated/incomplete path needs no special-case code
246
+
247
+ If one path to a sink was cut short by `path-query.js`'s own budget/depth
248
+ limits before reaching a transform call site, that path's own flow-group
249
+ `sortedT` simply won't contain that transform's id — the hop was never
250
+ walked, so `recognizeTransformation` never ran on it. Under the rule above,
251
+ that flow group's absence of `tid` in its own `sortedT` already forces
252
+ `appliesToAllPaths` to `false` for any `tid` present in a SIBLING flow group
253
+ in the same coarse key — the conservative answer falls out of the existing
254
+ conjunction, with no additional "is this path incomplete" branch needed.
255
+ `test/lineage/graph-builder.test.js`'s truncated-path test proves this is
256
+ real, not just argued in prose, by forcing a real `path-query.js` depth
257
+ truncation (via `buildDataFlowGraph`'s existing `opts.budget` passthrough)
258
+ on one branch while a sibling branch's transform completes.
259
+
260
+ ### Implementation
261
+
262
+ `graph-builder.js` only — a second pass over data the existing
263
+ flow-construction loop already builds (`groupsByFlowKey`/`transformsById`),
264
+ not new detection. Runs strictly after the flow-construction loop finishes
265
+ populating those maps and before `graph.transformations`/`graph.flows` are
266
+ assigned from them. See that file's own inline comment at the aggregation
267
+ block for the exact code.
268
+
269
+ ## 6. Explicitly deferred (named, not silently skipped)
270
+
271
+ - **`aggregate`'s own `'aggregated'` verdict** — needs shape-level
272
+ reasoning about a WHOLE collection, not a single hop. See §2's table.
273
+ Deferred, not scheduled.
274
+ - **Any UI/display of the handling taxonomy.**
275
+ - **Re-deriving `handling` for a flow whose reconstructed path changes**
276
+ after a later Sub-project A/E change — this increment computes it once,
277
+ at graph-build time, same as every other flow field.
278
+ - **Populating `edge.protection.handling`'s own verdict** from this
279
+ taxonomy — a later Milestone 2 analyzer's job, not this increment's
280
+ (§2's naming note).
281
+ - **`flow.protectionSummary` computation** (`mixed`/`unprotected`/
282
+ `protected`) — AC-12's own literal end-to-end verdict wording — stays
283
+ `'not_assessed'`. Needs a real protection-verdict analyzer consuming
284
+ `appliesToAllPaths` as one of its inputs (Sub-project B/C/G, none built
285
+ yet); increment 2 produces the SIGNAL, not the verdict.
286
+ - **The rare same-call-site-different-data-element `t.id` collision** (see
287
+ §5's "Conjunction across coarse groups") — handled conservatively (AND
288
+ across groups) but not separately tested with a dedicated collision
289
+ fixture, since no real catalog/parser shape produces it today.
290
+
291
+ ---
292
+
293
+ ## 7. A second consumer: `edge.protection.atRest` (Milestone 2, Sub-project C, increment 1)
294
+
295
+ **Status:** landed as Milestone 2, Sub-project C, increment **1** (FR-402's
296
+ application-layer at-rest evidence source), per
297
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-c1-plan.md`
298
+ and its own scoping doc. This is not a new analyzer module — it is a second,
299
+ additive consumer of the SAME `classifyHandling(p, callGraph)` result §4
300
+ already computes and writes to `flow.handling`, wired inline in
301
+ `graph-builder.js`'s flow-construction loop, right next to it.
302
+
303
+ FR-402 asks whether application-layer field encryption evidence sits
304
+ directly on the path to a store write. `classifyHandling`'s own
305
+ `KIND_TO_HANDLING` table (§2) already answers exactly that question for the
306
+ `'encrypted'` outcome — a recognized `transform-catalog.js` `encrypt`-kind
307
+ call found ON THE PATH — so this increment needed no new detection, only a
308
+ second write from the one already-computed result:
309
+
310
+ ```js
311
+ const handlingResult = classifyHandling(p, callGraph).handling;
312
+ if (handlingResult === 'encrypted' && snk.kind === 'store') {
313
+ const edge = edgesById.get(edgeIdStr);
314
+ if (edge) edge.protection.atRest = { verdict: 'protected', evidenceGrade: 'code' };
315
+ }
316
+ ```
317
+
318
+ `classifyHandling` is called exactly once per flow — the same call whose
319
+ result feeds `flow.handling` (§4) is reused for this check, never invoked a
320
+ second time.
321
+
322
+ **The gate.** `snk.kind === 'store'` — `sink-registry.js`'s
323
+ `CATEGORY_NODE_KIND` maps `database`/`file`/`object-storage`/`cache`/
324
+ `client-storage`/`backup`/`export` to `kind: 'store'`; `queue` has its own
325
+ distinct `kind: 'queue'` and is deliberately excluded (named as a future
326
+ widening question, not attempted here). Only `'encrypted'` triggers this —
327
+ every other `HANDLING_VALUES` member (`masked`/`hashed`/`tokenized`/`raw`/
328
+ `redacted`/`aggregated`/`unknown`) leaves `edge.protection.atRest` at
329
+ `emptyProtection()`'s own honest `{verdict: 'not_assessed', evidenceGrade:
330
+ 'none'}` default. Neither `masking` nor `hashing` nor `tokenization` is
331
+ at-rest PROTECTION evidence in FR-402's own sense — only encryption is.
332
+
333
+ **The anti-pattern guard holds by construction, not by new code.** FR-402
334
+ explicitly warns that "a cipher present anywhere in the same file or
335
+ repository cannot alone establish protection for an unrelated store."
336
+ `classifyHandling` walks `path.hops` — THIS flow's own reconstructed path,
337
+ never "the whole file" or "the whole repo." An `encrypt()`-shaped call that
338
+ exists elsewhere in the same file or function but is not on this specific
339
+ flow's own path to this specific store sink is structurally invisible to
340
+ `classifyHandling`, and therefore invisible to this check too — the same
341
+ precision property §4's own flow.handling already relies on, now reused for
342
+ a security VERDICT rather than a taxonomy label.
343
+
344
+ **What this does NOT do**, all deferred to future, separately-scoped
345
+ increments (C2/C3, per the sub-project's own scoping doc): storage/IaC
346
+ encryption configuration detection (an S3/RDS/Terraform-level
347
+ `ServerSideEncryptionConfiguration`/`StorageEncrypted`/`kms_key_id` signal);
348
+ database column or transparent-encryption configuration; widening the
349
+ `store`-kind filter to `queue`/`vector-store`/`model`/`training`; AC-12's
350
+ at-rest half of the aggregate `protectionSummary` verdict (needs a real
351
+ aggregation rule, the same boundary §5's own scope-boundary note already
352
+ drew for the transit/handling case). The ABSENCE case (no recognized
353
+ encryption, or a non-`store` sink) was already correct with zero new code —
354
+ `emptyProtection()`'s own default — before this increment; this increment's
355
+ own job was only to give the PRESENCE case something real to write.