@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.
- package/CHANGELOG.md +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +379 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +841 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +144 -0
- package/src/shared/frontend-root.js +52 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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": "
|
|
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
|
+
}
|