Feature/cc json 2 analysis (#4505)
* refactor(visualization): rename appearance module to mapState (structural)
Slice 5 structural pre-commit (Tidy First): git mv app/codeCharta/appearance
-> app/codeCharta/mapState and appearance.facade.ts -> mapState.facade.ts,
repoint all 87 facade importers, and rename the dep-cruiser
`shared-state-is-leaf` rule to `state-home-is-leaf` targeting
(appearance|mapState) so the rename leaves no unguarded window.
Byte-identical move: the runtime store shape is unchanged (the slices still
combine under appSettings); the store-key reshape into a real
state.mapState root follows as a separate behavioral commit.
tsc clean; npm test green with zero snapshot diff (45/45); lint:architecture
0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): give mapState its own state root (Slice 5 reshape)
Slice 5 store-key reshape (behavioral): the 21 map-view slices previously
combined under `state.appSettings` now have their own `state.mapState` root —
the first time "state has a runtime home" is true (Slice 4 was folder-only).
- model: split the 21 map-view keys out of `AppSettings` into a new `MapState`
interface; add `mapState` to `CcState` and `Settings`.
- store: new `mapState/store/mapState.reducer.ts` (combined reducer +
`defaultMapState`) and `mapState.selector.ts` (`mapStateSelector`); registered
in `state.manager` (appReducers/defaultState) with the dynamic-key path
`mapState.mapColors.markingColors`. Trim `appSettings.reducer` to its 10 keys.
- readers: repoint every `state.appSettings.<mapKey>` reader, string reset-key,
and selector base to `mapState`; add `getMapState()` to the arrow/threeScene
stores. Consumer-facing selector names are unchanged.
- persistence machinery generalized once (reused by Slices 6-10):
* loadInitialFile applier gains `applyMapState` + `mapMapStateToAction`;
`mapAppSettingToAction` trimmed to the staying keys.
* scenarioApplier patches key colors/labels under `mapState` (mergePatches too).
* IndexedDB `DB_VERSION` 2->3 with a real `upgrade` record transform
(`migrateCcStateRecordToV3`) that re-homes a persisted v2 blob's map-view
settings into `mapState` — closing the silent-data-loss landmine where
`_applyPartialState`'s isKeyOf guard would drop them to defaults.
URL round-trip needs no change (only metrics/mode/file are URL-serialized);
scenarios persist in section-shape (not store-shape) so their IndexedDB store
needs no transform — only the ccstate record does.
tsc clean; npm test green with zero snapshot diff (45/45, no -u), +5 IndexedDB
migration tests; lint:architecture 0 errors. dep-cruiser `state-home-is-leaf`
stays warn (flips to error for mapState in Slice 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 5 mapState-root completion
Add the CHANGELOG (unreleased) Slice 5 entry, the slice-5-mapstate-root.md
completion doc, mark Slice 5 done in roadmap-v2-state-homes.md (status +
"where we are" + Outcome), and note in CARRIED-FORWARD the two verified
scope-narrowing facts for Slices 6-10: the URL round-trip only serializes
metric/mode/file params, and scenarios persist section-shaped (so only the
ccstate IndexedDB record needs a reshape transform, not the scenarios store).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): relocate mapState stragglers into mapState/store (Slice 6 structural)
Slice 6 code-boundary move (structural, byte-identical): git mv the eight
map-view stragglers out of the state/ grab-bag reducers into mapState/store/,
repoint every importer, and add them to the mapState facade. No store-shape
change yet — the slices stay combined under their old dynamicSettings/appSettings/
appStatus reducers via transitional state/ -> mapState/ imports (the reverse of
Slice-5 commit 1); the selectors still read their old parent selector. The
store-key reshape is the follow-up commit.
Moved (folder + specs, byte-identical bodies):
- dynamicSettings/{colorMode,colorRange,margin} -> mapState/store/*
- appSettings/{layoutAlgorithm,isLoadingMap} -> mapState/store/*
- appStatus/{hoveredNodeId,rightClickedNodeData,selectedBuildingId} -> mapState/store/*
tsc clean; npm test green with zero snapshot diff (45/45, no -u);
lint:architecture 0 errors (one legend->colorRange new-must-not-import-legacy
warn bridge drops as colorRange leaves state/, 114 warnings remain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home the mapState presentation stragglers (Slice 6 reshape)
Slice 6 store-key reshape (behavioral): eight stragglers move out of the state/
grab-bags into the state.mapState root, reusing the Slice-5 reshape machinery
(each key only adds itself). appStatus is now left with only
currentFilesAreSampleFiles.
Moved into mapState:
- dynamicSettings: colorMode, colorRange, margin
- appSettings: layoutAlgorithm, isLoadingMap
- appStatus: hoveredNodeId, selectedBuildingId, rightClickedNodeData (transient)
- model: the eight keys move DynamicSettings/AppSettings/AppStatus -> MapState.
- store: add them to the mapState combineReducers + defaultMapState; trim the
three source reducers; the eight leaf selectors (+ globalSettings.layoutAlgorithm
+ 3dPrint colorRange/colorMode) now read mapStateSelector.
- applier: loadInitialFile dispatches colorMode/colorRange/margin/layoutAlgorithm
from mapMapStateToAction; isLoadingMap + the three interaction ids are no-ops
there (appStatus was never applied on load, so they must not be restored).
- scenarios: buildColorsPatch patches mapState.colorMode/colorRange.
- render gate: areAllNecessaryRenderDataAvailable folds mapState.colorRange back
into areDynamicSettingsAvailable so the first-render null-colorRange gate is kept.
- IndexedDB: DB_VERSION 3->4 + migrateCcStateRecordToV4 re-homes the eight keys in a
persisted blob (chained after v3), closing the silent-data-loss landmine; +5 tests.
- readers repointed: treeMapHelper/treeMapGenerator/streetLayoutGenerator, the
codeMap render + mouse stores, threeSceneService floor-labels; string reset-keys
(mapState.margin/colorMode/layoutAlgorithm); dataMocks default state.
Action barrels unchanged (save/rerender triggering byte-identical, as in Slice 5).
tsc clean; npm test green with zero snapshot diff (45/45, no -u), +4 migration
tests (2275 pass); lint:architecture 0 errors (114 warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(visualization): enforce mapState home boundary at error (Slice 6)
Flip state-home-is-leaf to error for mapState (drops the now-gone appearance
alternative) and add state-home-only-stores-import-ngrx at error: only
mapState/store/ may import @ngrx/store; the facade is a re-export barrel.
Both rules are clean (0 errors); the 114 pre-existing warn bridges are unchanged.
The rules extend to sharedView/preferences as those homes land (Slices 8/10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 6 mapState-stragglers completion
Mark Slice 6 done in the roadmap + CARRIED-FORWARD, add the slice-6 plan/outcome
doc. mapState now owns colorMode/colorRange/margin, layoutAlgorithm/isLoadingMap,
and the transient interaction ids; appStatus holds only currentFilesAreSampleFiles;
state-home-is-leaf + state-home-only-stores-import-ngrx are error for mapState.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): relocate the five metric-selection slices into mapState/store (Slice 7 structural)
Code-boundary move THEN store-key reshape: git mv areaMetric/heightMetric/
colorMetric/distributionMetric/edgeMetric from state/store/dynamicSettings into
mapState/store, ahead of the Slice 7 reshape. Bodies byte-identical (moved
reducers/selectors only adjust their setState-factory and dynamicSettings.selector
import paths, the transitional cross-import Slice 6 also used). The 5 slices stay
registered in the dynamicSettings combineReducers this commit; the store-key move
into mapState is the follow-up behavioral commit.
- 53 external importers repointed to mapState.facade (+15 facade re-exports)
- dynamicSettings.reducer/actions import the 5 slices from their new mapState home
- dep-cruiser: exempt spec/e2e from filestore-has-no-upward-deps (the loader spec
references metric action creators that moved from the rule-allowed state/ into the
forbidden mapState home; mirrors new-must-not-import-legacy's spec exemption; the
runtime source stays clean, no severity flip)
tsc clean; npm test 45/45 snapshots, zero diff (no -u), 2275 passing;
lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home metric selection into state.mapState (Slice 7 reshape)
Store-key reshape moving the five metric-selection keys (areaMetric, heightMetric,
colorMetric, distributionMetric, edgeMetric) out of the dynamicSettings grab-bag into
the mapState state-home, reusing the Slice 5/6 reshape machinery.
- model: MapState extends PrimaryMetrics + distributionMetric; DynamicSettings drops
them (now only sortingOption/focusedNodePath/searchPattern)
- mapState combineReducers/defaults gain the 5 slices; dynamicSettings trimmed
- the 5 leaf selectors + 3dPrint's duplicate selectors read mapStateSelector
- applier: the 5 metric cases move from mapDynamicSettingToAction to mapMapStateToAction
- URL round-trip: updateQueryParameters reads state.mapState; setMetricsFromUrlValues +
actionsRequiringUpdateQueryParameters dispatch/import the mapState metric actions
- scenarios: buildMetricsPatch keys mapState; buildScenarioSections reads state.mapState
- IndexedDB DB_VERSION 4→5 + migrateCcStateRecordToV5 (chained after v4) + tests
- availability gate folds the 5 metrics back in from mapState so the first-render check
stays value-identical (distributionMetric is not covered by areChosenMetrics)
- all dotted readers (treeMap/street layout, codeMap render, labels, mapMesh) + mocks
repointed to state.mapState
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2279 passing (+4 v5 migration
tests); lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move node-metric calc + rangeOfMetric to util kernel (Slice 7 structural)
Relocate the pure metric primitives out of the metrics lens into the util kernel,
byte-identical:
- git mv lenses/metrics/store/nodeMetricData.calculator.ts (+ spec) → util/metric/
- extract MetricRange + rangeOfMetric from metricsLens.selectors.ts → util/metric/metricRange.ts
These are pure functions over cc.json-derived data with no view-state reads. Hoisting
them to the kernel lets the Slice-7 derived selectors (next commit) compose them WITHOUT
importing the metrics-lens facade — which would otherwise close a
state → facade → repo → store → state module cycle (the lens store must keep serving
blacklist-filtered data for value-equality). The lens still owns the metric DATA via its
repos/store; only the stateless helpers move.
metricsLens.selectors.ts + attributes.repo + specs repoint to util/metric.
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2279 passing;
lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): parameterize the metrics lens off view state (Slice 7 P0-1)
Lift the two view-state reads out of the metrics lens so it no longer imports blacklist
or the metric-selection (grep-verified: no fileSettings/blacklist, no store/dynamicSettings
under lenses/metrics/).
- git mv the two view-state-aware selectors OUT of the lens into a derived module,
lenses/metrics/store/metricsLens.selectors.ts → state/selectors/nodeMetricData/
nodeMetricData.selector.ts. They compose the lens's raw projection (calculateNodeMetricData
from the util kernel) with the blacklist matcher + chosen colorMetric.
- metricsLens.store re-reads nodeMetricDataSelector/metricRangeSelector from the derived
module, so getNodeMetricData()/rangeOf()/selectedColorMetricData$ keep returning
blacklist-filtered data unchanged (value-equality preserved for the render path).
- metricsLens.facade drops the two selector re-exports; the 10 consumers import them from
state/selectors/nodeMetricData directly (inspectorMetricRows keeps attributeDescriptors
from the facade).
- parity test: derived nodeMetricData/metricRange value-equal calculateNodeMetricData ∘
rangeOfMetric under both an empty and a non-empty blacklist (guards the old-read deletion).
No cycle (the derived module imports the util kernel, not the lens facade). new-must-not-
import-legacy metrics-lens edges drop (114→109 warn bridges); no rule flip this slice.
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2281 passing;
lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 7 mapState-metrics + lens-parameterization completion
slice-7-mapstate-metrics.md (4-commit outcome); roadmap Slice 7 → DONE + Today/spine
updated (mapState now owns metric selection; metrics lens view-state-free); CARRIED-FORWARD
resolves #3 (metric selection → mapState) + #2b (edge selection + 3 edge effects), #5 note
repointed (metrics-lens-ngrx-guard now gated only on the Slice-11 legend move).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): add Slice 8 (sharedView) plan — todo
Concise plan for standing up the first brand-new state.sharedView root (focusedNodePath +
searchPattern out of dynamicSettings). Records the code-verified scope + machinery reuse and
the 5 landmines (the objectWithDynamicKeysInStore array-corruption rename, v6 being the first
new-root migration, no URL change, no availability fold-back, net-neutral legacy edges).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv focus+search into sharedView/store (Slice 8 structural)
Move the focusedNodePath + searchPattern leaf folders out of state/store/dynamicSettings
into a new app/codeCharta/sharedView/store/, add the sharedView.facade barrel, and repoint
~18 external consumers to it. The dynamicSettings combineReducers still wires both slices
(cross-imported from sharedView), so state.dynamicSettings.{focusedNodePath,searchPattern}
stays alive and runtime is unchanged — the store-key reshape into a real state.sharedView
root follows in the behavioral commit.
Zero snapshot diff (45/45), tsc clean, lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home focus+search into state.sharedView root (Slice 8 reshape)
Stand up the first brand-new state home, state.sharedView, and move focusedNodePath +
searchPattern out of dynamicSettings into it. After this slice dynamicSettings holds only
sortingOption.
- model: split DynamicSettings ({ sortingOption }); add SharedView; add sharedView to
Settings + CcState.
- sharedView root: combineReducers + defaultSharedView + sharedViewSelector; register
sharedView/defaultSharedView in state.manager; the 2 leaf selectors now read
sharedViewSelector; dynamicSettings.reducer trimmed.
- applier: applySharedView + mapSharedViewToAction (mirrors applyMapState); wired into both
load paths; focus/search cases removed from mapDynamicSettingToAction.
- scenarios: buildFiltersPatch focusedNodePath re-keyed dynamicSettings -> sharedView (+
mergePatches); scenarios.service reads state.sharedView.focusedNodePath.
- dotted readers: treeMapHelper (isVisible/isNodeFlat) read state.sharedView.*.
- state.manager objectWithDynamicKeysInStore: dynamicSettings.focusedNodePath ->
sharedView.focusedNodePath (array must be replaced wholesale, not deep-merged).
- IndexedDB DB_VERSION 5->6 + migrateCcStateRecordToV6 (the FIRST migration that CREATES a
new root: builds sharedView fresh from defaults + the two moved keys) chained after v5.
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2287 passing; lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(visualization): enforce sharedView state-home boundaries (Slice 8 dep-cruiser)
Extend state-home-is-leaf and state-home-only-stores-import-ngrx to also match
^app/codeCharta/sharedView/ (adding ^app/codeCharta/sharedView/store/ to the ngrx rule's
pathNot). Both stay error — sharedView is a leaf that imports no lenses/renderers/shell, and
only its store/ folder touches @ngrx/store. lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 8 (sharedView) completion
Mark Slice 8 DONE across the roadmap (outcome + ordering spine + rule-flip schedule),
flip the slice-8-sharedview plan to complete with an as-executed outcome, and log the
Done note in CARRIED-FORWARD (Slice 8 unblocks 9b/9c). A 5-reviewer adversarial pass
(reshape-completeness, IndexedDB v6, array-corruption, scenario behavior-neutrality,
structural purity) verified the diff against the code and raised 0 findings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv attributeTypes+descriptors into lenses/metrics/store (Slice 9a structural)
Move the two cc.json-source slices out of state/store/fileSettings into the
metrics lens's store/, seeding the first lens-owned store root. The moved leaf
selectors keep reading fileSettingsSelector transitionally, and fileSettings.reducer
keeps combining them (via the new metricsLens.load.facade) so state.fileSettings.*
is unchanged — zero snapshot diff.
- git mv attributeTypes/ + attributeDescriptors/ -> lenses/metrics/store/
- add metricsLensSource combineReducers root + defaultMetricsLensSource (unregistered)
- add metricsLens.load.facade (write actions + reducer/default + transitional leaf re-exports)
- metricsLens.facade: re-export raw {nodes,edges} attributeTypesSelector
- repoint external readers to the facade (accumulatedData, metricsBar, inspector, specs)
- repoint load applier + fileSettings.reducer/actions to the load facade
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2287 passing;
lint:architecture 0 errors (109->111 warns: transitional moved-slice->state edges).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home attributeTypes+descriptors into state.metricsLensSource (Slice 9a reshape)
Register the metrics lens's cc.json-source root at runtime: split
attributeTypes + attributeDescriptors out of the fileSettings state slice into
their own lens-owned state.metricsLensSource root. Per-file CCFile.settings still
bundles them (FileSettings & MetricsLensSource); only the merged STATE splits.
- model: FileSettings drops the two keys; new MetricsLensSource; CCFile settings
= FileSettings & MetricsLensSource; CcState gains metricsLensSource
- state.manager: register metricsLensSource in appReducers+defaultState; repoint
the two objectWithDynamicKeysInStore dotted paths fileSettings.* -> metricsLensSource.*
- lens: metricsLensSource.selector; leaf selectors read it (drops the 2
attributes.selectors -> legacy warn edges); read facade exposes raw attributeTypesSelector
- applier: applyMetricsLensSource + mapMetricsLensSourceToAction, wired into both
load paths; the two cases leave mapFileSettingToAction
- updateFileSettings.effect co-emits shrunken fileSettings + metricsLensSource in ONE setState
- repoint dotted/selector readers: treeMapHelper, treeMapGenerator, updateMapColors.effect,
accumulatedData, 3dPrint(selectors+stateAccess), inspectorMappingBlocks, metricsBar, fileDownloader
- IndexedDB DB_VERSION 6->7 + migrateCcStateRecordToV7 (new-root, mirrors v6) + tests
- mocks: STATE/DEFAULT_STATE split; spec expectations updated for the two roots
tsc clean; npm test 45/45 snapshots zero diff (no -u), 2291 passing (+4 v7);
lint:architecture 0 errors (109 warns, net-neutral: setState-factory offsets the dropped edges).
scenarios + URL untouched (neither references these keys).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(visualization): add lens-owns-ccjson-source dep-cruiser rule at warn (Slice 9a)
Guard that the metrics lens's cc.json source (attributeTypes/attributeDescriptors
slices + the metricsLensSource root under lenses/metrics/store/) is reached from
outside the lens only through a metrics-lens facade — never its store internals.
Locks "the cc.json source lives only under lenses" as fileSettings dissolves.
Staged at warn (0 violations today, all access is via facades); flips to error
once edges also move to a dependency-lens store.
lint:architecture 0 errors, 109 warnings (unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 9a (metricsLensSource cc.json source) completion
- add slice-9a-ccjson-source-lens.md (outcome, landmines, deferred edges, rollback)
- roadmap: Slice 9a DONE; runtime status → metricsLensSource root + v7; flip-schedule
lens-owns-ccjson-source landed at warn; edges DEFERRED
- CARRIED-FORWARD: CF #2a source resolved; record edges → dependency lens deferral
(merged render-model array, needs render-model home + injectable DependencyLensStore);
#5 net-neutral warn note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv blacklist store into sharedView (Slice 9b structural)
git mv state/store/fileSettings/blacklist -> sharedView/store/blacklist and repoint
all importers to the sharedView facade. The blacklist store is still combined
transitionally under fileSettings.reducer (via the sharedView facade) so
state.fileSettings.blacklist is unchanged (zero snapshot diff); blacklist.selector
keeps reading fileSettingsSelector until the behavioral step re-homes it. Save-trigger
blacklist actions stay in fileSettingsActions, now imported from the sharedView facade
(mirroring the Slice 9a attributeTypes precedent).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home blacklist into state.sharedView (Slice 9b behavioral)
Register blacklist under the sharedView combineReducers + SharedView model and drop
it from the fileSettings state slice (now { edges, markedPackages }). The .cc.json file
still carries blacklist per-file, so CCFile.settings.fileSettings keeps it via the
intersection FileSettings & MetricsLensSource & { blacklist } (mirrors Slice 9a).
blacklist.selector reads state.sharedView; the objectWithDynamicKeysInStore path is
renamed fileSettings.blacklist -> sharedView.blacklist (array replaced wholesale). The
load applier moves the blacklist case from mapFileSettingToAction to mapSharedViewToAction;
updateFileSettings.effect co-emits the merged blacklist under sharedView in the SAME
setState. Scenarios: buildFiltersPatch re-keys blacklist -> sharedView, buildScenarioSections
reads state.sharedView.blacklist. IndexedDB DB_VERSION 7->8 + migrateCcStateRecordToV8
(merge-into-existing sharedView, mirrors v3-v5), chained after v7 (+4 tests). 3dPrint
selectors/stateAccess repoint to the sharedView blacklist; the duplicate 3dPrint
blacklistSelector is deduped to a re-export of the sharedView facade selector.
npm test 2295 passing, 45/45 snapshots zero diff (no -u); tsc clean;
lint:architecture 0 errors (107 warns, unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): lift edge blacklist + edge-visibility out of the dependency lens (Slice 9b P0-1)
Move the view-state-aware edge selectors out of lenses/dependency/store/ into derived
selectors under state/selectors/edgeMetricData/ (the edge twin of nodeMetricData). The
dependency lens now exposes only the RAW pure calc calculateEdgeMetricData(visibleFiles,
matcher) through its facade; the derived selectors compose it with the sharedView blacklist
(edgeMetricDataResult/data/map/names) and the mapState edge-visibility showIncoming/
OutgoingEdges (sortedNodeEdgeMetricsMap). The two selector specs move with them as the
parity/characterization tests.
Consumers (metricData, accumulatedData, amountOfBuildingsWithSelectedEdgeMetric,
edgePreviewNodes, resetSelectedEdgeMetric + updateQueryParameters effects) repoint to the
derived selectors. After this slice neither lens imports any home selector (blacklist +
edge-visibility) — grep-verified; the edge calc keeps only the BlacklistMatcher parameter type.
npm test 2295 passing, 45/45 snapshots zero diff (no -u); tsc clean;
lint:architecture 0 errors (107 warns, unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 9b (blacklist → sharedView) completion
Add slice-9b-blacklist-sharedview.md; mark Slice 9b DONE in roadmap-v2-state-homes.md;
update CARRIED-FORWARD (blacklist half of item #5 resolved, dependency lens now
view-state-free, new-must-not-import-legacy flip now waits only on 9c markedPackages).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): correct Slice 9c/10 scope + new-must-not-import-legacy flip timing
Verified against current code (post-9b): (1) Slice 9c NARROWS fileSettings to { edges }
rather than deleting it — edges is deferred (CF #2a, needs a render-model home + injectable
dependency store). (2) new-must-not-import-legacy does NOT flip in 9c: 12 residual edges,
0 markedPackages-related — 7 state/-survivors clear in Slice 10, 5 legend/errorDialog edges
in Slice 11 (full flip after 10+11). (3) markedPackages is ~13 direct importers (not 37),
no runtime-only landmines, no selector-dedup — a smaller/lower-risk twin of 9b; IndexedDB v9.
Fixes Slice 10's "four grab-bags gone" / CcState-shape claim (fileSettings: { edges } survives
10 until the edges slice), the flip-schedule table, and the matching CARRIED-FORWARD / 9b-doc notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv markedPackages store into sharedView (Slice 9c structural)
git mv state/store/fileSettings/markedPackages -> sharedView/store/markedPackages (incl its
util/ addMarkedPackage + findIndexOfMarkedPackageOrParent) and repoint all importers to the
sharedView facade. The markedPackages store is still combined transitionally under
fileSettings.reducer (via the sharedView facade) so state.fileSettings.markedPackages is
unchanged (zero snapshot diff); markedPackages.selector keeps reading fileSettingsSelector
until the behavioral step re-homes it. Save-trigger markedPackages actions stay in
fileSettingsActions, now imported from the sharedView facade (mirroring the Slice 9a/9b
precedent). The facade re-exports the markedPackages actions/reducer/selector +
findIndexOfMarkedPackageOrParent so external consumers (incl markFolderItems.selector) reach
it only through the facade.
tsc clean; npm test 2295 passing, 45/45 snapshots zero diff (no -u); lint:architecture
0 errors, 107 warns (unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home markedPackages into state.sharedView (Slice 9c behavioral)
Register markedPackages under the sharedView combineReducers + SharedView model and drop it from
the fileSettings state slice (now { edges } — edges is DEFERRED). The .cc.json file still carries
markedPackages per-file, so CCFile.settings.fileSettings keeps it via the intersection
FileSettings & MetricsLensSource & { blacklist } & { markedPackages } (mirrors Slice 9a/9b).
markedPackages.selector reads state.sharedView; the objectWithDynamicKeysInStore path is renamed
fileSettings.markedPackages -> sharedView.markedPackages (array replaced wholesale). The load
applier moves the markedPackages case from mapFileSettingToAction to mapSharedViewToAction;
updateFileSettings.effect co-emits the merged markedPackages under sharedView in the SAME setState
(alongside blacklist). Scenarios: buildLabelsAndFoldersPatch re-keys markedPackages -> sharedView
(and the now-dead fileSettings mergePatches branch is dropped), buildScenarioSections reads
state.sharedView.markedPackages. treeMapHelper/streetViewHelper read state.sharedView.markedPackages.
fileDownloader's explicit per-file intersection grows to keep markedPackages. IndexedDB DB_VERSION
8->9 + migrateCcStateRecordToV9 (merge-into-existing sharedView, mirrors v3-v5/v8), chained after
v8 (+4 tests, extended the v2->v9 chain test to carry a markedPackages value into sharedView).
tsc clean; npm test 2299 passing (+4 v9 tests), 45/45 snapshots zero diff (no -u);
lint:architecture 0 errors, 107 warns (unchanged — NO rule flip in 9c).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 9c (markedPackages → sharedView) completion
Add slice-9c-markedpackages.md and mark Slice 9c DONE in the roadmap ordering spine + section
(2 commits: structural git mv + behavioral reshape). markedPackages moved out of the fileSettings
state slice into state.sharedView; state.fileSettings now holds ONLY { edges } (reducer NOT deleted
— edges DEFERRED). IndexedDB v8→v9 (merge-into-existing). No lens-parameterization, no
selector-dedup, no runtime-only landmine (verified); the one tsc-caught dual-role deviation was
fileDownloader's inline intersection. No dep-cruiser rule flip in 9c. Adversarial 7-landmine review
(7 hunters + adversarial verify): 0 findings. Update CARRIED-FORWARD (9c done; last movable
fileSettings member cleared) and the roadmap intro (NEXT WORK = Slice 10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move setState reducer factory state/ → util/ shared kernel (Slice 10)
Tidy First structural move: git mv state/store/util/setState.reducer.factory
(+ spec) → util/, and repoint all 46 importers. Byte-identical; the factory is a
generic ngrx reducer helper (setState/mergeState), so it belongs in the shared
util/ kernel that lenses/ and fileStore/ are allowed to import.
This clears the two `new-must-not-import-legacy` warns from the metrics-lens
attributeTypes/attributeDescriptors reducers (lenses → state/), and preempts a new
fileStore → state/ warn when Slice 10a re-homes the isLoadingFile reducer into
fileStore. lint:architecture drops 107 → 105 warns, 0 errors; tsc clean; npm test
2299 passing, 45/45 snapshots zero diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv fileStore flags into fileStore/store (Slice 10a structural)
Tidy First structural move ahead of the 10a store-key reshape: git mv the two
file-provenance flags into the fileStore that owns them —
state/store/appSettings/isLoadingFile → fileStore/store/isLoadingFile
state/store/appStatus/currentFilesAreSampleFiles → fileStore/store/currentFilesAreSampleFiles
— and repoint all 17 external importers. State SHAPE is unchanged: appSettings.reducer
and appStatus.reducer keep combining the moved reducers via a transitional cross-import
from fileStore, so state.appSettings.isLoadingFile / state.appStatus.currentFilesAreSampleFiles
still resolve (zero snapshot diff). The two moved selectors still read the old grab-bag
selectors transitionally (rewritten to read the top-level roots in the behavioral commit).
Clears the fileStore → state currentFilesAreSampleFiles.actions warn; adds 2 transitional
fileStore → state selector warns (net 105 → 106, 0 errors) that the behavioral commit removes.
tsc clean; npm test 2299 passing, 45/45 snapshots zero diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home fileStore flags to top-level roots + delete appStatus (Slice 10a behavioral)
Store-key reshape following the 10a structural move. The two file-provenance flags become
their own top-level CcState roots owned by the fileStore:
state.appSettings.isLoadingFile → state.isLoadingFile
state.appStatus.currentFilesAreSampleFiles → state.currentFilesAreSampleFiles
and the now-empty appStatus grab-bag reducer + selector are DELETED (first of the three
grab-bags Slice 10 dissolves).
- model: drop isLoadingFile from AppSettings, delete the AppStatus interface, add the two
roots to CcState.
- state.manager: register isLoadingFile + currentFilesAreSampleFiles roots; drop appStatus.
- appSettings.reducer: drop isLoadingFile from the combineReducers + defaultAppSettings.
- the two moved selectors read the top-level roots directly (no more grab-bag selector import,
clearing the 2 transitional fileStore → state warns from the structural commit).
- applier: mapAppSettingToAction drops the isLoadingFile no-op case (it left AppSettings);
isLoadingFile stays runtime-only (no root applier), currentFilesAreSampleFiles still
URL/sample-derived — behavior preserved.
- readers: files.repo + updateQueryParameters.effect read the new roots.
- IndexedDB DB_VERSION 9 → 10 + migrateCcStateRecordToV10 (promotes both scalar flags to roots,
deletes appStatus) chained after v9; +3 unit tests, extended the v2→v10 chain test.
tsc clean; npm test 2302 passing (+3 v10 tests), 45/45 snapshots zero diff (no -u);
lint:architecture 0 errors / 104 warns (−2 vs structural). No dep-cruiser rule flip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv the durable prefs into a preferences/ home (Slice 10b structural)
Tidy First structural move ahead of the preferences store-key reshape: git mv the seven
durable ex-appSettings preference leaves + the ex-dynamicSettings sortingOption into a new
preferences/store/ module, and add preferences.facade (the home's public barrel):
appSettings/{enableClipboard, enableExperimentalFeatures, isHeightAndColorMetricLinked,
isPresentationMode, maxTreeMapFiles, resetCameraIfNewFileIsLoaded,
sortingOrderAscending} → preferences/store/*
dynamicSettings/sortingOption → preferences/store/sortingOption
22 external importers repointed to preferences.facade. State SHAPE is unchanged:
appSettings.reducer + dynamicSettings.reducer keep combining the moved leaves via a transitional
cross-import from the facade, so state.appSettings.* / state.dynamicSettings.sortingOption still
resolve (zero snapshot diff). The four moved selectors still read the old grab-bag selectors
transitionally (rewritten to read preferencesSelector in the behavioral commit).
tsc clean; npm test 2302 passing, 45/45 snapshots zero diff; lint:architecture 0 errors / 104 warns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): stand up the preferences home + delete the appSettings/dynamicSettings grab-bags (Slice 10b behavioral)
Store-key reshape following the 10b structural move. The seven durable ex-appSettings prefs
and the ex-dynamicSettings sortingOption now register under a real state.preferences root, and
BOTH grab-bag reducers are DELETED — with Slice 10a's appStatus deletion, all three grab-bags
(appSettings/dynamicSettings/appStatus) are gone and "state has a home at runtime" is finally true.
- new preferences home: preferences.reducer (combineReducers of the 8 leaves) + preferences.selector
+ preferences.actions (the save-trigger list), exposed via preferences.facade; registered in
state.manager.
- model: new Preferences interface on Settings + CcState; AppSettings + DynamicSettings interfaces
deleted.
- the 4 moved leaf selectors + sortingOrderSelector read preferencesSelector; globalSettings
selectors + the areAllNecessaryRenderDataAvailable availability gate repointed (value-identical
{ sortingOption, ...metrics, colorRange } check).
- applier: applyAppSettings + applyDynamicSettings merged into applyPreferences; the two
map*ToAction switches merged into mapPreferenceToAction (sortingOrderAscending still ignored on
load, sortingOption still restored — behavior preserved).
- save trigger: actionsRequiringSaveCcState reconstitutes the EXACT ex-grab-bag action union,
now grouped by home (mapState/sharedView/preferences); setStandard already covered by fileActions.
- scenarios: resetCamera + isColorMetricLinked patches re-keyed appSettings → preferences
(+ mergePatches).
- dotted readers repointed: streetLayoutGenerator, codeMap.render.service, scenarios.service,
scenarioApplier, treeMapGenerator, codeMapMesh; and two RUNTIME landmines tsc missed
(State.getValue() is loosely typed) — ThreeSceneStore.getAppSettings and
CodeMapTooltipStore.getDynamicSettings — repointed to the preferences home.
- IndexedDB DB_VERSION 10 → 11 + migrateCcStateRecordToV11 (creates the preferences root, deletes
both grab-bags) chained after v10; +4 unit tests, extended the v2→v11 chain test.
- dep-cruiser: preferences added to state-home-is-leaf + state-home-only-stores-import-ngrx, both
now ERROR across all three homes (the roadmap's Slice-10 flip).
tsc clean; npm test 2305 passing (+4 v11, −1 merged applier test), 45/45 snapshots zero diff
(no -u); lint:architecture 0 errors / 104 warns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 10 core (grab-bags dissolved → preferences home) + defer localStorage/sorting-merge to 10c
The three grab-bag reducers (appSettings/dynamicSettings/appStatus) are gone; every setting sits in
a real state home. isLoadingFile + currentFilesAreSampleFiles → top-level fileStore roots; the 7
durable prefs + sortingOption → a real state.preferences home. state-home-is-leaf +
state-home-only-stores-import-ngrx are now error across all three homes.
The two DoD refinements — preferences→localStorage and the sortingOrderAscending+sortingOption merge —
are explicitly deferred to Slice 10c: localStorage is a real behavior seam with no snapshot/e2e
coverage for pref persistence (needs user sign-off + a characterization test); the sorting merge is
organizational (both prefs already live under preferences).
Adds slice-10-preferences.md; updates roadmap-v2-state-homes.md (Slice 10 → CORE DONE) and
CARRIED-FORWARD.md (items #7 localStorage + #8 sorting-merge).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): repoint the global-settings reset button to the current state homes (Slice 10 landmine)
The "Reset global settings" button passed five dotted state-path keys under the now-deleted
appSettings root to getPartialDefaultState, which walks each token into defaultState. Since
defaultState.appSettings is gone, every token was silently skipped → the reset produced an empty
patch and reset NOTHING (no compile error: settingsKeys is string[], and the keys live in an HTML
template tsc doesn't type-check).
Repoint all five to their real homes:
appSettings.resetCameraIfNewFileIsLoaded → preferences.resetCameraIfNewFileIsLoaded (Slice 10 regression)
appSettings.maxTreeMapFiles → preferences.maxTreeMapFiles (Slice 10 regression)
appSettings.hideFlatBuildings → mapState.hideFlatBuildings (stale since the Slice 5/6 appearance→mapState move)
appSettings.isWhiteBackground → mapState.isWhiteBackground (stale since Slice 5/6)
appSettings.layoutAlgorithm → mapState.layoutAlgorithm (stale since Slice 5/6)
The resetSettings.service.spec already expected the three appearance keys under mapState; its
remaining stale appSettings.* pass-through keys are aligned to preferences too. Found by an
adversarial template sweep (the earlier .ts-only grep missed .html).
tsc clean; npm test 2305 passing, 45/45 snapshots zero diff; lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record the reset-button landmine + adversarial review in the Slice 10 notes
Adds the third runtime landmine (the reset-settings HTML template's stale appSettings.* keys, caught
by the adversarial review sweep across .html) and the 3-hunter review outcome to slice-10-preferences.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): restore metric labels in the explorer hover tooltip
The hover tooltip showed the node name followed by three "undefined: —" rows. Its store method
(CodeMapTooltipStore.getDynamicSettings) read the metric names from the dynamicSettings slice, but
the area/height/color metric SELECTION moved to the mapState home in Slice 7 — so the destructured
areaMetric/heightMetric/colorMetric had been coming through undefined ever since. Slice 10b only
carried that latent breakage forward (dynamicSettings → preferences, still metric-less).
Fix in the migration's direction: read the selected metrics from their real home (mapState) via its
facade — getSelectedMetrics() composes areaMetricSelector/heightMetricSelector/colorMetricSelector.
The tooltip now renders "<metric>: <node value>" again. Strengthened the tooltip spec to assert the
metric rows (and no "undefined"), guarding against reading from the wrong home.
tsc clean; npm test 2305 passing, 45/45 snapshots zero diff; lint:architecture 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): co-locate the two sort prefs into a preferences/store/sorting/ folder (Slice 10c structural)
Pure git mv ahead of the Slice 10c behavioral merge (CARRIED-FORWARD #8): the
sortingOption/ and sortingOrderAscending/ store folders collapse into one sorting/
folder (the sortingOption files renamed to sorting.*; the ascending files ride along
under their own names until the merge absorbs them). Only the three deep-path
consumers (preferences.reducer/actions/facade) update their imports — every external
consumer goes through the preferences.facade barrel, whose public names are unchanged.
combineReducers still holds the two keys; no state-shape change, no IndexedDB bump,
snapshots byte-identical. tsc + scoped tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): merge the two file-explorer sort prefs into one preferences.sorting object + IndexedDB v12 (Slice 10c behavioral)
Closes CARRIED-FORWARD #8, the last shape-consolidation DoD item of Slice 10.
sortingOrderAscending (boolean) + sortingOption (enum) — both already living under the
preferences home — collapse into a single `sorting: { option, orderAscending }` object
handled by one reducer. New `Sorting` type; `Preferences.sorting` replaces the two flat
fields.
Public API held stable so the blast radius stays inside persistence + the store:
sortingOrderSelector still yields the option, sortingOrderAscendingSelector still yields
the order, and the three action names + the preferencesActions save-trigger union are
byte-identical. The entire sidebarExplorer sort feature and the render-availability gate
are therefore untouched.
IndexedDB v11 -> v12: migrateCcStateRecordToV12 is the first WITHIN-home nesting transform
(prior v3-v11 MOVE keys between homes) — it nests the two flat pref keys into `sorting`,
deletes the flats, and falls back to defaultSorting. Chained after v11 in the upgrade;
v2-blob chain test extended through v12; the v11 unit tests whose flat-sortingOption
default assertions are now v12's responsibility are retuned.
Load-applier asymmetry preserved exactly: mapPreferenceToAction("sorting") restores the
option (setSortingOption) but never the sort order — the pre-merge split ignored
sortingOrderAscending on load ("a file-explorer UI pref a loaded file must not override").
tsc + biome clean, 2309 passing (45/45 snapshots zero-diff), dep-cruiser 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): preserve setState reset-on-undefined in the merged sorting reducer + record Slice 10c (docs)
Follow-up to the Slice 10c sort merge, closing the one note from the adversarial
review: the merged `sorting` reducer's inline handlers dropped the reset-to-default-
on-undefined semantics that the two pre-merge reducers inherited from the
setState.reducer.factory. Restored with an explicit `=== undefined` check per field
(null still passes through, matching setState exactly) + two reducer spec cases. The
path is unreachable today (both dispatch sites pass defined values), so this is exact-
equivalence hygiene, not a bug fix.
Docs: CARRIED-FORWARD drops the completed #8 sorting-merge row and records the Slice 10c
Done note; #7 (preferences → localStorage) re-scoped to a dedicated later slice, held for
user sign-off + a rehydrate characterization test. slice-10c plan → complete, with the
review outcome (7 claims verified, 0 behavior-changing issues; the benign redundant-
dispatch note accepted).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): lift the legend's ngrx Store reads into feature-local stores (Slice 11 step 1)
Prep for re-homing the legend out of lenses/. legend.service dropped its @ngrx Store
injection: the six mapState reads (area/height/color/edge metric, colorRange, mapColors)
moved into a new LegendMapStateStore and the delta-state read into a LegendIsDeltaStateStore
(mirroring the metricsBar/sidebarInspector isDeltaState store idiom). The service now reads
those stores; the metric data still comes from the lens repos (valid internal access while
legend is still inside the lens — the repos → lens-facade swap lands with the git mv, since a
lens's internals may not import its own facade).
This removes the sole metrics-lens-ngrx-guard violation (legend.service no longer imports
@ngrx/store; the new stores/ folder is exempt). Behaviour-preserving: the public LegendService
method surface is unchanged, so every legend component is untouched. 2313 passing, 45/45
snapshots zero-diff, tsc + biome clean, dep-cruiser 0 errors (104→103 warns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): git mv the legend to features/legend/ and read the metrics lens via its facade (Slice 11 step 2)
The legend leaves the abandoned "features-in-lenses + shell" model:
lenses/metrics/features/legend/ → features/legend/ (the now-empty lenses/metrics/features/
shell dir is gone). Internal-import depths shrank two levels; the one external consumer
(codeCharta.component) now imports LegendPanelComponent from the features/legend/facade
barrel. legend.service swaps the two metrics-lens repo injections for the public
MetricsLensFacade (selectedColorMetricData$ + descriptors$ are the same observables) — now
a valid outside consumer reaching the lens only through its facade.
Behaviour-preserving: 2313 passing, 45/45 snapshots zero-diff, tsc + biome clean,
dep-cruiser 0 errors (103→100 warns: legend's three new-must-not-import-legacy edges
disappear now it is a feature, not lens code).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): flip metrics-lens-ngrx-guard to error + record Slice 11 (docs)
The legend was the last lens-code ngrx injection; with it re-homed to features/legend/,
metrics-lens-ngrx-guard flips warn → error (0 violations). Dropped the now-vestigial
lenses/*/features/ exclusion from its pathNot (that "shell" model is deleted) and refreshed
the Slice-1 boundary header. The roadmap's proposed feature-services-reach-a-lens-only-via-
its-facade rule was NOT added — it is already subsumed by the error-level
lens-external-access-only-via-public-surface.
Docs: CARRIED-FORWARD records the Slice 11 Done note, marks the metrics-lens-ngrx-guard half
of #5 done, and re-evaluates #4 to HOLD (its lenses/*/features/ target no longer exists; both
components are already facade-clean metricsBar UI). slice-11 plan → complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): drop the stale legend-in-lens location from the Slice 1 changelog entry (Slice 11 follow-up)
The unreleased Slice-1 entry claimed the legend "lives inside the metrics lens
(lenses/metrics/features/legend)"; Slice 11 re-homed it to features/legend/, so that
location no longer holds for the release. Keep the substantive Slice-1 fact (the legend's
per-concern services collapsed onto one view-model) and drop the now-reverted, non-user-facing
location detail. Surfaced by the Slice 11 adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record the CF#7 postpone decision + the two-button (wipe-all vs reset-global-settings) design intent
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): scope the legacy-boundary-close slice (flip new-must-not-import-legacy) — 6 edges → homes, split 12a/b/c
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home 4 legacy-boundary edges to util/ + fileStore (Slice 12a)
Structural (Tidy First) git mv + import repoints, no behaviour change:
- errorDialog.service + errorDialog.model -> util/errorDialog/ (component/html/po
stay in features/shared; it still register()s the root service)
- metricQueryParameter enum -> util/queryParameter/
- referenceFile.selector (+spec) -> fileStore/store/ (pure files-domain selector
over filesSelector; files.repo now reads it locally via ../store/)
Clears new-must-not-import-legacy edges 1,2,3,5. tsc + biome + dep-cruiser clean
(0 errors), affected specs green (63/63), snapshots untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move LoadInitialFileStore to a neutral load/ layer (Slice 12b)
The initial-file load orchestrator lived in state/ but writes into every home
(mapState/sharedView/preferences facades) + the metrics-lens load facade + fileStore
actions — it can't sink into fileStore (would trip filestore-has-no-upward-deps) and
must leave state/ to clear the legacy boundary. Give it its own top layer:
- git mv state/loadInitialFile/loadInitialFile.store.ts -> load/loadInitialFile.store.ts
(relative imports rebased one level up; state/ selector+edges.actions reads made explicit)
- repoint the loader (fileStore/loaders) at the new load/ path
- add dep-cruiser rule load-orchestrator-not-imported-by-lower-layers (error): homes,
lenses, renderers, shell must not import load/; fileStore's loader is the sole driver
Clears new-must-not-import-legacy edge 4 (fileStore -> state). The LoadInitialFileService
loader stays in fileStore (widely consumed via fileStore.facade); its move is a follow-up.
Behaviour-preserving: tsc + biome + dep-cruiser clean (0 errors, load rule 0 violations),
loadInitialFile.service.spec green (real store over a mock ngrx store), snapshots untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): invert metrics-lens view-aware outputs off state/ (Slice 12c)
The metrics lens store re-exposed two state read-model selectors (nodeMetricDataSelector,
metricRangeSelector) as pass-through members — the last lenses/ -> state/ edge
(new-must-not-import-legacy #6). These are VIEW-AWARE (they read blacklist + colorMetric),
so per the lens-no-view-state goal they belong outside the lens.
Sever them: drop nodeMetricData$/colorMetricRange$/getNodeMetricData/getColorMetricRange from
the lens store, delete AttributesRepo, and trim MetricsLensFacade to its cc.json-derived
descriptor/type surface. The five real consumers now read the SAME selectors through their own
feature stores (value- and memoization-identical):
- codeMap.render.service: nodeMetricDataSelector(codeMapRenderStore.getState())
- 3dPrint: Print3DStateAccessStore.getNodeMetricData(); export3DMapDialog uses it
- metricsBar: ColorRangeStore/Service.selectedColorMetricData$ (colorSettingsHeader, colorBandRow)
- legend: LegendMapStateStore.selectedColorMetricData$ (legend.service; keeps facade for descriptors$)
Clears new-must-not-import-legacy edge 6 -> 0 violations. Behaviour-preserving: tsc + biome +
dep-cruiser clean (0 errors), full unit suite green (385 suites / 2308 tests), 45 snapshots
byte-identical. Adversarially reviewed (4 lenses + verify): no parity break, no missed consumer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): flip new-must-not-import-legacy warn→error (Slice 12c)
All 6 residual lenses/|fileStore/ → features/|state/ edges are re-homed (Slice 12a/b/c),
so the migration boundary can be enforced. Flip the rule to error and drop the now-stale
"kept at warn … flips once state/ becomes interaction/appearance" framing (that state/
dissolution is a separate, later concern — the rule flips on the residue being cleared,
which it now is). Also refresh the Slice-1 header comment.
dep-cruiser: 0 errors, 0 new-must-not-import-legacy violations. The last transitional warn
bridge from the Slice-1 lens/fileStore boundary is now a hard wall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): mark Slice 12 (legacy-boundary-close) complete — 6 edges re-homed, rule flipped to error
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): scope Slice 13 (CQRS read/write facade split + *Store dedup); renumber; retire CF#5
- Add slice-13-cqrs-homes.md: split each home barrel into read/write facades (13a prefs → 13b
sharedView → 13c mapState), then dedup the ~36 duplicate read wrappers into injectable read-windows
(13d). Sized from a 2-agent map: 0 display-component dispatches (rule flips clean); write importers
38/15/11, read 37/20/7; 36 redundant read-copies concentrated in the mapState metric selectors.
- Reconcile numbering: legacy-boundary-close took slot 12, so CQRS→13 and renderer/structure→14.
- Retire CARRIED-FORWARD #5 (both warn→error flips done: metrics-lens-ngrx-guard in 11,
new-must-not-import-legacy in 12).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): CQRS-split preferences home read/write facade (Slice 13a)
Split the single preferences.facade barrel into a read facade (selectors +
root selector + default* fallbacks + store wiring) and a write facade (action
creators + the preferencesActions save-trigger union), so a display-only
consumer physically cannot dispatch. Repoint all 31 importers per-symbol:
readers → .read.facade, writers → .write.facade, dual-role feature stores → both.
Add the 3 CQRS dep-cruiser rules (scoped to preferences, at error, 0 violations):
- state-home-write-facade-is-sole-dispatch-surface
- state-home-read-facade-has-no-dispatch
- display-components-cannot-dispatch
Structural / behaviour-preserving: 385 suites green, 45/45 snapshots zero-diff
(no -u), tsc + biome + dep-cruiser (0 errors) clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): CQRS-split sharedView home read/write facade (Slice 13b)
Split the single sharedView.facade barrel into a read facade (selectors +
root selector + findIndexOfMarkedPackageOrParent helper + store wiring) and a
write facade (focus/search/blacklist/markedPackages action creators). Repoint
all 52 importers per-symbol, including the two watch-items: the legacy
state/store/fileSettings/fileSettings.actions re-export (writer → .write.facade)
and the 3dPrint blacklistSelector re-export (reader → .read.facade). The one
service writer (blackListExtension.service) also routes through the write facade.
Extend the 3 CQRS dep-cruiser rules to cover sharedView (still 0 violations).
Structural / behaviour-preserving: 385 suites green, 45/45 snapshots zero-diff
(no -u), tsc + biome + dep-cruiser (0 errors) clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): CQRS-split mapState home read/write facade (Slice 13c)
Split the single mapState.facade barrel (the largest home) into a read facade
(67 selector/reducer/wiring re-exports incl. the default* fallbacks like
defaultMapColors) and a write facade (34 action-creator re-exports). Repoint all
mapState consumers by symbol: the ~134 facade importers (readers → .read.facade,
writers → .write.facade) plus every external raw mapState/store/**/*.actions
import → .write.facade (required to seal the dispatch surface). Raw store
*selector* imports are intentionally left in place to avoid folding the
state/-dependent colorRange helper into the read-facade barrel (cycle risk);
routing those through the read-window is 13d/deferred-rule work.
Flip the 3 CQRS dep-cruiser rules to cover mapState — all three homes now enforce
read/write separation at error (0 violations across the tree).
Structural / behaviour-preserving: 385 suites green, 45/45 snapshots zero-diff
(no -u), tsc + biome + dep-cruiser (0 errors) clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): add MapStateReadWindow, collapse pure-read metric wrappers (Slice 13d)
Introduce the mapState home's injectable read-window (mapState/store/mapState.readWindow.ts,
re-exported via mapState.read.facade) wrapping the frequently-duplicated metric/color selectors
(area/height/color/edge/colorRange/colorMode/mapColors) once. Collapse the three pure-read
duplicate wrapper stores into it — delete ExplorerAreaMetricStore, InspectorMapColorsStore and
DistributionMetricStore, repointing their 5 consumers (3 explorer components + 2 services) to
inject MapStateReadWindow.
Value-identical (same selectors, same emissions): 383 suites green (−2 deleted wrapper specs),
45/45 snapshots zero-diff (no -u), tsc + biome + dep-cruiser (0 errors) clean.
First cluster of the read-wrapper dedup; read+write metric stores' read halves and the
cross-cutting clusters follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): delegate metricsBar metric stores' reads to MapStateReadWindow (Slice 13d)
Collapse the read halves of the six metricsBar read+write metric stores (areaMetric, heightMetric,
colorMetric, edgeMetric, colorMode, and colorRange's colorRange$) onto the shared MapStateReadWindow:
each now injects the read-window and sources its metric observable from it instead of re-wrapping the
same mapState selector in its own store.select(). Write halves and all other reads unchanged; consumers
untouched (the stores keep their public x$/set API).
Value-identical (same selectors, same emissions): 383 suites green, 45/45 snapshots zero-diff (no -u),
tsc + biome + dep-cruiser (0 errors) clean. Completes the mapState metric cluster of the read-wrapper
dedup; cross-cutting clusters (isDeltaState/selectedNode/isLoadingFile/hoveredNodeId) and the curated
multi-read windows remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 13 status — 13a/b/c done + 13d metric cluster; CF #9 for the rest
Mark the CQRS read/write facade split complete on all three homes (rules at error
tree-wide) and 13d's mapState metric cluster done; carry the cross-cutting
read-wrapper dedup forward as CARRIED-FORWARD item #9 (needs a home decision for
cross-home read-windows + manual smoke for hover/selection). Renumber the roadmap
spine so CQRS is Slice 13 and the renderer/structure slice is Slice 14.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): flip lens-owns-ccjson-source→error + add feature-reaches-state-home-only-via-facade (sharedView/preferences)
Two grep-verified 0-violation dep-cruiser hardenings, no source change:
- Flip lens-owns-ccjson-source warn→error — the last warn-level Viz-2.0 boundary
rule; decoupled from the edges move (the two node cc.json sources are already
lens-owned; the edge side extends the rule via the future dependency-lens store).
- Add feature-reaches-state-home-only-via-facade at error, scoped to sharedView +
preferences: outside code must reach these homes only via their read/write
facades, never store/ internals. No exemption needed — state.manager/appliers
already route through the facades post-Slice-13. mapState excluded until its ~12
raw store/*.selector imports fold onto the read facade (CF #9 read-window dedup).
lint:architecture clean (0 errors).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record the two post-Slice-13 rule flips in roadmap + CF #9
lens-owns-ccjson-source now error; feature-reaches-state-home-only-via-facade
added at error for sharedView + preferences (mapState excluded pending the CF #9
read-window dedup of its ~12 raw store/*.selector imports).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): author lens-no-view-state at error (Slice 14a)
A lens is data/projection — it never reads mutable view state. Add
lens-no-view-state forbidding lenses/** from importing a state home
(mapState/sharedView/preferences). Grep-verified 0 violations across all 15 lens
source files (Slice 7 lifted the metrics lens's blacklist/dynamicSettings reads,
Slice 9b the dependency lens's), so it lands straight at error, self-verifying via
lint:architecture. Freezes the invariant ahead of Slice 14's structure lens +
renderer-agnostic id + valueOf(id) work.
Also add the Slice-14 plan doc (decisions: PATH-in-app id; attributeTypes-home
POSTPONED → blocks 14c; risk-graded 14a-e ladder).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): name the RendererEngine load seam (Slice 14b)
Author the RendererEngine contract (rendererEngine.contract.ts) — the named
renderer-engine seam (DoD #8). Wire the one member unambiguous against today's
single codeMap renderer: CodeMapRenderService implements RendererEngine with a
`load(model)` that composes + lays out the render model (render + scaleMap); the
render effect calls `codeMapRenderService.load(...)` then requests a frame. The
remaining frozen-name members (highlight, applySettings, onSelect/onHover) have
signatures the design defers to renderer #2's dumb-engine wrapper, so they are
documented, not invented (an engine is not a store → must not inject @ngrx; the
selection/hover outputs stay in the store/page layer).
Structural / behaviour-preserving: 383 suites green (draw-trio coverage split into
a render.service `load` test + the effect's load+frame drive), 45/45 snapshots
zero-diff (no -u), tsc + biome + dep-cruiser (0 errors, no new warnings) clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 14a/14b landed + 14c blocked in roadmap/plan
lens-no-view-state@error (14a) and the RendererEngine load seam (14b) done; mark
the roadmap spine + rule table + Slice-14 section; note 14c blocked on the
postponed attributeTypes-home decision and 14d/e needing user smoke.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): ratify attributeTypes stay lens-owned; retire Slice-14 14c relocation
Decision (user): attributeTypes are lens data (ADR 12 — lenses.metrics = types;
lenses.dependency = edge types), never fileStore. node attributeTypes already home
in the metrics lens (Slice 9a); edge attributeTypes re-home to the dependency lens
bundled with CF #2. This vindicates lens-owns-ccjson-source and retires the
"relocate attributeTypes to fileStore" idea — the CF#1 cycle breaks by relayering
accumulatedData/aggregation above the lenses (folded into 14d), not by relocation.
Fix the stale attributes.selectors.ts comment that claimed the attribute state
"moves to the FileStore" (it also wrongly claimed it still reads fileSettings —
Slice 9a moved it to state.metricsLensSource). Update slice-14 plan + CARRIED-FORWARD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): stand up state.dependencyLensSource root (Slice 14, scaffolding)
Create the dependency-lens cc.json SOURCE store — the twin of the metrics lens's
metricsLensSource, one step later. New `lenses/dependency/store/` holds the edge
attribute-type map behind `dependencyLensSource` (combineReducers) + `defaultDependencyLensSource`
+ root selector + `setEdgeAttributeTypes`; exposed via `dependencyLens.load.facade` (write) and
`dependencyLens.facade` (read: `edgeAttributeTypesSelector`). Register the root in state.manager
(appReducers + defaultState + the objectWithDynamicKeysInStore wholesale-replace set), add the
`DependencyLensSource` model type + `CcState.dependencyLensSource`, extend the per-file
`CCFile.settings.fileSettings` intersection, and seed STATE/DEFAULT_STATE.
Purely structural: the root is registered but still empty and unread — the effect keeps
co-emitting the full attributeTypes to metricsLensSource and every reader still reads it there,
so behaviour and all snapshots are unchanged. The data-source swap (route edge types here, repoint
readers, IndexedDB v13) lands in the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home edge attributeTypes to the dependency lens (Slice 14)
Swap the cc.json edge attribute-type source from the metrics lens to the new
dependencyLensSource, value-identically. The updateFileSettings effect now SPLITS the
merged per-file attributeTypes at the load boundary — node types to metricsLensSource,
edge types to dependencyLensSource — co-emitted in one setState. The load applier gains
applyDependencyLensSource/mapDependencyLensSourceToAction (dispatching setEdgeAttributeTypes),
wired into both loadInitialFile.service rehydrate paths.
Every edge-type reader is repointed to recombine the full { nodes, edges } map from the two
lens facades: accumulatedData composes it inline for NodeDecorator (reading BOTH lens facades
is intended — the composing-layer relayering that untangles CF #1 is a later slice, 14d); the
metricsBar gets one shared attributeTypesSelector (metrics node types + dependency edge types)
that attributeTypes.store + createAttributeTypeSelector both consume. The metrics facade now
exposes nodeAttributeTypesSelector instead of the raw full-map selector, so the edge side is
only reachable via the dependency lens.
IndexedDB v12→v13 (migrateCcStateRecordToV13) splits a persisted metricsLensSource.attributeTypes
into the two roots; chained after v12, with the migration + chain tests extended. Two loose
fileExtensionBar spec state mocks gained the dependencyLensSource root (they exercise
accumulatedData).
Behaviour-preserving: 384 suites green, 45/45 snapshots zero-diff (no -u), tsc + biome clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): fence the dependency-lens cc.json source via lens-owns-ccjson-source (Slice 14)
Extend the error-level lens-owns-ccjson-source rule's `to` paths to also cover the new
dependency-lens source — lenses/dependency/store/attributeTypes/ + .../dependencyLensSource —
so outside code reaches the edge attribute types only through the dependency-lens read/load
facades, never the store internals (the twin of the metrics-lens fencing). 0 errors / 94
warnings, unchanged (0 violations — all access is already via the facades).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 14 edge-attributeTypes re-home (CF #2d done)
Mark item #2d (re-home the edge side of attributeTypes into the dependency lens) DONE in
CARRIED-FORWARD + the slice-14 plan: new state.dependencyLensSource root (twin of
metricsLensSource), effect split, applier, IndexedDB v13, lens-owns-ccjson-source extended.
Only #2a (edges array), #2b (injectable store), #2c (metricDataSelector deletion) remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): track the initial-file loader → load/ move as CARRIED-FORWARD #10
The fileStore/loaders/ccJson → load/ import (the ingestion boundary kicking off the
load orchestrator) is the one deliberately-allowed fileStore→load/ edge. The planned
cleanup is to lift the loader kickoff UP into load/ so fileStore has zero upward deps
— NOT to move LoadInitialFileStore down into fileStore (it drives homes+lenses, which
filestore-has-no-upward-deps forbids). Promote the rule-comment note to a first-class
backlog item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move getDeltaFile to util/ (Slice 14d prep)
The structure lens (Slice 14d) builds the undecorated unified tree and needs
getDeltaFile, but it lived under state/selectors/ — a lenses/ -> state/ import
is a new-must-not-import-legacy error. Relocate it to the util/ shared kernel
(byte-identical; only import paths change) so the lens can consume it cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): extract the structure lens; relayer accumulatedData above it (Slice 14d)
Stand up lenses/structure/ — a read-only projection of the visible cc.json files
that owns the undecorated unified tree (aggregation / delta / single-file fold),
exposed via structure.facade's structureTreeSelector. The build was previously
inlined in accumulatedData (_getUndecoratedAccumulatedData); it moves here with
its spec, byte-identical.
accumulatedData becomes a pure composing selector ABOVE the lenses: it reads
structureTreeSelector DOWNWARD, clones it (the selector is memoized, so its
instance is shared and must not be mutated in place) and layers metrics +
blacklist + the {nodes,edges} attributeTypes aggregation on the clone. Because
the id-decoration/aggregation now lives above the lenses, no lens can ever need
to read it back — the structural precondition for a cycle-free per-node valueOf
(CF #1). The lens reads only the fileStore selection (no view state), so it sits
cleanly under lenses/ (lens-no-view-state).
Value-identical: 384 suites / 2312 passed, 45/45 snapshots zero-diff, tsc + biome
+ dep-cruiser clean (0 errors, 94 warnings unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): add cycle-free per-node valueOf(id, metric) (Slice 14d, CF #1)
Complete CF #1's facade-contract item: a per-node metric lookup. valueOfSelector
resolves a node by id against the decorated tree's idToNode map and reads the
metric off its aggregated attributes, exposed as a memoized (id, metric) => value
lookup.
It lives in the composing layer above the lenses (next to idToNode), NOT on the
metrics lens: the lookup needs the decoration-time id->node map, and a lens
reaching up to it would re-close the exact cycle CF #1 warns about
(lens -> idToNode -> accumulatedData -> lens). Placing it above the lenses is what
makes the lookup cycle-free. Slice 14e promotes it onto the metrics lens facade
once the renderer-agnostic PATH id keys the lens's own attributes.
Purely additive (new selector + spec); 45/45 snapshots unaffected, tsc + biome +
dep-cruiser clean (0 errors, 94 warnings unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 14d (structure lens + relayer + cycle-free valueOf)
Mark 14d done in slice-14 plan + CARRIED-FORWARD: lenses/structure/ owns the
undecorated tree, accumulatedData relayered above the lenses as a pure composer,
valueOf(id, metric) added in the composing layer (cycle-free, resolves CF #1's
cycle). The NodeDecorator id/metric split, lens-native valueOf and idToNode
ownership are folded into 14e — all three need the renderer-agnostic PATH id.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): clear SonarCloud code smells (PR #4505)
Address 56 open SonarCloud issues (all except the deprecations and TODO):
- S3863 (40): merge duplicate imports into one statement per module
- S7744 (9): drop the redundant `?? {}` fallback on object spreads in
indexedDBWriter (spreading undefined/null is already a no-op)
- S7763 (2): re-export via `export ... from` instead of `export const X = Y`
- S1301 (2): convert single-case switches to if/throw in loadInitialFile.store
- S1479 (1): guard the 4 no-op mapState keys before the switch (now 30 cases)
- S6582 (1): use optional chaining in fileValidator
- S3776 (1): replace the 11-if migration chain with a data-driven table + loop
All changes are behavior-preserving. tsc and the full unit suite
(2315 tests, 45 snapshots) stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): promote interaction ids mapState → sharedView (Slice 14e-1)
Move hoveredNodeId / selectedBuildingId / rightClickedNodeData out of the mapState
home into sharedView, keeping them number/object-typed — the renderer-agnostic
ordinal→PATH re-expression is the separate behavioral Slice 14e-2.
- git mv the 3 store folders mapState/store → sharedView/store; leaf selectors read
sharedViewSelector
- rewire the mapState/sharedView combined reducers + defaults; move the CQRS
read/write facade exports; repoint every importer to the sharedView facades
- add an ignoredSharedViewKeys guard in the load applier so the ids stay
no-op-on-load (mapState's ignoredMapStateKeys did this before the move)
- IndexedDB v13→v14 (migrateCcStateRecordToV14): move the 3 keys mapState →
sharedView and NULL them — persisted values are stale decoration ordinals
(14e-2 re-expresses the id as a PATH)
Structural / behavior-preserving: tsc + full unit suite (2317 tests, 45/45
snapshots zero-diff), dep-cruiser 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): re-express interaction ids as canonical PATHs (Slice 14e-2)
Retype hoveredNodeId / selectedBuildingId / rightClickedNodeData.nodeId from the
decoration-time ordinal number to the node's canonical PATH string — the
renderer-agnostic id (sha-16 only at the serialization boundary). Set-sites emit
node.path; the three resolving selectors (selectedNode / hoveredNode /
rightClickedCodeMapNode) switch to a new pathToNodeSelector (path→node), stable
across re-decoration / blacklist / reload where the ordinal is not.
- new pathToNodeSelector alongside the ordinal idToNodeSelector (kept for the
mesh-highlight paths)
- hoverNode(id: number) -> hoverNode(path: string), matching building.node.path
- arrow.service resolves the hovered path via getMapMesh().getBuildingByPath
(as it already does for edge highlighting), dropping IdToBuildingService
- the mesh instance index building.id / ordinal node.id stay NUMBER — the two
id-spaces are deliberately separate (IdToBuildingService stays ordinal-keyed
until the later NodeDecorator id/metric split)
The v14 blob already nulls the persisted ids, so no migration change is needed.
Verified: root tsc + AOT production build (strictTemplates) + full unit suite
(2317 tests, 45/45 snapshots zero-diff) + dep-cruiser 0 errors. The WebGL
hover/select highlight is not snapshot/e2e-testable in this sandbox (no
WebGL-capable browser); the pixel-level "correct building highlights" check
remains a manual smoke.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): scope the migration to the CodeMap renderer only
Ratify (2026-07-04) that the Visualization 2.0 migration targets the CodeMap
(map) renderer ONLY. Every other renderer and the multi-renderer machinery —
Graph/LSM, WordCloud, the Report page, the Terms lens, graphState, the physical
renderers/ folder move, the engine settings-inversion, and the
renderer-engine-stays-dumb / page-uses-engine-public-api flips — is explicitly
OUT of the migration and lands later as separate work. The renderer-engine seam
stays contract-only (14b froze the names; signatures land with renderer #2);
DoD #8 is satisfied by that contract alone.
Recorded in the roadmap (SCOPE callout + DoD #8), CARRIED-FORWARD #6, and the
slice-14 doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): plan Slice 15 — dissolve the legacy state/ folder
Post-migration capstone that fully deletes app/codeCharta/state/. Scoped by a
3-area file->destination mapping (store+root / selectors+composing / effects).
Re-homes 67 files into 3 new homes — renderModel/ (composing layer), store/
(root composition), url/ — plus fileStore/util/load/features/lenses. 6-step
ladder 15a-15f, structural-before-behavioral per CONVENTIONS. Absorbs CF #2a
(edges) and CF #9 (homeless cross-home selectors).
Also point the roadmap ordering spine + CARRIED-FORWARD at Slice 15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): fold in two decisions — edges→dependency lens, 14e-3 before Slice 15
- Edges home = the dependency lens (ADR-12: lenses.dependency owns edges +
edge types), via an injectable DependencyLensStore; the edge-visibility fold
stays in the composing/feature layer so lens-no-view-state holds.
- 14e-3 runs BEFORE Slice 15 (user: finish the migration; only the not-yet-built
renderers stay out). So idToNode/valueOf become lens-owned in 14e-3 and are
NOT moved into renderModel/ — Slice 15 moves only accumulatedData + the rest.
Updated slice-15, slice-14, and the roadmap spine accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): split NodeDecorator structure pass from metric pass (Slice 14e-3)
Extract the view-state-independent structure pass — ordinal id assignment +
mergeFolderChain — into NodeDecorator.decorateMapWithStructure, leaving
decorateMapWithMetricData as a metric-only attribute-init pass that runs the
structure pass first. The split is behavior-preserving: id assignment + merge
are deterministic, so the decorated tree keeps identical ids and shape; the
metric pass now iterates the merged tree (merged-away nodes are unreachable, so
skipping their attribute init is unobservable). Full suite green, 45/45
snapshots zero-diff.
This gives the structure lens a deterministic structure pass it can run on its
own undecorated tree to own id -> node resolution (next commit) without reaching
up to the composing layer or reading view state — the structural break of CF #1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): promote idToNode onto the structure lens (Slice 14e-3)
Move id -> node resolution out of the composing layer (state/selectors/
accumulatedData/idToNode) onto the structure lens: a new idToNodeSelector runs
the deterministic structure pass (id + mergeFolderChain) on the lens's own
undecorated tree and indexes by ordinal id, exposed via structure.facade. The
highlight consumers (hover + constant-highlight, which read only .id + child
structure) repoint to the facade.
Because the structure pass is deterministic and view-state-independent, the
lens-owned map resolves the SAME id -> descendant structure as the old
fully-decorated tree — proven by a parity test — while needing neither the
blacklist/metric decoration nor accumulatedData. This structurally breaks the
CF #1 cycle: a lens can no longer be reached back from idToNode, and
new-must-not-import-legacy (error) forbids the reverse edge.
The two service specs mock the store selector module but import idToNodeSelector
through the facade, keeping lens-external-access-only-via-public-surface green.
valueOf temporarily reads the facade's idToNode; it moves onto the metrics lens
next. Full suite green, 45/45 snapshots zero-diff, dep-cruiser 0 errors.
Note: hover/constant-highlight are not snapshot-covered — needs the user's e2e +
manual side-by-side vs main (highlight a node + its descendants on hover/select).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): promote valueOf onto the metrics lens facade (Slice 14e-3)
Move valueOf(id, metric) out of the composing layer (state/selectors/
accumulatedData/valueOf) onto the metrics lens: it resolves a node via the
structure lens's idToNode (a legal lens -> lens facade import) and reads the
metric off it, exposed via metricsLens.facade.
This completes the CF #1 break: both idToNode (structure lens) and valueOf
(metrics lens) are now lens-owned, reaching only DOWNWARD into the structure
lens's own tree resolution — never up into accumulatedData. The old cycle
(lens -> idToNode -> accumulatedData -> metricsLens.facade -> lens) is now
structurally impossible (new-must-not-import-legacy @ error forbids lens ->
state/). valueOf was unconsumed, so the value-source change (raw per-node
attributes instead of the decorated tree) has no behavioral impact.
Full suite green, 45/45 snapshots zero-diff, dep-cruiser 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 14e-3 done (idToNode/valueOf lens-owned, CF#1 broken)
Mark Slice 14 complete: 14e-3 split the NodeDecorator structure pass, promoted
idToNode onto the structure lens and valueOf onto the metrics lens facade, and
permanently broke the CF#1 cycle. Retire CF#1 from the carried-forward backlog
and note that Slice 15's 14e-3 prerequisite is now satisfied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move getNodesByGitignorePath kernel to util/blacklist (Slice 15a)
Relocate the pure gitignore-path node filter out of state/selectors/searchedNodes/
into util/blacklist/ (next to blacklistMatcher, whose returnIgnore/transformPath it
uses). Byte-identical logic; only import paths change. Structural foundations step
toward dissolving state/ (Slice 15).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): re-home isDeltaState + areMultipleMapsVisible to fileStore/store (Slice 15a)
Both are pure file-state projections (over filesSelector / visibleFileStatesSelector),
so their rightful home is the fileStore source layer, not state/selectors/. git-mv the
two selector files (+ areMultipleMapsVisible spec) into fileStore/store/ and repoint the
~22 importers. Closes CF #9's two homeless cross-home read selectors. Byte-identical
projection logic; only paths change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 15 decisions + 15a done
Settle the 4 open architecture decisions (renderModel/ name + barrel facade;
updateQueryParameters folds into load/; renderModel hosts selectors only;
single-feature derived selectors default to renderModel/). Mark 15a complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): relocate composing selectors state/selectors -> renderModel/ (Slice 15b)
Move the entire composing-layer selector tree (accumulatedData cluster, derived
metric selectors, node-resolving + render-gate selectors) out of state/selectors/
into a new top-level renderModel/ home, preserving substructure. Pure git-mv +
import repoint: internal cross-references preserved; out-of-tree imports lose one
../; the ~41 source + ~27 spec external importers repointed. Per-file imports for
now; the barrel facade + boundary rule follow in the next commit. Value-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): add renderModel barrel facade + boundary rule (Slice 15b)
Introduce renderModel.facade.ts as the single public surface of the composing layer
(export * over the 17 selector modules); repoint the ~41 source consumers onto it
while specs keep per-file paths (test wiring — jest.mock/spyOn need module
granularity, and export * re-exports are not spyable). Move the pure MetricMinMax
type from the composing layer into util/metric/metricRange (its documented superset),
so the mapState colorRange home + the util color/label helpers reference it downward
instead of reaching UP into renderModel. Add render-model-is-top-derived-layer at
error (lenses/fileStore/homes must not import renderModel; 0 violations).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 15b done (state/selectors -> renderModel)
* refactor(visualization): move codeMap effects into the feature (Slice 15c)
Move the render cluster (renderCodeMap + actionsRequiringRerender, autoFitCodeMap,
setLoadingIndicator) from state/effects/ into features/codeMap/effects/, behind a
codeMap.effects.ts bundle that the app composition root registers directly. The
effects import codeMap services from source (not the feature facade) so the facade
never pulls the effects' cross-feature deps into a cycle. dep-cruiser: exempt
features/*/effects/ in feature-only-stores-can-import-ngrx-store (effects are ngrx
state-reactors); allow effects/<feature>.effects.ts as a public registration surface
in feature-no-external-access-to-internals; exempt .spec.ts test-wiring in
feature-cross-feature-only-via-public-api (consistent with every other boundary rule).
0 errors / 94 warnings unchanged, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move metricsBar effects into the feature (Slice 15c)
Move the 7 metric-reactive effects (resetChosenMetrics, resetColorRange,
updateMapColors, linkColorMetricToHeightMetric, updateEdgePreviews,
updateAmountOfEdgePreviews, resetSelectedEdgeMetric...) from state/effects/ into
features/metricsBar/effects/ behind a metricsBar.effects.ts bundle registered by the
composition root. Extract the shared setDefaultMetrics helper out of the effect class
into its own module and expose it via the metricsBar facade, so the globalSettings
map-reset store reaches it cross-feature through the public API (the effect imports no
other feature, so the facade re-export forms no cycle). 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move labelSettings/sidebarExplorer/fileExtensionBar effects into features (Slice 15c)
Move updateVisibleTopLabels -> labelSettings, blacklistSearchPattern ->
sidebarExplorer, blacklistExtension -> fileExtensionBar, each behind a
<feature>.effects.ts bundle registered by the composition root. Same-feature service/
store importers repointed; blacklistSearchPattern keeps its cross-effect DI on the
shared blacklist guard (still in state/effects until 15d). Extract the shared
getNumberOfTopLabels helper to util/ (used by both the effect and state/store's
getPartialDefaultState) rather than routing state/store through the labelSettings
facade, which would close a codeMap<->labelSettings module cycle. 0 errors / 94
warnings unchanged, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 15c done (effects -> features)
* refactor(visualization): move unfocusNodes + persistence effects into load/ (Slice 15d)
Move unfocusNodes (view-reset on file-set change), saveCcState (CcState persistence)
and updateQueryParameters (browser-URL sync — folded into load/ per the settled
decision, no separate url/ module) from state/effects/ into load/effects/ behind a
load.effects.ts bundle registered by the composition root. load/ is the top
persist/load layer and imports ngrx freely. Same folder depth, so only state/store
refs shifted. 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move blacklist guard to features/shared, extract shared stream to injectable (Slice 15d)
Move addBlacklistItemsIfNotResultsInEmptyMap out of state/effects/ into
features/shared/effects/, and break the effect-injects-effect coupling the migration
flagged: extract the shared doBlacklistItemsResultInEmptyMap$ stream into a
root-provided BlacklistExclusionGuard injectable. The guard effect and the
sidebarExplorer blacklistSearchPattern effect now inject that single shared guard
(sidebarExplorer via the new features/shared facade) instead of one effect reaching
into another. Move the pure resultsInEmptyMap helper to util/blacklist. Registered via
a features/shared effects bundle. 45/45 snapshots zero-diff, 0 errors / 94 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move updateFileSettings into load/, dissolving state/effects/ (Slice 15d)
Move updateFileSettings (+ its per-file mergers) from state/effects/ into load/effects/,
folded into the load.effects bundle. This is the load-boundary merge that splits the
merged per-file attributeTypes into the metrics-lens and dependency-lens sources, so it
belongs in the load/persist layer. Same folder depth -> only state/store refs shifted;
value-identical (45/45 snapshots zero-diff). app.config now registers every effect via
per-feature/load bundles. state/effects/ is fully dissolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 15d done (state/effects dissolved)
* refactor(visualization): move getMergedEdges kernel to util/edges (Slice 15e)
Relocate the pure edge-merge helper out of the updateFileSettings effect's utils into
util/edges/ so the dependency lens can compose it into a derived edges selector (15e).
Byte-identical; only import paths change. Structural prep for deriving edges instead of
storing it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): add derived edges selector to the dependency lens (Slice 15e)
Add lenses/dependency/store/edges.selector (edgesSelector) that derives the merged
edges of the visible files from fileStore via getMergedEdges — the value the old
state.fileSettings.edges slice was always re-set to. Expose it on dependencyLens.facade
(ADR-12: the dependency lens owns edges). A parity spec proves it reproduces exactly
getMergedEdges(getVisibleFiles(files), isPartialState(files)) (single/multi/hidden-file
cases). Additive — the old slice stays until the swap+delete in the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization)!: derive edges + delete the fileSettings root slice (Slice 15e)
Swap every reader onto the derived dependency-lens edgesSelector and delete the
state.fileSettings root — the last member, edges, was never owned/mutated (addEdge/
removeEdge dispatched nowhere), only ever re-derived as getMergedEdges(visibleFiles),
so it was a materialized view, not state. Changes: repoint edgeVisibility.selector,
treeMapHelper and codeMapArrow.store (a getValue().fileSettings.edges runtime landmine
tsc missed) onto edgesSelector; drop the edges branch from the updateFileSettings
effect + the fileSettings applier (applyFileSettings/mapFileSettingToAction + its two
loader call-sites); delete the edges + fileSettings store slices; remove fileSettings
from appReducers/defaultState/objectWithDynamicKeysInStore and from CcState (the
per-file FileSettings type stays); drop the edge actions from the save-trigger union;
IndexedDB v14->v15 (migrateCcStateRecordToV15 drops the fileSettings key). Value-parity
proven in the prior commit; 385 suites / 45-45 snapshots zero-diff, 0 errors.
BREAKING CHANGE: state.fileSettings removed from CcState; persisted blobs migrate at v15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 15e done (edges derived, fileSettings deleted)
* refactor(visualization): move root store to store/ + DELETE state/ (Slice 15f)
The migration capstone: state/ is gone. Move the ngrx root out of state/store/ into a
new top-level store/: the composition (appReducers + setStateMiddleware) → store/store.ts
(the SOLE store-wiring module, only app.config imports it — new rule
root-store-is-sole-composer at error); the reusable root-state contract (defaultState +
the _applyPartialState deep-merge kernel) → store/state.manager, the global setState
action → store/state.actions, getPartialDefaultState → store/, all freely importable.
Repoint every importer (path swaps; specs wire the composition from store/store).
new-must-not-import-legacy loses its now-gone state/ target but keeps fencing
lenses/fileStore → features/. app/codeCharta/state/ deleted. 385 suites / 45-45
snapshots zero-diff, 0 errors / 94 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): Slice 15 COMPLETE (state/ deleted)
* docs(visualization): close CF #2a + record Slice 15 done in carried-forward
* docs(visualization): add Slice 16 architecture-cleanup plan (post-15 debt sweep)
Sequenced plan to reach a zero-exemption, zero-cycle config: break all 94 no-circular
warnings (73 3DPreview SCC + 19 codeMap/threeViewer cross-feature + 2 orphan cycles),
fence mapState, dedup isDeltaState wrappers, loader kickoff -> load/, comment/naming
sweep, then flip no-circular warn->error last. Code-grounded (8-cluster design + critic).
* docs(visualization): point carried-forward backlog at the Slice 16 cleanup plan
* docs(visualization): add Slice 16i (make util/ a leaf kernel) to cleanup plan
util/ silently accreted upward deps (render/layout engine treeMapLayout+streetLayout,
indexedDBWriter persistence, a mapState constant) because it was the only layer with
NO outgoing-dep fitness function. 16i relocates those concerns to their owning layer
(render layer / store/ / model/) and adds util-is-a-leaf-kernel@error.
* tech(visualization): add util-is-a-leaf-kernel fitness function (warn)
util/ was the only layer with no outgoing-dep rule, so it silently accreted upward
edges (the render/layout engine + the indexedDBWriter persistence writer + a mapState
constant — 4 files, 9 edges). Add util-is-a-leaf-kernel as a positive allow-list:
util/ may import only util/ + model/ + the *.model.ts type contracts + node_modules;
any other app/codeCharta/ path (incl. any future layer) is fenced. Staged at warn;
Slice 16i relocates the 4 files to their owning layer, then flips it to error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): break the 3DPreview circular SCC (Slice 16a)
Extract GeometryOptions into a leaf module (geometryOptions.ts) so the
21 mesh/strategy files stop importing it back from preview3DPrintMesh,
dissolving the hub cycle. Break the two strategy<->mesh 2-cycles by
widening ColorChangeStrategy.execute + BaseplateColorChangeStrategy.execute
to three.Mesh instead of importing GeneralMesh/BaseplateMesh.
All broken edges are type-only (erased at compile) -> no behavior change.
no-circular warnings 94 -> 21; tsc + biome clean; full suite 385 suites /
2320 passed, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): break the generateXML Volume cycle (Slice 16b)
Move the Volume interface into a leaf module (generateXML/volume.ts) so
build3mfModelConfig stops importing it back from serialize3mf.service,
which runtime-imports getXMLmodelConfig from build3mfModelConfig. Type-only
move -> no behavior change. no-circular 21 -> 20.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): break the horizontal/verticalStreet cycle (Slice 16b)
horizontalStreet and verticalStreet each ran `child instanceof <sibling>`
and wrote the sibling's orientation enum at runtime -- a genuine 2-cycle.
Add an abstract reverseOrientation() to the Street base (Horizontal->LEFT,
Vertical->DOWN) and replace the two `if (child instanceof Sibling)
child.orientation = ...` branches with `child.reverseOrientation()`; widen
the overhang `instanceof <sibling>` guards to `instanceof Street`. Identical
semantics because createBoxes strictly alternates orientation, so the only
Street subtype in a street's rows is its perpendicular sibling. Behavioral,
gated on streetLayout specs + treeMapLayout snapshots (13/13 zero-diff).
no-circular 20 -> 19.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): fence mapState + fold its read-window (Slice 16d)
The last 5 non-spec files reaching raw mapState/store/*.selector now go
through the public read surface: add margin$ to MapStateReadWindow and
refold MarginStore onto it; retarget selectorsTriggeringAutoFit +
metricColorRangeValues.selector to mapState.read.facade; git mv the pure
calculateInitialColorRange helper to util/color/ (it is not ngrx-bound).
With 0 external raw store importers left, mapState joins
feature-reaches-state-home-only-via-facade (error) — all three state
homes are now fenced. Structural / value-identical: tsc + biome clean,
full suite 385 suites / 2320 passed, 45/45 snapshots zero-diff, cruise
0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): dedup the isDeltaState read wrappers (Slice 16e)
Three features (legend, sidebarInspector, metricsBar) each minted a
byte-identical isDeltaState.store wrapper over isDeltaStateSelector.
Collapse them into one features/shared/stores/IsDeltaStateStore (mirrors
the IsLoadingFileStore precedent), re-exported via features/shared/facade
so the three feature services inject it cross-feature through the public
surface. Delete the 3 orphan wrappers + 2 specs.
Structural / value-identical: tsc + biome clean, full suite 383 suites /
2318 passed, 45/45 snapshots zero-diff, cruise 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): comment/naming/docs sweep (Slice 16g)
Rename the dep-cruiser rule new-must-not-import-legacy ->
source-layers-must-not-import-features (state/ is gone; it now only
fences lenses/fileStore -> features/), updating its comment + the 2
in-code references. Fix stale rule/source comments that still cite the
deleted state/ folder (state-home-is-leaf; metricsLens.store/facade,
dependencyLens.facade, metricData/nodeMetricData selectors now point at
renderModel/{node,edge}MetricData; drop the 'legacy metricDataSelector'
wording). Exempt ambient .d.ts from no-orphans (clears the index.d.ts
info). Annotate Ideas/dependency-cruiser.2.0.refined.cjs as a superseded
pre-landing proposal.
Docs/config-only: tsc + biome clean, full suite 383 suites / 2318 passed,
45/45 snapshots zero-diff, cruise 0 errors 0 info.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move the initial-file loader into load/ (Slice 16f)
The initial-file loader (LoadInitialFileService) lived in fileStore and
injected LoadInitialFileStore from load/ — the last tolerated fileStore ->
load/ edge. git mv the service + spec into load/ (siblings of the
orchestrator store) and:
- extract the wire-DTO sample assets (sampleFile1/2, cast as ExportCCFile)
into fileStore/loaders/ccJson/sampleFiles.ts so they stay fenced by
wire-dto-only-in-filestore-boundary;
- widen fileStore.facade (add buildHtmlMessage, getContentChecksum,
NO_FILES_LOADED_ERROR_MESSAGE, the NameDataPair type, source sampleFiles
from the new module) so the loader reaches fileStore ONLY through the
facade, never api.model/deep paths;
- add a load/load.facade.ts barrel; repoint codeCharta.component +
confirmResetMapDialog to it.
Rule: add fileStore/ (spec-exempt) to load-orchestrator-not-imported-by-
lower-layers — fileStore now has ZERO upward deps. Structural: tsc + biome
clean, full suite 383 suites / 2318 passed, 45/45 snapshots zero-diff,
cruise 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): extract the shared Three layer into threeViewer/ (Slice 16c step 1)
git mv the shared 3D infra OUT of features/codeMap into a new top-level
threeViewer/ layer (sibling of lenses/, renderModel/, mapState/, fileStore/):
the Three scene/renderer/controls/camera services, floorLabels, the rendering/
mesh+building primitives, codeMap.tooltip.service, idToBuilding.service, and
the threeScene/codeMapTooltip stores. Add threeViewer/threeViewer.facade.ts as
its public surface; repoint every sibling feature (viewCube, viewCubeToolbox,
labelSettings, sidebarInspector, sidebarExplorer, scenarios, nodeContextMenu,
3dPrint, fileExtensionBar) + specs off ../codeMap/facade onto it; codeMap keeps
only CodeMapComponent/CodeMapRenderService/CodeMapMouseEventService.
Pure git mv (byte-stable): no-circular 19 -> 5, cruise 0 errors, tsc + biome
clean, full suite 383 suites / 2318 passed, 45/45 snapshots zero-diff.
(threeRenderer's GlobalSettings read, the cursor coupling, colorCategoryCounts
and the grandfather-drop follow in the next steps.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): sever the threeViewer -> globalSettings edge (Slice 16c step 2)
threeRenderer.service read the background theme via GlobalSettingsFacade
(features/globalSettings) — the render layer's one upward dependency on a
feature. isWhiteBackground is a mapState field, so add a mapState
isWhiteBackgroundSelector (re-exported through mapState.read.facade) and have
threeRenderer read it via store.select directly. Value-identical (the
GlobalSettings facade already projected the same mapState field); the existing
spec dispatches setIsWhiteBackground and still passes. threeViewer now has zero
feature imports. Full suite 383 suites / 2318 passed, 45/45 snapshots zero-diff.
Manual smoke owed (WebGL clearColor is not snapshot-covered): toggle white
background vs main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): extract the cursor indicator into threeViewer/ (Slice 16c step 3)
Move CursorType + the static changeCursorIndicator (a pure document-cursor
write) out of codeMap.mouseEvent.service into threeViewer/cursorIndicator.ts.
viewCube.mouseEvents drove the cursor via CodeMapMouseEventService.changeCursor-
Indicator — its LAST codeMap import; it now reaches the standalone helper via
threeViewer.facade. This closes the viewCube<->codeMap and the residual
codeMap<->labelSettings cross-feature couplings: no-circular 5 -> 0.
Behavior-identical (same enum values, same document.body.style.cursor write).
tsc + biome clean, full suite 383 suites / 2318 passed, 45/45 snapshots
zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): invert colorCategoryCounts into a shared store (Slice 16c step 4)
colorCategoryCounts (positive/neutral/negative building counts) was a
BehaviorSubject published by CodeMapRenderService and read by labelSettingsPanel
+ colorBandsSection — a labelSettings/metricsBar -> codeMap render-data edge.
Relocate the subject into threeViewer/stores/ColorCategoryCountsStore: the
render service writes it via setColorCategoryCounts, the two features read
colorCategoryCounts$ from it (via threeViewer.facade). labelSettings is now
FULLY codeMap-free.
Value-identical (same counts, same initial {0,0,0}, requireSync preserved).
tsc + biome clean, full suite 383 suites / 2318 passed, 45/45 snapshots
zero-diff, no-circular still 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): drop the codeMap/viewCube cross-feature-cycle grandfather (Slice 16c step 5)
With the shared Three layer extracted (threeViewer/), the cursor indicator +
colorCategoryCounts relocated out of codeMap, and viewCube's last codeMap import
severed, every cross-feature cycle edge is broken. Remove the (codeMap|viewCube)
carve-out from feature-no-circular-dependencies-between-features so the WHOLE
feature graph is enforced with zero exemption. lint:architecture: 0 errors
(only the 9 staged util-is-a-leaf-kernel warnings remain, cleared in 16i).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move the IndexedDB persistence writer into store/ (Slice 16i-1)
indexedDBWriter composes every home's default + drives the v3->v15 CcState
migration chain — a root-store/persistence concern, not a util kernel helper
(it was one of the 4 files leaking upward edges out of util/). git mv
util/indexedDB/ -> store/indexedDB/ (same depth, so the writer's own imports
are unchanged) and repoint its 4 consumers. Its home read/load-facade imports
are now legitimate downward reads (store/ composes the homes).
Pure git mv: util-is-a-leaf-kernel 9 -> 4 warnings, tsc + biome clean, full
suite 383 suites / 2318 passed, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move defaultAmountOfTopLabels down to model/ (Slice 16i-3)
The pure getNumberOfTopLabels helper imported defaultAmountOfTopLabels from
mapState.read.facade — a util -> state-home leak. The value is a plain domain
default, so define it in model/state.model.ts; the amountOfTopLabels reducer
re-exports it (mapState's public surface is unchanged) and the helper now reads
it from model/ (a legal util -> model import).
Value-identical: util-is-a-leaf-kernel 4 -> 3 warnings, tsc + biome clean, full
suite 383 suites / 2318 passed, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move the layout engine into threeViewer/ (Slice 16i-2)
treeMapLayout + streetLayout are the render/layout ENGINE (consumed by
codeMap.render.service, codeMapMesh, floorLabels, threeSceneService), not a
util kernel — and treeMapHelper/streetViewHelper read renderModel + the
dependency lens, leaking upward edges out of util/. git mv util/algorithm/ ->
threeViewer/algorithm/ (a render layer): those renderModel/lens-facade reads
are now legal DOWNWARD reads. Repoint the ~8 consumers + all util-sibling
imports of the moved files.
Also fix the model -> streetLayout edge: move the Rectangle geometry class
(CodeMapNode.rect's type) down to model/rectangle.ts so model/ no longer
imports a layout algorithm.
Pure structural / snapshot-stable: cruise now 0 errors AND 0 warnings (the last
util-is-a-leaf-kernel violations cleared), tsc + biome clean, full suite 383
suites / 2318 passed, 45/45 snapshots zero-diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* tech(visualization): flip util-is-a-leaf-kernel warn->error (Slice 16i-4)
With the 4 leak files relocated to their owning layers (16i-1/2/3), util/ has
zero upward edges. Flip the fitness function whose absence let the rot in from
warn to error. lint:architecture: no dependency violations found. util/ is now
a genuine, enforced leaf kernel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* tech(visualization): flip no-circular warn->error — CAPSTONE (Slice 16h)
Precondition met: depcruise reports no cycles. Flip the app-wide no-circular
rule from warn to error. npm run lint:architecture is now 0 errors AND 0
warnings — the DoD of the Slice 16 cleanup: a zero-cycle, zero-exemption
dependency-cruiser config. The graph is acyclic and every fitness function is
enforced at error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(visualization): record Slice 16 COMPLETE (zero-cycle, zero-exemption config)
Mark slice-16-architecture-cleanup.md state: complete; update CARRIED-FORWARD
(close CF #10 via 16f + the cleanup halves of CF #9 via 16d/16e; note the
interaction-layer remainder stays open). lint:architecture is now 0 errors,
0 warnings with every fitness function at error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve P0 cc.json 2.0 review findings (C1-C3, #12, #17)
Analysis:
- C1: `ccsh merge` no longer silently drops legacy 1.x inputs. A legacy
file now fails with the `ccsh convert` hint and writes no output
(plain, --mimo, --large) instead of emitting a partial merge or
crashing; genuinely corrupt files are still skipped with a warning and
a single valid project still merges. Adds LegacyFileException so the
deserializer tells a real 1.x file apart from arbitrary JSON.
- C2: edge-only projects (e.g. CodeMaat) keep their edges through a 2.0
round-trip. The 2.0 writer materializes a file node for every edge
endpoint absent from the file tree, so `codemaatimport | edgefilter`
no longer yields an empty result.
- #12: `convert` warns when it drops a source file's blacklist /
markedPackages, which the cc.json 2.0 format does not carry.
Visualization:
- C3 (and #1): a 2.0 map is no longer replaced by the sample files on
reload, nor emitted as an unreadable download. The flat export now
stamps a 1.x apiVersion instead of copying "2.0" onto a 1.x body.
CI:
- #17: the analysis and visualization test workflows also run on changes
to dev_docs/cc-json-2.0.schema.json, so the schema drift guards fire
when the source of truth changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: align cc.json 2.0 documentation with shipped behaviour (#20, #21)
The cc.json 2.0 format flip left several docs describing the old
analysis-first staging where the visualization could not yet read 2.0.
- dev_docs/cc-json-2.0-format.md: 2.0 is now implemented on both the
analysis and visualization sides (the viz reads 2.0 natively and
normalizes legacy 1.x on load); drop the "viz can't read 2.0 yet"
limitation and the reference to the deleted ProjectToCcJson15Mapper
(there is no 1.5 writer; 1.x is read only on the `ccsh convert`
on-ramp).
- gh-pages: new Convert tool page (+ sidebar) documenting `ccsh convert`
and the 2.0 format; rewrite the versioning page's stale long-term
merge/delta promise around the 2.0 breaking change and the convert
on-ramp; note the 2.0-only shell and the convert path on the
CodeCharta Shell overview and Merge Filter pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(analysis): put node type in the cc.json 2.0 id and canonicalize tree shape (#2, #3)
NodeId now hashes sha-256(type.name + canonicalPath), so a File and a Folder that
legally share a name under one parent get distinct ids instead of colliding (a
write-time crash or a silent metrics overwrite). The preimage stays injective
because a canonical path always begins with "/" and a NodeType name never
contains "/". Edge endpoints carry no type on the wire, so the writer resolves
each endpoint's real type from the tree (File-preferred, order-independent), so
folder-targeting edges keep resolving instead of silently dropping.
PathFactory now drops "." / collapses ".." so a producer's tree shape matches the
ids (src/./App.kt no longer emits a phantom "." folder that collides with src),
and the writer's duplicate-id guard is unconditional (its own seen-ids set, above
the attributes check) so attribute-less and folder collisions fail loud instead
of corrupting silently.
Regenerates the 43 golden 2.0 fixtures whose parser/importer output is compared
(ids and checksum only) and the three cross-tool sha-256 anchors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(visualization): key normalized 1.x node ids by path and type (#13)
normalizeToCcJson2 synthesised a bare-path id, so a legal 1.x File and Folder
sharing a name under one parent collapsed onto one id: the second node's
attributes clobbered the first and both read one metrics bag. The id is now
`${path}|${type}`, and edge endpoints are qualified with the endpoint node's type
(path->type map, File default) so they still resolve. The shared 2.0 reader keys
the bag and idToPath by node.id already, so native-2.0 files are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(analysis): route metric descriptors to their owning lens (#7, #9)
#9: LensSet.fromLegacy dropped a descriptor shared by a node and an edge attributeType from the metrics lens; it now routes such a metric to both lenses. Node-only and edge-only routing is unchanged. This also fixes ccsh convert of edgefilter 1.5 output.
#7: ProjectMerger built through the flat legacy projection, which re-split descriptors by attributeType membership and relocated an edge descriptor with no matching edge type into the metrics lens. It now assembles from the merged typed lenses via the new ProjectBuilder.buildFromLenses, skipping the lossy round-trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(analysis): single-pass cc.json 2.0 write via digest stream (#22)
The 2.0 writer serialized the {files, lenses} body twice: once to compute meta.checksum (via toJsonTree, a full intermediate String, and a byte copy for MD5) and again to write the output.
ProjectToCcJsonV2Mapper.writeProject now serializes the body exactly once through a DigestOutputStream into a byte buffer, yielding both the checksum and the reusable bytes, then writes {meta, <body>} reusing them. The hot serializeProject(OutputStream) path uses it; the DTO/Writer/String paths are unchanged.
Output and checksum are byte-identical to before (pinned by new tests) and non-ASCII names round-trip as UTF-8; ~1.5x faster on the serialize+checksum step with lower peak memory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: cc.json 2.x downward-compatible additive-only versioning (#18)
Unpin the format version: readers accept any major-2 cc.json, not exactly "2.0". apiVersion const "2.0" becomes pattern ^2\.\d+$ in all three schema copies (dev_docs source, viz-vendored, and the ccsh-check bundled schema's 2.0 anyOf branch). additionalProperties:false is kept everywhere, so a new minor may only add optional fields and an older tool cleanly rejects a newer file that uses a field it does not know (downward compatible, not upward). A breaking change is a new major (3.0), which major-2 tools reject.
The analysis and viz readers already gate on the major being 2, so no reader code changed. Documented the policy in cc-json-2.0-format.md, both CHANGELOGs, and CC_JSON_SCHEMA_CHANGELOG.md. Added validator tests (accept a newer 2.x minor, reject 3.0 and unknown fields) and removed the now-dead TWO_POINT_ZERO enum member.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(analysis): surface convert hint and non-zero exit on unreadable legacy input (#10, #11)
Piped legacy input logs the `ccsh convert` hint (e.message) instead of dumping the whole file to stderr, and still returns null so an optional/empty pipe stays non-fatal. modify/inspect implement IExitCodeGenerator so a named input file that cannot be read exits non-zero; the stdin path still exits 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): guard overlay merge against silent leaf drops and mis-merges (#6)
Refuse content-hash matches shared by more than one incoming leaf, and refuse any resolved-target-path collision, so overlay merge no longer collapses distinct leaves onto one reference path via mapKeys (covers both the content-hash and path-suffix stages).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): fail loud on --large opaque-lens corruption and opaque-lens conflicts (#8)
--large aborts when an input carries a data-bearing opaque lens or non-empty metrics.clusters (opaque payloads may key by node id, which re-pathing into a subfolder would dangle). ProjectMerger's opaque-lens union now throws on a genuine same-name conflict instead of silently keeping the first; an empty reserved slot yields to a data-bearing one. Removes the now-dead mergeOpaqueLenses helper and stops the MIMO summary mislabeling any merge failure as a legacy file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(analysis): reconcile cc.json 2.0 breaking-change CHANGELOG (#19)
Remove the self-contradictory notes: ccsh emits 2.0 only, 1.x is read exclusively by `ccsh convert`, and the visualization reads 2.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): clear pending spinner on blacklist-guard rejection (#14)
A guard-rejected exclude now calls clearPendingHeavyDispatch() so the full-screen spinner disappears with the error dialog instead of soft-locking the app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): cover mapState and lens-source rehydrate appliers (#15)
Adds specs restoring all differing applyMapState keys (with isLoadingMap genuinely exercised as skipped) and both metrics/dependency lens-source appliers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(visualization): fence threeViewer and prune stale dep-cruiser rules (#16)
Adds three-viewer-engine-does-not-import-up, removes four rules that fenced the removed lenses/*/features shell and phantom renderers/shell/interaction dirs, and extends filestore-has-no-upward-deps to the sharedView + preferences homes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): NFC-normalize node names in merge and the 2.0 wire (#4, #5)
Node identity (NodeId) hashes in Unicode NFC, but merge name-matching compared
by code point and the 2.0 reader/writer kept the original spelling. An NFD
(macOS) and NFC (git) spelling of the same file therefore survived a merge as
two siblings that then collided on one id at the writer's duplicate-id guard,
and an NFD-named 2.0 tree read back no longer matched its NFC edge endpoints, so
EdgeFilter silently stopped aggregating and inserted ghost duplicate nodes.
Introduce NodeId.normalizeName as the single NFC source of truth and route both
merge name-matching (namesMatch + suffix normalizedEdges) and the 2.0 read/write
name boundaries through it. Names are now NFC on the wire, self-consistent with
their id and edge endpoints, giving a byte-idempotent round-trip and a stable
meta.checksum; the domain model still keeps the original spelling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): drop dead code from the cc.json 2.0 migration
Remove symbols with no remaining consumers and refresh stale comments:
- valueOfSelector + NodeMetricValueLookup: undecorated, zero consumers, and
documented as the per-node metric lookup it never actually served.
- setSortingOrderAscending: never dispatched (the UI toggles, the load applier
deliberately ignores sort order); drop the action, reducer case, spec and its
entry in the preferences save-trigger list.
- Lenses.opaqueLenses: the analysis side flattens opaque lenses to top-level
wire keys, so this field is never populated and was read nowhere.
- correct the v14 -> v15 IndexedDB migration-chain comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): persist edge/blacklist toggles and validate 2.0 loads
Close three runtime gaps in the cc.json 2.0 load path:
- Add setShowIncomingEdges/setShowOutgoingEdges and removeBlacklistItems to the
CcState save-trigger union; they were dispatched at runtime but never
persisted (the debounce spec's shared writeCcState mock is now cleared per
test so its exact-count assertion no longer depends on test order).
- Surface a dropped 2.0 dependency edge (unresolved endpoint id) as a load
warning in the load-warnings dialog instead of only console.warn.
- Enforce sibling name|type uniqueness for 2.0 files in checkErrors2_0, mirroring
the 1.x check, and remove the now-unused detectApiVersionMajor export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(visualization): resolve id-to-node from the decorated tree (#24)
Selection recompute cloned the structure tree and re-ran the structure
pass twice: once inside accumulatedData and again in the structure
lens's idToNode selector. Relocate id-to-node into renderModel and
index the already-decorated accumulatedData tree by id, dropping one
full-tree klona clone and one decorateMapWithStructure pass per
recompute.
The lens no longer needs to own id-to-node (valueOf was removed) and
both consumers sit above renderModel, so this introduces no cycle. The
resolved nodes are now identity-equal to the mesh's idToBuilding tree
rather than equal only by deterministic re-derivation.
Also add the missing accumulatedDataSelector spec (clone-before-decorate,
unique ids, cross-selector mesh parity) and a renderModel idToNode spec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): reshape into the views/renderer/stores layer stack (slice 17)
Collapse the flat pile of ~13 top-level siblings under app/codeCharta/ into the
layered stack ratified in migration-2-0-plans/TARGET-ARCHITECTURE.md. Pure structural
reorg — no dependency edge changes, no behavior change.
Moves (git mv; history preserved):
- threeViewer/, renderModel/ -> renderer/
- mapState/, sharedView/, preferences/,
fileStore/ -> stores/
- codeCharta.model.ts, api.model.ts -> model/
- codeCharta.component.* -> views/
- load/, store/ parked at top level; util/model/features/lenses/resources unchanged.
A deterministic codemod recomputed 1285 import specifiers across 612 files (relative,
baseUrl-absolute app/codeCharta/*, and the jest.mock/requireActual family that tsc does
NOT resolve-check). The live .dependency-cruiser.js was evolved to equal the pre-authored
.dependency-cruiser.target.js (rule set byte-identical): renamed three-viewer-engine ->
renderer-does-not-import-up (fences all of renderer/ from features/+views/+load/), new
nothing-imports-views, exact-path wire-DTO fence on model/codeCharta.api.model.ts, and a
simplified util-is-a-leaf-kernel allow-list. package.json schema:generate repointed.
Gates: tsc clean; lint:architecture 0 errors/0 warnings, acyclic, 1170 modules / 4618
deps (== baseline, graph-neutral); full suite 384/384, 45/45 snapshots zero-diff (no -u).
Verified by a 5-auditor adversarial pass that caught the source-layers fence being
silently un-fenced by a (lenses|fileStore) alternation the bulk rewrite skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): relocate getPartialDefaultState to features/shared (slice 19a)
Sever the sole store/ -> renderer/ edge: store/getPartialDefaultState.ts imported
codeMapNodesSelector from renderer/renderModel only to seed the default amountOfTopLabels
during a reset. It is a features/ reset helper, so git-mv it to features/shared/ (above
both renderer/ and store/, so its renderModel + state.manager reads stay legal downward
edges) and expose it via features/shared/facade.ts. Pure structural move — byte-identical
logic, tsc + lint:architecture 0/0, affected specs green.
First of two steps dropping the ngrx composition root store/ to just above the state
homes stores/ in the dependency LSM (slice 19b moves the two lens-source roots into stores/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): move the lens source state into stores/ (slice 19b)
Drop the ngrx composition root store/ below lenses/ in the dependency LSM by severing
its last upward edge (store -> lenses, 8 imports). The two lenses no longer OWN ngrx
state: git-mv their cc.json SOURCE roots out of lenses/ into new state homes
stores/metricsLensSource/ (node attributeTypes + attributeDescriptors)
stores/dependencyLensSource/ (edge attributeTypes)
each with a read facade (reducer + default + source selectors) and a write facade (the
load-time seed actions). The moves are same-depth, so the moved files are byte-identical.
The metrics + dependency lenses become PURE read-only projections: their derived node/edge
selectors now read the source DOWNWARD from the new store facades (a legal lenses -> stores
edge). store/store.ts, state.manager and indexedDBWriter register/seed the two reducers from
the new read facades; the load pipeline dispatches via the write facades; the two now-empty
lens load facades are retired.
State keys metricsLensSource/dependencyLensSource are preserved verbatim, so DB_VERSION stays
15 -- no IndexedDB migration, zero render-snapshot diff. Rule edits: lens-owns-ccjson-source ->
stores-own-ccjson-source (facade-only access to the two source homes); both homes added to
state-home-is-leaf, state-home-only-stores-import-ngrx, render-model-is-top-derived,
load-orchestrator-not-imported-by-lower-layers and source-layers-must-not-import-features;
lens-no-view-state left untouched (a lens reading its own source downward is legal).
store/ now imports only stores/ (26), model/ (5) and util/ (1) -- no lenses/, no renderer/ --
so it sits just above the state homes. tsc clean; lint:architecture 0/0 acyclic (1172 modules);
full suite 384/384, 45/45 snapshots zero-diff (no -u).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): nest store/ under stores/ (slice 19c)
Now that store/ depends only on stores/ + util/ + model/ (slices 19a/19b severed its
edges to renderer/ and lenses/), the ngrx composition root moves INTO the band it
composes: git mv store/ -> stores/store/. This removes the confusing top-level store/
vs stores/ sibling pair — store/ is gone from the top level, and stores/ now holds the
six leaf homes plus the store/ composer.
A codemod repointed 141 import specifiers across 101 importers (relative + baseUrl-absolute
+ jest.mock). The whole stores/ subtree now has ZERO upward edges — it depends only on
util/ + model/. Rule root-store-is-sole-composer retargeted to stores/store/store.ts.
Pure structural move: tsc clean; lint:architecture 0/0 acyclic (1172 modules); full suite
384/384, 45/45 snapshots zero-diff (no -u). (load/ remains the last parked composition-root
shard — TARGET-ARCHITECTURE open question.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): rename stores/store -> stores/rootStore (slice 19d)
Give the nested ngrx composition root a concept name instead of the stutter
stores/store/store.ts. rootStore/ reads as what it is: the root store that composes
the home stores (appReducers + setState meta-reducer + the defaultState/merge kernel +
the global setState action + IndexedDB persistence).
Pure rename: codemod repointed 108 specifiers across 95 importers (relative +
baseUrl-absolute + jest.mock); rule root-store-is-sole-composer retargeted to
stores/rootStore/store.ts. tsc clean; lint:architecture 0/0 acyclic (1172 modules);
full suite 384/384, 45/45 snapshots zero-diff (no -u).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): sweep 37 dead exports/files (slice 18a)
knip (git-verified) found dead exports hiding behind the state-home export *
barrels. Triaged each finding: delete confirmed-dead exports/files, un-export
module-local symbols, keep forward-looking 2.0 contract types.
- delete 9 dead selector files (7 duplicated label selectors + distributionMetric
+ currentFilesAreSampleFiles) and their read-facade export * lines; the reducer
slices stay wired.
- delete dead per-box geometry path (addBoxToVertexData + helpers), the dead
render-availability gate (areAllNecessaryRenderDataAvailableSelector), and a
handful of dead functions/types/interfaces.
- un-export module-local symbols (drop the export keyword, keep the symbol):
getHeightValue, resolveHeightValue, getOutgoingEdgePoint, gradientCalculator,
isPathBlacklisted, getMedian, pushSorted, _calculatePathToNode, and others.
- trim the files.store barrel + 3 dead facade re-export lines.
Removing exports is runtime-neutral: tsc clean, lint:architecture clean, full
suite green with 45/45 snapshots byte-identical. knip now reports 0 unused
exports and 0 unused files (the 2.0 ccjson2 types + API version enums are the
KEEP set, allow-listed by the knip config in slice 18b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(visualization): add knip dead-code gate (slice 18b)
Adds knip + a committed knip.json so the dead exports 18a removed can never
silently return. knip resolves 'export *' barrels, so it catches dead code the
state-home facades would otherwise hide.
- knip.json: entry = spec/e2e/po tests (Angular plugin supplies app/main.ts);
project = app/**/*.ts; ignore mocks/. Dependency-hygiene rules
(dependencies/devDependencies/unlisted) are off — this gate guards dead CODE,
not package.json (many are CSS/build/jest false positives, out of scope).
- lint:deadcode script (knip --no-config-hints) + a lint aggregate
(architecture + deadcode); wired into test_visualization.yml after the
architecture check. Baseline is 0.
- The forward-looking cc.json 2.0 contract types + the wire-contract API enums
are kept and allow-listed in place via @public JSDoc (one-line reason each),
not deleted — the 2.0 reader will import them directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): explicit named facade re-exports (slice 18c)
Converts the 7 state-facade 'export *' barrels (mapState/sharedView/preferences
read+write + renderModel) to explicit named re-exports via a ts-morph-style
codemod (TS checker resolves each module's real exports, classifying value vs
type).
Two payoffs: each facade is now a readable manifest of its public surface, and
the read/write boundary is enforced PER SYMBOL — a .selector/.reducer can no
longer leak an action into the read facade by a file-naming accident (the CQRS
dep-cruiser rules now belt-and-suspenders the explicit lists).
Naming the re-exports made knip see through the barrels per-binding: the WRITE
facades expanded clean (all action re-exports are consumed via the facade), but
the READ facades re-exported ~90 sub-reducers + default* fallbacks that every
consumer actually imports DIRECTLY from store/ — dead surface the 'export *'
hid. Pruned those (runtime-neutral: the sources stay, direct importers
unaffected), leaving each read facade its real consumed surface. Routing those
consumers back through the facade is a separate behavioral cleanup (out of scope).
tsc clean, lint:architecture clean, knip 0 unused exports, full suite green with
45/45 snapshots byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: mark slice 18 (facade hygiene) plan complete
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(visualization): gate load + renderModel behind their facades
Two zero-violation fitness functions that ratify the existing status quo:
external code already reaches these layers only through their facade, but no
rule enforced it (unlike lenses + the state homes, which are already fenced).
- render-model-external-access-only-via-facade: outside code reaches the
renderModel composing layer only via renderModel.facade.ts, never its internal
selector modules. Companion to render-model-is-top-derived (which fences
direction only); this fences the surface.
- load-external-access-only-via-facade: outside code enters the load/ layer only
via load.facade.ts, except the ngrx effects manifest (load.effects.ts) which
the composition root imports directly. Complements
load-orchestrator-not-imported-by-lower-layers (direction) with entry-point.
Both mirror lens-external-access-only-via-public-surface. depcruise: 0 new
violations (1163 modules, 4543 deps).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): gate threeViewer behind its facade
Completes the 3D engine's public surface and fences it: outside code now
reaches renderer/threeViewer only through threeViewer.facade.ts.
- Grow threeViewer.facade.ts with the 5 remaining public symbols consumers
needed but reached raw: ThreeViewerService, ThreeStatsService,
createTreemapNodes, StreetLayoutGenerator, treeMapSize.
- Repoint the 7 bypassing files (features/codeMap/* + labelSettings'
labelCreation.service) from threeViewer internals to the facade — 20 import
edges, all mechanical path changes (same symbols).
- Add threeviewer-external-access-only-via-facade at error (mirrors
lens-external-access-only-via-public-surface); 0 violations.
tsc clean, knip 0 unused exports, lint:architecture clean, full suite green
with 45/45 snapshots byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): gate fileStore behind its facade
fileStore was the last state home not fenced behind its facade. Grow
fileStore.facade.ts with the files-slice public surface and repoint every
external consumer, then enforce facade-only access.
- Add 22 re-exports to fileStore.facade.ts: the read selectors (files,
referenceFile, visibleFileStates, isDeltaState, areMultipleMapsVisible,
isLoadingFile), the action creators (files actions, setIsLoadingFile,
setCurrentFilesAreSampleFiles), and the slice reducers + defaults the
rootStore composition registers — wired exactly like the other homes.
- Repoint 35 files (46 import/re-export edges) from stores/fileStore/store/*
to the facade: renderModel selectors, both lens stores, the load pipeline,
feature stores/effects, the page view, and the rootStore composition itself.
All mechanical path changes (same symbols); store/ internals stay private.
- Add filestore-external-access-only-via-facade at error (mirrors
lens-external-access-only-via-public-surface); 0 violations.
tsc clean, knip 0 unused exports, lint:architecture clean, full suite green
with 45/45 snapshots byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): reject legacy 1.x files in `ccsh check` with a convert hint
`ccsh check` bypassed ProjectDeserializer and validated straight against the
bundled schema, whose anyOf still accepts the wrapped 1.x shape — so a legacy
file either passed silently (wrapped) or failed with an opaque schema stack
trace (unwrapped), inconsistent with every other command. Add a legacy
version-gate in EveritValidator that throws the shared LegacyFileException
convert hint, extracted into LegacyFileException.CONVERT_HINT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): treat a legacy file without apiVersion as 1.x, not current
ProjectJsonDeserializer is the legacy-1.x-only reader, but a file predating the
apiVersion field fell back to Project.API_VERSION (now "2.0"), so
isAPIVersionCompatible wrongly reported it as current — a latent landmine for
any guard that reads a legacy-parsed project's version. Default to
ApiVersion.ONE_FIVE.versionString instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): emit 2.0 children in a canonical, deterministic order
Node.children is a LinkedHashSet, so sibling emission order followed producer
insertion / merge history / filesystem walk order, making the same logical
project serialize to byte-different files with different meta.checksum across
producers and platforms. Sort siblings (NFC name, then File before Folder) at
the single 2.0 write boundary, which canonicalizes the files tree, the
DFS-populated metrics-lens key order, and the checksum together.
Regenerate the three order-sensitive golden fixtures (data identical apart from
order + checksum) and make the Tokei round-trip assertions look nodes up by
name instead of position.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): give an actionable error when a node name contains '/'
NodeId.canonicalize rejected a segment containing the '/' separator with an
internal-sounding "segments must be pre-split" precondition message, but the
trigger is user data (e.g. csvimport with a non-'/' --path-separator, or convert
of a 1.x name containing '/') and it surfaces as a raw stack trace. Reword the
require to name the offending value and point at the likely cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): warn when a metrics-lens entry has an unresolved node id
On 2.0 read, a metrics-lens entry whose id resolves to no file node was dropped
silently — asymmetric with the edge-endpoint path, which already warns. Emit the
same kind of warning so the loss is visible, reusing the id-to-endpoint map the
mapper already builds for edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(analysis): drop the dead mergeEdges parameter from DependencyLens.merge
The sole caller (ProjectMerger) always passed mergeEdges = true, so the else
branch was unreachable and the KDoc contradicted the caller. Union edges
unconditionally, matching MetricsLens.merge's signature, and update the two
mergeEdges-based tests. No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): preserve both lenses' descriptors when flattening to the legacy map
LensSet.allAttributeDescriptors() used Map + Map, so a metric carrying a
descriptor on both the metrics and dependency lens silently lost the metrics-lens
one (right operand wins). On every filter rebuild via ProjectBuilder.fromLenses,
that dropped the node-side metadata for a same-name node+edge metric. Merge such
collisions instead: keep the metrics descriptor and union in the edge lens's
analyzers. This is the lens->flat complement of the flat->lens routing fix (#9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(analysis): make the golden convert test catch a wrong output format
check_convert only ran `ccsh check`, whose schema still accepts the legacy 1.x
shape, so a convert regression that emitted an un-converted file stayed green;
the fixture was also degenerate (no edges/types/descriptors/blacklist), so data
loss went unnoticed too. Enrich legacy_1_5.cc.json with an edge, node+edge
attributeTypes, a descriptor, blacklist and markedPackages, and assert the
output is genuine 2.0 (meta/lenses envelope + edge fromId) and not the legacy
shape, plus that the #12 blacklist/markedPackages drop-warning fires. Add a
deterministic ConvertTool unit test pinning that edges/types/descriptors survive
into the 2.0 lenses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(analysis): fix the broken analyser README links
The branch-added UnifiedParser row linked to a non-existent parser/UnifiedParser/
README.md (#9). The whole table set shared the same pre-existing rot — parsers
under import/|parser/, importers/filters/exporters/tools under bare prefixes, the
SourceMonitor row even pointing at CSVImporter, and DEV_START_GUIDE off by a dir.
Repoint every link to its real analysers/... path; all 17 now resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: gitignore generated call-graph exports (*.cg.json)
Keeps the large analysis.cg.json (and any other *.cg.json) out of accidental
commits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: track the branch-review findings + 2026-07-07 analysis MINOR pass
Records the full multi-agent branch review and marks the 9 analysis MINOR
findings done (commit hashes inline), with #5 part (b) tracked as an open
follow-up and #8's gold-file omission noted as deliberate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(analysis): fail ccsh check on dangling 2.0 references
ccsh check ran pure JSON-schema validation, which cannot express that a
metrics-lens key or an edge fromId/toId must resolve to an existing
file-tree node id. So a schema-valid 2.0 file with a dangling reference
passed check (exit 0) while the reader silently dropped the orphaned
entry (CcJsonV2ToProjectMapper only warns) -- review finding #5 part (b).
Add a semantic pass in EveritValidator.validate() after schema.validate():
collect every files-tree node id, then throw ReferentialIntegrityException
listing every metrics-lens key and every edge endpoint that resolves to no
node id. The exception propagates to a non-zero exit exactly like a schema
ValidationException; check still bypasses ProjectDeserializer, so the
read-path warn-and-drop contract is unchanged.
Verified end-to-end through the built ccsh: dangling edge/metric -> exit 1
with a clear message; real convert output, a 164-edge codemaatimport, and
the full integrationTest (every analyser output re-checked) -> exit 0;
tampering a real edge fromId -> exit 1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* tech(visualization): close dep-cruiser boundary-rule gaps
Close the four dep-cruiser gaps flagged in the branch review, all latent-gap
closures (0 violations today, each new fence proven to fire via throwaway probes):
- mocks invisible: move mocks/ + *.mocks.ts from options.exclude to
options.doNotFollow so inbound prod->mock edges are recorded, and add a
no-prod-import-of-mocks fence (prod code must never import a test fixture).
- cross-home raw-store: add a back-referenced state-home-no-cross-home-raw-store
rule so a home reaches a sibling only through its facade; $1 exempts the home's
own store/.
- bootstrap outside fences: broaden the 7 facade-surface rules' from.path from
^app/codeCharta/ to ^app/ so app/app.config.ts + app/main.ts can no longer
bypass a facade; root-store-is-sole-composer exempts app.config.ts (the sole
legit composer).
- renderModel facade-only was already enforced (161eba44c); only inherited the
bootstrap-anchor weakness, now fixed.
lint:architecture: 0 violations (1165 modules, 4597 deps).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): close named test gaps from the branch review
- edges.selector.spec: replace the 3 tautological cases (which asserted the
selector against the very production expression it runs) with hand-computed
expected values, and add the missing delta-mode case (Reference+Comparison =>
raw-keyed union, comparison overwrites reference per-attribute, no path prefix).
- indexedDBWriter.spec: add a dedicated migrateCcStateRecordToV15 describe
(drops the whole fileSettings root) and fix the full-chain upgrade title that
still said v14 => now "+ v14 + v15".
- 3dPrint.selectors.spec: rename 5 "from dynamicSettings" titles to
"from mapState" (dynamicSettings was deleted; the selectors read mapState).
- loadFile.service.spec: add a service-level cc.json 2.0 load test (meta/files/
lenses envelope) asserting id-keyed metric attributes land on nodes with
list-valued authors stripped, edges resolved from id to path, and
attributeTypes split across the metrics/dependency lenses.
Full suite green: 384 suites, 2339 passed, 45 snapshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): make bundled sample files real cc.json 2.0
The bundled first-run samples were still 1.2 format, so the default view never
exercised the 2.0 reader. Replace sample1..4.cc.json (both the webpack-bundled
app/…/assets/ and the served public/…/assets/ twins) with real `ccsh convert`
2.0 output — opaque hashed ids, real MD5 checksums, split metrics/dependency
lenses — so first-run and ?file= loads go through the 2.0 path.
- sampleFiles.ts: retype the bundled imports as CcJson2; display fileName stays
"*.cc.json" so the mapSelector strip regex + explorer-tree/e2e names are
unchanged.
- Drop the hand-written assets/sample1.cc2.json (fake checksum/path ids); its
role is now served by the real ccsh sample1.
- Preserve the original 1.x sample1 as resources/sample1_legacy_1_2.cc.json and
repoint the ccJson2ToCCFile render-parity spec at it — the spec now parses a
real ccsh-produced fixture, and its opaque ids prove the reader's id→path join
doesn't rely on id==path. Map compare is order-insensitive (ccsh sorts nodes
folders-first/alphabetical); edge order + values are identical.
- url.e2e: the "load a 2.0 file" test now uses the (now-2.0) sample1.cc.json.
Full suite green (384 suites, 2339 passed, 45 snapshots), tsc 0 errors,
lint:architecture 0 violations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(visualization): enable biome organizeImports and sort imports
Sonar flagged duplicate imports (S3863): 11 files carried several import
statements from the same module, e.g. codeMap.mouseEvent.service.ts imported
from threeViewer.facade on eight separate lines. Biome never merged them
because the organizeImports assist action was disabled.
Enable it and apply it across visualization/app. Biome's assist both merges
and sorts, so 581 files are reordered.
The format script becomes `biome check --write --linter-enabled=false`, since
`biome format --fix` does not apply assist actions -- without this the
pre-commit hook would leave imports unorganized while format:check (which runs
the assist) failed in CI. The linter stays disabled in the write path so the
hook keeps applying only formatting + import organization, as before.
Sorting is alphabetical by module path, which detached the role-grouped section
comments in three barrels. Rewrite them so they no longer mislabel their
neighbours: fileStore.facade lifts its four audience groups into the file
docblock, metricsLens.facade and dependencyLens.facade re-scope the comments to
the exports they actually describe. No export surface changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: apply biome formatter to eight drifted files
`npm run format:check` (biome check, run over the whole repo by the Format
Tests workflow) failed on eight tracked files, so CI was red on this branch.
Three of them are fallout from 649b0135a: `biome check` runs the formatter
before the organizeImports assist, so the imports that commit merged in
contextMenuBlacklist.store.ts and primaryMetricNames.selector.ts were never
re-wrapped and exceeded the 140-char line width, and version-manager.ts never
had its two imports reordered at all. The format script is not idempotent
across an import merge -- a second pass is what fixes them.
The remaining five (the dependency-cruiser configs, the two cc.json schemas)
had drifted independently; lint-staged only ever reformats staged files, so
nothing forced them back into shape.
Formatting only, no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(config): lint in the format write path
Biome is this repo's linter, but it never ran on the write path: `format` was
`biome format --fix` (formatter only), then `biome check --write
--linter-enabled=false` after 649b0135a added the organizeImports assist. Lint
was enforced in exactly one place -- `format:check` in the Format Tests
workflow -- so a lint error surfaced in CI rather than at commit time.
Drop the flag. `npm run format`, which lint-staged runs pre-commit, now also
applies biome's safe lint fixes and fails on what it cannot fix. `biome lint`
over the repo currently reports zero errors, so this blocks no existing work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(visualization): guard the non-delta case in the structure tree selector
_getUndecoratedAccumulatedData wrapped its whole delta path in an
`if (isDeltaState(fileStates))` block whose only alternative was falling off the
end of the function. Invert it into an early return so the delta handling reads
at the top level.
No behavior change; the four existing branch tests cover it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): always exclude the .git directory from scans
Analysers walked the repository's own .git object store, which accounted for
roughly 47% of the nodes in a merged map of this repo. GitignoreHandler now
prunes .git during the init walk and short-circuits shouldExclude before the
empty-cache early return, matching the directory as a whole path component so
.github, .gitignore and .gitattributes are untouched.
RawTextParser and UnifiedParser add the exclude pattern unconditionally rather
than relying on the handler, since --bypass-gitignore consults no handler at
all and a root .gitignore suppresses the build-folder fallback.
RawTextParser's private DEFAULT_EXCLUDES is dropped for the shared
CodeChartaConstants.BUILD_FOLDERS it duplicated; the shared list additionally
excludes /node_modules/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): stop widening whole-number attributes to doubles
GSON's default number policy deserializes every JSON number as a Double, so any
read-then-write round trip rewrote an integer attribute `1` as `1.0`. A cc.json
that passed through a filter therefore no longer matched the file it came from,
and the drift compounded across a merge chain.
Both readers switch to ToNumberPolicy.LONG_OR_DOUBLE, which keeps integral
values as Long. CoberturaStrategy's casts are hardened to (as Number).toDouble()
because they can now see either type. The SVN golden file and the Tokei and
DependaCharta expectations are updated to the values that are actually produced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): do not merge a file into a same-named folder
UNION merging matched nodes by name alone, so a File and a Folder sharing a name
across two inputs were merged onto one node. The survivor took one of the two
types arbitrarily, and its children collided with the file's own id.
MergeResolverStrategy now matches type-aware and treats a File/Folder pair as a
clash rather than a match. NodeMaxAttributeMerger warns when it is nonetheless
handed nodes carrying more than one concrete type, so a clash that slips past
the matcher is visible instead of silent.
Two existing RecursiveNodeMerger tests passed Folder operands where they meant
to exercise the merge-into-one path; they now pass Files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): drop gitlog coupling edges pointing at absent files
Temporal coupling edges were emitted for every path ever seen in the log,
including files renamed or deleted before HEAD. Serialization then materialized
each dangling endpoint as an empty File node, so the map grew ghost files that
exist in no working tree.
ProjectConverter filters the edge list against the files actually present in the
log before prefixing paths with /root/. ProjectToCcJsonV2Mapper still
materializes missing endpoints -- edge-only producers such as CodeMaat rely on
it -- but now warns with the count, so a producer leaking dangling edges is
visible rather than silently absorbed.
VersionControlledFile.resolveEdge did `attributes.toMutableMap().putAll(attributes)`,
copying a map into itself and discarding the result, which dropped a second
metric's contribution to an existing edge. It now merges the two maps.
Remapping renamed paths at metric-evaluation time, and the resulting
highly_coupled_files inflation, remain open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): keep git metrics for non-ASCII paths
Git quotes and octal-escapes any path containing a non-ASCII byte unless
core.quotepath is off, so an escaped path from `git log` never matched the same
path from `git ls-files` and the file silently lost all of its git metrics.
Both commands now run with -c core.quotepath=off.
That flag is not sufficient on its own: git still C-quotes a path holding a
literal quote, backslash or control character, and logs captured elsewhere carry
the octal escaping regardless. GitPathUnquoter decodes both forms, reassembling
octal escapes as raw bytes before decoding UTF-8 so a multi-byte character split
across several escapes rebuilds correctly. It returns any path that is not fully
double-quoted unchanged, so it is safe to apply to every path.
Encoding detection was the second half of the bug: guessEncoding mis-detected a
UTF-8 log as WINDOWS-1252, and the log and the file list could be read with
different charsets. determineLogEncoding prefers UTF-8 whenever the log is valid
UTF-8, and both reads now share one charset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(config): fix duplicate sonar ignore key and exclude cc.json fixtures
The `w1` ignore key was declared once in `sonar.issue.ignore.multicriteria` but
assigned twice. In a Java properties file the later assignment wins, so the
`Web:BoldAndItalicTagsCheck` exemption — with its comment about font-awesome `<i>`
tags — silently had no effect. Rename the second block to `w2` and list it.
Both rules report zero open issues today, so the lost exemption was latent rather
than active. Pre-existing; not introduced by this branch.
Also exclude `**/*.cc.json` from analysis. The four `json:S2260` "parsing error"
issues Sonar reports against them are false positives on files that are valid JSON
(confirmed with python, node and jq); they are data fixtures, not source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(visualization): drop the ts-jest devDependency and a stray @jest/globals import
We never import `ts-jest` ourselves. It reaches us through `jest-preset-angular`,
whose `NgJestTransformer` does `require("ts-jest")` and extends `TsJestTransformer`
— it is the actual TypeScript transformer, and jest-preset-angular declares
`ts-jest@^29.4.0` as its own dependency, so it still resolves. Declaring it directly
claims a dependency we do not have.
`store.spec.ts` was the only file out of ~200 specs importing `@jest/globals`, which
is declared nowhere. Every other spec takes `expect` from the ambient `@types/jest`
globals. This was knip's one genuine `unlisted` finding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(visualization): document what the knip dead-code gate does not catch
The `entry` globs were redundant: knip's Jest plugin already registers `*.spec.ts`,
its Playwright plugin registers `*.e2e.ts` from `playwright.config.ts`'s `testMatch`,
every `*.po.ts` is imported by an e2e file, and the Angular plugin supplies
`app/main.ts` from `angular.json`. Removing them changes nothing — verified.
That also means the gate cannot report a production export whose only remaining
consumer is its own test, because in the default mode we run, a spec counts as a
consumer. Catching those needs `knip --production` with `!`-suffixed `entry` and
`project` globs, or it silently analyzes nothing and exits 0. Class members are out
of reach entirely: knip 6 has no such issue type.
Record all of that in the config rather than leaving the next reader to rediscover it.
Renamed to `knip.jsonc` because knip tolerates comments in a `.json` but Biome, which
formats the repo on commit, parses `.json` strictly and rejects them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(visualization): replace two stale TODOs with the constraints they describe
Both comments carried information worth keeping, so complete them rather than
deleting them. This also clears two `typescript:S1135` issues.
`setRenderStateFromUrl` asked whether it still worked "with the updated file
selection (no more single mode!)". It does: `setRenderState` only ever recognizes
`"Delta"`, and only with at least two loaded files. Everything else, the removed
single-file mode included, falls through to the default state. Say that instead.
`unaryMetric.ts` opened with "TODO: Remove the unary metric." above three lines
explaining why the constant lives in `util/metric` rather than inside the metrics
lens. Drop the aspiration, keep the rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(visualization): mark never-reassigned injected services readonly
Sonar flagged one member per file (`typescript:S2933`), but its leak period only
reports lines this PR touched. `codeMap.render.service.ts` has three more injected
services with identical inject-once, never-reassigned usage, and fixing only the
flagged one would leave the constructor arbitrarily half-readonly. Fix all five in
the two files. The repo-wide sweep (~255 hits under `features/`) is separate work.
The specs swapped in test doubles by assigning to the members after construction,
which `readonly` rejects. Both files already had the answer: `labelSettingsFacade`
was `readonly` and its double is installed with `Object.defineProperty`. Use that
idiom for the rest. Each `withMocked*` helper runs at most once per freshly
constructed service, so a non-configurable redefine is safe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(visualization): delete the orphaned map-download code path
`FileDownloader.downloadCurrentMap` has had no production caller since commit
0633424c3 ("Remove download button #3157", 2023-11-07) deleted the two components
that called it. Only its own spec kept it alive, which is why neither knip nor
review caught it: knip counts a spec as a consumer, and it cannot see class members
at all.
Remove `downloadCurrentMap` and the seven helpers reachable only from it, the
`DownloadableSetting` type, and the whole spec — every one of its tests drove the
dead code, and `downloadData` was mocked out in all of them. `getProjectDataAsCCJsonFormat`
was the sole reason this module imported the wire DTO, so `fileDownloader.ts` leaves
the `wire-dto-only-in-filestore-boundary` allow-list, taking it from three entries
to two.
The apiVersion-stamping fix that `downloadCurrentMap` also carried is unaffected:
the live re-export path is `fileParser.getExportCCFile`, and `fileParser.spec.ts`
already pins both the 2.0-origin and 1.x-origin cases.
Also drop the now-unreferenced `TEST_FILE_DATA_DOWNLOADED` mock and correct the
`apiVersion.ts` doc comment, which still named FileDownloader as an emitter of the
flat 1.x export shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): do not merge an ambiguous node into every match
`nodesMatch` is not transitive. Only a File-vs-Folder pairing counts as a clash, so
a node whose type is neither File nor Folder matches a same-named File *and* a
same-named Folder — and those two can now coexist as siblings, precisely because
dcbbd3b5a stopped merging them. `mergeOrAppendNode` merged the incoming node into
every match, copying its attributes and children onto both survivors and counting
one merge for two.
Before dcbbd3b5a, and on main before that, matching was by name alone. Name equality
is an equivalence relation, so at most one sibling could ever match and the `.map`
merged into exactly that one. The type guard broke that invariant without changing
the `.map`.
Refuse the ambiguous merge and keep the node instead, with a warning — the same
"skipped, never guessed" rule OVERLAY already applies to ambiguous content-hash and
suffix matches. A single match still merges exactly as before, so every input the
old name-only matcher could construct behaves identically. The kept node has its own
type, hence its own 2.0 id, and passes the writer's duplicate-id guard.
The regression test reproduces the duplication against the unfixed code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: trim internal entries from the changelogs
A changelog entry exists to tell a user what changed for them. Internal
restructuring — module moves, state-home reshapes, lint rules, CI wiring — does not
belong there, no matter how large the diff. Keep a Changelog's first principle is
that changelogs are for humans, not a commit log.
visualization: cut seven of the ten added entries. Six say so in their own text
("No user-facing behavior change (render and metric values are identical)"). The
five Slice 1-5 narratives and the two metrics-lens Chore entries move to
`migration-2-0-plans/INTERNAL-CHANGELOG.md`, which is real engineering history and
worth keeping — just not here.
analysis: cut one of seventeen. A GitHub Actions path filter for the 2.0 schema
drift guards is invisible to anyone running `ccsh`.
The kept "cc.json 2.0 map survives reload" entry also advertised a fix "for the
download button". There is no download button — it was deleted in 2023 by 0633424c3,
and the code path it named has no production caller. The fix is real for IndexedDB
persistence; the entry now claims only that.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(analysis): index the reference leaves once per overlay merge
`normalizedEdges` carried a comment claiming it folded a path "once per path instead
of re-allocating it on every suffix comparison". It did not. `unambiguousSuffixMatch`
called it for every reference path on every incoming leaf, so merging I incoming
leaves over R reference leaves folded R paths I times. The exact-position stage was a
linear scan that re-folded every edge of every candidate, and the content-hash stage
rebuilt a filtered map per leaf. `suffixFit` also wrapped both edge lists back into
`Path` objects on every comparison, purely to call `fittingEdgesFromTailWith`.
Build the three lookups once, in a `ReferenceIndex`. The exact stage becomes a map
lookup keyed on the folded edge list, keeping the first path per key so a fold
collision resolves the way the scan's `firstOrNull` did. `suffixFit` compares the
folded lists directly.
This also removes a genuine inconsistency. `namesMatch` folded case with
`String.equals(ignoreCase = true)` while `normalizedEdges` used `String.lowercase()`,
and those disagree: lowercasing "İ" yields two characters, so exact-position matching
saw `İstanbul.kt` and `istanbul.kt` as one file and suffix matching did not. Both now
fold through `foldedName`, which reproduces `equals(ignoreCase = true)` exactly —
verified exhaustively over a set of adversarial names including "ß"/"ẞ" and the
Turkish dotted I. With the default `ignoreCase = false` both folds were already plain
NFC, so nothing changes there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(analysis): split the merge resolver into its two modes
`mode` was fixed at construction — MergeFilter picks one resolver and never switches —
so `when (mode)` was a runtime branch for a compile-time choice, taken once per node.
The two halves barely overlapped: `nodesMatch`/`isFileFolderClash`/`mergeRecursively`
were dead in OVERLAY, and the ten leaf-resolution helpers, `keepOrDrop`,
`nodesUnmatched` and `addUnmatchedNodes` were dead in UNION. `recursive()` even passed
`addUnmatchedNodes = true`, a value UNION never read.
`MergeResolverStrategy` becomes a sealed base holding only what both modes share — the
name fold, the fold-and-merge skeleton, and its two counters — with the per-node step
and the stats line abstract. `UnionMergeResolver` and `OverlayMergeResolver` take a
half each. UNION's recursion still works: `mergeRecursively` re-enters the base's
`mergeNodeLists`, which dispatches back through the abstract step.
Purely structural. The base keeps its name and its `recursive()`/`leaf()` factories, so
MergeFilter and all four test classes are untouched; nothing outside the class ever
referenced `Mode`, which is gone. Both stats strings are preserved verbatim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(analysis): record why overlay merging has no File/Folder clash guard
The guard dcbbd3b5a added is UNION-only, and the omission is correct rather than an
oversight. Overlay's per-node step only ever sees project roots — ProjectMerger folds
one singleton root per project and the step never recurses into children — so there is
no sibling to keep a clashing node apart from, and its else-branch drops rather than
appends. Adding the guard there would silently discard an entire incoming project.
Also record where a File/Folder clash in this mode genuinely does happen, so the next
reader does not re-derive the trace: re-inserting resolved leaves goes through
NodeInserter, which resolves parents by name, so an incoming leaf under a folder `foo`
nests inside a same-named reference file `foo`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(visualization): give the 1.x carryover fields their own types
`blacklist`, `markedPackages` and `fixedPosition` were tagged `@deprecated` on the
pure cc.json 2.0 types to mean "1.x-only, absent in 2.0". That is not what the tag
says. Every linter reads `@deprecated` as "stop calling this, it is going away", so
the 1.x→2.0 normalizer and its reader were flagged five times (`typescript:S1874`)
for doing the one job they exist to do.
Move the three fields onto `CcJson2WithCarryover` and `FileNodeWithCarryover`, which
the normalizer returns and the reader consumes. `CcJson2` and `FileNode` go back to
being exactly the 2.0 shape: a consumer of them cannot see the carryover fields at
all, which is the constraint the tag was reaching for. Both are still assignable in
each direction — every carryover member is optional — so no other call site, spec or
mock needed a type change. The pure-lens helpers keep taking `CcJson2` to signal that
they touch only 2.0 data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(analysis): give the descriptor-mismatch warnings brace bodies
The four checks were split across lines mid-condition, which put the
comparison and its log on separate visual levels. Uniform brace bodies
read the same for all four.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(analysis): warn when the legacy curation inspection fails
The inspector exists to announce that 1.x markedPackages are dropped by
the 2.0 conversion. On a parse failure it returned 0, which the caller
reports as "none to drop" — indistinguishable from "could not tell", and
silent about the very data loss this check is meant to surface. Warn on
that path; the conversion itself is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(analysis): regenerate the rust sample golden in cc.json 2.0
The rust language sample merged in from main in the old 1.5 envelope and
was never converted with the other fixtures, so UnifiedParserTest failed
comparing 2.0 parser output against a 1.5 golden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(analysis,visualization): remove implementation-narrative comments
Delete ~845 comment lines that narrate what the code does rather than why
a non-obvious choice was made. Kept: NFC/NFD platform quirk notes, BVH
invariants, charset heuristics, memoization clone constraints, and
concise DB migration one-liners. Deleted: class KDoc mega-blocks, Slice
history references, facade barrel-export audience lists, method JSDoc
that paraphrase the method name, and inline step-by-step narration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style(visualization): apply biome formatting after comment removal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(visualization): un-export internal cc.json 2.0 types
Meta2, Lenses, MetricsLensData, DependencyLensData and DependencyEdge are
only referenced within ccjson2.model.ts, so knip flagged them as dead
exports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(visualization): complete the state home read windows
* refactor(visualization): unwrap persisted attributeTypes halves in v16
* refactor(visualization): narrow attributeTypes slices to the map they own
* refactor(visualization): fold re-declared selectors back into their homes
* refactor(visualization): drop passthrough stores in metricsBar
* refactor(visualization): drop passthrough stores in labelSettings, globalSettings, sidebarExplorer
* refactor(visualization): drop passthrough stores in sidebarInspector, 3dPrint, codeMap, fileExtensionBar
* refactor(visualization): drop passthrough stores in 6 features
Remove passthrough stores and services, wiring components and services
directly to the home facades' read windows and repos, and consolidate
per-feature writes into a single write store where one was warranted.
Affected: nodeContextMenu, scenarios, shared, legend, navBar,
viewCubeToolbox. bottomBar had no passthrough stores to remove.
* refactor(visualization): fence home root selectors to their home
* docs: record the Slice 20 outcome
* docs: record e2e green for Slice 20
* refactor(visualization): add sync read accessors and CcStateSnapshot
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): route raw state reads through the home read windows
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): fence raw ngrx State outside the state homes
* docs: record the Slice 22 outcome
* refactor(visualization): own URL query params in one module
Introduce QueryParamsService as the single owner of the URL query string:
URLSearchParams for read and write, one history.replaceState in one place,
and a declarative param<->state map.
Retires UrlExtractor's hand-rolled regex reader and the two hand-rolled
string splitters in UpdateQueryParametersEffect, and severs the odd edge
UpdateQueryParametersEffect -> LoadInitialFileService, which existed only
to re-read a query parameter.
Renames the MetricQueryParemter enum (note the typo) to QueryParameter and
folds the raw "file" and "mode" string literals into it.
Behavior changes, all deliberate:
- the whole query string is written in a single replaceState instead of five
- params in the hash fragment are no longer read (URLSearchParams reads
location.search, the regex matched location.href); nothing depends on it
- duplicate non-file params collapse on write instead of being duplicated
- the delete path no longer corrupts valueless keys into "key=undefined"
Preserved verbatim: absent param -> null, empty param -> "", "+" -> space,
percent-decoding, ordered getAll("file"), and the first-occurrence rule that
makes "?file=&file=x" boot from IndexedDB rather than from the URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): route every file load through LoadFilesUseCase
Five entry points each re-implemented part of the load pipeline. There is now
one: LoadFilesUseCase owns the decision tree for the ?file= URL, IndexedDB, the
sample files, the file-picker upload and the reset-map dialog, and emits exactly
one `filesLoaded` action per load carrying the provenance of that load (which
source, whether these are sample files, the URL metrics, whether the camera must
be fitted anyway).
The reset dialog's hand-copied decision tree is deleted, not fixed. It had
drifted: it swallowed load errors silently instead of raising the error dialog,
and it omitted both the URL metrics and the sample-file flag. Going through the
use-case, it now behaves like a fresh ?file= boot.
IndexedDB is read exactly ONCE per boot, through the new CcStatePersistence
seam, and threaded through every branch. The old path read it twice whenever a
?file= boot fell back to the sample files, and a third time inside the sample
loader. CcStatePersistence reports a read failure rather than throwing, because
the two boot branches raise different error dialogs for it.
UrlExtractor becomes DI-provided; it was `new`ed in two places.
Preserved exactly: both apply-orders (settings-then-files when the persisted
files match the URL, all-settings-first when they differ), every error dialog
and its title, the NO_FILES_LOADED suppression, and the fact that an IndexedDB
restore never touches currentFilesAreSampleFiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(visualization): add the post-load reconciliation sequence
Nobody owned "a file was loaded -> re-initialize". Nine things independently
reacted to the file selectors and each reset one thing, in an emergent order —
which is why resetColorRange needed `skip(1), take(1)` to come out right, and
why on a `?file=&area=` boot with saved state the areaMetric was written by the
persisted path, then again by the URL path, with the metric reset possibly
firing in between. It worked by luck.
ReconcileAfterLoadEffect now owns the whole sequence, deterministically:
1. merge the file settings (blacklist / markedPackages / attributeTypes /
attributeDescriptors)
1b. point the file root at the reference file
2. derive the metric data — AFTER step 1, so it already sees the merged
blacklist
3. resolve the metric selection
4. derive the color range from the resolved color metric, once
5. unfocus the nodes, lower the top-label count
6. fit the camera (AutoFitCodeMapEffect — it needs the renderer, so it stays
in features/codeMap, but keys off the same signals)
THE PRECEDENCE RULE IS THE DELIVERABLE: URL > persisted > computed default,
written down and enforced in one pure function, resolveMetricSelection. A
candidate only wins if the metric it names exists in the files that were
actually loaded; a URL metric naming an absent metric is dropped silently and
the next candidate wins. area/height/color resolve as a combination, edgeMetric
independently.
Deriving the metric data from the state snapshot in step 2, rather than from a
selector subscription, is what makes the rxjs workarounds unnecessary: by
construction everything the sequence reads is post-step-1. `skip(1), take(1)`,
the `pairwise()` boot guard and the stale `withLatestFrom` reads are gone.
Three named triggers, because three of the old subscribers legitimately fired on
non-load changes and those paths must not be lost:
- a file-set change (a load, OR a file-panel change: delta switch, file
removal, re-selection) runs steps 1-6
- a metric-data change without a load (a blacklist edit removed the chosen
metric) runs steps 2-4 only — it must NOT unfocus nodes or reset the label
count, which the old effects did not do either
- a view change that alters the map's extent runs step 6 only
The file-set burst is REDUCED, not merely debounced: `filesLoaded` is not
necessarily the last action in it (the restore branch sets the persisted file
states afterwards), so taking the last trigger would silently drop the
provenance and with it the URL metrics.
The stringly-typed "StartWithGlobalOption:resetCameraIfNewFileIsLoadedSetToFalse"
action is replaced by `forceAutoFit` on the filesLoaded payload.
The nine old subscribers are unregistered here but still on disk with their
specs passing — parity before delete. They are deleted in the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): delete the nine post-load subscribers
Their behavior is now owned by the reconciliation sequence, and the parity specs
added in the previous commit pass against it. Deleted:
- UnfocusNodesEffect -> step 5
- UpdateFileSettingsEffect -> step 1 (its four mergers move
with it, snapshots unchanged)
- UpdateVisibleTopLabelsEffect -> step 5 (its pairwise() boot
guard and stringify compare
are gone; the sequence only
fires on a real change)
- ResetChosenMetricsEffect + setDefaultMetrics -> step 3
- ResetSelectedEdgeMetricWhenItDoesntExistAnymoreEffect -> step 3
- ResetColorRangeEffect.resetColorRange$ -> step 4 (with its `skip(1),
take(1)`; the user-driven
half of the effect survives)
- LoadFileService.referenceFileSubscription -> step 1b (LoadFileService is
no longer stateful)
- MapResetStore.resetMetricsToDefault -> step 3 (its first()-subscribe
read the metric data of the
map it had just wiped)
areChosenMetricsAvailableSelector and its selector tree go with them: their only
consumer was ResetChosenMetricsEffect, and resolveMetricSelection makes the same
all-or-nothing availability check on the metric data it derives itself.
The labelSettings effect bundle is now empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): give the loading indicator a single owner
isLoadingFile had five writers — loadingState.store, renderCodeMap.effect,
setLoadingIndicator.effect, the boot component, and the scenario applier (which
is not a file load at all) — plus an imperative `isUploading` boolean living
outside the store that renderCodeMap.effect reached back into.
It now has two, both inside the load pipeline: LoadFilesUseCase raises it at the
start of every load (before the fetch, so the spinner is up while a large file is
read), and LoadingIndicatorEffect lowers it once the map it was raised for is on
screen. The effect also raises it for a file-panel change, which rebuilds the map
without being a load.
The clear now has a safety net: it races the render-quiet period against a
maximum wait, so a load that produces no renderable map can no longer leave the
spinner up forever (which would hang every e2e waiting on #loading-gif-file).
Applying a scenario is not a file load, so it stops writing isLoadingFile and
gets its own flag. The spinner combines the three busy signals it actually has:
a file load, a pending heavy dispatch, and a scenario being applied. This also
removes the two imperative booleans (`isUploading`, `isApplying`) that
renderCodeMap.effect used to consult, leaving that effect to do one thing:
render.
isLoadingMap is deleted outright. It had four writers and, as it turns out, zero
readers — no selector, no template. It was dead state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): collapse the persisted-state apply loops
The five apply loops (preferences / mapState / metricsLensSource /
dependencyLensSource / sharedView) were character-identical modulo names. They
become one generic applySlice, with the two behaviors that actually differ —
the optional-key set, and each slice's own key->action mapper — passed in.
Preserved exactly, because they are load-bearing: iterating the CURRENT slice's
keys rather than the persisted ones (which is what makes an older persisted
state forward-compatible), the safe-stable-stringify deep compare, the
optional/ignored key sets, and the "Unhandled key" tripwire in every mapper. The
five exact-dispatch-count tests pass unchanged — that is the proof of
equivalence.
LoadInitialFileService is deleted: LoadFilesUseCase replaced it, and only spec
stubs still referenced it.
setMetricsFromUrlValues is deleted with it. The use-case was still applying the
URL metrics directly as a bridge while the reconciliation was being built; now
that the sequence owns the precedence rule, applying them twice was both
redundant and a second place where precedence could drift. The use-case carries
them on the provenance and nothing else. This also takes CcStateSnapshot and the
metricData selector out of the store, which no longer reads derived state at all.
The stringly-typed "StartWithGlobalOption:resetCameraIfNewFileIsLoadedSetToFalse"
dispatch goes too — its listener was replaced by filesLoaded.forceAutoFit, so it
had a dispatcher and no listener.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): cover the load pipeline end to end
The plan's verification list called for e2e on paths that had none: booting from
IndexedDB, the URL metric precedence, the URL write-back, and — the biggest
regression risk in the slice — a file-panel change, which runs the reconciliation
sequence without being a load.
Each of these also proves the loading indicator clears, because the e2e helper's
goto() fails if #loading-gif-file never hides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): restore the persisted session over the file-derived merge
An adversarial review of the slice found four regressions. All four came from the
same root: moving work into the deferred sequence silently inverted an ordering
that the old, synchronous effects had guaranteed.
1. A restored session's blacklist and marked packages were wiped on every reload.
The old UpdateFileSettingsEffect merged the file settings SYNCHRONOUSLY inside
setFiles — i.e. BEFORE the persisted sharedView was applied — so the persisted
values won. The sequence runs a macrotask later, so its file-derived merge
landed on top of them. A user's exclusions and markings live ONLY in the
persisted state (they are never written back into a file's own fileSettings),
so the merge erased them — and saveCcState then persisted the loss.
2. Same inversion un-focused a focused folder on every reload (step 5's
unfocusAllNodes landed after the restore), and clobbered the persisted
attributeTypes / attributeDescriptors.
Fix for both: the persisted view slices are no longer applied by the use-case.
They travel on the filesLoaded provenance and are applied by the sequence as
its LAST step, which states the precedence explicitly — persisted beats
file-derived — and reproduces the old net order.
3. "Reset map to default" stopped resetting the metric selection. setState with
the default state does not clear the metric keys, because _applyPartialState
skips null values and every default metric IS null — so the old selection
survived the reset, remained available in the reloaded files, and won the
precedence as the "persisted" candidate. The reset now carries
forceDefaultMetrics, which drops that candidate: URL still wins, otherwise the
computed default does. The button's own tooltip promises this.
4. The loading indicator's max-wait dismissed the spinner MID-LOAD. It is armed
when a load starts — before the file is even fetched — so a 5s deadline fired
on any slower boot, told the app the load was done while it was still writing
to the store, and (via loadFiles clearing localStorage afterwards) silently
destroyed a just-saved scenario. This surfaced as a ~50% flake in the e2e
suite. The deadline is now 60s: a last resort for a load that never renders,
not a bound on how long a load may take.
Each fix is pinned by a test that fails without it, plus e2e for the reset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: record the Slice 21 outcome
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(visualization): clear the dead-code backlog
`npm run lint:deadcode` (knip) is a CI gate, and it had been failing on a
standing backlog of 9 unused exports. It now exits 0.
None of the nine were dead selectors: each is still used inside its own state
home, by that home's read window. What was dead was the facade RE-EXPORT — the
homes advertised nine selectors on their public surface that nothing outside
them ever imported. The selectors stay; the re-export lines go.
Also removes the unused imports this slice left behind in five specs, which
`biome check` classes as unsafe-fixable and so had survived `npm run format`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): wait for the specific file to persist before reload
The restore test reloaded the page right after the URL load, but `goto()` only
waits for the map to render — not for the state to be persisted (the save is
debounced). So the reload could outrace the write and boot from a stale database.
`waitForCcStatePersisted` now waits for the SPECIFIC file this test just loaded,
not merely "any files": IndexedDB can still hold a prior test's persisted state
(afterEach clearing is not perfectly reliable for file:// origins), so an
"any files" check is satisfied instantly by that stale record and never actually
waits for this test's save. Waiting for the exact file is immune to both the leak
and machine speed — proven: on a seeded samples record the old predicate returns
true, the new one false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): make the scenario list reads web-first
The "save" and "delete" scenario tests read the list with a one-shot
`allInnerTexts()` right after the dialog attaches. The list is a radio-accordion
whose expanded group's rows paint a change-detection tick later, so the read
could land before the row was there and return empty names — a flaky, non-retrying
read (the scenario was correctly persisted; only the read raced). Slice 21 made
the boot busier, widening the window enough to surface it.
Assert the specific scenario row with web-first `expect(locator).toBeVisible()` /
`.toHaveCount(0)`, and make `getScenarioNames` wait for the first row to render
before reading (every caller expects at least one row; the empty case checks the
"no results" message instead).
Verified: scenarios repeated 12× each and the full e2e suite three times, all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): serve the e2e app over http instead of file://
The IndexedDB-restore test flaked only when run outside CI: `npm run e2e` runs
in parallel locally (the config uses workers=1 only on CI), and a `file://`
origin's IndexedDB is shared across all parallel browser contexts. So a
concurrent test's storage clear/delete could wipe the restore test's persisted
state in the window between its persist and its reload, and the reboot then
booted the default sample files ("sample1 +1") instead of the restored file.
This is invisible in a serial run, which is why CI (and my earlier --workers=1
runs) never caught it.
Serve the built app over http://localhost via a tiny dependency-free node static
server (Playwright `webServer`). Each Playwright context gets its own isolated,
persistent origin storage over HTTP, so parallel tests can no longer clobber each
other's IndexedDB. This is the standard Playwright recommendation — never drive a
real app from file://.
Also: the invalid-file error assertion in url.e2e is now transport-agnostic (the
exact HTTP status text differs between file:// and a real server), and the
now-unneeded --allow-file-access-from-files launch flag is dropped.
Verified: full e2e suite 40/40, eight consecutive PARALLEL runs over http — the
exact condition that was failing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): fall back to bundled chromium on linux arm64
Every Playwright test failed at browser launch on linux arm64: the config
hard-codes the "chrome" channel, and Google Chrome is not built for that
platform at all (`npx playwright install chrome` refuses with "not
supported on Linux Arm64"), so the suite could never run there.
Keep Chrome as the default — headless WebGL needs it on macOS, and CI's
ubuntu-latest x64 runners ship it preinstalled — and switch to Playwright's
bundled Chromium only on linux/arm64, where its headless WebGL works.
Verified: three consecutive full parallel runs, 40/40 green each.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): persist the cc state with strict durability
The e2e restore test failed on a parallel local macOS run: the reboot loaded
the sample files although the test had CONFIRMED the persisted record was in
IndexedDB moments before the navigation. Every app- and test-level race was
ruled out empirically (instrumented IndexedDB wrappers, 20x CPU throttling, a
3s-stalled boot read — no writer ever touches the record before the boot read,
and contexts are storage-isolated). The record was lost inside Chrome itself:
the failing worker's browser also hung teardown into the 10s timeout with a
truncated trace, and all nine workers had cold-launched Chrome simultaneously.
Chrome commits IndexedDB transactions with relaxed durability by default:
success is reported BEFORE the data reaches disk, so a storage-process crash
in that window silently loses the whole persisted session — the exact loss
the restore test is built to catch, and one that can hit real users too
(the desktop app included).
Write the cc state with { durability: "strict" } so a confirmed save has
reached the platform before anything relies on it. At one debounced write per
500ms burst the fsync cost is negligible.
Not reproducible on Linux (nor in CI, which runs --workers=1); needs a
parallel macOS run to confirm the flake is gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add plan for feature-layer dependency-cruiser fencing
Captures the investigation of facade-only external access + intra-feature
component/service/store layering rules: current violation counts (87),
per-feature breakdown, and open rollout decisions to resolve before
implementing.
* fix(analysis): keep a same-named File and Folder as siblings in NodeInserter
OVERLAY merge could nest an incoming leaf under a same-named reference
File, emitting a structurally invalid File node that owns children
(finding 7e). NodeInserter resolved a path's parent, and merged
same-named terminal nodes, by name alone with no type check.
Make both lookups type-aware: the folder-traversal branch now only
treats a same-named child as the parent if it is itself a Folder, and
the terminal merge branch now refuses to merge a File into a Folder
(or vice versa) — mirroring UnionMergeResolver's isFileFolderClash
guard for the same collision. Either clash now falls through to
keeping both nodes as separate siblings instead of corrupting one.
Every NodeInserter.insertByPath caller (every parser, importer,
EdgeFilter, MergeFilter) shares this path, so the guard fixes UNION's
existing File/Folder clash guard's gap in OVERLAY without duplicating
it per merge strategy.
* docs: close finding 7e and record the overlay merge file/folder fix
Update the branch-review plan's finding 7e with a pointer to the fix
commit, and mark the implementation plan complete with a note that the
fix scope grew beyond the folder-traversal lookup during TDD.
* refactor(visualization): fence feature layer via dependency-cruiser
Tighten dependency-cruiser so outside code reaches a feature only through
its facade.ts (components/ is no longer a public surface) and add a
services -> components ban. Re-export dialog/dumb components through the
shared, scenarios, 3dPrint, globalSettings, labelSettings and changelog
facades, break the changelog self-cycle, and move scenarioView.model.ts
into scenarios/model/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): clear lingering edge arrows on hover change
Always clear arrows on a hover change so edges from the previously hovered
building do not linger when moving onto a building with no edges; restore
the selected building's edges or preview and add the hovered building's
edges only when it is edge-applicable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(visualization): rename E2E_BASE_URL to CC_URL
Collapse the duplicate E2E_BASE_URL/CC_URL constants into a single CC_URL
and update the playwright config to use it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): run camera auto-fit reliably after map load
The auto-fit request raced the throttled, shared render stream: after
the CPU-heavy synchronous gzip inflate the render's trailing edge fired
before the effect's take(1) subscribed, so autoFitTo() was never called
and the previous camera pose survived the load. Wait for the
deterministic ThreeSceneService.mapMeshChanged$ signal emitted when the
new mesh is placed into the scene, and retry the fit for up to 10
frames instead of silently giving up while the geometry is missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(visualization): keep the front view on camera auto-fit
fitCameraToBoundingSphere() ran controls.update() between placing the
camera and updating the target and zoom limits, so OrbitControls
clamped the fresh position against the previous map's min/maxDistance
and target — dragging the camera off the front-view axis whenever the
new map was much larger than the previous one (or the orbit target had
been panned far away) — and setZoomPercentage() then locked the skewed
direction in. Assign the new limits and target before a single
controls.update() so the fit always yields the canonical front pose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(visualization): extract resolveTriple and tidy load code
Extract the all-or-nothing area/height/color combination logic from
resolveMetricSelection into a dedicated resolveTriple helper, use
optional chaining in geometryGenerator, and group missingProperties
pushes in loadFiles.useCase. Also reflow the dependency-cruiser rule and
update migration/feature plan notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(visualization): clean up naming and comments in resolveMetricSelection
Rename the four-field MetricTriple to NodeMetricSelection, use
availability-revealing names, and keep only why-comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(analysis): clean up naming and comments in NodeInserter
Extract insertOrMergeChild, replace the getFolderNode/let/!! chain with
insertNewFolderChild, use find* names for nullable lookups, and trim
comments to the rationale only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(visualization): run the indexeddb restore e2e on a disk-backed page
A default Playwright context keeps its IndexedDB in memory only, where
Chrome's storage service can drop it under parallel load and the
strict-durability save never reaches a disk — the restore test kept
failing on macOS despite the record being confirmed persisted. Launch
it in a persistent context instead, harden goto() against passing
before the app has booted, and skip the storage clear on unnavigated
pages so teardown cannot hang.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(config): enforce clean code standards as mandatory requirement
Update CLAUDE.md to explicitly state clean code is enforced, not optional.
Add CODE_QUALITY.md with comprehensive 9-point standard covering variable
naming, method size, comments, parameters, duplication, idioms, complexity,
type clarity, and review checklist. All code now requires compliance before
merge.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(analysis): union edge attributes when merging duplicate dependency edges (#10)
DependencyLens.merge deduplicated edges by directed endpoint pair with
distinctBy, keeping the first edge and silently dropping any other edge's
attributes for the same pair. Merging two edge-metric files for the same
dependency (e.g. pairingRate from one, avgCommits from another) therefore
lost the second metric.
Fold same-pair edges into one and union their attribute maps instead,
first lens winning on a conflicting key — matching the first-wins rule the
attribute-type and descriptor merges already use, and mirroring how node
attributes already combine on merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(analysis): warn on duplicate node id in the cc.json 2.0 reader (#12)
collectEndpoints built idToEndpoint with a plain map write, so a foreign or
hand-authored 2.0 file that reuses an id silently overwrote the binding and
re-pointed that id's edges at the last node — mirroring the unresolved-id
case, which already warns, but without any signal.
Keep the first-declared binding via putIfAbsent and warn on a collision so
the ambiguity surfaces. Tool-generated files never trigger this: the writer
derives every id from its unique tree position.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: scope the changelogs to user-facing changes
The analysis Fixed section documented bugs in the cc.json 2.0 pipeline that
this same release introduces. A user upgrading from 1.143.0 never saw them,
because contentHash, lenses, convert and the 1.x rejection do not exist there
yet -- those seven entries read as a work-in-progress bug log and are dropped.
The convert blacklist/markedPackages warning is folded into the Changed bullet
that already covers that loss, since it is real behavior rather than a fix.
Five pre-existing defects that shipped in 1.143.0 had no entry at all and are
added: the .git directory being scanned, gitlogparser losing metrics for
non-ASCII paths and emitting ghost coupling edges, whole-number attributes
widening to doubles through any filter, and modify mutating its input project.
The visualization feature-architecture migration entry says "No user-facing
behavior change" in its own text, which is INTERNAL-CHANGELOG's stated
criterion, so it moves there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(visualization): draw only edges of the selected edge metric on hover
Hovering or selecting a building drew every edge of the visible files,
regardless of which edge metric carried it. On a map with several edge
metrics (e.g. dependencies and temporal_coupling) that put far more lines
on screen than the building's edge count reported: the count is per-metric
(node.edgeAttributes[edgeMetric]) while the arrows were not filtered at all.
The two arrow paths read different sources. addEdgePreview goes through
edgeVisibilitySelector, whose setEdgeVisibility gates on
`edge.attributes[edgeMetric] !== undefined`. buildPairingEdges read the raw
edgesSelector and applied no edge-metric predicate, so the preview and the
hover path could disagree. buildPairingEdges now applies the same predicate.
A consequence worth naming: with no edge metric selected, hover now draws
nothing rather than everything, which is what the preview path already did.
Pre-existing bug, not a regression of the cc.json 2.0 work — main behaves
the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: define clusters as a top-level cc.json 2.0 lens (#4411)
The 2.0 draft reserved an empty `clusters` array inside the metrics lens.
Clusters are structurally a cross-cutting relation joined to `files` by node
id — like edges, not like a per-node metric map — and cluster merging has its
own semantics (union members, reconcile weights, drop members whose node id did
not survive) that do not belong inside the metrics lens. So `clusters` becomes
a top-level lens, sibling to `dependency`, fully defined in all three schemas:
a `clusterings` map keyed by clustering id, each declaring `membership`
(partition/weighted/overlay) and an optional `weightBasis`, holding clusters of
members that reference node ids.
The move is free only before release; after it, the additive-only versioning
rules would make it breaking. 2.0 therefore ships the definition without
producer or visualization support — the lens rides the existing unknown-lens
passthrough, so readers preserve it on round-trip today. See
dev_docs/cc-json-2.0-clusters-lens.md for the shape, merge and rationale.
Two consequences worth naming. The `--large` merge guard on data-bearing
`metrics.clusters` is gone: clusters now arrives as an untyped lens, so the
`opaqueLenses` guard above it covers the same case. And `meta.checksum` is an
MD5 over the serialized lenses, so dropping the slot invalidated it — the 41
fixtures whose checksums were correct are regenerated here; fixtures whose
checksums were already stale are left alone, since the reader never validates
`meta.checksum` on read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove obsolete migration plans and stories
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(visualization): mark never-reassigned members as readonly (#4505)
Resolve SonarCloud S2933 across the renderer/codeMap services: fields and
injected dependencies that are only assigned once are now readonly. Specs
that swapped these members for mocks use Object.defineProperty (the existing
in-repo pattern for readonly members) instead of direct reassignment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Christian Hühn <christian.huehn@maibornwolff.de>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> C
Christian Hühn committed
f286731042fcaa9268acfe9373f84b7ce2cc71bb
Parent: 640a8b9
Committed by GitHub <noreply@github.com>
on 7/17/2026, 4:14:12 PM