Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/engineering/incremental-build-equivalence-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Implementation Plan: Incremental Build Equivalence

## Context

Spec: [Incremental Build Equivalence](incremental-build-equivalence-spec.md)

Issue: [#686](https://github.com/cssbruno/GoWDK/issues/686)

## Assumptions

- The first checked-in slice can live in `internal/buildgen` because current
incremental SPA generation is implemented there.
- `gowdk-build-report.json` records progress-path information by design, so the
equivalence helper compares artifact-relevant report events and exact bytes
for every other file.

## Proposed Changes

- Add a reusable scenario helper for clean-versus-incremental output tree
comparison.
- Compare served output plus sibling compiler report output.
- Add deterministic multi-step coverage for route moves, component changes,
content-hashed assets, and layout changes.
- Remove stale asset-manifest files during incremental output publication.

## Files Expected To Change

- `internal/buildgen/equivalence_test.go`
- `internal/buildgen/build.go`
- `internal/buildgen/manifests.go`
- `docs/engineering/testing.md`
- `docs/engineering/incremental-build-equivalence-spec.md`
- `docs/engineering/incremental-build-equivalence-plan.md`

## Data And API Impact

- No public API changes.
- Incremental builds now remove stale generated asset files that were present in
the previous asset manifest but are absent from the current generated asset
set.

## Tests

- Unit: focused `internal/buildgen` equivalence scenario.
- Integration: existing generated-output determinism script remains separate and
continues to cover clean-build determinism.
- End-to-end: generated app equivalence remains a follow-up.
- Manual: none.

## Verification Commands

```sh
go test ./internal/buildgen -run 'TestIncrementalBuildMatchesCleanBuildEquivalence|TestBuildIncremental' -count=1
go test ./internal/buildgen -count=1
go build ./cmd/gowdk
```

## Rollback Plan

- Revert the stale asset cleanup and equivalence test if incremental publication
needs to return to previous behavior.
- Existing clean build output remains unaffected by this slice.

## Risks

- The current report projection could miss a future artifact-relevant build
report event. Add such event kinds to the projection with the generator change
that introduces them.
- The deterministic matrix is not yet broad enough for generated app, split
target, backend endpoint, config, or WASM output equivalence.
97 changes: 97 additions & 0 deletions docs/engineering/incremental-build-equivalence-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Feature Spec: Incremental Build Equivalence

## Problem

`gowdk dev` serves incrementally generated output. Focused incremental tests can
pass while stale files, manifests, or generated assets remain from an earlier
source state. For the same final project state, incremental output must converge
to the compiler-owned artifact tree produced by a clean build.

## Goals

- Compare incremental output with a clean build of the same final state.
- Cover complete served output, compiler-owned report output, file bytes, and
meaningful file modes.
- Include manifests and report-relevant metadata, not only HTML.
- Identify the edit step and divergent file when equivalence fails.
- Keep intentional normalization rules narrow and documented.

## Non-Goals

- Requiring every edit to use the incremental path; full rebuild fallback
remains valid.
- Comparing timestamps, wall-clock timings, or debug progress logs.
- Optimizing invalidation before correctness is covered.

## Users And Permissions

- Primary users: GOWDK maintainers and contributors changing build generation or
dev-loop invalidation.
- Roles or permissions: repository test access.
- Data visibility rules: test fixtures must not include secrets or local
environment-specific data.

## User Flow

1. A test defines an initial compiler source state.
2. The harness builds that state into an incremental output directory.
3. Each scenario step mutates source/config state and runs the incremental
build path.
4. The harness builds the same current state into a fresh clean directory.
5. The harness compares the incremental tree with the clean tree and reports the
first divergent file for the responsible step.

## Requirements

### Functional

- Compare relative file paths, file bytes, and permission bits.
- Detect files present only in incremental output or only in clean output.
- Compare the served output directory and sibling `.gowdk/reports/<target>/`
compiler report directory.
- Canonicalize only `gowdk-build-report.json` fields that are path-dependent or
progress-path-dependent between clean and incremental runs.
- Exercise at least one multi-step scenario where edit order can expose stale
state.

### Non-Functional

- Performance: focused scenarios must stay cheap enough for normal Go test CI.
- Reliability: failures must name the step and artifact path.
- Accessibility: not applicable.
- Security/privacy: generated security posture reports are compared as compiler
output, but fixtures do not contain secret material.
- Observability: failure output is file-level and byte-offset-level.

## Acceptance Criteria

- [x] A reusable test harness compares incremental output with clean output.
- [x] Comparison covers artifact paths, bytes, modes, stale-file absence, served
output, and compiler-owned report output.
- [x] Manifest equivalence is included through exact file comparison.
- [x] Build report equivalence is included through a narrow canonical report
projection.
- [x] A deterministic multi-edit scenario covers route, component, asset, and
layout changes.
- [ ] Generated app and split frontend/backend output equivalence are covered.
- [ ] Backend endpoint, contract, config, selected-target, and WASM scenarios are
added.
- [ ] A bounded seeded state-machine test is added.

## Edge Cases

- Content-hashed asset turnover must remove stale emitted files.
- Route moves must remove stale page outputs.
- Security manifests live outside the served output directory and must still be
compared.

## Dependencies

- Internal: `internal/buildgen` clean and incremental SPA builders.
- External: none.

## Open Questions

- Which generated app targets should be the first appgen equivalence slice?
- Should broader state-machine coverage run in every pull request or only in a
scheduled lane once the deterministic matrix is larger?
11 changes: 10 additions & 1 deletion docs/engineering/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ must pass `--config <file>`.
| Parser fuzz smoke | `scripts/test-parser-fuzz.sh` | Parser, tokenizer, syntax recovery, and diagnostic recovery changes. |
| Generated app integration | `scripts/test-generated-app-integration.sh` | Generated binary, SSR, action, fragment, CSRF, and contract adapter changes. |
| Generated output determinism | `scripts/test-generated-output-determinism.sh` | Generated HTML, manifest, report, inspect, or route/sitemap output changes. |
| Incremental build equivalence | `go test ./internal/buildgen -run TestIncrementalBuildMatchesCleanBuildEquivalence -count=1` | Incremental SPA output, stale generated asset cleanup, route cleanup, or build report comparison changes. |
| Runtime race detector | `scripts/test-runtime-race.sh` | Shared-state runtime packages, lifecycle, contracts, SSE, rate limiting, trace, or testkit changes. |
| VS Code extension syntax | `node --check editors/vscode/extension.js` | Editor extension changes and broad verification. |
| VS Code extension behavior | `node --test editors/vscode/*.test.js` | Editor extension pure helper changes and broad verification. |
Expand Down Expand Up @@ -82,6 +83,12 @@ must pass `--config <file>`.
- `scripts/test-generated-output-determinism.sh` is the generated output and
report determinism slice. It compares two clean builds plus manifest, sitemap,
routes, and asset-graph report output.
- `TestIncrementalBuildMatchesCleanBuildEquivalence` compares an incremental
output tree with a clean build of the same final state. It includes served
output, sibling compiler reports, manifests, file bytes, and file modes; the
only normalization is the documented `gowdk-build-report.json` projection for
progress-path events that intentionally differ between clean and incremental
runs.
- `scripts/test-runtime-race.sh` is the focused race-detector slice. It runs an
explicit package list under `go test -race -count=1` and fails preflight when
a selected package has no tests.
Expand Down Expand Up @@ -143,7 +150,9 @@ runtime integration, race detection, or nondeterministic output.
generated file modification times so local dev loops do not retrigger on
identical output.
- `internal/buildgen` tests cover incremental changed-page rendering, complete
route manifest refreshes, and stale route output cleanup.
route manifest refreshes, stale route output cleanup, stale generated asset
cleanup, and clean-versus-incremental output equivalence for the current SPA
slice.
- `internal/project` tests cover literal `gowdk.config.go` parsing for source
discovery, module source groups, build output, and build targets.
- Nested optional adapter modules cover Chi, Echo, Fiber, Gin, Redis Streams,
Expand Down
2 changes: 1 addition & 1 deletion docs/product/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ implemented.
| Component CSS | Make component CSS explicit, compiler-scoped, and documented; Tailwind and processors remain optional. | Partial |
| Accessibility | Add accessibility diagnostics as compiler warnings with stable codes and spans. | Partial — `missing_img_alt`, `missing_form_label`, `empty_link_text`, `missing_button_type`, and `heading_order_skip` warn on literal view markup in pages, components, and layouts. Broader ARIA and full WCAG rule coverage remain outside the current compiler slice. |
| Diagnostics and LSP | Expand diagnostic catalogue before broad parser recovery; prioritize hover, semantic tokens, go-to-definition, and route/type navigation. | Partial — the diagnostic registry, `gowdk explain`, JSON check output, safe fix metadata, exact ranges for high-value parser/IR-backed diagnostics, LSP diagnostics/formatting/completions/hover/definitions/references/code actions/semantic tokens, dirty-buffer `g:command`/`g:query` binding diagnostics, CLI route/sitemap/inspect reports, and [diagnostics/navigation contract](diagnostics-and-navigation.md) exist; parser recovery, remaining aggregate/addon exact-span gaps, direct markup-family emitted codes, and workspace route/type navigation remain planned. |
| Testing and scaffolding | Add optional Go handler tests, generated app smoke tests, template/addon selection, and editable generated examples. | Partial — `gowdk init --tests` writes a starter `go.mod` and non-skipping generated app smoke test, `gowdk test` builds temporary generated output/app/binary artifacts and runs ordinary Go tests with `GOWDK_TEST_*` context, `runtime/testkit` provides HTTP scenario helpers, cookie-preserving clients, response assertions, and in-memory contract registry/event assertions, `examples/contracts/patients/contracts_test.go` demonstrates command event capture, `gowdk audit --run` builds a temporary generated app and runs generated runtime audit tests, and explicit repository scripts cover parser fuzz smoke, generated-app integration, generated-output/report determinism, and focused runtime race detection. Broader IR-generated endpoint test files and first-class browser/E2E scaffolds remain planned. |
| Testing and scaffolding | Add optional Go handler tests, generated app smoke tests, template/addon selection, and editable generated examples. | Partial — `gowdk init --tests` writes a starter `go.mod` and non-skipping generated app smoke test, `gowdk test` builds temporary generated output/app/binary artifacts and runs ordinary Go tests with `GOWDK_TEST_*` context, `runtime/testkit` provides HTTP scenario helpers, cookie-preserving clients, response assertions, and in-memory contract registry/event assertions, `examples/contracts/patients/contracts_test.go` demonstrates command event capture, `gowdk audit --run` builds a temporary generated app and runs generated runtime audit tests, and explicit repository scripts/tests cover parser fuzz smoke, generated-app integration, generated-output/report determinism, incremental-versus-clean SPA output equivalence, and focused runtime race detection. Broader IR-generated endpoint test files, generated app equivalence coverage, and first-class browser/E2E scaffolds remain planned. |
| Deployment and operations | Prefer docs and optional generators for static hosts, Docker, systemd, reverse proxies, CDN policy, health checks, metrics, logging, binary deploy, rollback, and CSRF secret rotation. | Partial — [deployment.md](../reference/deployment.md) documents static output, one-binary, generated Docker contexts, split frontend/backend, backend-only, Docker, systemd, reverse proxy, CDN/cache, health, metrics, logging, readiness, graceful shutdown, artifact layout, rollback, CSRF secret rotation, backup ownership, and incident boundaries. `gowdk build --docker` emits a minimal non-root Dockerfile and `.dockerignore` beside a compiled one-binary artifact. `gowdk build --deploy-recipe` and `Build.Targets[].DeployRecipes` emit optional static-host, systemd, Caddy, Nginx, and split frontend/backend starting points without owning secrets, domains, TLS, CDN policy, storage, backups, incident response, or rollout logic. Release workflows pin third-party action SHAs, extension publishing uses locked local `vsce`, `govulncheck` is pinned through `tools/govulncheck`, and docs distinguish convenience and high-assurance install paths. |
| Full-page hydration | Keep full-page hydration out of the repository core; use static pages, progressive enhancement, server fragments, and explicit islands. | Intentionally out of scope |
| Island ergonomics | Improve compiler-owned island syntax, lifecycle cleanup, focus helpers, local batching, and diagnostics without exposing arbitrary JavaScript as the app contract. | Partial — generated JS islands support idempotent mount/remount, cleanup, lifecycle/effect blocks, bounded refs such as focus/blur/scroll, local batching, and diagnostics; broader HMR-style ergonomics remain deferred. |
Expand Down
8 changes: 8 additions & 0 deletions internal/buildgen/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,10 @@ func BuildIncrementalFromValidatedProgram(config gowdk.Config, validated compile
if err != nil {
return Result{}, reporter.fail("manifest", err)
}
previousAssets, err := readAssetManifestIfExists(outputDir)
if err != nil {
return Result{}, reporter.fail("manifest", err)
}
reporter.debug("manifest", "previous_route_manifest_read", "previous route manifest read", BuildEvent{
Data: map[string]string{"routes": fmt.Sprint(len(previousRoutes.Routes))},
})
Expand Down Expand Up @@ -894,6 +898,10 @@ func BuildIncrementalFromValidatedProgram(config gowdk.Config, validated compile
result.RobotsPath = robotsPath
reporter.info("seo", "robots_written", "robots.txt written", BuildEvent{Path: eventPath(outputDir, robotsPath)})
}
if err := removeStaleAssetManifestFiles(outputDir, previousAssets, result.CSSArtifacts, result.AssetArtifacts); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve assets still referenced by unchanged pages

In BuildIncrementalFromValidatedProgram, only pages listed in changedPageSources are re-rendered, but this cleanup removes every previous manifest asset that is not in the current asset set. When a global CSS processor asset such as Tailwind's app stylesheet changes during a single-page incremental rebuild, unchanged pages keep their old hashed stylesheet link while this deletes that old file, turning those pages' CSS URL into a 404; either force all pages that include changed global assets to render or delay deleting stale assets until no unchanged HTML can reference them.

Useful? React with 👍 / 👎.

return Result{}, reporter.fail("cleanup", err)
}
reporter.info("cleanup", "stale_assets_removed", "stale generated assets removed", BuildEvent{})
assetManifestPath, err := writeAssetManifest(outputDir, result.Artifacts, result.CSSArtifacts, result.AssetArtifacts)
if err != nil {
return Result{}, reporter.fail("manifest", err)
Expand Down
Loading
Loading