@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
@@ -1 +1 @@
1
- 72173ed6629d8035f14bf6bce9c6f268d40d179aeff64167dd471e8a829ef189 agentic-security.mjs
1
+ 0cfc21d9bdecf4422a0934216444b948dccc8c4c978b0f7eca92d318d8b61e9c agentic-security.mjs
@@ -5,7 +5,7 @@
5
5
  "license": "US Federal regulation (public)",
6
6
  "url": "https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164",
7
7
  "scope": "SELECTIVE SUBSET. 8 of the Security Rule technical safeguards. Administrative and physical safeguards are outside what a code scanner can observe and are NOT represented here.",
8
- "controlsDigest": "a99f9c4015ffbf72",
8
+ "controlsDigest": "7e94038dd9a64d3d",
9
9
  "controlCount": 8,
10
10
  "controls": [
11
11
  {
@@ -102,7 +102,8 @@
102
102
  ],
103
103
  "mapsTo": [
104
104
  "family:crypto-tls-no-verify",
105
- "family:crypto-tls-version"
105
+ "family:crypto-tls-version",
106
+ "graph:transit-protection:PHI:external:transit:protected"
106
107
  ]
107
108
  }
108
109
  ]
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Data Flow Explorer — Illustrative demo data</title>
7
+ <link rel="stylesheet" href="styles/tokens.css" />
8
+ <link rel="stylesheet" href="styles/shell.css" />
9
+ <link rel="stylesheet" href="styles/architecture-view.css" />
10
+ <link rel="stylesheet" href="styles/inspector.css" />
11
+ <link rel="stylesheet" href="styles/privacy-view.css" />
12
+ <link rel="stylesheet" href="styles/trace-view.css" />
13
+ <link rel="stylesheet" href="styles/filter-rail.css" />
14
+ <link rel="stylesheet" href="styles/inventory-view.css" />
15
+ <link rel="stylesheet" href="styles/query-bar.css" />
16
+ </head>
17
+ <body>
18
+ <div id="app-root"></div>
19
+ <script type="module" src="./src/main.js"></script>
20
+ </body>
21
+ </html>
@@ -0,0 +1,176 @@
1
+ import { mountShell, buildContextRailText } from './shell.js';
2
+ import { computeArchitectureViewModel, renderArchitectureView, renderFlowSummary } from './views/architecture-view.js';
3
+ import { computePrivacyViewModel, renderPrivacyView } from './views/privacy-view.js';
4
+ import { computeTraceViewModel, renderTraceView } from './views/trace-view.js';
5
+ import { computeInventoryViewModel, renderInventoryView } from './views/inventory-view.js';
6
+ import { computeInspectorViewModel, renderInspector } from './components/evidence-inspector.js';
7
+ import { computeFilterFacets, renderFilterRail } from './components/filter-rail.js';
8
+ import { computeQueryBarViewModel, renderQueryBar, compileQuerySafely } from './components/query-bar.js';
9
+ import {
10
+ showUpstream, showDownstream, showAllPaths, showShortestPath,
11
+ showExternalPathsOnly, showUnprotectedPathsOnly, showAliases, showDisconnected,
12
+ } from './lib/focus-controls.js';
13
+ import { el, clear } from './lib/dom.js';
14
+
15
+ // Milestone 3, sub-project M3-UX-Query, Task 4's own resolution of the task
16
+ // brief's Step 4 open design question: a focus control's own {nodeIds,
17
+ // edgeIds} result (from lib/focus-controls.js) has no single canonical
18
+ // `selectedId` to thread through the existing shell.js state mechanism, and
19
+ // is real-but-transient UI state (not meaningfully shareable — it holds
20
+ // Sets, which don't serialize to the URL hash cleanly), matching the same
21
+ // precedent A11y's own `inspectorOverlayOpen` and M3-Render's own
22
+ // `currentViewport` already established: module-local state, not persisted.
23
+ // Lives here (app.js), the one place that already orchestrates every view's
24
+ // own compute/render call, rather than inside architecture-view.js itself.
25
+ let currentFocusSelection = null;
26
+
27
+ const FOCUS_CONTROLS = [
28
+ { id: 'upstream', label: 'Show upstream', needsNode: true, run: (graph, anchor) => showUpstream(graph, anchor.nodeId) },
29
+ { id: 'downstream', label: 'Show downstream', needsNode: true, run: (graph, anchor) => showDownstream(graph, anchor.nodeId) },
30
+ { id: 'all-paths', label: 'Show all paths', needsNode: true, run: (graph, anchor) => showAllPaths(graph, anchor.nodeId) },
31
+ { id: 'shortest-path', label: 'Show shortest path', needsEdge: true, run: (graph, anchor) => showShortestPath(graph, anchor.edge.from, anchor.edge.to) },
32
+ { id: 'external-only', label: 'Show external paths only', run: (graph) => showExternalPathsOnly(graph) },
33
+ { id: 'unprotected-only', label: 'Show unprotected paths only', run: (graph) => showUnprotectedPathsOnly(graph) },
34
+ { id: 'aliases', label: 'Show aliases', needsNode: true, run: (graph, anchor) => showAliases(graph, anchor.nodeId) },
35
+ { id: 'disconnected', label: 'Show disconnected', run: (graph) => showDisconnected(graph) },
36
+ // resetToOverview is deliberately NOT a lib/focus-controls.js function
37
+ // (per that file's own header note) — it is implemented here directly:
38
+ // clear the focus override AND the underlying single selection.
39
+ { id: 'reset', label: 'Reset to application overview', isReset: true },
40
+ ];
41
+
42
+ // Determines which node/edge a focus control acts on for the CURRENT
43
+ // selection. A directly-selected NODE is its own anchor. A directly-
44
+ // selected EDGE carries both endpoints, used only by "Show shortest path"
45
+ // (the one control that genuinely needs two nodes). A directly-selected
46
+ // FLOW has no single node id of its own, so its own source node is used as
47
+ // a reasonable "origin" anchor — a real, disclosed scoping choice, not an
48
+ // oversight (the query language and focus controls are both real DSLs over
49
+ // this graph, but a flow selection's own node-shaped controls have to pick
50
+ // SOME node, and the flow's source is the least arbitrary choice available).
51
+ function resolveFocusAnchor(graph, state) {
52
+ if (!state.selectedId) return null;
53
+ const node = graph.nodes.find((n) => n.id === state.selectedId);
54
+ if (node) return { nodeId: node.id, edge: null };
55
+ const edge = graph.edges.find((e) => e.id === state.selectedId);
56
+ if (edge) return { nodeId: edge.from, edge };
57
+ const flow = graph.flows.find((f) => f.id === state.selectedId);
58
+ if (flow) return { nodeId: flow.source, edge: null };
59
+ return null;
60
+ }
61
+
62
+ // Appends the focus-control button group into an already-populated context
63
+ // rail (the architecture-view.js's own renderFlowSummary(), or the shell's
64
+ // plain textContent fallback, has already run and populated it — this
65
+ // APPENDS, it never clears). Only rendered on Architecture View, since a
66
+ // focus selection's {nodeIds, edgeIds} only has a visible effect there
67
+ // (computeArchitectureViewModel's own new 3rd parameter). Gated on there
68
+ // being an active selection at all, matching the task brief's own wording
69
+ // ("offering the 9 named controls when a node/flow is selected").
70
+ function renderFocusControlMenu(graph, state, contextRailEl, shellApi, rerender) {
71
+ const anchor = resolveFocusAnchor(graph, state);
72
+ if (!anchor && !currentFocusSelection) return;
73
+
74
+ const buttons = FOCUS_CONTROLS.filter((control) => {
75
+ if (control.isReset) return true;
76
+ if (control.needsEdge) return Boolean(anchor?.edge);
77
+ if (control.needsNode) return Boolean(anchor?.nodeId);
78
+ return true; // graph-wide controls (external/unprotected/disconnected) need no anchor
79
+ }).map((control) =>
80
+ el(
81
+ 'button',
82
+ {
83
+ class: 'focus-control-menu__button',
84
+ type: 'button',
85
+ 'data-focus-control': control.id,
86
+ onClick: () => {
87
+ if (control.isReset) {
88
+ currentFocusSelection = null;
89
+ shellApi.setSelection(null); // notifies onStateChange, which re-invokes rerender itself
90
+ return;
91
+ }
92
+ currentFocusSelection = control.run(graph, anchor);
93
+ rerender();
94
+ },
95
+ },
96
+ control.label,
97
+ ),
98
+ );
99
+
100
+ contextRailEl.appendChild(el('div', { class: 'focus-control-menu' }, buttons));
101
+ }
102
+
103
+ export function bootstrap(rootEl, graph) {
104
+ const shellApi = mountShell(rootEl, graph);
105
+ const filterFacets = computeFilterFacets(graph);
106
+
107
+ // Any NEW single-item selection made through an EXISTING selection path
108
+ // (a node/edge click on Architecture View, a Privacy/Trace/Inventory row
109
+ // click) must clear a stale focus-control override — otherwise a user
110
+ // could click "Show upstream" and then click an unrelated node, and see
111
+ // the OLD focus set still applied instead of the new plain selection.
112
+ function selectAndClearFocus(id) {
113
+ currentFocusSelection = null;
114
+ shellApi.setSelection(id);
115
+ }
116
+
117
+ function rerender() {
118
+ const state = shellApi.getState();
119
+
120
+ const queryBarViewModel = computeQueryBarViewModel(state);
121
+ let queryPredicate = () => true;
122
+ if (!queryBarViewModel.error) {
123
+ const compiled = compileQuerySafely(graph, state.filters?.query ?? '');
124
+ queryPredicate = compiled.predicate;
125
+ // A syntax-clean query can still fail at evaluation time (an
126
+ // unrecognized field name, thrown by query-language.js's own
127
+ // evaluateNode — see compileQuerySafely's own comment). Surface that
128
+ // the same way a syntax error is surfaced, rather than silently
129
+ // falling back to "no filter" with no visible explanation.
130
+ if (compiled.error) queryBarViewModel.error = compiled.error;
131
+ }
132
+ renderQueryBar(queryBarViewModel, shellApi.getQueryBarEl(), (nextQuery) => {
133
+ shellApi.setFilters({ ...(state.filters ?? {}), query: nextQuery });
134
+ });
135
+
136
+ if (state.view === 'architecture') {
137
+ const viewModel = computeArchitectureViewModel(graph, state, currentFocusSelection);
138
+ renderArchitectureView(viewModel, shellApi.getCanvasEl(), selectAndClearFocus);
139
+ const contextRailEl = shellApi.getContextRailEl();
140
+ if (viewModel.flowSummary) {
141
+ renderFlowSummary(viewModel.flowSummary, contextRailEl);
142
+ } else {
143
+ clear(contextRailEl);
144
+ contextRailEl.textContent = buildContextRailText(graph);
145
+ }
146
+ renderFocusControlMenu(graph, state, contextRailEl, shellApi, rerender);
147
+ } else if (state.view === 'privacy') {
148
+ const viewModel = computePrivacyViewModel(graph, state, queryPredicate);
149
+ renderPrivacyView(viewModel, shellApi.getCanvasEl(), selectAndClearFocus);
150
+ shellApi.getContextRailEl().textContent = buildContextRailText(graph);
151
+ } else if (state.view === 'trace') {
152
+ const viewModel = computeTraceViewModel(graph, state);
153
+ renderTraceView(viewModel, shellApi.getCanvasEl(), selectAndClearFocus);
154
+ shellApi.getContextRailEl().textContent = buildContextRailText(graph);
155
+ } else if (state.view === 'inventory') {
156
+ const viewModel = computeInventoryViewModel(graph, state, queryPredicate);
157
+ renderInventoryView(viewModel, shellApi.getCanvasEl(), selectAndClearFocus, (tableId) => shellApi.setTable(tableId));
158
+ shellApi.getContextRailEl().textContent = buildContextRailText(graph);
159
+ }
160
+
161
+ const inspectorViewModel = computeInspectorViewModel(graph, state.selectedId);
162
+ renderInspector(inspectorViewModel, shellApi.getInspectorEl());
163
+
164
+ if (state.view === 'privacy' || state.view === 'inventory') {
165
+ renderFilterRail(filterFacets, state.filters ?? {}, shellApi.getLeftRailEl(), (nextFilters) => shellApi.setFilters(nextFilters));
166
+ } else {
167
+ const railEl = shellApi.getLeftRailEl();
168
+ railEl.textContent = 'Filters apply to Privacy View and some Inventory tables.';
169
+ }
170
+ }
171
+
172
+ shellApi.onStateChange(rerender);
173
+ rerender();
174
+
175
+ return shellApi;
176
+ }
@@ -0,0 +1,141 @@
1
+ import { el, clear } from '../lib/dom.js';
2
+ import { protectionVisual } from '../lib/protection-visual.js';
3
+
4
+ export function computeInspectorViewModel(graph, selectedId) {
5
+ if (!selectedId) return null;
6
+
7
+ const flow = graph.flows.find((f) => f.id === selectedId);
8
+ const edge = !flow && graph.edges.find((e) => e.id === selectedId);
9
+ const node = !flow && !edge && graph.nodes.find((n) => n.id === selectedId);
10
+ const dataElement = !flow && !edge && !node && graph.dataElements.find((d) => d.id === selectedId);
11
+ const transformation = !flow && !edge && !node && !dataElement && graph.transformations.find((t) => t.id === selectedId);
12
+ const target = flow || edge || node || dataElement || transformation;
13
+ if (!target) return null;
14
+
15
+ const kind = flow ? 'flow' : edge ? 'edge' : node ? 'node' : dataElement ? 'dataElement' : 'transformation';
16
+ const evidenceRefs = target.evidenceRefs ?? [];
17
+ const evidenceItems = evidenceRefs
18
+ .map((id) => graph.evidence.find((ev) => ev.id === id))
19
+ .filter(Boolean);
20
+ const supporting = evidenceItems.filter((e) => !e.conflict);
21
+ const conflicting = evidenceItems.filter((e) => e.conflict);
22
+
23
+ return {
24
+ kind,
25
+ id: target.id,
26
+ claim: buildClaimText(graph, kind, target),
27
+ supporting,
28
+ conflicting,
29
+ limitations: target.limitations ?? [],
30
+ target,
31
+ };
32
+ }
33
+
34
+ function buildClaimText(graph, kind, target) {
35
+ if (kind === 'flow') {
36
+ const dataElement = graph.dataElements.find((d) => target.dataElementIds.includes(d.id));
37
+ const source = graph.nodes.find((n) => n.id === target.source);
38
+ const sink = graph.nodes.find((n) => n.id === target.sink);
39
+ return `${dataElement?.name ?? 'field'} flows from ${source?.label ?? 'unknown source'} to ${sink?.label ?? 'unknown destination'}: ${target.protectionSummary}`;
40
+ }
41
+ if (kind === 'edge') {
42
+ const from = graph.nodes.find((n) => n.id === target.from);
43
+ const to = graph.nodes.find((n) => n.id === target.to);
44
+ return `${from?.label ?? '?'} → ${to?.label ?? '?'}: handling ${target.protection.handling.verdict}, transit ${target.protection.transit.verdict}, at rest ${target.protection.atRest.verdict}`;
45
+ }
46
+ if (kind === 'dataElement') {
47
+ return `${target.name}: ${(target.dataClasses ?? []).join(', ') || 'no data classes recorded'}`;
48
+ }
49
+ if (kind === 'transformation') {
50
+ return `${target.kind} transformation (${target.reversibility})`;
51
+ }
52
+ return `${target.label} (${target.kind}/${target.subtype})`;
53
+ }
54
+
55
+ /** @param {ReturnType<typeof computeInspectorViewModel>} viewModel */
56
+ export function renderInspector(viewModel, inspectorEl) {
57
+ clear(inspectorEl);
58
+ if (!viewModel) {
59
+ inspectorEl.appendChild(el('p', { class: 'inspector-empty' }, 'Select a node, edge, or flow to see its evidence.'));
60
+ return;
61
+ }
62
+
63
+ const container = el('div', { class: 'inspector' });
64
+ container.appendChild(el('h3', { class: 'inspector-title' }, 'Evidence inspector'));
65
+
66
+ container.appendChild(el('p', { class: 'inspector-claim' }, viewModel.claim));
67
+
68
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'Supporting evidence'));
69
+ if (viewModel.supporting.length === 0) {
70
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'No supporting evidence recorded.'));
71
+ } else {
72
+ container.appendChild(
73
+ el(
74
+ 'ul',
75
+ { class: 'inspector-evidence-list' },
76
+ viewModel.supporting.map((ev) => renderEvidenceItem(ev)),
77
+ ),
78
+ );
79
+ }
80
+
81
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'Conflicting evidence'));
82
+ if (viewModel.conflicting.length === 0) {
83
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'None recorded.'));
84
+ } else {
85
+ container.appendChild(
86
+ el(
87
+ 'ul',
88
+ { class: 'inspector-evidence-list' },
89
+ viewModel.conflicting.map((ev) => renderEvidenceItem(ev)),
90
+ ),
91
+ );
92
+ }
93
+
94
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'What the scanner does not know'));
95
+ if (viewModel.limitations.length === 0) {
96
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'No limitations recorded for this claim.'));
97
+ } else {
98
+ container.appendChild(
99
+ el(
100
+ 'ul',
101
+ { class: 'inspector-limitations-list' },
102
+ viewModel.limitations.map((text) => el('li', {}, text)),
103
+ ),
104
+ );
105
+ }
106
+
107
+ if (viewModel.kind === 'edge' || viewModel.kind === 'flow') {
108
+ container.appendChild(renderVerdictBadges(viewModel));
109
+ }
110
+
111
+ inspectorEl.appendChild(container);
112
+ }
113
+
114
+ function renderEvidenceItem(evidence) {
115
+ return el('li', { class: 'inspector-evidence-item' }, [
116
+ el('span', { class: 'inspector-evidence-claim' }, evidence.claim),
117
+ el('span', { class: 'inspector-evidence-location' }, evidence.location?.note ?? 'location unknown'),
118
+ ]);
119
+ }
120
+
121
+ function renderVerdictBadges(viewModel) {
122
+ const target = viewModel.target;
123
+ const dims = viewModel.kind === 'edge'
124
+ ? [
125
+ ['Transit', target.protection.transit.verdict],
126
+ ['At rest', target.protection.atRest.verdict],
127
+ ['Handling', target.protection.handling.verdict],
128
+ ]
129
+ : [['Protection summary', target.protectionSummary]];
130
+ return el(
131
+ 'div',
132
+ { class: 'inspector-verdicts' },
133
+ dims.map(([label, verdict]) => {
134
+ const visual = protectionVisual(verdict);
135
+ return el('div', { class: 'inspector-verdict-row' }, [
136
+ el('span', { class: 'inspector-verdict-dim-label' }, `${label}: `),
137
+ el('span', { class: 'inspector-verdict-badge', style: `border-color: var(${visual.colorVar})` }, `${visual.glyph} ${visual.label}`),
138
+ ]);
139
+ }),
140
+ );
141
+ }
@@ -0,0 +1,119 @@
1
+ import { el, clear } from '../lib/dom.js';
2
+
3
+ // A fixed enum, not derived from "whatever protectionSummary values happen
4
+ // to be present in this fixture" — a filter chip for `unprotected` must
5
+ // still exist even on a graph where nothing currently is, so a user can
6
+ // confirm that's genuinely true rather than the chip silently not existing.
7
+ const PROTECTION_TIERS = Object.freeze(['protected', 'unprotected', 'mixed', 'unknown']);
8
+
9
+ export function computeFilterFacets(graph) {
10
+ const dataClasses = [...new Set((graph.dataElements ?? []).flatMap((d) => d.dataClasses ?? []))].sort();
11
+ const sourceCategories = [...new Set(graph.nodes.filter((n) => n.kind === 'source').map((n) => n.subtype).filter(Boolean))].sort();
12
+ const sinkCategories = [...new Set(graph.nodes.filter((n) => n.kind === 'sink').map((n) => n.subtype).filter(Boolean))].sort();
13
+ const destinationExternalities = [...new Set(graph.nodes.map((n) => n.externality?.value).filter(Boolean))].sort();
14
+ const transitVerdicts = [...new Set(graph.edges.map((e) => e.protection.transit.verdict))].sort();
15
+ const atRestVerdicts = [...new Set(graph.edges.map((e) => e.protection.atRest.verdict))].sort();
16
+ const handlingVerdicts = [...new Set(graph.edges.map((e) => e.protection.handling.verdict))].sort();
17
+ const policyVerdicts = [...new Set(graph.flows.map((f) => f.policyVerdict))].sort();
18
+ return {
19
+ dataClasses, protectionTiers: PROTECTION_TIERS,
20
+ sourceCategories, sinkCategories, destinationExternalities,
21
+ transitVerdicts, atRestVerdicts, handlingVerdicts, policyVerdicts,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * @param {ReturnType<typeof computeFilterFacets>} facets
27
+ * @param {{dataClass?: string[], protection?: string[], ai?: boolean}} currentFilters
28
+ * @param {HTMLElement} railEl
29
+ * @param {(next: object) => void} onFiltersChange
30
+ */
31
+ export function renderFilterRail(facets, currentFilters, railEl, onFiltersChange) {
32
+ clear(railEl);
33
+
34
+ const dataClassChips = el(
35
+ 'div',
36
+ { class: 'filter-rail-group' },
37
+ [el('h4', {}, 'Data class'), ...facets.dataClasses.map((cls) => renderChip(cls, currentFilters.dataClass?.includes(cls) ?? false, () => toggleListFilter(currentFilters, 'dataClass', cls, onFiltersChange)))],
38
+ );
39
+
40
+ const protectionChips = el(
41
+ 'div',
42
+ { class: 'filter-rail-group' },
43
+ [el('h4', {}, 'Protection'), ...facets.protectionTiers.map((tier) => renderChip(tier, currentFilters.protection?.includes(tier) ?? false, () => toggleListFilter(currentFilters, 'protection', tier, onFiltersChange)))],
44
+ );
45
+
46
+ const sourceCategoryChips = el(
47
+ 'div',
48
+ { class: 'filter-rail-group' },
49
+ [el('h4', {}, 'Source category'), ...facets.sourceCategories.map((cat) => renderChip(cat, currentFilters.sourceCategory?.includes(cat) ?? false, () => toggleListFilter(currentFilters, 'sourceCategory', cat, onFiltersChange)))],
50
+ );
51
+
52
+ const sinkCategoryChips = el(
53
+ 'div',
54
+ { class: 'filter-rail-group' },
55
+ [el('h4', {}, 'Sink category'), ...facets.sinkCategories.map((cat) => renderChip(cat, currentFilters.sinkCategory?.includes(cat) ?? false, () => toggleListFilter(currentFilters, 'sinkCategory', cat, onFiltersChange)))],
56
+ );
57
+
58
+ const destinationExternalityChips = el(
59
+ 'div',
60
+ { class: 'filter-rail-group' },
61
+ [el('h4', {}, 'Destination externality'), ...facets.destinationExternalities.map((val) => renderChip(val, currentFilters.destinationExternality?.includes(val) ?? false, () => toggleListFilter(currentFilters, 'destinationExternality', val, onFiltersChange)))],
62
+ );
63
+
64
+ const transitVerdictChips = el(
65
+ 'div',
66
+ { class: 'filter-rail-group' },
67
+ [el('h4', {}, 'Transit'), ...facets.transitVerdicts.map((v) => renderChip(v, currentFilters.transitVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'transitVerdict', v, onFiltersChange)))],
68
+ );
69
+
70
+ const atRestVerdictChips = el(
71
+ 'div',
72
+ { class: 'filter-rail-group' },
73
+ [el('h4', {}, 'At rest'), ...facets.atRestVerdicts.map((v) => renderChip(v, currentFilters.atRestVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'atRestVerdict', v, onFiltersChange)))],
74
+ );
75
+
76
+ const handlingVerdictChips = el(
77
+ 'div',
78
+ { class: 'filter-rail-group' },
79
+ [el('h4', {}, 'Handling'), ...facets.handlingVerdicts.map((v) => renderChip(v, currentFilters.handlingVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'handlingVerdict', v, onFiltersChange)))],
80
+ );
81
+
82
+ const policyVerdictChips = el(
83
+ 'div',
84
+ { class: 'filter-rail-group' },
85
+ [el('h4', {}, 'Policy verdict'), ...facets.policyVerdicts.map((v) => renderChip(v, currentFilters.policyVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'policyVerdict', v, onFiltersChange)))],
86
+ );
87
+
88
+ const aiChip = el(
89
+ 'div',
90
+ { class: 'filter-rail-group' },
91
+ [el('h4', {}, 'AI'), renderChip('AI processing', currentFilters.ai === true, () => onFiltersChange({ ...currentFilters, ai: !currentFilters.ai }))],
92
+ );
93
+
94
+ railEl.appendChild(el('div', { class: 'filter-rail' }, [
95
+ dataClassChips, protectionChips,
96
+ sourceCategoryChips, sinkCategoryChips, destinationExternalityChips,
97
+ transitVerdictChips, atRestVerdictChips, handlingVerdictChips,
98
+ policyVerdictChips, aiChip,
99
+ ]));
100
+ }
101
+
102
+ function toggleListFilter(currentFilters, key, value, onFiltersChange) {
103
+ const current = currentFilters[key] ?? [];
104
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
105
+ onFiltersChange({ ...currentFilters, [key]: next });
106
+ }
107
+
108
+ function renderChip(label, active, onClick) {
109
+ return el(
110
+ 'button',
111
+ {
112
+ class: 'filter-chip',
113
+ 'data-active': String(active),
114
+ 'aria-pressed': String(active),
115
+ onClick,
116
+ },
117
+ label,
118
+ );
119
+ }
@@ -0,0 +1,126 @@
1
+ // Query bar: PRD §15.2's query-language DSL, wired into a real text input
2
+ // plus two saved-view chips. Split the same way every other component/view
3
+ // in this repo is: a pure compute function and a thin DOM-building render
4
+ // function.
5
+ //
6
+ // `computeQueryBarViewModel` is deliberately SYNTAX-only (it calls
7
+ // `parseQuery`, never `compileQuery`) — it operates on `state` alone, no
8
+ // graph, matching the exact signature this task's own brief specifies. A
9
+ // query can still fail at EVALUATION time even after parsing cleanly (an
10
+ // unrecognized field name — see query-language.js's own `compileQuery`
11
+ // comment), which needs a real graph to detect. `compileQuerySafely` below
12
+ // is the graph-aware superset app.js actually uses to build a safe-to-call
13
+ // predicate; `computeQueryBarViewModel` stays graph-free so it can be
14
+ // unit-tested in total isolation, per the brief's own contract.
15
+
16
+ import { parseQuery, compileQuery } from '../lib/query-language.js';
17
+ import { el, clear } from '../lib/dom.js';
18
+
19
+ // The two saved-view query strings this sub-project's own task brief names,
20
+ // spot-checked against the real flagship fixture (frontend/src/data/
21
+ // flagship-graph.js) before finalizing: `class:PCI` matches 5 of 8 real
22
+ // flows, `class:(PII,PHI) AND ai:true` matches 1 of 8 — both non-empty and
23
+ // sensible (a broad PCI-exposure view vs. a narrow AI+regulated-data
24
+ // intersection), so neither needed adjustment from the brief's own text.
25
+ export const SAVED_VIEWS = Object.freeze([
26
+ Object.freeze({ label: 'PCI Exposure', query: 'class:PCI' }),
27
+ Object.freeze({ label: 'AI + Regulated Data', query: 'class:(PII,PHI) AND ai:true' }),
28
+ ]);
29
+
30
+ /**
31
+ * @param {{filters?: {query?: string}}} state
32
+ * @returns {{queryText: string, error: {message: string, pos: number} | null}}
33
+ */
34
+ export function computeQueryBarViewModel(state) {
35
+ const queryText = state.filters?.query ?? '';
36
+ const { error } = parseQuery(queryText);
37
+ return { queryText, error: error ?? null };
38
+ }
39
+
40
+ /**
41
+ * Compiles a query string end-to-end against a real graph: parse, compile,
42
+ * and a trial evaluation against every real flow in `graph` — so a caller
43
+ * gets EITHER a real, safe-to-call predicate OR a structured error, never a
44
+ * predicate that might throw partway through filtering some rows and not
45
+ * others. A malformed query (a syntax error, OR an unrecognized field name,
46
+ * which only throws at EVALUATION time — see query-language.js's own
47
+ * compileQuery comment) never narrows the active filter: on error this
48
+ * returns a pass-through predicate (`() => true`, matching every flow), so
49
+ * views render exactly as if no query were active rather than silently
50
+ * hiding all/some rows.
51
+ *
52
+ * @param {object} graph
53
+ * @param {string} queryText
54
+ * @returns {{predicate: (flow: object) => boolean, error: {message: string, pos: number|null} | null}}
55
+ */
56
+ export function compileQuerySafely(graph, queryText) {
57
+ const { ast, error } = parseQuery(queryText ?? '');
58
+ if (error) return { predicate: () => true, error };
59
+ try {
60
+ const predicate = compileQuery(ast, graph);
61
+ for (const flow of graph.flows) predicate(flow);
62
+ return { predicate, error: null };
63
+ } catch (err) {
64
+ return { predicate: () => true, error: { message: err && err.message ? err.message : String(err), pos: null } };
65
+ }
66
+ }
67
+
68
+ function renderError(errorEl, error) {
69
+ errorEl.textContent = error ? `Query error: ${error.message}${typeof error.pos === 'number' ? ` (position ${error.pos})` : ''}` : '';
70
+ errorEl.setAttribute('data-visible', String(Boolean(error)));
71
+ }
72
+
73
+ /**
74
+ * @param {ReturnType<typeof computeQueryBarViewModel>} viewModel
75
+ * @param {HTMLElement} railEl
76
+ * @param {(nextQuery: string) => void} onQueryChange - called ONLY with a
77
+ * query that parses cleanly. On a parse error the input's own displayed
78
+ * value still updates (so the user can see/fix what they typed) but this
79
+ * is never invoked with the broken text — the caller's active filter
80
+ * never changes on a malformed query.
81
+ */
82
+ export function renderQueryBar(viewModel, railEl, onQueryChange) {
83
+ clear(railEl);
84
+
85
+ const errorEl = el('div', { class: 'query-bar__error' }, '');
86
+
87
+ const input = el('input', {
88
+ type: 'text',
89
+ class: 'query-bar__input',
90
+ value: viewModel.queryText,
91
+ placeholder: 'e.g. class:PCI AND ai:true',
92
+ 'aria-label': 'Query',
93
+ onInput: () => {
94
+ const text = input.value;
95
+ const { error } = parseQuery(text);
96
+ renderError(errorEl, error ?? null);
97
+ if (!error) onQueryChange(text);
98
+ },
99
+ });
100
+ renderError(errorEl, viewModel.error);
101
+ input.setAttribute('aria-invalid', String(Boolean(viewModel.error)));
102
+
103
+ const chips = el(
104
+ 'div',
105
+ { class: 'query-bar__saved-views' },
106
+ SAVED_VIEWS.map((view) =>
107
+ el(
108
+ 'button',
109
+ {
110
+ class: 'query-bar__saved-view-chip',
111
+ type: 'button',
112
+ 'data-query': view.query,
113
+ onClick: () => {
114
+ input.value = view.query;
115
+ input.setAttribute('value', view.query);
116
+ renderError(errorEl, null);
117
+ onQueryChange(view.query);
118
+ },
119
+ },
120
+ view.label,
121
+ ),
122
+ ),
123
+ );
124
+
125
+ railEl.appendChild(el('div', { class: 'query-bar' }, [input, errorEl, chips]));
126
+ }