You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TracePcap is a modular wrapper around a growing suite of functions — the architecture exists so new capability is added by dropping a module into a stage, not by editing a core. The seams for this exist (analysis/spi/, DeviceClassificationSignal, story detectors, HostServiceLogExtractor); the code has drifted from them. This epic defines the target model, enforces its rules, and refactors toward it slice by slice.
Design — SETTLED (2026-07-15)
The target model is documented in docs/architecture/layers.rst and enforced by backend/src/test/java/com/tracepcap/architecture/LayerDependencyTest.java. Summary:
Ingest → Extract → Scan → Adjudicate → Narrate Present = read surface over all
Extract — observations with witnesses: append-only, conflict-preserving (no winner-picking at write time), graded MEASURED / REPORTED / INFERRED (schema-level, per fact type), plus a run manifest (extractor status + version → enables backfill). Two sub-kinds: wire extractors (read pcap) and reference extractors (GeoIP/OUI — backfillable without the capture). Extraction may be lazy (session reconstruction, carving).
Scan — additive scanners; read facts, never the pcap; judgment is the boundary (no judgment = a view, lives with Present). Contradiction scanners are first-class (fact conflicts are findings: spoofing, impersonation). Scope (per-capture vs cross-snapshot) is an attribute. Loop rule: scanners consuming adjudications treat them as INFERRED unless human-confirmed (Research: learn and document organisational addressing conventions across and within networks #507 circularity guard, structural).
Annotation is the envelope, not a stage — origin/llmSuggested/confirmedByHuman/staleSince on every fact and conclusion (NodeRoleEntity is the reference implementation); human authority enters at Adjudicate.
Raw-capture access confined to Ingest plumbing / Extract (eager or lazy) / evidence export — 6 frozen (all FilterService, the known grey case)
The frozen store (backend/src/test/resources/archunit_store) may only shrink; a new violation fails the build naming the exact class (verified with a planted probe).
Mass concentration: analysis 6,774 LOC (2× next); god classes SessionReconstructionService (1,416), ReportService (1,407).
Acceptance Criteria
Layer model defined and documented ✅ docs/architecture/layers.rst (five stages, three grades, contracts, worked example, divergence list) + C4 diagram.
Boundaries enforced, not just documented ✅ Four frozen ArchUnit rules; store committed; only-shrink policy; verified to catch new violations.
monitor ↔ insights and analysis ↔ file cycles broken (events or shared contracts; NodeRoleChangedEvent shows the pattern). Cycle store → 0. (Note from slice 1: fixing a cycle can surface previously-unreported ones — monitor ↔ subnets appeared only after analysis ↔ file was broken.)
Cross-module access to analysis migrates onto analysis.spi/DTOs; the 755-entry store trends to 0, slice by slice.
Conflict-preserving extraction: HostnameResolverService stores all claims (à la IpMacObservationEntity); winner-picking moves to a hostname adjudicator.
6. SPI migration — the 755, module by module (biggest consumers first: measure per-module counts at slice start).
7. Pipeline decomposition — stages as units.
Follow-ups filed as the map exposes them (god-class splits, FilterService grey case, frontend repatriation beyond identity).
Notes
Related: #416 (predecessor epic — its invariant holds at 0 violations and is now frozen), #496/#498/#499/#501/#506/#507/#511 (each maps to a slice above — the model carves them at their joints), #492 (frontend), #502/#503/#504/#505/#510 (new modules that should land on the target seams, not the old ones).
Key files: docs/architecture/layers.rst, docs/architecture/enforcement.rst, backend/src/test/java/com/tracepcap/architecture/LayerDependencyTest.java, backend/src/test/resources/archunit_store/ (the shrink-only baseline).
Description
TracePcap is a modular wrapper around a growing suite of functions — the architecture exists so new capability is added by dropping a module into a stage, not by editing a core. The seams for this exist (
analysis/spi/,DeviceClassificationSignal, storydetectors,HostServiceLogExtractor); the code has drifted from them. This epic defines the target model, enforces its rules, and refactors toward it slice by slice.Design — SETTLED (2026-07-15)
The target model is documented in
docs/architecture/layers.rstand enforced bybackend/src/test/java/com/tracepcap/architecture/LayerDependencyTest.java. Summary:Five stages (data changes shape: packets → facts → insights → conclusions → narrative):
ScoreBoardis the embedded prototype; [Enhancement] Conflicting device information: nodeType, deviceType and role can contradict each other on one host #499 = missing host-identity adjudicator; [Enhancement] Surface classification uncertainty on the graph instead of only in the click-through popup #498 = exposing the margin.origin/llmSuggested/confirmedByHuman/staleSinceon every fact and conclusion (NodeRoleEntityis the reference implementation); human authority enters at Adjudicate.Dependency rules (all enforced, frozen):
analysis.spi/dto, never repositories/entities — 755 frozen violationsmonitor ↔ insights,analysis ↔ file)FilterService, the known grey case)The frozen store (
backend/src/test/resources/archunit_store) may only shrink; a new violation fails the build naming the exact class (verified with a planted probe).Evidence of drift (measured)
analysisrepositories/entities directly.monitor ↔ insights(decoupling eventNodeRoleChangedEventalready exists, unevenly applied),analysis ↔ file(single edge:AsyncAnalysisService).HostnameResolverServiceadjudicates at write time and destroys competing claims (the [Enhancement] Expand hostname resolution — wire in nDPI SNI/Host, TLS cert subject, LLMNR #511 spoofing case is undetectable today);IpMacObservationEntitydoes it right.WebServerLogExtractormixes Extract + Scan (the api/web role decision [Enhancement] Server/client classification: TLS participation and low ports wrongly force a Web Server label #496 flags).UnknownAppDetectorcan't distinguish "nDPI didn't run" from "unknown" (Research: how unknown/unidentified protocols are flagged and handled (incl. proprietary payloads) #501).networkService.tsnodeType;getNodeColorprecedence) — client-side, on a truncated node set; [Enhancement] Server/client classification: TLS participation and low ports wrongly force a Web Server label #496/[Enhancement] Conflicting device information: nodeType, deviceType and role can contradict each other on one host #499 are the symptoms.ScoreBoard's prototype; conclusions ship un-reconciled ([Enhancement] Conflicting device information: nodeType, deviceType and role can contradict each other on one host #499).analysis6,774 LOC (2× next); god classesSessionReconstructionService(1,416),ReportService(1,407).Acceptance Criteria
Layer model defined and documented✅docs/architecture/layers.rst(five stages, three grades, contracts, worked example, divergence list) + C4 diagram.Boundaries enforced, not just documented✅ Four frozen ArchUnit rules; store committed; only-shrink policy; verified to catch new violations.monitor ↔ insightsandanalysis ↔ filecycles broken (events or shared contracts;NodeRoleChangedEventshows the pattern). Cycle store → 0. (Note from slice 1: fixing a cycle can surface previously-unreported ones — monitor ↔ subnets appeared only after analysis ↔ file was broken.)analysismigrates ontoanalysis.spi/DTOs; the 755-entry store trends to 0, slice by slice.UnknownAppDetector(and peers) consult it — fixes the Research: how unknown/unidentified protocols are flagged and handled (incl. proprietary payloads) #501 conflation.HostnameResolverServicestores all claims (à laIpMacObservationEntity); winner-picking moves to a hostname adjudicator.networkService.tsscanning/getNodeColorprecedence retired).WebServerLogExtractorsplit: extraction stays, the api/web role decision becomes a scanner ([Enhancement] Server/client classification: TLS participation and low ports wrongly force a Web Server label #496 fix lands there).AnalysisServicepipeline expressed as composable units, not comments in one method.Slices (one PR each, roughly ordered)
analysis ↔ file— single edge (AsyncAnalysisService); event or port. Smallest structural win; proves the store shrinks.UnknownAppDetectorconsults it (Research: how unknown/unidentified protocols are flagged and handled (incl. proprietary payloads) #501's immediate bug fixed here).monitor ↔ insightsandmonitor ↔ subnets(PR Break monitor ↔ insights and monitor ↔ subnets — module cycles to zero (#512 slice 3) #514, cycles → 0) — extend theNodeRoleChangedEventpattern; the subnets edge is ChangeDetectionService:64.FilterServicegrey case, frontend repatriation beyond identity).Notes
Related: #416 (predecessor epic — its invariant holds at 0 violations and is now frozen), #496/#498/#499/#501/#506/#507/#511 (each maps to a slice above — the model carves them at their joints), #492 (frontend), #502/#503/#504/#505/#510 (new modules that should land on the target seams, not the old ones).
Key files:
docs/architecture/layers.rst,docs/architecture/enforcement.rst,backend/src/test/java/com/tracepcap/architecture/LayerDependencyTest.java,backend/src/test/resources/archunit_store/(the shrink-only baseline).