Skip to content

refactor(data-handling): rename the statistics panel to Data Handling and split it into step components and feature services - #56

Open
KFilippopolitis wants to merge 32 commits into
masterfrom
refactor/data-handling-split
Open

KFilippopolitis wants to merge 32 commits into
masterfrom
refactor/data-handling-split

Conversation

@KFilippopolitis

@KFilippopolitis KFilippopolitis commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Renames statistic-analysis-panel to Data Handling (DataHandlingComponent, app-data-handling) and splits the ~4.8k-line component / ~1.8k-line template into one component per pipeline step, per-sub-step components, component-scoped feature services (preprocessing state, summary surfaces, summary histogram, summary export, transformation drafts and statistics) and shared CSS partials. Each extracted piece was also cleaned up as it moved: typed instead of any, signal input()s, de-duplicated templates and helpers, shorter methods, and a focused spec for each new piece.

Behaviour is meant to be identical. Same DOM, visuals, wiring, data-guide anchors, e2e selectors, request payloads and sessionStorage keys, and no new dependencies. Every commit is green (npm run verify plus the specs). Every extraction up to the master merge was compared against master with the visual harness (method below); the later shell-slimming commits were not, see Shell slimming.

What each piece owns (file by file)

All paths are under src/app/pages/experiment-studio/data-handling/.

Piece Owns Quality changes
data-handling.component.* Shell and orchestration: describe requests, apply/persist, stage add/remove, the preprocessing request config. Provides DATA_HANDLING, SectionAnchors, RuleBuilderRegistry and the feature services Template 1836 → 122 lines; CSS 74.6 kB → 8.3 kB across its 3 sheets; dead imports removed; ChartBuilderService no longer injected
data-handling.models.ts, data-handling.context.ts Shared types; the DATA_HANDLING token that sections use to read the pipeline any replaced by VariableRow, PivotBlock, SummaryView, …
how-to-strip/ (details[app-how-to-strip]) "How to use" strip input()
pipeline-terminal/ (div[app-pipeline-terminal]) Run / terminal node input()/output()
summary-workspace/ (div.workflow-section-body[app-summary-workspace]) Variable browser, statistics table, charts, histograms and export, shared by the Source / Raw / Processed surfaces Group and tab constants, @let coverage, skeleton arrays; 3 near-copies became one component
source-data-station/ Step 1: raw data snapshot —
cohort-filter-station/ Step 2: folded rule overview, rule builder card, preview/apply, Raw preview #filterModal and its calls live in the station
preprocessing-station/ Step 3: header, folded sub-step rail, station cards and docs, the #prepFooter action bar, longitudinal visit controls, Processed preview —
…/prep-variable-rail/ (aside[app-prep-variable-rail]) One variable rail for all three sub-steps (heading, search, state groups, no-match state) Replaces three ~60-line copies. Per-step row markup goes in typed <ng-template prepRailSlot="lead|trail|action" let-variable> slots (directive with a context guard, read via contentChildren() + computed())
…/missing-values-step/, …/outlier-step/, …/longitudinal-step/ (section[app-…-step]) Each sub-step's workspace: rail configuration plus its detail panel @let rule / @let state (the rule was read 6 / 10 times per render); a redundant constant check removed; outlier label clamp is now one boundLabelShift() method
…/preprocessing-state.service.ts Pending/applied rules for missing / outlier / longitudinal, the "applied by the user" flag, and the comparisons (missingHasPendingChange, outlierHasPendingChange, isUsingDefaultMissing, …) Seven private panel helpers became tested pure utils
transformation-station/ Step 4: header, folded rail, type chooser, nominal cards (chrome, status, counts chart, apply bar), K-means card and the #transformationStats template Each card's status and new work read once via @let (were read 3 / 5 times)
…/categorical-column-card/ (div.transformation-create[app-categorical-column-card]) Nominal card editor: name, categories with their rule builders, fallback draft as a signal input(); inline draft.x = $event; … replaced by rename() / setCategory() / setFallback(); @let column gives type narrowing
…/kmeans-cluster-source/ Moved under the transformation station (paths only) —
…/transformation-drafts.service.ts The draft list: status, new work, configs, payload, duplicates, pending change, add/remove/reset, rule add/remove Typed TransformationColumnConfig and TransformationDraftStatus
…/transformation-statistics.service.ts Category counts of the derived columns (one describe over all of them); the panel hands it its pending preprocessing and labels through ports —
statistic-summary.service.ts The three summary surfaces, their loaded cache keys, rawKeyFor, describe response → SummaryView (including box-plot / grouped-bar state) Typed DistributionState
summary-histogram.service.ts The Histogram tab's per-variable, per-split requests and cache. Reads the panel's blocks, pending preprocessing and Cohort Filtering preview through six ports. A request that settled (data or error) is not repeated until its surface is rebuilt takeUntilDestroyed; histogramSubscriptionsByVariable (it only ever held Subscription.EMPTY) is now histogramSettledByVariable
summary-export.service.ts A surface's PDF report and CSV, and the exporting signal behind the page overlay. No ports Pure parts in utils/summary-view.utils; the PDF path now has a spec
section-anchors.ts SectionAnchors + [dhSectionAnchor]; replaces six @ViewChild refs for goToSection scrolling. Also runs the scroll request (requestScroll): it waits for the render that follows an open/close, and a newer request replaces one that has not run —
transformation/rule-builder-registry.ts RuleBuilderRegistry + app-filter-config-modal[ruleBuilderFor]. Each category's rule builder is looked up by its rule, replacing the positional @ViewChildren ruleModal lookups The ~20 spec sites rewritten
utils/* Pure, spec'd helpers: formatting, summary pivot, SUMMARY_METRICS, view helpers (selectionKey, nextStatisticSelection, statisticBlockKey), preprocessing state (pendingOrDefault, groupPreprocessingVariables, missing-rule helpers), documentation HTML, transformation drafts, histogram preview parsing, outlier bounds, summary coverage / distribution / category reads, the section layout goToSection applies, export helpers —
styles/*.shared.css @import partials for rules that several components render: pipeline-node (step chrome, sub-step tree, connectors), statistics-panel, search-and-empty-states (+ glass-feedback and its keyframes), tables (+ .table-input/.table-select, button:disabled), preprocessing-step, transformation-rules Summary-workspace copies de-duplicated; every component stylesheet is under the 20 kB budget

Docs: the decision-log entry "Data Handling sections are attribute-hosted components over the pipeline's state", plus module-index and conventions.

File sizes (panel)

master (statistic-analysis-panel) this branch (data-handling)
component .ts 4796 lines / 197 kB 3179 lines / 131 kB
template .html 1836 lines / 107 kB 122 lines / 5 kB
.component.css 1084 lines / 25.4 kB 137 lines / 4.1 kB
.preprocessing.css 1131 lines / 27.2 kB 92 lines / 2.8 kB
.results.css 870 lines / 22.0 kB 52 lines / 1.4 kB

The largest new piece is summary-workspace (271-line template, 12.6 kB CSS). Station templates are 33–289 lines, and component TS files are 19–67 lines, except kmeans-cluster-source, which already existed.

Tests

  • npm run verify: OK. The only warnings were already on master: the algorithm-panel CSS budget, and jszip not being ESM.
  • Vitest (full, headless Chromium, run locally on the final code commit): 107 files / 1307 tests passed (this branch before the shell slimming: 103 / 1247; first validation of the branch: 94 / 1117; master then: 79 / 1068). New specs: the stations, rail, steps, card, the services, the registries and the utils.
  • CI on c58e3f6: Vitest unit tests, Playwright experiment studio, typecheck / dead code / build, SAST, CodeQL, gitleaks and the dependency scans all pass. (The earlier local Playwright run passed 42.)
  • Visual equivalence: ALL IDENTICAL in 8 scenarios, up to the master merge. Not rerun for the shell-slimming commits. See below.

Visual-equivalence method

A local-only Playwright harness drives the pipeline in 8 scenarios against a master worktree and against this branch:

  1. source snapshot
  2. missing-values and outlier editing with the unapplied warning
  3. processed preview with histogram
  4. rule builder
  5. applied filters with preview
  6. nominal and K-means derived columns
  7. longitudinal
  8. docs / how-to

In each state it records:

  • a DOM dump (tags, classes, attributes, text)
  • the computed styles of every element in the pipeline (px values rounded to integers)
  • a full-page screenshot

Chrome runs with deterministic flags: --font-render-hinting=none, --disable-font-subpixel-positioning, --disable-lcd-text, --disable-gpu, --force-device-scale-factor=1. The comparison was rerun after every extraction and once more on the head before the shell-slimming commits; all 64 artefacts match master. The "s4 filters" scenario sometimes times out under parallel workers; on its own it passes and matches.

Before any CSS rule was moved into a partial (an @import must come first in a sheet), a script checked that the move did not cross an equal-specificity rule targeting the same element. Every overlap it flagged was checked by hand and was a false positive.

Shell slimming (f8162da to f7d842f)

The shell was ~3.6k lines after the master merge and is 3179 now. One commit per piece:

  • f8162da Coverage reads (coverageFor, blockCoverage, selectionCoverage, missingShareNote, coverageByDataset, distributionFor, categoryCountsFor) are pure functions in utils/summary-coverage.utils.ts. The panel keeps one-line delegates, so the sections and specs call the same members.
  • 018b492 SummaryHistogramService owns the histogram request/cache loop. Payloads, cache keys and request order are unchanged.
  • 61728ef SummaryExportService owns the PDF/CSV export. isExporting stays as a getter over its signal; the off-screen chart containers stay in the panel's template.
  • cd794d5 Scroll requests moved into SectionAnchors; the open/preview/added-stage transition of goToSection is utils/section-layout.utils.ts. The sectionOpen / summaryExpanded / addedSteps signals stay panel fields.
  • c58e3f6, f7d842f An export-spec fix for the SAST gate (innerHTML in a spec, now createElement) and a decision-log correction.

Dead code removed on the way: the panel's metricLabel, statisticMetric*, isCountMetric, mipVersion, pivotBlocksWithDatasetLabels, and the clearHistogramSubscriptions calls that ran right before a summary was replaced (or in ngOnDestroy) and did nothing.

Not touched: any .html or .css file, request payloads, sessionStorage keys, routes, guards, auth, sharing, deletion, Docker/nginx/CI, runtime env, dependencies.

Risk

  • The scroll effect now lives in a component-scoped service (SectionAnchors). The panel's scrollIntoView specs and a new SectionAnchors spec pass, but it is the one change with a timing component. Worth a manual click-through of the stepper, "Preview data" and a PDF export.
  • SummaryView.histogramSubscriptionsByVariable was renamed to histogramSettledByVariable. It is an internal shape (the models, emptySummaryView, StatisticSummaryService), read by nothing outside the panel.
  • The visual harness was not rerun on these commits.

Rollback: each commit builds and passes on its own. Revert in reverse order; c58e3f6 belongs with 61728ef.

Open questions / risk areas

  • Filter edit mode (cohort-filter station): the rule builder's edit / preview / apply flow now lives in the station. It is covered by the e2e filter flows and the harness, but it has the most state crossing between the panel and a child, so it deserves a careful review.
  • Derived-column stage (transformation station + TransformationDraftsService): drafts are still edited in place. The card mutates the draft object the service holds, and the panel reacts through onTransformationInput(). Moving this to immutable updates is a follow-up, not part of this behaviour-preserving PR.
  • ruleFor / outlierRuleFor write while reading: they create the default pending rule on first read, as on master. The new @let reads keep the same order of first reads.
  • Docs styles that are dead on master: the .preprocessing-documentation-content .preprocessing-doc-* rules never match, because that markup is injected with [innerHTML] and has no _ngcontent attribute. They are kept as they are, in the panel. Fixing them (::ng-deep or a styled wrapper) would be a visible change and needs a decision.
  • Panel is still ~3.2k lines of orchestration: the three describe requests, apply/persist and hydrating the stored preprocessing, the editors' validation and request-config builders, and the stage add/remove side effects. The histogram loop, exports, coverage reads and section layout have moved out. Extracting the describe requests and apply/persist is a redesign rather than ports: they share the *DescribeSeq counters, the rule maps and emitProgressState, and about a dozen panel methods (43 assignment sites) still write the pending/applied rule maps directly, so the writers have to move into PreprocessingStateService together.
  • Section components use ChangeDetectionStrategy.Eager where they read the panel's plain fields through DATA_HANDLING. This matches how the inline template refreshed. They can move to OnPush once the panel state becomes signals.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3

…ingComponent

Moves src/app/pages/experiment-studio/statistic-analysis-panel to data-handling/,
renames StatisticAnalysisPanelComponent to DataHandlingComponent (selector
app-data-handling) and updates the experiment studio wiring, specs and docs.
No behavior change.
…e component

data-handling.models.ts holds the pipeline types and SUMMARY_GUIDE_ANCHORS;
utils/ holds the number formatting, the preprocessing documentation builder and
the transformation draft factory, each with a focused spec. No behavior change.
…ipComponent

details[app-how-to-strip] replaces the #howToStrip ng-template. It attaches to the
<details> element itself, so the DOM keeps the same shape and parent rules such as
`.workflow-section-body.open > *` still reach it. Its CSS moved with its markup;
the station-card margin uses :host-context. No behavior or visual change.
…erminalComponent

The "next run will use" card, the hand-off button and the unapplied-changes
guard move to data-handling/pipeline-terminal/. The component is attached to
the card element (div[app-pipeline-terminal], host class pipeline-terminal-card)
so the pipeline column, the studio reveal animation and every selector see the
same DOM as before. It is presentational: the parent passes the retention,
counts and preprocessing line in and handles continue / apply / discard / stay.

Its rules move with it; the shared width rule is split so the parent keeps
.pipeline-node.is-compact and the child owns :host(.pipeline-terminal-card).
Computed styles and screenshots match master.
…rkspaceComponent

The variable browser / statistics / charts / histogram / export workspace that the
Raw data snapshot, the Raw preview and the Processed preview all render moves from
an ng-template outlet to data-handling/summary-workspace/. It attaches to each
section's body element (div.workflow-section-body[app-summary-workspace]), so the
DOM, the studio reveal animation, data-guide anchors and spec selectors are
unchanged.

The workspace's state stays on DataHandlingComponent; sections reach it through the
new DATA_HANDLING token (data-handling.context.ts) and use Eager change detection,
so they refresh exactly when the inline template did.

Cleanups in the moved code:
- the duplicated Numerical / Categorical variable groups and the three tab buttons
  are rendered from typed constants (VARIABLE_GROUPS, SUMMARY_TABS);
- coverage is computed once per variable button (@let) instead of twice;
- the skeleton grid uses named cell arrays instead of inline number literals;
- createEmptySummary becomes the pure utils/summary-view.utils emptySummaryView.

CSS: rules for elements that only live in the workspace moved with it; shared rules
(tables, empty states, search, button:disabled) are repeated in the panel's cascade
order. The node-frame rule becomes :host-context(.pipeline-node). Computed styles and
screenshots match master in all harness scenarios.

Specs: SummaryWorkspaceComponent (10) and emptySummaryView (2).
Moves the pure part of the statistic summary out of DataHandlingComponent into
utils/statistic-summary.utils.ts and types it with the existing VariableStats
model instead of any:

- pivotByDataset: one table per variable from describe's featurewise rows. The
  metric list (SUMMARY_METRICS) now uses the payload's own keys, which replaces a
  six-level ternary that translated num_datapoints/num_missing back to
  num_dtps/num_na; the counts / categories / full-metrics branches share one row
  builder.
- variableLabel, isCategoricalVariable, enumLabelMap, categoryCounts and
  pooledTotalRows: small helpers the component re-implemented inline (the public
  variableLabel / isCategoricalVariable methods delegate, so templates and specs
  are unchanged).
- SummaryView.featurewiseRows is VariableStats[] (was any[]), which removes the
  remaining `any` casts in buildSummaryFromResponse, buildDistributionState,
  buildHistogramFromDescribeCounts and getSummaryTotalRows.

Rendered tables are unchanged (harness: identical DOM, styles and screenshots).
Spec: statistic-summary.utils (9).
…ocessing steps

hasPendingMissingChanges and hasPendingOutlierChanges each re-implemented the
per-variable comparison that prepVariableHasPendingChange already did (and walked a
code set built from three sources that always reduced to the current variables).
Each step now has one per-variable check (missingRuleHasPendingChange,
outlierRuleHasPendingChange); the step-level getters are `some()` over it, and the
"edit, else step default while nothing is applied" resolution both steps used is the
pure utils/preprocessing-state.utils pendingOrDefault.

No behaviour change: same variables, same comparison, same implicit-default rule.
Spec: pendingOrDefault (3).
Decision log entry for how Data Handling is split (attribute-hosted sections,
DATA_HANDLING context + Eager change detection, CSS repeated in cascade order,
pure utils), and module-index / conventions pointers to the new pieces.
…partials

The summary workspace repeated 22 rules of the panel's stylesheets. They now live
once in data-handling/styles/ and are @imported by both:

- statistics-panel.shared.css: the statistics panel frame and code chip
- search-and-empty-states.shared.css: inline search fields and empty states
- tables.shared.css: data tables, the statistics table frame, row errors,
  disabled buttons (and their 768px overrides)

The panel imports each partial at the top of the sheet the rules came from, so they
move only within that file; selector lists that mixed shared and panel-only
selectors (.preprocessing-search/.table-input/.table-select, .empty-state,
button:disabled, .data-table td:first-child) were split so only the shared selector
moved. The workspace imports all three ahead of its own rules (no rule of its own
targets the same element at equal specificity before them).

summary-workspace.component.css: 16.4 kB -> 12.5 kB. Harness: identical DOM,
computed styles and screenshots against master.
…position

The panel located each derived-column category's filter builder through
@ViewChildren('ruleModal') and a running index over drafts x rules, which only works
while every builder is rendered by the panel's own template, in card order.

data-handling/transformation/rule-builder-registry.ts adds the seam that lets the
categorical card move into its own component:
- RuleBuilderRegistry (provided by the panel): rule -> live builder;
- RuleBuilderForDirective (app-filter-config-modal[ruleBuilderFor]): the builder
  registers itself for the rule it edits, follows a row's rule changes and
  unregisters on destroy (ngOnChanges, so it is findable right after detectChanges,
  which addTransformationRule relies on).

commitTransformationRuleFilters, finishTransformationRule and addTransformationRule
look the builder up by rule (ruleModalAt -> ruleBuilderAt); QueryList/ViewChildren
are gone from the panel.

Specs: the 19 sites that assigned fake QueryLists now call a local
useRuleBuilders(component, [...]) helper (same positional fakes, registered by rule);
the two that read real builders use component.ruleBuilders.get(rule). New
rule-builder-registry spec (3). Harness: identical against master.
@ViewChild

Six @ViewChild(#...Section) refs and the sectionElement switch become a SectionAnchors
registry (provided by the panel) fed by a property-bound [dhSectionAnchor] directive,
so a section's scroll target can live inside a station component. No DOM change
(property binding), same scroll behaviour. Spec: section-anchors (1).
…tionComponent

Step 1 (read-only snapshot: header, Records out, the source summary workspace)
moves to data-handling/source-data-station/, attached to its pipeline-node
section (section[app-source-data-station]); the panel keeps the section element and
its state classes. Its scroll target registers through [dhSectionAnchor].

The step chrome every station shares (header, icon, title, badge, records out, body,
section frame) moves once into styles/pipeline-node.shared.css, imported by the panel
at the top of its first sheet (checked: no equal-specificity rule it used to override
sits above it) and by the station. The two rules about the snapshot itself move with
it as :host(.pipeline-node.is-readonly ...) rules; .pipeline-node .workflow-section-body
is repeated for the station as :host(.pipeline-node) .workflow-section-body.

Spec: SourceDataStationComponent (3). Harness: identical against master.
…ationComponent

Step 2 (header, folded rule overview, rule builder card with preview/apply, and the
Raw preview it feeds) moves to data-handling/cohort-filter-station/, attached to its
pipeline-node section (section[app-cohort-filter-station]); the #filterModal template
ref and every call that takes it now live in the station.

The remaining step chrome it shares with the other steps (Remove Step button, status
badge variants, the folded sub-step tree) joins styles/pipeline-node.shared.css
(moved to the top of the panel's first sheet; the one equal-specificity overlap the
check reports, `.btn-add-step i`, never matches the same element). The match chip
rules move with the station; .pipeline-node .workflow-section-body is repeated as a
:host rule.

Spec: CohortFilterStationComponent (3). Harness: identical against master (the s4
filter scenario is timing-sensitive under 4 workers and was confirmed on its own).
…ionComponent

Step 3 (header, folded sub-step rail with its add rows, the missing-values /
outlier / longitudinal station cards, the #prepFooter action bar template and the
Processed preview) moves to data-handling/preprocessing-station/, attached to its
pipeline-node section (section[app-preprocessing-station]). SummaryWorkspace is now
imported by the station only.

Styles: the sub-step status badge rules join styles/pipeline-node.shared.css and
.glass-feedback (with its feedbackSlideIn keyframes, which emulated encapsulation
scopes per stylesheet) joins styles/search-and-empty-states.shared.css; both are
used by the panel and the station. The step's own rules move with it.
.pipeline-node .workflow-section-body is repeated as a :host rule.

Left in the panel on purpose: the .preprocessing-documentation-content
.preprocessing-doc-* rules. That markup is injected with [innerHTML], so it never
carries the _ngcontent attribute and these rules do not match on master either; it
is kept as-is and flagged in the PR rather than changed here.

Interim: the station's compiled stylesheet is over the 20 kB warning budget (the
build reports it under its first @import); the next commits split the station's
sub-steps out and bring it back under.

Spec: PreprocessingStationComponent (3). Harness: identical against master.
…preprocessing rails

The missing-values, outlier and longitudinal steps each carried a ~60-line copy of
the same variable rail (heading + count, search, state groups, rows, no-match
state). They now share aside[app-prep-variable-rail]
(preprocessing-station/prep-variable-rail/), configured with inputs (step, heading,
searchLabel, searchId, emptyHint, groupStates). What really differs per step stays
in the step's template: its batch menu is projected (.batch-menu) and its per-row
markup is given as typed templates, <ng-template prepRailSlot="lead|trail|action"
let-variable> (PrepRailSlotDirective with a context guard, so `variable` is a
VariableRow), read through contentChildren() + computed().

The rail reads each row's state label once (@let) instead of three times.
The DOM is unchanged: the outlier and longitudinal rails gain a state-default class
binding that never applies (only missing values have a Default state), and the
longitudinal groups still carry no data-state.

The grouping itself moves out of the component into
utils/preprocessing-state.utils.ts as groupPreprocessingVariables(variables,
stateOf): a table of the four groups in display order plus one pass over the
variables, replacing the private buildPreprocessingGroups and its three predicate
arguments.

Styles: the rail's rules move from the station's sheet as
:host(.preprocessing-browser) ... (same specificity once compiled), with the
search/empty-state partial and the icon nudge. A copy of
.preprocessing-browser .empty-state-block that no longer matched anything in the
station is dropped.

Specs: PrepVariableRailComponent (3), groupPreprocessingVariables (2). Harness:
identical against master.
… own components

Each sub-step's workspace (variable rail + detail panel) moves out of the station,
attached to its existing <section>:
- section[app-missing-values-step]: the "Set all" batch menu and coverage/action rail
  slots, the action choice, the fill value, and the coverage / distribution /
  category-frequency evidence;
- section[app-outlier-step]: the "Batch" menu and on/off rail switch, the method /
  clip / fold controls, the bounds strip, the distribution aside and the report;
- section[app-longitudinal-step]: the type-label rail and the strategy select.
The station keeps the step header, folded rail, the station cards with their docs,
add/remove buttons, the longitudinal visit controls and the Processed preview.

Quality:
- the selected variable's rule and state label are read once per render (@let rule /
  @let state) instead of 6 / 10 / 2-3 times per step;
- the redundant `action === 'constant'` re-check inside the constant branch is gone;
- the outlier label clamp (12% / 88%), written out twice inline, is one method,
  OutlierStepComponent.boundLabelShift().

Styles: the rules the steps share (detail panel, sections, headings, controls,
evidence, stat strips, rail action line, batch menu) become
styles/preprocessing-step.shared.css, imported by each step; .table-input /
.table-select join styles/tables.shared.css (the station also uses them). The
conflict check reports no real overlap (three `... i` / `... > span` pairs never
match the same element). Each step's own rules move with it; the outlier width
rules become :host(.outlier-preprocessing-workspace) .preprocessing-detail-panel.
The station drops statistics-panel.shared, which it no longer renders, and its
compiled stylesheet is back under the 20 kB budget.

Specs: MissingValuesStepComponent (3), OutlierStepComponent (3),
LongitudinalStepComponent (2). Harness: identical against master.
…tationComponent

Step 4 (header, folded derived-column rail, the nominal column cards, the type chooser
and the K-means card) moves to data-handling/transformation-station/, attached to its
pipeline-node section (section[app-transformation-station]). The #transformationStats
template, used only by the K-means card, moves with it. kmeans-cluster-source/ moves
under transformation-station/ (paths only).

Each nominal card's editor body (name, category rows with their rule builders,
fallback) is CategoricalColumnCardComponent, attached to the card's
div.transformation-create and given the draft as a signal input. The rule builders
keep registering through [ruleBuilderFor] (RuleBuilderRegistry), so no positional
lookups are involved. Quality:
- the three inline `draft.x = $event; dh.onTransformationInput()` handlers are
  rename() / setCategory() / setFallback();
- the card reads the draft once (@let column), which also gives template type
  narrowing on openRuleIndex;
- in the station, each card's status and "has new work" are read once (@let)
  instead of 3 and 5 times.

Styles: the pipeline connector rules join styles/pipeline-node.shared.css (also
drawn between derived-column cards); the rule table grid that the K-means block and
the nominal card share becomes styles/transformation-rules.shared.css; everything
else moves with its owner. The panel drops the imports it no longer uses.

Specs: TransformationStationComponent (2), CategoricalColumnCardComponent (3).
Harness: identical against master.
…-column drafts

The draft list and every question asked of it move out of the 4.3k-line panel into
a component-scoped service (transformation-station/transformation-drafts.service.ts,
provided by DataHandlingComponent): isShown, status, hasNewWork, configs, payload,
hasDuplicateCodes, hasAnyWork, hasPendingChange, rulesNeedFilters, firstHidden, and
the in-place edits add / remove / reset / addRule / removeRule.

The pure parts live in utils/transformation-draft.utils.ts and are now exported and
typed: draftHasWork, hasUnfinishedCategory, transformationConfigFor (returning a
TransformationColumnConfig instead of Record<string, unknown>),
transformationDraftStatus (a TransformationDraftStatus label union),
hasDuplicateColumnCodes and firstFilterCondition.

The panel keeps its public members (templates and specs are unchanged;
transformationDrafts is a getter/setter onto the service) and keeps the side effects:
store writes, category counts, rule builders and change detection. Removed from the
panel: transformationConfigs, draftHasWork, hasUnfinishedCategory,
firstFilterCondition, transformationConfigPayload and five method bodies now
one-line delegations.

Specs: TransformationDraftsService (5), transformation draft utils (3).
…sing rule maps

The pending and applied rules of the three sub-steps (missing values, outliers,
longitudinal strategies) and the session's "applied by the user" flag move into a
component-scoped service (preprocessing-station/preprocessing-state.service.ts,
provided by DataHandlingComponent), together with the reads and comparisons on
them: missingRule / outlierRule (lazy step defaults), hasAppliedMissing,
hasAppliedOutlier, isUsingDefaultMissing, missingHasPendingChange,
outlierHasPendingChange (built on pendingOrDefault).

The pure rule helpers become exported, tested functions in
utils/preprocessing-state.utils.ts: defaultMissingRule, emptyMissingRule,
isDefaultMissingRule, serializeMissingRule, cloneMissingRules.

The panel keeps its member names as getter/setter pairs onto the service
(pendingPreprocessingRules, appliedPreprocessingRules, pending/appliedOutlierRules,
pending/appliedLongitudinalStrategies), so loading, hydration, apply and
persistence are untouched; ruleFor, outlierRuleFor, isPreprocessingVariableUsingDefault
and the hasApplied* helpers are one-line delegations, and seven private helpers are
gone from the panel. Specs that read the private userPreprocessingApplied now read
component.preprocessingState.userApplied.

Specs: PreprocessingStateService (4), missing-values rule helpers (2).
…faces

The three summary surfaces (Source snapshot, Raw preview, Processed preview), the
selection each was loaded for, and the describe-response -> SummaryView assembly move
into a component-scoped service (data-handling/statistic-summary.service.ts, provided
by DataHandlingComponent): get(kind), loadedKey, rawKeyFor(filterOverride),
fromDescribeResponse(response, current, variables) and the box-plot / grouped-bar
distribution state it builds (now typed as a DistributionState pick).

Pure helpers join utils/summary-view.utils.ts: statisticBlockKey,
nextStatisticSelection and selectionKey (was the private buildSelectionKey).

The panel keeps rawSummary / processedSummary / sourceSummary and the three key
fields as getter/setter pairs onto the service, so requests, histograms and exports
are untouched; buildDistributionState, buildSelectionKey, statisticBlockKey and
nextStatisticSelection leave the panel, which no longer injects ChartBuilderService.
Specs that called the private buildSummaryFromResponse / rawSummaryKeyFor or wrote
the private key now use component.statisticSummary; the pipeline spec's summaryOf
fixture is typed as a SummaryView.

Specs: StatisticSummaryService (2), summary view helpers (2).
… CSS partials

Updates the decision-log entry on Data Handling sections (station and sub-step
components, the three component-scoped feature services, the registries that
replaced view queries, the shared CSS partials), the module index and the
stylesheet convention for @import partials.
Ports master's ordered transformation steps and PCA column creator onto the split:
the transformation station renders one card per store step, and
TransformationDraftsService owns the step-keyed drafts, card state, titles,
name-clash checks, removal rules and the chooser.
…tation CSS

- remove addTransformationDraft, kmeansClusterStatusLabel and
  selectedSummaryChartTypeLabel (no callers)
- delete the .preprocessing-documentation-content .preprocessing-doc-* rules:
  the markup is injected with [innerHTML], so emulated encapsulation never matches them
- fix members that lost their indentation in earlier extractions
…isticsService

The panel keeps only the side effects; the service owns the placeholder shapes, the
single describe over every derived column, K-means column views and invalidation.
The panel passes its pending preprocessing and labels through ports; the transformation
views read the service's signals instead of the panel's plain fields.
… util

mapBinsToEnumLabels, withDefaultDropFor, the describe-counts histogram, the failed-response
message and the response parser leave the panel as pure functions; the panel keeps the
request, caching and change detection.
Comment on lines +14 to +21
template: `
<aside app-prep-variable-rail class="statistics-browser preprocessing-browser" [step]="step" heading="Variables · action"
searchLabel="Search preprocessing variables" [searchId]="searchId" [groupStates]="groupStates" emptyHint="Clear the search.">
<div class="batch-menu">batch</div>
<ng-template prepRailSlot="lead" let-variable><small class="lead">{{ variable.code }}</small></ng-template>
<ng-template prepRailSlot="action" let-variable><span class="prep-rail-action">{{ variable.code }}-action</span></ng-template>
</aside>
`,
Comment on lines +14 to +21
template: `
<aside app-prep-variable-rail class="statistics-browser preprocessing-browser" [step]="step" heading="Variables · action"
searchLabel="Search preprocessing variables" [searchId]="searchId" [groupStates]="groupStates" emptyHint="Clear the search.">
<div class="batch-menu">batch</div>
<ng-template prepRailSlot="lead" let-variable><small class="lead">{{ variable.code }}</small></ng-template>
<ng-template prepRailSlot="action" let-variable><span class="prep-rail-action">{{ variable.code }}-action</span></ng-template>
</aside>
`,
@Component({
imports: [SectionAnchorDirective],
providers: [SectionAnchors],
template: `@if (shown()) {<div id="target" [dhSectionAnchor]="section()"></div>}`,
@Component({
imports: [SectionAnchorDirective],
providers: [SectionAnchors],
template: `@if (shown()) {<div id="target" [dhSectionAnchor]="section()"></div>}`,
@Component({
imports: [StubBuilderComponent, RuleBuilderForDirective],
providers: [RuleBuilderRegistry],
template: `@for (rule of rules(); track $index) {<app-filter-config-modal [ruleBuilderFor]="rule" />}`,
@Component({
imports: [StubBuilderComponent, RuleBuilderForDirective],
providers: [RuleBuilderRegistry],
template: `@for (rule of rules(); track $index) {<app-filter-config-modal [ruleBuilderFor]="rule" />}`,
coverageFor, blockCoverage, selectionCoverage, missingShareNote,
coverageByDataset, distributionFor and categoryCountsFor only ever read a
pivot block, so they become pure functions in utils/summary-coverage.utils.ts
(with the severity/percent/bar/title helpers and the group label that sit
beside them). The panel keeps one-line delegates so the sections, the studio
and the specs call the same members as before.

Dead code dropped on the way: the panel's metricLabel map (SUMMARY_METRICS
already carries the labels), statisticMetricText/Number/MissingValue and
isCountMetric. displayNumber and isCountMetricLabel join the existing format
and summary utils.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
…ummaryHistogramService

ensureHistogramForBlock, refreshActiveHistogramPreview and the histogram
preprocessing choice become SummaryHistogramService, provided by the panel
next to StatisticSummaryService. The panel hands it what it owns through six
ports: the selected block, what a block is plotted as (code, split, variable
and split label, the last two read when the answer lands), the pending
preprocessing, the Cohort Filtering preview filter, group labels and a redraw.
Requests, payloads and cache keys are unchanged. Teardown uses
takeUntilDestroyed instead of the panel's destroyed subject.

Legacy removed on the way: histogramSubscriptionsByVariable never held a real
subscription (only Subscription.EMPTY once a request answered), so it is now
histogramSettledByVariable, a plain flag record with the same gate. The
clearHistogramSubscriptions calls that ran right before a summary was replaced
by a fresh one, and in ngOnDestroy, did nothing and are gone; the one that
mattered (the Source refetch) resets the flags in place.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
…ExportService

exportAllDescriptiveToPDF and exportSummaryToCSV only ever read the surface they
export plus the studio and the two export services, so they become
SummaryExportService with no ports. The panel keeps both methods as one-line
delegates (the summary workspace calls them), and isExporting stays as a getter
over the service's exporting signal so the shell's overlay reads the same.
The off-screen chart containers stay in the panel's template; the service's doc
comment names the class contract the PDF reads.

The pure parts (summaryKindLabel, datasetDisplayLabel, labelDatasetColumns,
summaryCsvRows) move to utils/summary-view.utils with specs. The PDF path had no
spec before; the service spec covers the context rows per surface, the chart
containers it picks, and the failure and busy states.

Dead after the move and removed: the panel's mipVersion and RuntimeEnvService,
pivotBlocksWithDatasetLabels, and its PdfExportService/CsvExportService fields.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
… the section layout into a util

SectionAnchors already knew each section's scroll target, so it now owns the
scroll request too: requestScroll() waits, through an effect, for the render
that follows an open/close and a newer request replaces one that has not run.
The panel drops its request signal, id counter, effect and scrollIntoView
helper; its stage Close actions call requestScroll as before.

What goToSection does to the page before its side effects (which flags open,
which preview shows, which stage it adds) is a pure function of the section,
so it becomes utils/section-layout.utils (sectionLayout, STEP_SECTIONS,
CLOSED_SECTIONS, isStepShown) with a spec. isStepOpen, the initial state and
collapseAllWorkflowSections use it. The sectionOpen/summaryExpanded/addedSteps
signals stay panel fields, which the sections, the studio and the specs read.
The repeated 'filters' | 'setup' | 'transformation' unions become StageStep.

addStep/removeStep stay: removeStep resets preprocessing and transformation
state and would need that state's writers moved with it. Docs record what is
left in the shell and why.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
… innerHTML

The SAST gate blocks on the insecure-innerhtml rule even for static strings in a
spec. createElement builds the same off-screen containers the PDF export reads.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
The entry claimed every extraction was compared against master. The shell
slimming commits after the master merge were not run through the local
DOM/computed-style/screenshot harness; their coverage is specs and CI e2e.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EtXkKpquRRkLrfA8GJBfR3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants