Test Merge Fokus Agent.md

This commit is contained in:
Michael 2026-10-02 08:23:25 +02:00
parent ec0d9f2d3e
commit d234f94315
11 changed files with 184 additions and 11 deletions

View File

@ -1,5 +1,9 @@
# ASSESSOR – Umsetzungsplan # ASSESSOR – Umsetzungsplan
2026-10-02 - Phase 7 / interactive Data Flow diagram (follow-up to the preceding overview): on renewed Presentation/Core Tests/Platform release, replaced the always-open engine table with an interactive architecture diagram. Nine selectable modules include all three engines, source inputs, canonical PostgreSQL, read-only port, UI consumers and report outputs. Nine labelled relationships show directional delivery, bidirectional Core database read/write and the dashed, explicitly Planned Context-to-Report target. Selection highlights connected edges and opens an inspector; native buttons support keyboard activation and deselection. The complete relationship table remains in a closed disclosure. Narrow views scroll the diagram without shrinking 14px information text; inspector stacks. Coordinates live in CSS classes, using current tokens and flat tiles, with no engine imports, requests or data writes. Existing schema flow, table popups and dictionary retained. Version decision: same unreleased, related 2.11.2 refinement batch; no additional version increment. Permanent synthetic tests cover module selection, deselection, directed/bidirectional markers, planned output and disclosure; 20 focused tests passed before the final CSS correction, and 14 final diagram/layout/import tests passed afterward. Full suite: 1783 passed, 15 skipped, one unrelated 5-second timeout in node-import-engine; all five tests in that unchanged suite passed in the separate rerun (format test 3504ms). Security/design checks passed. Both production builds passed; final client rebuild after the CSS-class and read/write-marker corrections also passed. Existing bundle-size warning remains. Live mouse/keyboard selection, both themes and horizontal overflow checked. Phase status unchanged; planned report Context integration remains outside this UI change.
2026-10-02 - Phase 7 / Data Flow engine overview (2.11.2 patch): renamed DB Structure to Data Flow in navigation and header. Added Core, Context and Report with nine directed data-delivery relationships, canonical PostgreSQL, read-only Core port, UI consumers and separate report archive. Context-to-Report is explicitly Planned: current report generation does not consume published Context results. Architecture includes disabled engines. Corrected obsolete Result DB wording and assigned result_db_rows/result_db_state to Core-owned disclosure materialization before its consumer. Existing table details preserved. No API, persistence or engine behavior changes. Verified: 1783 tests passed, 15 skipped; UI/API version contracts, architecture and engine invariance passed; security/design checks and both production builds passed, client build repeated after final order correction. Live inspection in both themes; backend healthy at 2.11.2 with PostgreSQL. Existing bundle warning remains. Version decision: backward-compatible refinement, patch 2.11.1 -> 2.11.2, package/lock synchronized. Phase status unchanged. Follow-up request for a more futuristic diagram awaits renewed Core consent under the per-message rule.
2026-10-01 - Phase 7/13 / Maester description table rendering: shared Markdown presentation recovers flattened pipe tables with retained line breaks inside prose cells, without crossing blank lines, headings, code fences, lists or another delimiter. The reader also handles tables mixed with supported inert inline HTML (links, emphasis and br); original retained fields are untouched. Shared rich-text tables use the agreed 14px data typography. Permanent synthetic reader regressions cover both cell-break representations, full cell text, headers, links and unchanged originals; the shared parser regression preserves a following list. Synthetic browser checks and visual inspection passed in dark and light. Verification: 1782 tests passed, 15 skipped; security/design checks passed; client and server production builds passed, including a refreshed client build after the typography correction. Version decision: bundled into the concurrently established 2.11.1 patch batch; package/lock and UI/API version checks already agree and were not overwritten by this change. No importer, persistence, verdict or score change; no reimport required. Ambiguous table structures remain source text. No phase completion claimed. 2026-10-01 - Phase 7/13 / Maester description table rendering: shared Markdown presentation recovers flattened pipe tables with retained line breaks inside prose cells, without crossing blank lines, headings, code fences, lists or another delimiter. The reader also handles tables mixed with supported inert inline HTML (links, emphasis and br); original retained fields are untouched. Shared rich-text tables use the agreed 14px data typography. Permanent synthetic reader regressions cover both cell-break representations, full cell text, headers, links and unchanged originals; the shared parser regression preserves a following list. Synthetic browser checks and visual inspection passed in dark and light. Verification: 1782 tests passed, 15 skipped; security/design checks passed; client and server production builds passed, including a refreshed client build after the typography correction. Version decision: bundled into the concurrently established 2.11.1 patch batch; package/lock and UI/API version checks already agree and were not overwritten by this change. No importer, persistence, verdict or score change; no reimport required. Ambiguous table structures remain source text. No phase completion claimed.
2026-10-01 - Phase 13E / client Context loading and All count (2.11.1): Client opening now prepares the enabled Context Engine after the canonical workspace is ready and before publishing the loaded client. A Core API request cache, weakly owned by the dataset and keyed by the exact scope, shares that attempt with every reader; no engine implementation is imported into Core. The Context hook makes no request for an empty dataset and never returns a previous scope's result while a new scope loads. Failed attempts remain visible instead of silently restarting on reader mount; Core results remain accessible, with an English notice to reopen the client to retry. Disabled Context makes no request. Work and Pathfinder's shared Context Cluster selector shows All (N), counting each matching record once, including records without cluster membership, while respecting other filters and excluding its own selection. Overlapping clusters do not inflate the total. Patch version 2.11.1 records this backward-compatible correction; package/lock and UI/API contracts agree. Verified: 37 focused tests passed, including the actual Open/client-switch interactions, deferred Context response, failure/reopen, disabled engine, scope changes, filtered and zero-hit All counts, and version contracts. Synthetic browser checks and visual inspection passed in dark and light. Both production builds passed; the final client build also includes the synchronous stale-scope guard. Local-only and design checks passed. Full suite: 1781 passed, 15 skipped, one unrelated failure in tests/shared/markdown.test.ts:40 (wrapped flattened table followed by a list); the Markdown source and test were not changed by this task. Running backend reports 2.11.1 and unchanged repository revision 535; frontend HTTP 200. Limits: the screenshot did not expose the original failed HTTP response, so its specific backend error was not independently reproduced. This change removes the verified premature/duplicate request path; it does not speed up Context computation itself. No persistence changes, no phase completion claimed. 2026-10-01 - Phase 13E / client Context loading and All count (2.11.1): Client opening now prepares the enabled Context Engine after the canonical workspace is ready and before publishing the loaded client. A Core API request cache, weakly owned by the dataset and keyed by the exact scope, shares that attempt with every reader; no engine implementation is imported into Core. The Context hook makes no request for an empty dataset and never returns a previous scope's result while a new scope loads. Failed attempts remain visible instead of silently restarting on reader mount; Core results remain accessible, with an English notice to reopen the client to retry. Disabled Context makes no request. Work and Pathfinder's shared Context Cluster selector shows All (N), counting each matching record once, including records without cluster membership, while respecting other filters and excluding its own selection. Overlapping clusters do not inflate the total. Patch version 2.11.1 records this backward-compatible correction; package/lock and UI/API contracts agree. Verified: 37 focused tests passed, including the actual Open/client-switch interactions, deferred Context response, failure/reopen, disabled engine, scope changes, filtered and zero-hit All counts, and version contracts. Synthetic browser checks and visual inspection passed in dark and light. Both production builds passed; the final client build also includes the synchronous stale-scope guard. Local-only and design checks passed. Full suite: 1781 passed, 15 skipped, one unrelated failure in tests/shared/markdown.test.ts:40 (wrapped flattened table followed by a list); the Markdown source and test were not changed by this task. Running backend reports 2.11.1 and unchanged repository revision 535; frontend HTTP 200. Limits: the screenshot did not expose the original failed HTTP response, so its specific backend error was not independently reproduced. This change removes the verified premature/duplicate request path; it does not speed up Context computation itself. No persistence changes, no phase completion claimed.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "assessor", "name": "assessor",
"version": "2.11.1", "version": "2.11.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "assessor", "name": "assessor",
"version": "2.11.1", "version": "2.11.2",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "assessor", "name": "assessor",
"version": "2.11.1", "version": "2.11.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -43,7 +43,7 @@ function AreaIcon({ area }: { area: AppArea }) {
} }
const AREA_TOPBAR_TITLE: Record<AppArea, string> = { const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
'db-structure': 'DB Structure', 'db-structure': 'Data Flow',
'data-hub': 'Data Hub', 'data-hub': 'Data Hub',
dashboard: 'Client Info', dashboard: 'Client Info',
graph: 'Pathfinder', graph: 'Pathfinder',
@ -56,7 +56,7 @@ const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
das Panel nicht gerundet sein. Die Liste steht deshalb an einer Stelle, das Panel nicht gerundet sein. Die Liste steht deshalb an einer Stelle,
damit die Ecke der Reihenfolge folgt und nicht einem Bereichsnamen im CSS. */ damit die Ecke der Reihenfolge folgt und nicht einem Bereichsnamen im CSS. */
const NAV_AREAS = [ const NAV_AREAS = [
['db-structure', 'DB Structure'], ['db-structure', 'Data Flow'],
['data-hub', 'Datahub'], ['data-hub', 'Datahub'],
['dashboard', 'Client Info'], ['dashboard', 'Client Info'],
['work', 'Work'], ['work', 'Work'],

View File

@ -5,6 +5,7 @@ import { TriangleIcon } from '../../components/TriangleIcon';
import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState'; import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState';
import { BackendConnectionDialog } from '../../components/BackendConnectionDialog/BackendConnectionDialog'; import { BackendConnectionDialog } from '../../components/BackendConnectionDialog/BackendConnectionDialog';
import { FloatingDataWindow } from '../../components/FloatingDataWindow/FloatingDataWindow'; import { FloatingDataWindow } from '../../components/FloatingDataWindow/FloatingDataWindow';
import { EngineDataFlow } from './EngineDataFlow';
import { buildDbStructureFlow, buildDbStructureViews, dbStructureNeighbours, dbStructureShortName, type DbFlowStep } from './db-structure-flow'; import { buildDbStructureFlow, buildDbStructureViews, dbStructureNeighbours, dbStructureShortName, type DbFlowStep } from './db-structure-flow';
const assessorApi = new AssessorApiClient(); const assessorApi = new AssessorApiClient();
@ -146,6 +147,7 @@ function DbStructureFlow({ tables }: { tables: DbStructureTable[] }) {
</div> </div>
</div> </div>
<div className="db-flow__merge" aria-hidden="true"><span /><span /><span /></div> <div className="db-flow__merge" aria-hidden="true"><span /><span /><span /></div>
{shared('result-materialization', false)}
{/* Both paths end in two consumers: the Result DB read view and Work with its reviews. */} {/* Both paths end in two consumers: the Result DB read view and Work with its reviews. */}
<div className="db-flow__consumers"> <div className="db-flow__consumers">
{views.map(view => <FlowStep key={view.id} step={{ ...view, lane: 'shared', tables: view.sources }} {...props} continued={false} {views.map(view => <FlowStep key={view.id} step={{ ...view, lane: 'shared', tables: view.sources }} {...props} continued={false}
@ -225,6 +227,7 @@ export function DbStructurePanel({ backendStatus }: DbStructurePanelProps) {
<article><span>Relationships</span><strong>{metrics.relationships.toLocaleString('en-US')}</strong><small>Declared foreign keys</small></article> <article><span>Relationships</span><strong>{metrics.relationships.toLocaleString('en-US')}</strong><small>Declared foreign keys</small></article>
<article><span>Approximate rows</span><strong>{metrics.countedTables > 0 ? metrics.approximateRows.toLocaleString('en-US') : '—'}</strong><small>{metrics.countedTables > 0 ? `Reported by ${metrics.countedTables.toLocaleString('en-US')} tables` : 'No estimates reported'}</small></article> <article><span>Approximate rows</span><strong>{metrics.countedTables > 0 ? metrics.approximateRows.toLocaleString('en-US') : '—'}</strong><small>{metrics.countedTables > 0 ? `Reported by ${metrics.countedTables.toLocaleString('en-US')} tables` : 'No estimates reported'}</small></article>
</section>} </section>}
<EngineDataFlow />
{structure.tables.length > 0 && <DbStructureFlow tables={structure.tables} />} {structure.tables.length > 0 && <DbStructureFlow tables={structure.tables} />}
<details className="db-structure-dictionary"> <details className="db-structure-dictionary">
<summary><TriangleIcon direction="right" />Table dictionary by layer</summary> <summary><TriangleIcon direction="right" />Table dictionary by layer</summary>

View File

@ -0,0 +1,62 @@
import { useId, useState } from 'react';
/** Presentation-only architecture map; never imports engine implementations. */
const nodes = [
{ id: 'input', name: 'Source inputs', caption: 'Scanner reports · GPO backups · catalogs', description: 'Original scanner reports, GPO backups and reference catalogs enter the Core import and evaluation pipeline.' },
{ id: 'core', name: 'Core Engine', caption: 'Authoritative results · always active', description: 'All existing ASSESSOR functions: import, normalization, evaluation, scoring, client records and disclosure. Owns canonical PostgreSQL data. Works independently of optional engines.' },
{ id: 'database', name: 'Canonical PostgreSQL', caption: 'Core-owned data · disclosed Result DB', description: 'Canonical local datastore. The Core owns persistence, including the stored and verified Result DB projection.' },
{ id: 'port', name: 'Core read-only port', caption: 'Scoped results · provenance', description: 'Declared read-only Core port backed by read-only database access. Optional engines read authoritative results without changing stored data.' },
{ id: 'context', name: 'Context Engine', caption: 'Derived context · optional', description: 'Reads findings and GPO verdicts through the read-only Core port and read-only database access. Produces versioned clusters, confidence and evidence. In-memory results; no own persistence or Core writes.' },
{ id: 'report', name: 'Report Engine', caption: 'Read-only reporting · optional', description: 'Reads Core results through the read-only Core port and a separate read-only PostgreSQL pool. Produces report projections, management and technical reports. Own file archive; no Core writes.' },
{ id: 'ui', name: 'Core UI & exports', caption: 'Findings · scores · Result DB', description: 'Application views show authoritative Core findings, verdicts and scores. Result DB exports obey the Core disclosure policy.' },
{ id: 'context-ui', name: 'Context in the UI', caption: 'Work · Client Info · Pathfinder', description: 'Published Context results appear through existing UI extension points. They never override technical results or client records.' },
{ id: 'archive', name: 'Report outputs', caption: 'File archive · downloads', description: 'Generated reports are stored in the separate Report Engine file archive and offered for download; no report storage in Core tables.' }
];
const relationships = [
{ from: 'input', to: 'core', label: 'Import', detail: 'Import and evaluation', path: 'M240 340 H310' },
{ from: 'core', to: 'database', label: 'Read / write', detail: 'Owns, reads and writes Core data, including disclosed Result DB rows', path: 'M420 390 V520', bidirectional: true },
{ from: 'database', to: 'port', label: 'Read only', detail: 'Read-only database access for optional engines', path: 'M530 570 H600' },
{ from: 'port', to: 'context', label: 'Read only', detail: 'Scoped authoritative results and provenance', path: 'M710 520 V390' },
{ from: 'port', to: 'report', label: 'Read only', detail: 'Scoped results, client records and disclosed projections', path: 'M820 570 H890' },
{ from: 'core', to: 'ui', label: 'Core results', detail: 'Authoritative findings, verdicts, scores and disclosure rules', path: 'M420 290 V160' },
{ from: 'context', to: 'context-ui', label: 'Context', detail: 'Published context results through existing UI extension points', path: 'M710 290 V160' },
{ from: 'report', to: 'archive', label: 'Reports', detail: 'Generated reports; separate from Core database storage', path: 'M1000 520 V390' },
{ from: 'context', to: 'report', label: 'Planned', detail: 'Published context results; documented target, not yet connected in report generation', path: 'M820 340 H855 V490 H940 V520', planned: true }
];
const nodeName = (id: string) => nodes.find(node => node.id === id)!.name;
export function EngineDataFlow() {
const markerId = useId().replace(/:/g, '');
const [selected, setSelected] = useState('core');
const active = nodes.find(node => node.id === selected);
const connections = relationships.filter(edge => edge.from === selected || edge.to === selected);
const related = new Set(connections.flatMap(edge => [edge.from, edge.to]));
return <section className="db-engine-flow" aria-label="Engine data flow">
<header className="db-engine-flow__heading"><h3>Engines & relationships</h3><span>Solid: implemented · Dashed: planned</span></header>
<div className="db-engine-flow__scroll" role="region" aria-label="Interactive engine diagram" tabIndex={0}>
<div className="db-engine-flow__map">
<svg className="db-engine-flow__edges" width="1130" height="650" aria-hidden="true">
<defs><marker id={markerId} viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 Z" /></marker></defs>
{relationships.map(edge => <path key={`${edge.from}-${edge.to}`} d={edge.path} markerEnd={`url(#${markerId})`} markerStart={edge.bidirectional ? `url(#${markerId})` : undefined}
className={`${edge.planned ? 'is-planned' : ''} ${connections.includes(edge) ? 'is-related' : ''}`} />)}
</svg>
{relationships.map(edge => <span key={`${edge.from}-${edge.to}`} data-edge={`${edge.from}-${edge.to}`} className={`db-engine-flow__edge-label${edge.planned ? ' is-planned' : ''}`}>{edge.label}</span>)}
{nodes.map(node => <button type="button" key={node.id} aria-pressed={selected === node.id}
className={`db-engine-flow__node${['core', 'context', 'report'].includes(node.id) ? ' is-engine' : ''}${related.has(node.id) ? ' is-related' : ''}`}
data-node={node.id} onClick={() => setSelected(node.id === selected ? '' : node.id)}>
<strong>{node.name}</strong><span>{node.caption}</span>
</button>)}
</div>
</div>
{active ? <section className="db-engine-flow__inspector" aria-label="Selected flow details" aria-live="polite">
<div><h4>{active.name}</h4><p>{active.description}</p></div>
<ul>{connections.map(edge => <li key={`${edge.from}-${edge.to}`}><strong>{nodeName(edge.from)} {edge.bidirectional ? '↔' : '→'} {nodeName(edge.to)}</strong><span>{edge.detail}{edge.planned ? ' · Planned' : ''}</span></li>)}</ul>
</section> : <p className="db-engine-flow__boundary">Select a module to inspect its data flow. Modules also support keyboard selection.</p>}
<p className="db-engine-flow__boundary">Arrows show data delivery. Engine dependencies point toward the Core; the Core never depends on an optional engine. Engines cannot override verdicts, scores or client records. Every output follows Core privacy and Result DB disclosure rules.</p>
<details className="db-engine-flow__dictionary"><summary>All relationships (9)</summary><div className="db-engine-flow__scroll"><table aria-label="Directed engine relationships">
<thead><tr><th scope="col">From</th><th scope="col">To</th><th scope="col">Data flow</th><th scope="col">Implementation</th></tr></thead>
<tbody>{relationships.map(edge => <tr key={`${edge.from}-${edge.to}`}><td>{nodeName(edge.from)}</td><td>{edge.bidirectional ? '↔' : '→'} {nodeName(edge.to)}</td><td>{edge.detail}</td><td>{edge.planned ? 'Planned' : 'Implemented'}</td></tr>)}</tbody>
</table></div></details>
<p className="db-engine-flow__boundary">This architecture overview includes disabled engines. The shared Engines control governs optional UI requests; disabling an engine does not cancel submitted backend work.</p>
</section>;
}

View File

@ -45,7 +45,9 @@ export const DB_FLOW_STEPS: DbFlowStepDefinition[] = [
{ id: 'review', lane: 'shared', title: 'Work & review', purpose: 'Client-owned review state and accepted risks for findings of both paths.', { id: 'review', lane: 'shared', title: 'Work & review', purpose: 'Client-owned review state and accepted risks for findings of both paths.',
tables: ['reviews'] }, tables: ['reviews'] },
{ id: 'comparison', lane: 'shared', title: 'Comparison over time', purpose: 'Timeslot comparisons and the lifecycle of findings between assessments.', { id: 'comparison', lane: 'shared', title: 'Comparison over time', purpose: 'Timeslot comparisons and the lifecycle of findings between assessments.',
tables: ['comparisons', 'timeslot_evidence_comparisons', 'finding_obligations', 'finding_obligation_transitions', 'finding_obligation_scope_decisions'] } tables: ['comparisons', 'timeslot_evidence_comparisons', 'finding_obligations', 'finding_obligation_transitions', 'finding_obligation_scope_decisions'] },
{ id: 'result-materialization', lane: 'shared', title: 'Result DB disclosure & storage', purpose: 'Core checks and stores the disclosed result projection when source data changes. UI and exports read the same verified rows.',
tables: ['result_db_rows', 'result_db_state'] }
]; ];
export interface DbFlowStep extends DbFlowStepDefinition { export interface DbFlowStep extends DbFlowStepDefinition {
@ -92,8 +94,8 @@ export interface DbFlowViewDefinition {
} }
export const DB_FLOW_VIEWS: DbFlowViewDefinition[] = [ export const DB_FLOW_VIEWS: DbFlowViewDefinition[] = [
{ id: 'result-db', title: 'Result DB', purpose: 'Read view, no table of its own: combined at query time from scanner findings, environment facts, GPO comparisons, GPO findings and GPO backup consistency.', { id: 'result-db', title: 'Result DB', purpose: 'UI and exports read the stored, disclosed projection. Original source tables below supply its Core-owned materialization.',
sources: ['finding_definitions', 'finding_occurrences', 'finding_occurrence_categories', 'result_categories', 'evidence', sources: ['result_db_rows', 'result_db_state', 'finding_definitions', 'finding_occurrences', 'finding_occurrence_categories', 'result_categories', 'evidence',
'environment_observations', 'policy_evaluation_results', 'policy_snapshot_states', 'policy_finding_occurrences', 'gpo_backup_instances'] } 'environment_observations', 'policy_evaluation_results', 'policy_snapshot_states', 'policy_finding_occurrences', 'gpo_backup_instances'] }
]; ];

View File

@ -1870,6 +1870,57 @@ button.client-overall-ring:focus-visible { outline: var(--bw-emphasis) solid var
Steps are flat tiles; connectors are neutral hairlines; selection uses the shared marking role. */ Steps are flat tiles; connectors are neutral hairlines; selection uses the shared marking role. */
.db-flow { --db-flow-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, .8fr); --db-flow-gap: var(--space-5); display: grid; gap: var(--db-flow-gap); min-width: 0; margin: var(--space-5) 0; } .db-flow { --db-flow-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, .8fr); --db-flow-gap: var(--space-5); display: grid; gap: var(--db-flow-gap); min-width: 0; margin: var(--space-5) 0; }
.db-flow__header h3 { margin: 0; color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--fw-semibold); } .db-flow__header h3 { margin: 0; color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--fw-semibold); }
.db-engine-flow { display: grid; gap: var(--space-4); margin: var(--space-5) 0; min-width: 0; }
.db-engine-flow__heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.db-engine-flow__heading h3 { margin: 0; color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--fw-semibold); }
.db-engine-flow__heading > span { color: var(--muted); font-size: var(--type-table-body-size); }
.db-engine-flow__map { position: relative; width: 1130px; height: 650px; margin: 0 auto; }
.db-engine-flow__edges { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.db-engine-flow__edges > path { fill: none; stroke: var(--line-strong); stroke-width: 2; }
.db-engine-flow__edges > path.is-related { stroke: var(--marking-color); }
.db-engine-flow__edges > path.is-planned { stroke-dasharray: 6 5; }
.db-engine-flow__edges marker path { fill: var(--line-strong); }
.db-engine-flow__node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: var(--space-2); width: 220px; min-height: 100px; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: 0; background: var(--tile-surface); color: var(--text); text-align: left; cursor: pointer; }
.db-engine-flow__node.is-engine { border-left: var(--marker-width) solid var(--line-strong); }
.db-engine-flow__node.is-related { border-color: var(--line-strong); }
.db-engine-flow__node[aria-pressed="true"] { outline: var(--bw-emphasis) solid var(--marking-color); outline-offset: -2px; border-left-color: var(--marking-color); }
.db-engine-flow__node:hover { background: var(--tile-surface-hover); }
.db-engine-flow__node:focus-visible { outline: var(--bw-emphasis) solid var(--focus-ring); outline-offset: 2px; }
.db-engine-flow__node strong { font-size: var(--type-h4-size); font-weight: var(--fw-semibold); }
.db-engine-flow__node > span { font-size: var(--type-table-body-size); color: var(--muted); line-height: 1.4; }
.db-engine-flow__edge-label { position: absolute; transform: translate(-50%, -50%); padding: var(--space-1); background: var(--surface-app); color: var(--muted); font-size: var(--type-table-body-size); white-space: nowrap; pointer-events: none; }
.db-engine-flow__node[data-node="input"] { left: 20px; top: 290px; }
.db-engine-flow__node[data-node="core"] { left: 310px; top: 290px; }
.db-engine-flow__node[data-node="database"] { left: 310px; top: 520px; }
.db-engine-flow__node[data-node="port"] { left: 600px; top: 520px; }
.db-engine-flow__node[data-node="context"] { left: 600px; top: 290px; }
.db-engine-flow__node[data-node="report"] { left: 890px; top: 520px; }
.db-engine-flow__node[data-node="ui"] { left: 310px; top: 60px; }
.db-engine-flow__node[data-node="context-ui"] { left: 600px; top: 60px; }
.db-engine-flow__node[data-node="archive"] { left: 890px; top: 290px; }
.db-engine-flow__edge-label[data-edge="input-core"] { left: 275px; top: 312px; }
.db-engine-flow__edge-label[data-edge="core-database"] { left: 420px; top: 455px; }
.db-engine-flow__edge-label[data-edge="database-port"] { left: 565px; top: 540px; }
.db-engine-flow__edge-label[data-edge="port-context"] { left: 710px; top: 455px; }
.db-engine-flow__edge-label[data-edge="port-report"] { left: 855px; top: 540px; }
.db-engine-flow__edge-label[data-edge="core-ui"] { left: 420px; top: 225px; }
.db-engine-flow__edge-label[data-edge="context-context-ui"] { left: 710px; top: 225px; }
.db-engine-flow__edge-label[data-edge="report-archive"] { left: 1000px; top: 455px; }
.db-engine-flow__edge-label[data-edge="context-report"] { left: 855px; top: 416px; }
.db-engine-flow__inspector { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-5); padding: var(--tile-inset); background: var(--tile-surface); border: var(--border-hair) solid var(--tile-border); }
.db-engine-flow__inspector h4 { margin: 0 0 var(--space-2); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); }
.db-engine-flow__inspector p { margin: 0; font-size: var(--type-body-size); line-height: 1.5; }
.db-engine-flow__inspector ul { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
.db-engine-flow__inspector li { display: grid; gap: var(--space-1); font-size: var(--type-table-body-size); line-height: 1.4; }
.db-engine-flow__inspector li strong { font-weight: var(--fw-semibold); }
.db-engine-flow__inspector li span { color: var(--muted); }
.db-engine-flow__dictionary > summary { color: var(--heading); font-size: var(--type-table-body-size); cursor: pointer; }
.db-engine-flow__boundary { margin: 0; color: var(--muted); font-size: var(--type-table-body-size); line-height: 1.45; }
.db-engine-flow__scroll { overflow-x: auto; }
.db-engine-flow table { width: 100%; border-collapse: collapse; color: var(--text); font-size: var(--type-table-body-size); }
.db-engine-flow th, .db-engine-flow td { padding: var(--space-3); text-align: left; vertical-align: top; border-bottom: var(--border-hair) solid var(--line); }
.db-engine-flow th { font-weight: var(--fw-semibold); }
@media (max-width: 1050px) { .db-engine-flow__inspector { grid-template-columns: minmax(0, 1fr); } }
.db-flow__step { position: relative; z-index: 1; display: grid; align-content: start; gap: var(--space-2); min-width: 0; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); background: var(--tile-surface); } .db-flow__step { position: relative; z-index: 1; display: grid; align-content: start; gap: var(--space-2); min-width: 0; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); background: var(--tile-surface); }
.db-flow__step > header { display: flex; align-items: baseline; gap: var(--space-2); } .db-flow__step > header { display: flex; align-items: baseline; gap: var(--space-2); }
.db-flow__step h4 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); } .db-flow__step h4 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); }

View File

@ -9,7 +9,10 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => { describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => { it('shows the centrally maintained product version and opens About from the brand', () => {
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.11.1'); expect(ASSESSOR_PRODUCT_VERSION).toBe('2.11.2');
expect(app).toContain("'db-structure': 'Data Flow'");
expect(app).toContain("['db-structure', 'Data Flow']");
expect(app).not.toContain("'DB Structure'");
expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';"); expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';");
expect(app).toContain('aria-label="About ASSESSOR"'); expect(app).toContain('aria-label="About ASSESSOR"');
expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';"); expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';");

View File

@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { AssessorApiClient } from '../../src/api/assessor-api-client';
import type { DbStructureTable } from '../../src/domain/db-structure'; import type { DbStructureTable } from '../../src/domain/db-structure';
import { DbStructurePanel } from '../../src/features/db-structure/DbStructurePanel'; import { DbStructurePanel } from '../../src/features/db-structure/DbStructurePanel';
import { EngineDataFlow } from '../../src/features/db-structure/EngineDataFlow';
import { buildDbStructureFlow, dbStructureNeighbours, DB_FLOW_STEPS } from '../../src/features/db-structure/db-structure-flow'; import { buildDbStructureFlow, dbStructureNeighbours, DB_FLOW_STEPS } from '../../src/features/db-structure/db-structure-flow';
const table = (name: string, references: string[] = [], rows: number | null = null): DbStructureTable => ({ const table = (name: string, references: string[] = [], rows: number | null = null): DbStructureTable => ({
@ -16,6 +17,35 @@ const table = (name: string, references: string[] = [], rows: number | null = nu
describe('DB Structure data flow', () => { describe('DB Structure data flow', () => {
afterEach(() => vi.restoreAllMocks()); afterEach(() => vi.restoreAllMocks());
it('selects engine modules, highlights only their directed connections and discloses planned flow', async () => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const container = document.createElement('div');
const root = createRoot(container);
await act(async () => root.render(createElement(EngineDataFlow)));
expect(container.querySelectorAll('.db-engine-flow__node')).toHaveLength(9);
expect(container.querySelectorAll('.db-engine-flow__edges > path')).toHaveLength(9);
expect(container.querySelectorAll('.db-engine-flow__edges > path[marker-start]')).toHaveLength(1);
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(3);
expect(container.querySelector('.db-engine-flow__dictionary')?.hasAttribute('open')).toBe(false);
const context = [...container.querySelectorAll<HTMLButtonElement>('.db-engine-flow__node')]
.find(button => button.textContent?.startsWith('Context Engine'))!;
await act(async () => context.click());
expect(context.getAttribute('aria-pressed')).toBe('true');
expect(container.querySelector('.db-engine-flow__inspector h4')?.textContent).toBe('Context Engine');
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(3);
expect(container.querySelector('.db-engine-flow__edges > path.is-planned.is-related')).not.toBeNull();
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('not yet connected in report generation · Planned');
await act(async () => context.click());
expect(container.querySelector('.db-engine-flow__inspector')).toBeNull();
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(0);
expect(container.querySelectorAll('.db-engine-flow__node')).toHaveLength(9);
const dictionary = container.querySelector<HTMLDetailsElement>('.db-engine-flow__dictionary')!;
await act(async () => dictionary.querySelector('summary')!.click());
expect(dictionary.open).toBe(true);
expect(dictionary.querySelectorAll('tbody tr')).toHaveLength(9);
act(() => root.unmount());
});
it('assigns each table to exactly one step and never hides an unassigned table', () => { it('assigns each table to exactly one step and never hides an unassigned table', () => {
const assigned = DB_FLOW_STEPS.flatMap(step => step.tables); const assigned = DB_FLOW_STEPS.flatMap(step => step.tables);
expect(new Set(assigned).size).toBe(assigned.length); expect(new Set(assigned).size).toBe(assigned.length);
@ -45,7 +75,20 @@ describe('DB Structure data flow', () => {
expect([...container.querySelectorAll('.db-flow__lane-title')].map(item => item.textContent)).toEqual(['Scanner reports', 'GPO backups', 'Reference data']); expect([...container.querySelectorAll('.db-flow__lane-title')].map(item => item.textContent)).toEqual(['Scanner reports', 'GPO backups', 'Reference data']);
expect(container.querySelector('.db-structure-intro-copy')).toBeNull(); expect(container.querySelector('.db-structure-intro-copy')).toBeNull();
expect(container.querySelector('.db-flow__header p')).toBeNull(); expect(container.querySelector('.db-flow__header p')).toBeNull();
expect(container.querySelector('.db-flow__header h3')?.textContent).toBe('Data flow'); expect(container.querySelector('.db-flow .db-flow__header h3')?.textContent).toBe('Data flow');
expect([...container.querySelectorAll('.db-engine-flow__node.is-engine strong')].map(item => item.textContent))
.toEqual(['Core Engine', 'Context Engine', 'Report Engine']);
const relations = container.querySelector('table[aria-label="Directed engine relationships"]');
expect(relations?.querySelectorAll('tbody tr')).toHaveLength(9);
expect(relations?.textContent).toContain('Core read-only port');
expect(relations?.textContent).toContain('Planned');
expect(relations?.textContent).toContain('not yet connected in report generation');
await act(async () => {
container.querySelector<HTMLButtonElement>('.db-structure-intro-actions button')!.click();
await new Promise(resolve => setTimeout(resolve, 0));
});
expect(container.querySelectorAll('.db-engine-flow__node.is-engine')).toHaveLength(3);
expect(container.querySelectorAll('.db-engine-flow tbody tr')).toHaveLength(9);
expect(container.querySelector('.db-structure-intro-actions button')?.textContent).toBe('Refresh schema'); expect(container.querySelector('.db-structure-intro-actions button')?.textContent).toBe('Refresh schema');
expect(container.querySelector('.db-flow__lane.is-reference')?.textContent).toContain('Input to GPO step'); expect(container.querySelector('.db-flow__lane.is-reference')?.textContent).toContain('Input to GPO step');
const chips = [...container.querySelectorAll<HTMLButtonElement>('.db-flow__table')]; const chips = [...container.querySelectorAll<HTMLButtonElement>('.db-flow__table')];
@ -77,6 +120,11 @@ describe('DB Structure data flow', () => {
}); });
describe('DB Structure read views', () => { describe('DB Structure read views', () => {
it('assigns stored disclosed Result DB rows to their Core-owned materialization step', () => {
const flow = buildDbStructureFlow([table('result_db_rows'), table('result_db_state')]);
expect(flow.steps.map(step => step.id)).toEqual(['result-materialization']);
expect(flow.other).toEqual([]);
});
it('shows the Result DB as a read view over tables owned by the processing steps', async () => { it('shows the Result DB as a read view over tables owned by the processing steps', async () => {
const { buildDbStructureViews } = await import('../../src/features/db-structure/db-structure-flow'); const { buildDbStructureViews } = await import('../../src/features/db-structure/db-structure-flow');
const views = buildDbStructureViews([table('finding_occurrences'), table('policy_evaluation_results'), table('reviews')]); const views = buildDbStructureViews([table('finding_occurrences'), table('policy_evaluation_results'), table('reviews')]);

View File

@ -59,7 +59,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`); const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({ expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.11.1', apiVersion: 1, repositoryRevision: 0, persistence: 'memory' status: 'ok', application: 'ASSESSOR', productVersion: '2.11.2', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
}); });
expect(health.headers.get('access-control-allow-origin')).toBeNull(); expect(health.headers.get('access-control-allow-origin')).toBeNull();
}); });