Logo FS-Skia-UI

Refactoring Analysis

This report assesses whether the repository warrants refactoring after the Breakout demo feedback work. The answer is yes, but the refactor should be targeted. The repository already has a useful package split and strong governance gates; the main problem is not architectural absence, it is localized accumulation in orchestration files, generated template source, compatibility runtime code, and evidence/report helper duplication.

Executive Assessment

Refactoring is warranted in three areas:

  1. Evidence and report-writing helpers are repeated across generated product code, viewer code, testing helpers, build automation, and test support.
  2. Several files have become coordination hubs that carry unrelated responsibilities in one compilation unit.
  3. Generated template source is doing too much in one Program.fs, which makes generated guidance harder to audit and increases risk when adding evidence commands.

Refactoring is not warranted as a broad rewrite. The package boundaries are mostly coherent, test coverage is broad, and many apparent repetitions are the result of deliberate package or template isolation. The correct first move is a behavior-preserving extraction pass with public API stability as a hard constraint.

Evidence Snapshot

The largest implementation files are concentrated in a small number of areas:

File

Lines

Primary concern

build.fsx

4071

Target model, process execution, template packaging, generated scanning, package validation, evidence writing, and process-health policy all live in one script.

src/Lib/Library.fs

2408

Compatibility package combines scene helpers, parity reporting, Vulkan host implementation, and legacy viewer runtime.

src/SkiaViewer/SkiaViewer.fs

2381

Public viewer implementation, legacy conversions, diagnostics, window lifecycle, app hosting, visual evidence, and screenshot evidence are co-located.

tests/SkiaViewer.Tests/Tests.fs

1452

Large behavioral test surface mirrors the large viewer implementation surface.

src/Lib/KeyboardInput.fs

1398

YAML parsing, validation, runtime update, display model, rendering, and bigram analysis share one module.

template/base/src/Product/Program.fs

1276

Multiple template profiles, product app state, layout evidence, visual evidence, launch evidence, screenshot evidence, and CLI dispatch live together.

src/Testing/Testing.fs

879

Generated product assertions, consumer validation, layout validation, warning classification, readiness discovery, and report helpers share one file.

src/Scene/Scene.fs

773

Scene DSL, deterministic evidence, shape evidence, and layout evidence classification are combined.

The concentration itself is not automatically wrong. The issue is that several files now combine type contracts, infrastructure, validation policy, host effects, and generated workflow details. That makes future feature work more expensive because a small evidence or template change requires reading a large unrelated surface.

Current Architecture Strengths

The existing structure gives a good base for incremental refactoring:

These strengths argue for a conservative refactor. Preserve contracts, move responsibilities, and let existing verification detect accidental behavior change.

Debt Classes

1. Evidence And Report Duplication

There are multiple ways to create parent directories, write key-value reports, detect images, and classify evidence results:

Helper family

Examples

Parent directory creation

ensureParent in build.fsx, ensureParentDirectory in SkiaViewer.fs, local write helpers in generated product code.

Report writing

EvidenceReports.write in Testing.fs, writeEvidenceReport, writeLaunchEvidenceReport, writeLaunchFailureReport, and writeBoundedSmokeReport in template/base/src/Product/Program.fs, markdown/verdict writers in build.fsx.

Image checks

isPngFile in generated product tests and template source, isPngPath and imageDecodable in viewer implementation, output-field validation in testing helpers.

Geometry checks

Rectangle intersection and containment helpers appear in Scene.fs and generated product code.

Parsing helpers

parseScalar and parseInlineList exist in both build.fsx and tests/Governance.Tests/TestSupport.fs.

Process execution

runProcess variants exist in build.fsx, template/base/build.fsx, smoke tests, and governance test support.

Some duplication is acceptable. Generated projects should not be forced to depend on repository-only test support, and package boundaries prevent every helper from moving to one place. The problem is that there is no documented policy that distinguishes intentional local copies from accidental copies.

Recommended policy:

2. Oversized Coordination Modules

The largest files are not just long; they cross responsibility boundaries.

build.fsx is the highest-value extraction target. It currently contains:

The FAKE entrypoint should remain the stable public command surface, but helper logic should move into loaded script modules. This keeps build.fsx readable as the target graph rather than a repository-wide implementation container.

src/SkiaViewer/SkiaViewer.fs is the main runtime hotspot. It contains:

The implementation has clear subdomains that can be split without changing the public .fsi: diagnostics, host capability detection, visual evidence, window-behavior validation, and generated app host adapter.

template/base/src/Product/Program.fs is the main generated-source hotspot. It mixes profile-conditional source, sample product behavior, command-line evidence generation, and report writing. The template should generate multiple source files so each profile still produces simple code. A generated Program.fs should read as a product entrypoint, not as a mini framework.

3. Compatibility Package Accumulation

src/Lib/Library.fs is large because it remains the compatibility package for lower-level paths. It includes public scene-like types, parity reporting, Vulkan host runtime, and legacy viewer behavior.

This file is a real hotspot, but it is not the safest first refactor. It is close to public API compatibility, and the newer split packages already depend on it in some places. The better sequence is:

  1. stabilize evidence/template cleanup,
  2. split SkiaViewer implementation internals,
  3. only then consider a compatibility migration plan for FS.Skia.UI.

4. Generated Product Complexity

The generated product template currently supports multiple profiles from a single source file using conditional comments. This creates two problems:

The app profile also contains a small duplicated transition case:

| Paused, Escape -> Main, ...
| Paused, Escape -> Main, ...

That specific duplicate is low-risk to remove, but it is a symptom of the larger issue: when the generated product file carries too many unrelated examples, small mistakes become harder to see.

5. Test Support Mirrors Production Sprawl

Large tests are not inherently bad here because the repo depends on governance and evidence checks. However, test helper duplication increases maintenance cost:

Repository-local test support should be extracted where it does not blur package boundaries. This is especially valuable for process execution and fixture parsing because those helpers encode governance behavior.

Refactoring Priorities

Priority 1: Generated Evidence And Template Cleanup

This is the best first slice because it directly addresses duplication surfaced by the Breakout feedback and has strong existing validation.

Actions:

Expected payoff:

Primary risks:

Priority 2: Build Script Decomposition

The build script should remain the command surface, but its helper implementation should be modular.

Actions:

Expected payoff:

Primary risks:

Priority 3: SkiaViewer Internal Boundary Split

This should be done after template/evidence cleanup because it touches runtime behavior.

Actions:

Expected payoff:

Primary risks:

Priority 4: Testing Helper Consolidation

Actions:

Expected payoff:

Priority 5: Compatibility Package Review

This is a later, explicit design project, not a cleanup task.

Actions:

Expected payoff:

Primary risks:

Detailed Recommendations

Prefer Extraction Over Abstraction

Most of the current debt comes from co-location, not from bad algorithms. Extract files and modules first. Add new abstractions only when there are two or more real callers and the contract is obvious.

Good first extractions:

Avoid first:

Make Duplication Policy Explicit

Before deleting every duplicate helper, classify it:

Classification

Meaning

Action

Intentional template copy

Needed so generated products are standalone or profile-light.

Keep, but keep tiny and documented.

Package-boundary copy

Prevents an inappropriate dependency between runtime packages.

Keep or move to the lower-level package only if dependency direction stays correct.

Repository-local duplication

Same behavior repeated in build/test support.

Consolidate.

Drift-prone semantic copy

Same report or evidence semantics repeated with different fields or status behavior.

Consolidate first.

This policy matters because the repository intentionally produces standalone generated products. Not every repeated helper is a bug.

Keep Public API Stable During Cleanup

The first refactoring pass should not change:

If a cleanup requires any of those changes, promote it to a separate feature with explicit package surface and template migration evidence.

Use The Existing Verification System As Refactor Guardrails

The repository already has the right gates for this work. A refactor should be accepted only when the relevant gates prove behavior stability:

Change area

Required checks

Generated product source split

TemplateCheck, GeneratedGuidanceCheck, TemplateDrift, generated product tests

Public package internals only

Targeted package tests, PackageSurfaceCheck, FsiTranscripts

Build script decomposition

Dev, focused target checks, Verify if target graph or readiness paths change

Evidence/report behavior

Testing.Tests, generated evidence command tests, readiness convention checks

SkiaViewer internals

SkiaViewer.Tests, bounded smoke where host supports it, unsupported-host evidence checks

Proposed Implementation Sequence

Phase 0: Baseline

Phase 1: Generated Product Report Cleanup

This phase gives a quick payoff with minimal file movement.

Phase 2: Generated Product File Split

This phase reduces the most visible template bloat.

Phase 3: Build Script Internal Modules

This phase reduces the largest single repository maintenance hotspot.

Phase 4: SkiaViewer Internals

This phase is higher risk and should wait until template/report cleanup is stable.

Phase 5: Compatibility Package Decision

Acceptance Criteria

A refactoring pass should be considered successful only if:

Anti-Goals

Do not use this refactor to:

Final Recommendation

Proceed with a phased refactor, starting with generated evidence/report cleanup and template file splitting. That path directly addresses the bloat visible to template consumers and has the strongest verification coverage. Defer SkiaViewer internal decomposition until the generated evidence path is stable. Defer compatibility package restructuring until it has its own design decision and migration plan.

The success metric is not fewer lines by itself. The goal is that future evidence, screenshot, generated app, or template-profile changes can be reviewed inside a small owned module instead of requiring a scan through several thousand lines of mixed responsibilities.

Type something to start searching.