Skip to content

feat(contracts): complete local web adapter path#349

Merged
cssbruno merged 6 commits into
mainfrom
codex/m6-contracts-web-adapter
Jun 13, 2026
Merged

feat(contracts): complete local web adapter path#349
cssbruno merged 6 commits into
mainfrom
codex/m6-contracts-web-adapter

Conversation

@cssbruno

@cssbruno cssbruno commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a local single-binary contracts example with scoped source/CSS discovery, g:command/g:query, contracts addon config parsing, and build-report verification for name/kind/path/guards metadata.
  • Stabilize generated contract web adapters around JSON success/error response shapes, including redacted generic 5xx errors, explicit HandlerError messages for client-safe failures, JSON contract input decode failures, and JSON command form parse, oversized body, and CSRF failures.
  • Add durable contract event IDs, memory/file/Redis seen stores, and generated RunContractEventWorkerWithSeenStore helpers so duplicate event IDs can be skipped while fresh IDs are marked seen only after dispatch and source ack succeed.
  • Keep file outbox durable row IDs separate from event IDs, preserving duplicate event rows independently for ack/nack while replaying the stable event ID to workers.
  • Write file-backed outbox, dead-letter, and seen-store JSON Lines state through temp-file replacement so failed rewrites preserve the previous durable file.
  • Scope contractscan emitted events to the package that owns the handler, keep long-lived NATS/Redis event sources consuming across idle periods, preserve already-decoded NATS batch events when a later drained message fails to decode, and reap disconnected websocket fanout clients with CloseRead.
  • Document local outbox/replay helper behavior, split worker/cron deferral, dedup delivery caveats, and harden backend diagnostics for ambiguous handlers, broken same-package Go, go list failures, and scoped-script errors.
  • Preserve executable config loading when statically parseable built-in addons are combined with earlier dynamic config fields.

Issue Closure

Closes #110
Closes #111
Closes #112
Closes #113
Closes #114
Closes #155
Closes #161
Closes #180

Verification

  • I ran the relevant tests, lint, and build commands.
  • I ran scripts/test-go-modules.sh when Go code or compiler behavior changed.
  • I ran go build ./cmd/gowdk when CLI, compiler, runtime, addon, or release behavior changed.
  • I ran node --check editors/vscode/extension.js when editor files changed. N/A: no editor files changed.
  • I updated docs for behavior, setup, or architecture changes.
  • I added or updated tests for changed behavior.
  • I considered security-sensitive surfaces such as auth, CSRF, redirects, request-time handlers, logs, diagnostics, embedded assets, editor commands, WASM, contracts, and realtime behavior.

Commands run:

  • go test ./runtime/response ./runtime/contracts ./runtime/contracts/fileoutbox ./runtime/contracts/membroker ./runtime/contracts/sse -count=1
  • go test ./internal/appgen ./internal/buildgen ./internal/compiler ./internal/project ./internal/contractscan ./cmd/gowdk -count=1
  • go test ./internal/appgen -run 'TestGenerateWiresCSRFForCommandContracts|TestGeneratedBinaryContractAdaptersReturnJSONErrors|TestGeneratedBinaryContractCommandCSRFReturnsJSONError|TestGeneratedGoMatchesGoldenFixture' -count=1
  • go test ./internal/appgen -count=1
  • go test ./runtime/contracts/fileoutbox -count=1
  • go test ./runtime/contracts/fileoutbox -run 'TestSeenStore' -count=1
  • go test ./internal/appgen ./runtime/contracts/fileoutbox -count=1
  • go test ./runtime/contracts ./runtime/contracts/fileoutbox ./internal/project -count=1
  • go test ./... -count=1 in runtime/contracts/redisstream
  • go test ./... -count=1 in runtime/contracts/natsbroker
  • go test ./... -count=1 in runtime/contracts/websocketfanout
  • go test ./...
  • scripts/test-go-modules.sh
  • go build ./cmd/gowdk
  • go run ./cmd/gowdk check --config examples/contracts/gowdk.config.go examples/contracts/patients.page.gwdk
  • go run ./cmd/gowdk build --config examples/contracts/gowdk.config.go --out /tmp/gowdk-contracts-build --app /tmp/gowdk-contracts-app --bin /tmp/gowdk-contracts-site examples/contracts/patients.page.gwdk
  • test -x /tmp/gowdk-contracts-site && rg -n '"(name|kind|path|guards|method|contract|adapter|role)"|gwdk.contract|patients.CreatePatient|patients.GetPatientPage' /tmp/gowdk-contracts-build/gowdk-build-report.json /tmp/gowdk-contracts-build/openapi.json /tmp/gowdk-contracts-build/asyncapi.json
  • go run ./cmd/gowdk check --ssr examples/pages/*.gwdk examples/actions/*.gwdk examples/partials/*.gwdk examples/api/*.gwdk examples/ssr/*.gwdk examples/go-interop/*.gwdk examples/components/base/*.gwdk examples/components/css/*.gwdk examples/components/assets/*.gwdk examples/embed/*.gwdk examples/css/*.gwdk examples/tailwind/*.gwdk examples/contracts/*.gwdk

GitHub CI:

  • CI / Verify passed on latest head c4c36a8 via workflow-dispatch run 27468043897; CodeQL checks also passed on latest head c4c36a8.

LLM Assistance

  • LLM session summary: Codex implemented the M6 contracts local web-adapter slice, JSON response stabilization, event deduplication windows, runtime lifecycle fixes, package-scoped contractscan emits, diagnostics hardening, docs, examples, verification, and review-comment fixes.
  • Human-reviewed assumptions: Deduplication checks the seen store before dispatch but marks fresh event IDs only after subscriber dispatch and source Ack both succeed; docs call out that subscribers must still tolerate redelivery outside the configured window, after seen-store write failures, and across concurrent workers.
  • Follow-up work: None required for the M6 issue set.

@cssbruno cssbruno changed the title feat(contracts): harden local web adapter path feat(contracts): complete local web adapter path Jun 13, 2026
@cssbruno cssbruno marked this pull request as ready for review June 13, 2026 11:34

@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: 1fb5eda69a

ℹ️ 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 thread runtime/contracts/worker.go Outdated
out = append(out, event)
continue
}
fresh, err := seen.MarkIfNew(ctx, event.ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark events seen only after successful dispatch

When a seen store is configured, this call records every fresh ID before PublishEnvelopesForRole runs. If a worker subscriber then returns an error and the source accepts Nack, the durable source will redeliver the batch, but those IDs are already considered duplicates and dispatchEventBatch will ack duplicate-only batches without invoking subscribers. This drops events after transient subscriber failures; defer marking until after successful dispatch/ack or make the seen-store write rollbackable.

Useful? React with 👍 / 👎.

Comment on lines +851 to +852
case contractsaddon.ImportPath:
return contractsaddon.Addon(), true

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 executable config loading for contracts addons

Treating contractsaddon.Addon() as an AST-parsable built-in exposes the existing config.Addons, needsExecutableLoad = parseAddons(...) overwrite: when a config has an earlier dynamic field such as AppName: os.Getenv("APP") and then Addons: []gowdk.Addon{contractsaddon.Addon()}, this new case returns false from parseAddons and clears the earlier needsExecutableLoad, so LoadConfigFile no longer evaluates the executable config and silently uses zero values for those dynamic fields. Preserve any prior needsExecutableLoad value when parsing built-in addons.

Useful? React with 👍 / 👎.

Cond: notNil("err"),
Body: block(
writeNoStoreHandlerErrorExprStmt(id("err"), sel("http", "StatusInternalServerError")),
writeNoStoreHandlerJSONErrorExprStmt(id("err"), sel("http", "StatusInternalServerError")),

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 Return JSON for contract input decode failures

This change makes contract adapter execution errors JSON, but requests that fail earlier while decoding command/query input still go through contractDecodeInputStmtsifErrReturnInvalidForm(), which writes WriteNoStoreError text/plain. For any contract with scanned input fields, a bad scalar value or unknown submitted field therefore returns invalid form as a non-JSON response even though the new contract says adapter failures use {"error":"..."} JSON; use the JSON error helper on this path too.

Useful? React with 👍 / 👎.

@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: 1a50729320

ℹ️ 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 +136 to +137
if err != nil {
return contracts.EventBatch{}, err

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 drained NATS events on later decode errors

When the opportunistic drain has already appended at least one valid message, a decode error from a later tryNextMessage now returns an empty batch error here. For Core NATS subscriptions the message has already been consumed and this adapter intentionally provides no Ack/Nack hook, so the earlier valid events in events are never dispatched and cannot be redelivered whenever a malformed or undecodable message follows them in the same receive window. Return the accumulated batch when len(events) > 0 or otherwise avoid consuming additional messages before the current batch is handed off.

Useful? React with 👍 / 👎.

define([]ast.Expr{id("values")}, values),
define([]ast.Expr{id("input"), id("err")}, call(sel(contractDecoderName(exposure)), id("values"))),
ifErrReturnInvalidForm(),
ifErrReturnInvalidJSONForm(),

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 Return JSON for command form parse failures

Switching the contract decoder error to ifErrReturnInvalidJSONForm only covers failures after request.ParseForm succeeds; command contract routes still prepend actionParseFormStmts, which writes the non-JSON WriteNoStoreError response for malformed URL-encoded bodies, oversized bodies, and CSRF failures. In those cases a g:command endpoint still violates the new documented stable application/json {"error":"..."} error shape, so contract command parsing/CSRF should use JSON-specific error writers too.

Useful? React with 👍 / 👎.

}
record := Record{
ID: store.nextID(),
ID: event.ID,

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 Keep outbox record IDs distinct from event IDs

Using event.ID as the file outbox record key makes the Ack/Nack maps operate on event IDs rather than unique persisted rows (decodeRecordsLocked keys acked by record.ID, and removeRecordsLocked/markRecordsFailedLocked update every matching record). If a caller stores two pending envelopes with the same durable event ID and the batch size only delivers one of them, acking or nacking that one also removes, retries, or dead-letters the other undelivered row. Keep a separate unique outbox record ID and store the durable event ID in its own field.

Useful? React with 👍 / 👎.

@cssbruno

Copy link
Copy Markdown
Owner Author

@codex

@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: bd29f75f4b

ℹ️ 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".

}
return nil
}
file, err := os.OpenFile(store.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)

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 Write seen-store updates atomically

When MarkSeen or MarkIfNew rewrites an existing seen-store file, this opens the live path with O_TRUNC and then streams JSON lines back into it. If the process is interrupted or the filesystem returns a partial-write error after truncation, the seen store can be left empty or with an invalid trailing JSON line; subsequent Seen calls then either forget delivered IDs or fail on readRecordsLocked, which defeats worker deduplication for the file outbox path. Please use the same temp-file-and-rename pattern as the outbox writer so each update either preserves the old file or installs a complete new one.

Useful? React with 👍 / 👎.

@cssbruno cssbruno merged commit d4081d0 into main Jun 13, 2026
5 checks passed
@cssbruno cssbruno deleted the codex/m6-contracts-web-adapter branch June 13, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment