Skip to content

refactor: manifest→IR migration — extract internal/source, decouple gwdkir, migrate buildgen#144

Open
cssbruno wants to merge 7 commits into
mainfrom
refactor/manifest-to-ir-step1
Open

refactor: manifest→IR migration — extract internal/source, decouple gwdkir, migrate buildgen#144
cssbruno wants to merge 7 commits into
mainfrom
refactor/manifest-to-ir-step1

Conversation

@cssbruno

@cssbruno cssbruno commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Substantial progress on the manifest → IR migration tracked in docs/engineering/architecture.md ("Compatibility Records"). Plan: .llm/plans/manifest-to-ir-migration.md.

What this does (7 commits, each independently green)

  1. compiler.ManifestFromIR — centralize the IR→manifest converter (was a private duplicate in buildgen) + add IR-first validation entrypoints ValidateProgram / ValidateBackendBindingPolicyIR.
  2. internal/source — extract the shared leaf value types (SourceSpan, SourcePosition, NamedSpan, RouteParam, InlineScript, BackendInputField, BackendBindingStatus/BackendSignatureKind + consts) into a neutral package. manifest re-exports them as aliases (non-breaking). This was the key blocker: gwdkir previously depended on manifest only for these leaf types.
  3. gwdkir Page methods — port CachePolicy, RenderMode, HasGoBlock, DynamicParams, TypedRouteParams onto the IR so render helpers can consume IR directly.
  4. buildgen — render helpers now thread gwdkir models; IR-first entrypoints validate via ValidateProgram instead of reconstructing a manifest.
  5. appgen — leaf types sourced from internal/source; 6 files drop their manifest import.
  6. compiler / parser / gwdkanalysis / lsp / contractscan / gwdkast — ~400 leaf-type references swapped to internal/source.
  7. docs — architecture + plan updated to reflect shipped vs. pending.

Verified outcomes

  • internal/gwdkir, internal/gwdkast, internal/contractscan are now fully manifest-free (go list -deps shows 0). The IR is a manifest-independent handoff — the original goal of the compatibility-records cleanup.
  • buildgen render helpers no longer consume manifest models (only a gotypes bridge + public-API compat types remain at the edges).
  • ✅ Full module gate: 47 packages, 0 failures (non-cached). scripts/test-go-modules.sh green including nested adapter modules. gofmt/vet clean.

Deliberately NOT in this PR (pending, tracked in the plan)

  • internal/compiler still validates the manifest model (~980 refs) — making validation IR-native is the next large step.
  • Collapsing AST → manifest → IR to AST → IR in gwdkanalysis.
  • Public manifest JSON wire format stays until a release plan deprecates it.

Pre-existing examples/{login,css,tailwind} build errors (main-less fixture dirs) are unrelated.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3cae21ff2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 22 to 23
BackendBindings: make([]manifest.BackendBinding, 0, len(ir.Endpoints)),
}

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 standalone endpoints when rebuilding the manifest

When ir.Endpoints contains standalone Go-comment endpoints (the records produced from manifest.Endpoints by addStandaloneEndpoint), this converter only turns endpoint records into BackendBindings and leaves manifest.Manifest.Endpoints empty. As a result, the new ValidateProgram/IR policy path skips the validators that read app.Endpoints (validateRouteMethodConflicts and validateStandaloneEndpoints), so invalid standalone handler names/routes or conflicts with page/API routes can pass validation when callers validate IR directly. Please reconstruct standalone endpoint declarations from ir.Endpoints in addition to backend bindings, while avoiding duplicating page block actions/APIs/fragments that are already represented under Pages.

Useful? React with 👍 / 👎.

Comment on lines 33 to 37
for _, endpoint := range ir.Endpoints {
binding := buildBackendBindingFromIR(endpoint)
binding := backendBindingFromIR(endpoint)
if binding.Status != "" || binding.ImportPath != "" || binding.FunctionName != "" {
app.BackendBindings = append(app.BackendBindings, binding)
}

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 Include load bindings in the reconstructed backend bindings

For an IR program that has a request-time load {} page plus any action/API/fragment binding, this loop only appends endpoint bindings to app.BackendBindings; the page LoadBinding is copied onto the page but never added to the binding list that ValidateBackendBindingPolicy checks. Because the presence of any endpoint binding prevents the fallback BindBackendHandlers branch from running, a production build can accept a missing or unsupported load handler as long as another backend binding exists. Add non-empty page load bindings to BackendBindings as kind load when rebuilding the manifest.

Useful? React with 👍 / 👎.

Comment on lines +15 to +16
func ValidateBackendBindingPolicyIR(config gowdk.Config, ir gwdkir.Program) error {
return ValidateBackendBindingPolicy(config, ManifestFromIR(ir))

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 Reject unbound fragment-only IR in production

When the new IR policy is called on an unbound IR program that only declares fragment endpoints, ManifestFromIR reconstructs page.Blocks.Fragments but no BackendBindings, and the manifest fallback only rebinding-checks actions, APIs, and load blocks. In that context a production build can return nil even though the IR has a fragment endpoint with no handler metadata; ensure fragment endpoints trigger binding/policy validation before delegating through the manifest path.

Useful? React with 👍 / 👎.

…ifestFromIR

Step 1 of the manifest→IR migration (see .llm/plans/manifest-to-ir-migration.md).

buildgen carried its own private gwdkir→manifest converter (buildModelFromIR
in internal/buildgen/ir.go) and used it solely to feed the manifest-typed
compiler validators while its public entrypoints already take IR. That meant
the real CLI build path validated a *reconstructed* manifest through a
converter duplicated outside the package that owns validation.

This moves that converter into compiler as the exported, single IR→manifest
seam `compiler.ManifestFromIR`, and adds IR-first validation entrypoints
`compiler.ValidateProgram` and `compiler.ValidateBackendBindingPolicyIR`.
buildgen now calls the shared converter; its duplicate is deleted.

Behavior-preserving: the relocated converter is byte-identical in output
(git records it as a rename), and the IR path already validated the same
reconstructed manifest before. New differential tests assert
ValidateProgram(ir) ≡ ValidateManifest(ManifestFromIR(ir)) and that the
production binding policy behaves identically on the IR path.

No import cycle (buildgen already depended on compiler). Render helpers still
consume manifest; migrating them to read IR directly is a later step.

Full module test suite passes (46 packages, 0 failures); gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cssbruno cssbruno force-pushed the refactor/manifest-to-ir-step1 branch from a3cae21 to 3d6d4d9 Compare June 9, 2026 18:56
cssbruno and others added 6 commits June 9, 2026 15:59
…om manifest

Prerequisite for the manifest→IR migration. The shared leaf value types
(SourceSpan, SourcePosition, NamedSpan, RouteParam, InlineScript,
BackendInputField, BackendBindingStatus, BackendSignatureKind) lived in
internal/manifest, which forced any package needing a SourceSpan to depend on
the whole manifest page/component model — including internal/gwdkir, the IR
that is supposed to be the manifest-independent compiler handoff.

- New internal/source package holds these neutral, behavior-free types.
- manifest re-exports them as type aliases, so every existing manifest.* usage
  keeps compiling (non-breaking extraction).
- gwdkir now references source.* directly and no longer imports manifest
  (go list -deps: gwdkir→manifest = 0).

Full module builds; compiler/buildgen/manifest suites pass; gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port CachePolicy, RenderMode, HasGoBlock, DynamicParams, and TypedRouteParams
(plus the RouteParamsFromPath helper chain) onto the IR Page type, mirroring
the manifest.Page methods. This lets generated-output packages consume the IR
page model directly instead of a reconstructed manifest. Methods read IR fields
only; gwdkir remains manifest-free. Equivalence unit tests added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nifest

buildgen's render/plan helpers now thread gwdkir.Page/Component/Layout/Blocks
(and source.* leaf types) instead of manifest models. The IR-first entrypoints
(planFromIR, SSRArtifactsFromIR, buildFromIR/buildMemoryFromIR/
buildIncrementalFromIR) iterate ir.Pages/Components/Layouts directly and
validate via compiler.ValidateProgram / ValidateBackendBindingPolicyIR instead
of reconstructing a manifest.

Remaining manifest references in non-test buildgen are the irreducible floor for
this step:
- gotypes_convert.go: a field-for-field IR→manifest bridge because internal/
  gotypes still consumes manifest.Import/GoTypeRef/StateContract.
- Public entrypoints keep manifest.Manifest params for backward compat
  (gwdkanalysis.BuildIR is called internally).
- SSRArtifact.LoadBinding stays manifest.BackendBinding (public output type
  appgen reads); converted field-for-field from gwdkir.Binding. Verified appgen
  reads only Status/ImportPath/PackageName/FunctionName/Signature.

Full module suite: 47 packages, 0 failures. gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap manifest.* → source.* for the shared leaf types appgen uses:
BackendBinding{Bound,Missing,UnsupportedSignature} + BackendBindingStatus, all
BackendSignature* consts + BackendSignatureKind, BackendInputField, RouteParam,
SourceSpan. Six files drop their manifest import entirely.

Also renamed local identifiers named `source` (a param in ir.go, locals in
scripts.go) that would otherwise shadow the newly imported source package — no
behavior change, prevents a latent footgun.

Remaining non-test manifest references (37) are legitimate and left in place:
public manifest.Manifest entrypoints, manifest.ErrorPagePath (lives in
manifest), the goblockgen.Source API which takes []manifest.Import/GoBlock, and
the manifest.BackendBinding struct currency that carries Kind/PageID/Method/
Route fields gwdkir.Binding lacks and is threaded from manifest-input paths.

Full module suite: 47 packages, 0 failures. gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… packages

Sweep gwdkast, parser, gwdkanalysis, compiler, contractscan, and lsp to
reference shared leaf value types (SourceSpan, SourcePosition, NamedSpan,
RouteParam, InlineScript, BackendInputField, BackendBindingStatus/Signature
types and consts) from internal/source instead of internal/manifest. ~400
references swapped.

gwdkast and contractscan now drop the manifest import entirely. The manifest
model types (Page, Component, Layout, Blocks, Manifest, ...) and manifest-
resident functions (ErrorPagePath, RouteParamsFromPath, CachePolicyWithReval-
idate) are intentionally left as manifest.X — the compiler legitimately
validates the manifest model; migrating those is a later phase.

Renamed local identifiers/params named `source` that would shadow the newly
imported source package (to src/body/sourcePath). No behavior change.

Full module suite: 47 packages, 0 failures. gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the Compatibility Records section and the migration plan to reflect the
shipped work: internal/source extraction, gwdkir/gwdkast/contractscan now
manifest-free, buildgen render helpers IR-native, and leaf-type swaps across
the compiler packages. Remaining work (compiler validation IR-native, AST→IR
collapse, public JSON) is listed as pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cssbruno cssbruno changed the title refactor(compiler): centralize IR→manifest conversion (manifest→IR migration, step 1) refactor: manifest→IR migration — extract internal/source, decouple gwdkir, migrate buildgen Jun 9, 2026
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.

1 participant