Skip to content

feat(shooter): qualify bounded receiver decals with atlas and fade [VQ11] - #394

Merged
jonit-dev merged 16 commits into
developfrom
fix/vq11-decal-material-lifetime
Oct 2, 2026
Merged

jonit-dev merged 16 commits into
developfrom
fix/vq11-decal-material-lifetime

Conversation

@jonit-dev

@jonit-dev jonit-dev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Completes VQ11 from #391 in this single PR. Repeated impacts previously leaked cloned materials and the shooter's procedural map. The existing game-owned DecalField now releases its owned resources and supports opt-in bounded projection onto static/rigid opaque receivers using upstream DecalGeometry and existing hit/LOD0 geometry.

  • Deterministic cap/eviction; receiver-local marks follow rigid transforms and survive render LOD changes. Unsupported deforming/instanced/batched/nonuniform/material cases explicitly refuse projection.
  • Owned projected geometry, per-slot materials and caller-owned texture have exact teardown ownership; borrowed receiver resources remain borrowed.
  • Optional authored atlas UV rectangles, duration/opacity curve and bounded normal bias; expiry/reuse resets correctly. Persistent full-map place() and the default shooter picture remain unchanged.
  • Maintained real browser/native fixture covers cap, motion, LOD, removal, restart, atlas, visible fade, expiry and repeated residency. Hidden-material controls must fail their exact visible-effect assertions.

Actual qualification

Run 37015640669, exact source 8f85fff19913c955a44b7b85d2cc117b972bd19c, passed browser and all ten native cases. Native captures are actual 960×540 device.screenshot output from packaged ARM64 QuickJS/wgpu on Mesa llvmpipe. Browser uses Chromium SwiftShader WebGPU. All authored pixel predicates, startup, console, named-adapter and capture/hash guards remain enforced.

Six native scene generations each retain 259 geometries, 4 textures and 6,522,894 tracked bytes after three stable completed-render observations: zero growth. The fixture retains 256 marks / 275,076 projected bytes, with 516 maximum measured world draws. Atlas cells differ visibly, partial opacity ~0.475 remains visible, and expiry releases all 321 created buffers to zero active marks. This qualifies software-adapter correctness, not mobile or hardware performance.

Minimal native provenance links all actual PNGs, source/build/runtime hashes and original artifact. Earlier native motion/removal/restart images remain preserved.

Local checks and remaining promotion gates

  • Root typecheck and lint pass. Changed playtest runner rebuild, browser fixture, four native bundles, documentation links, mirrors and evidence budget pass.
  • 57 focused decal/adjacent tests pass. Expanded affected suite: 463 pass with the temporary-scaffold Basis lookup initially failing; that exact test passes with NODE_PATH pointed to the existing installed Three. Additional drift checks: 99 pass; pristine scaffold types pass with the shared pnpm/cache environment. Both original setup failures are recorded in the PRD.
  • Root build/test commands hit this container's tsx IPC EPERM; affected build steps pass via Node's loader. The Xvfb-wrapper test cannot run because Xvfb is absent. No complete local aggregate pass is claimed.
  • Independent review cleared the full implementation and original acceptance, including actual native report/console/pixel re-evaluation and adversarial empty-image controls.

Develop 924b92f8 is merged without rewriting ancestry. Current combined source 6ad1864b3fdebca0c46a75c13a42278440f1abd8 passed browser and all ten native cases in run 37020760469. All actual native report/console/pixel checks were re-evaluated, the inspected native PNGs are byte-identical to the attached proof, and six generations again show zero growth. Independent combined review confirms all non-documentation changes since reviewed 8f85fff19 exactly match landed develop. Ready for ordinary required CI; protected squash auto-merge is authorized, with no bypass.

Combined native artifact 11233786723 SHA-256 100f5bd5bfe8d6b33006b351c506996a8c040a0cb52917307a36b4487597e186; browser artifact 11233025899 SHA-256 19e2a5f95e4299b667809e6ca8892e149b2965a2caac843679dcd060f6ddcf09.

PRD checklist

  • Reuse upstream projection with existing picking/base geometry and explicit receiver support.
  • Deterministic pool limits, eviction, receiver cleanup and borrowed-resource ownership.
  • Authored material/atlas composition with bounded bias and fade controls.
  • Rigid motion, LOD changes and repeated teardown without detached marks or resource growth.
  • Complete browser WebGPU impact sequence and visible-effect controls.
  • Same sequence on packaged Linux native with actual matching observations and pixels.
  • Supported receivers retain marks through motion/teardown with bounded resource use.
  • Persistent fallback remains and owned resources release through repeated lifecycle transitions.

Proof commands, exact sources and results are beside every criterion in the archived PRD. Computed progress: prd:100%.

@jonit-dev jonit-dev added the prd:0% PRD implementation has no verified phase yet label Oct 2, 2026 — with ChatGPT Codex Connector
@jonit-dev jonit-dev changed the title fix(shooter): release owned decal materials on teardown [VQ11] feat(shooter): add bounded receiver decal projection [VQ11] Oct 2, 2026
@jonit-dev jonit-dev removed the prd:0% PRD implementation has no verified phase yet label Oct 2, 2026
@jonit-dev jonit-dev added the prd:25% PRD phase 1 verified label Oct 2, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

Actual runtime screenshot progress from run 36989297734, source 7396738b33a2b3018a2e6235a0e59ca07620a324, browser WebGPU, Google SwiftShader, 960 × 540. These are exact unmodified artifact PNGs, now retained durably in commit fb2c819c; each raw URL was downloaded and byte-compared with the original artifact.

Static: visible mark grids on both receivers, 256 live marks after 321 placements.
Static receiver decals

Rigid motion: the right receiver and its marks move together.
Rigid receiver motion

Receiver teardown: the right receiver and its marks are removed; 128 left-side marks remain.
Receiver teardown

Raw capture summary. Artifact 11218713068, verified SHA-256 f432d6d5ca32cc09ee0633d3c3560445dc01ebc6e13b0280fa657774d2842652. The four authored scenarios passed, with no error/device-loss diagnostics. Software rendering is correctness-only evidence.

Progress evidence, not full acceptance. Human inspection confirms visible marks here. Independent review found that the current automated verifier's counters plus whole-frame nonblank test do not themselves discriminate hidden decals; a real visible-effect discriminator and hidden-decals negative control are next. Native execution, atlas/fade controls, repeated projected-scene GPU residency, required CI and final review remain open. PR remains draft.

@jonit-dev jonit-dev removed the prd:25% PRD phase 1 verified label Oct 2, 2026
@jonit-dev jonit-dev added the prd:50% PRD half the phases verified label Oct 2, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

The visible-mark gate now passes with a real hidden-material negative control.

Run 36991562324 tested source 4cfc3c2024da1cb37533dc67fd07679e364c27f6: four marked scenarios passed at 960 × 540 on Google SwiftShader WebGPU. Each receiver interior must contain 1–20% dark mark pixels at luminance ≤0.35; static measured 5.34% / 5.66%, and moving right receiver 5.18%.

Actual visible frame:
Visible receiver marks

Actual hidden-material control, same static scene:
Hidden material negative control

The control retained 256 slots, 321 placements, real projection buffers and LOD observations. Both receiver regions measured zero mark pixels and failed exactly visual.1.region.darkPixels and visual.2.region.darkPixels; all other assertions passed. Its expected two pixel errors were the only diagnostics. The four positive runs had no errors or device-loss warnings. I inspected all five actual PNGs.

Raw summary · Raw control report. Artifact 11218994493 SHA-256 verified: a84190c40b3cdd6679ff9fea858dd6cfe2373646f090192ee531411fbaba1070. All four positive PNGs are byte-identical to the earlier retained images; restart equals static, so binaries are reused without editing.

CPU/package verification: 39/39 focused/adjacent tests, typecheck, changed-file lint, fixture build and 2,405 documentation links pass. Documentation suites: 178 pass; two evidence-budget CLI subprocess tests are blocked by this container's tsx Unix-socket EPERM. The actual staged evidence budget passes via the Node loader.

The browser phase box is verified; computed progress is prd:50%. This remains software-adapter correctness evidence. Atlas/fade controls, repeated GPU residency, Linux-native execution, required general CI and full acceptance/review remain open. PR stays draft.

Preserve persistent defaults, bound bias, and release expired projected buffers.
Merge develop 416ffd7 non-force. Include independently reviewed intermediate-fade
pixel discrimination and hidden-during-fade control. Runtime qualification pending.

Copy link
Copy Markdown
Contributor Author

Actual atlas/fade progress from run 37003284983, source 021e622380c61695b8c5dc0e7b9ffc604e956b8c, 960 × 540 Google SwiftShader WebGPU:

Atlas cells visibly separate impact marks and solid environment marks:
Atlas cells

Partial fade retains actual translucent marks at material opacity 0.5:
Visible partial fade

Expiry removes all marks and releases all 321 created projection buffers:
Expired marks

The expiry PNG is byte-identical to the earlier retained empty-plate PNG above. Atlas, partial fade, expiry, original four scenarios and both hidden controls passed their individual checks. Positive intermediate coverage measured 6.55% left / 43.44% right; hiding materials after fade starts preserves opacity 0.5 and 256 slots but fails exactly the two positive pixel assertions. I inspected these actual frames; positive appearance cases have no error/device-loss diagnostics.

The overall run failed residency qualification. Across warmed scene samples it observed 258/259 geometries and 6,522,318/6,522,894 bytes, a maximum +1 geometry/+576 bytes; textures stayed 4. The first sample was taken before the world finished rendering, so fixed simulation ticks are insufficient for the sample boundary. That timing/LOD issue is being diagnosed; zero-growth limits are unchanged.

Raw summary. Artifact 11225280829 SHA-256 verified: 8fad5bcda560bedc2bb9899f747ba6e6e5aa250c50fccade4d89eca02ec586aa. This is software-adapter correctness progress; native, residency and full acceptance remain open. PR stays draft.

@jonit-dev jonit-dev removed the prd:50% PRD half the phases verified label Oct 2, 2026
@jonit-dev jonit-dev added the prd:75% PRD phases in, acceptance open label Oct 2, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

The completed-render residency gate now passes without changing its zero-growth ceilings.

Run 37005420971, source 3bb9942c41dfd5c4a8690354373ad9c53259d0ee, passes all ten capture cases. Across six scene instances, each sampled after three stable completed world-render intervals, every sample is 259 geometries, 4 textures, 6,522,894 tracked bytes. Cumulative render calls advance 56 → 64 → 72 → 80 → 88 → 96. Geometry, texture and byte growth are all zero.

The earlier +1/+576 result came from sampling fixed simulation ticks before stable world rendering. The correction uses bounded resource waits and actual render-call progress; it retains the original limits.

Raw summary. Artifact 11225394658 SHA-256 verified: 9c2811282094efd8229fc341f2a126dea451feefef7587c37882426365488493. All ten PNGs are byte-identical to the already attached actual screenshots; I rechecked atlas, partial fade, the sixth scene and hidden-during-fade. All positive cases are free of error/device-loss diagnostics, and each negative control has only its two intended pixel failures.

The browser phases are verified, with 55 focused/adjacent tests and independent code/proof review. Progress is prd:75%. This is SwiftShader correctness and observed renderer residency, not hardware performance or indefinite-duration proof. Linux-native qualification, required aggregate gates and full acceptance remain open; PR stays draft. Native qualification is next using the supported hosted desktop route.

Copy link
Copy Markdown
Contributor Author

Actual native progress from run 37010879478, source 563ab23e9448a993013cb0d58c75ab6ab83917c9. The ARM64 QuickJS/wgpu host built and packaged successfully. Static, rigid motion, receiver removal and scene restart passed startup, strict console, state assertions and the unchanged external pixel gates. These are genuine 960×540 device.screenshot captures on Mesa llvmpipe (LLVM 20.1.2, 128 bits), inspected visually; this establishes software-rendered correctness only.

Static receiver marks:
Native static receiver marks

Rigid receiver motion:
Native rigid receiver motion

Receiver removal:
Native receiver removal

Restart restores both receivers and marks (the actual restart PNG is byte-identical to static):
Native scene restart

The native run is incomplete: lifecycle timed out waiting for generation 1 residency readiness, so native atlas/fade/expiry and negative controls were not reached. The fixture observed maxDrawCalls=0 during updates despite the above rendered frames; I am tracing the counter reset and render boundary before changing that observation. Zero-growth and actual draw-completion requirements remain unchanged. Failed lifecycle report, capture provenance, and actual static host console are retained.

Native artifact 11228900169 SHA-256: 0ee5614f0ec8f859d8a23055062e58792f1c94d2af9c8bfd92b15d0e1f327a55. Runtime SHA-256: da16138cc37938a14ce6831476aa58972a0d469ba2172b6a06faf6d1d56c6042. The linked PNG bytes were verified against the artifact. The browser job at this same source passed all 10 cases. PR remains draft with native/full acceptance open.

Sample draw counters after the ordinary world render and consume each snapshot once on the next game update. Preserve the original 256-draw, stable-interval and zero-growth gates. Includes a real Three Info.reset regression and strict missing/repeated snapshot controls.

Independently reviewed at local 185b1ef7; actual repaired native qualification remains pending.
@jonit-dev jonit-dev changed the title feat(shooter): add bounded receiver decal projection [VQ11] feat(shooter): qualify bounded receiver decals with atlas and fade [VQ11] Oct 2, 2026
@jonit-dev jonit-dev removed the prd:75% PRD phases in, acceptance open label Oct 2, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

All ten native cases now qualify in run 37015640669, source 8f85fff19913c955a44b7b85d2cc117b972bd19c, alongside the complete browser sequence. These actual 960×540 device screenshots come from packaged ARM64 QuickJS/wgpu on Mesa llvmpipe (LLVM 20.1.2, 128 bits).

Atlas selection:
Native atlas cells

Visible intermediate fade (actual opacity ~0.475):
Native partial fade

Expiry releases all 321 created projection buffers:
Native expired marks

The six-generation lifecycle now measures the same 259 geometries, 4 textures and 6,522,894 tracked bytes at every sample, after three stable completed render intervals: zero growth. At the cap: 256 marks, 275,076 projected bytes, maximum 516 observed world draws. Both hidden-material controls fail exactly their two intended pixel assertions; strict state/startup/console/capture guards pass. The empty PNG above is also byte-identical to both actual hidden-control captures.

Minimal provenance. Artifact 11231490134 ZIP SHA-256 07925a63d4091f2a53d06ef7b178619d775ef9185890949afb4645c83e9d519f. Images were inspected and verified unchanged against their published bytes. Full raw evidence remains in the workflow artifact.

The original VQ11 acceptance is verified and archived; this is software-adapter correctness, without a mobile/hardware-performance claim. Develop 924b92f8 is merged in current head 6ad1864b3; the changed runner is being requalified in run 37020760469. PR remains draft until that combined-source result and review; ordinary required CI still gates promotion.

@jonit-dev
jonit-dev marked this pull request as ready for review October 2, 2026 14:46
@jonit-dev
jonit-dev enabled auto-merge (squash) October 2, 2026 14:46

Copy link
Copy Markdown
Contributor Author

Handoff for the next AI, checked 2026-10-02 17:18 UTC. Stopping work on #394 at the owner's request.

  • Current head is 6ad1864b3fdebca0c46a75c13a42278440f1abd8 on fix/vq11-decal-material-lifetime. PR is open, ready for review, unmerged, labeled prd:100%. GitHub reports mergeable=true, mergeable_state=behind. Current develop is 304b2f44299ab4eded9a9d5e29d51f6e57f823c3; the qualified head includes earlier develop 924b92f8.
  • Squash auto-merge is enabled and has been left unchanged. Required checks still gate merging; no bypass.
  • Exact-head bounded-decals run 37020760469 is successful. The PR body records complete browser and all ten native cases, actual capture/pixel checks, hidden-material controls and six-generation zero-growth residency. Archived acceptance and native provenance retain evidence and limitations. These are software-adapter correctness results, not hardware/mobile performance qualification.
  • Ordinary CI 37022205728 remains failed. Its 40 jobs comprise 37 successful, one skipped and two failed: template-nonvisual (tower-defense, 1/1) and aggregate ci-required. Failing job 110888851990 records tower-defense-target-priority: TN_PLAYTEST_OPERATION_TIMEOUT, “Bridge operation 'ready' exceeded 15000ms.”, zero frames. Build, lint, typecheck, unit, browser, playtest and native jobs succeeded. The startup timeout's root cause is not established by this handoff.
  • Remaining work for the next owner: reconcile the newer develop base, investigate the required CI failure without weakening gates, and requalify any changed combined source before ordinary protected promotion. The PR body preserves the earlier local setup limitations; no full local aggregate pass is claimed.

This wrap-up made no implementation changes, CI retries, merges or rebases. All further work on #394 is handed over.

@jonit-dev
jonit-dev merged commit 840d353 into develop Oct 2, 2026
80 of 84 checks passed
@jonit-dev

Copy link
Copy Markdown
Contributor Author

Promoted. Reconciled base and requalified on the combined source.

Merges (no ancestry rewrite): develop 569fdb267 -> a0edc1487, then develop 42fa306c0 -> ebac0c9c4. Squash auto-merge landed ebac0c9c4 as 840d35303 at 2026-10-02 19:33 UTC. No bypass, no gate weakened.

Requalification of the changed combined source at ebac0c9c4:

  • Run 37049488262 — Integration bounded decals green: browser job plus all ten Linux native cases, unchanged pixel/state/startup/console/provenance guards.
  • Run 37049488719 — required CI green (all jobs), after one re-run of the flaky template-nonvisual (rain, 1/1) shard.
  • Root pnpm typecheck also run locally on the first merge (a0edc1487) with the workspace built; exit 0.

Investigation of the earlier required-CI failure. tower-defense-target-priority failed with TN_PLAYTEST_OPERATION_TIMEOUT, "Bridge operation 'ready' exceeded 15000ms.", frames: 0, on the software adapter. The call that timed out is the initial readiness handshake in connectPlaytestBridgeTransport (packages/playtest/src/runner/bridgeClient.ts): it runs immediately after waitForBridge, i.e. during the application's first-use compilation, but it is budgeted with PLAYTEST_PROTOCOL_LIMITS.operationTimeoutMs (15 s) as if it were an ordinary request/reply round trip. waitForStartupReady already treats a timed-out ready as BUSY and keeps polling; the earlier handshake has no such tolerance, so a shader compile that blocks the main thread for >15 s is reported as a hung page. bridgeWaitTimeoutMs and advanceTimeoutMs were fixed for exactly this reason; the handshake ready call is the remaining gap. Not changed here — it is runner behaviour, outside this PR's diff, and it is intermittent (tower-defense passed on develop's neighbouring runs).

A second, independent flake was observed in the same window: rain-reduced-motion-suppresses-every-flash failed with TN_PLAYTEST_CAPTURE_PROVENANCE_MISSING ("could not read a renderer adapter description"), after four sibling scenarios in the same job reported TN_PLAYTEST_SOFTWARE_DEVICE_LOST. Both classes are software-adapter startup/device-loss timing, not this PR's changes; each passed on re-run.

Residual risk: the initial ready handshake budget in packages/playtest can still time out on a contended software-adapter runner. Worth a dedicated playtest PRD/PR rather than folding it into this one.

jonit-dev added a commit that referenced this pull request Oct 3, 2026
* ci(prd-481): integration and pipeline-cache triggers fire once, on paths they read

Phase 3 of PRD-481. The audit behind it cost 2.4k runner-minutes on
pipeline-cache alone, all of it on a branch whose lane had landed three
weeks earlier.

- pipeline-cache.yml: `push.branches` loses feat/prd-368-persistent-pipeline-cache.
- integration-csg.yml: the unfiltered `push` beside `pull_request` is gone, so a
  commit with a PR no longer runs the proof twice.
- integration-decals.yml: the per-branch `push` for fix/vq11-decal-material-lifetime
  is gone; PR #394 merged on 2026-10-02 and the branch outlived it.
- every integration lane now skips draft pull requests, and every one of those
  triggers gained `ready_for_review` so the draft guard cannot silence the lane
  instead of cheapening it.
- ci-structure.spec.ts: a workflow-file allow-list read off disk (a new workflow
  has to be named here to land), the push+pull_request and per-branch rules with
  fixture cases, and an assertion that pipeline-cache fires only on paths its own
  steps read.

Both new assertions fail on the pre-change workflow files and pass after.

* ci(prd-481): consumers take the compiled workspace from build-artifacts; native cache key drops ci.yml

Nine jobs each compiled the workspace dist (71-95 s) because they started in
parallel with its producer. They now need build-artifacts and download its
upload. The native-build cache key no longer hashes ci.yml, so a workflow
edit stops forcing a cold 1.35 GiB Linux host build.

* ci(prd-481): shard counts that put every measured leg inside six minutes

Phase 2's third box. Both lanes shard by a rule that knows nothing about duration, so
the run's slowest leg is decided by arithmetic nobody measured. Timestamped on run
37049488719.

test-unit, 3 -> 4 slices. `vitest --shard` sorts the file list by sha1 of its path and
slices that order, so the split is exact about file count and blind about time. The
three legs ran 163s / 285s / 472s of vitest over 397s / 712s / 709s of test time.
Replaying vitest's own partition over those per-file durations reproduces all three
assignments, and at four slices predicts 80s / 114s / 211s / 320s of vitest — three
legs inside six minutes once the 40-121s of job overhead is added.

The fourth leg cannot be, and no shard count fixes it: it carries
`create-threenative/__tests__/template-assets-compile.spec.ts` at 320s on its own, and
a shard cannot split a file. A fifth slice leaves both numbers unchanged and buys a
runner.

template-nonvisual. Per-scenario seconds on the same run, with the ~65s each leg spends
on its own setup:

  shooter 26 at 32-47s, 922s total -> 4 -> 363s, 5 -> 356s, 6 -> 288s. Six.
  sailing 6 at 17/163/23/75/22/44  -> 2 -> 347s.
  rain    5 at 331/202/222/78/61   -> 5 -> 396s, and that is its floor: five scenarios,
    one per slice, heaviest alone 331s of software rendering. It cannot reach six
    minutes at any shard count, and it is not split past its scenario count to pretend
    otherwise.

The box stays unticked: both lanes still have a leg over six minutes that no shard
count reaches, and its proof is a full CI run's per-job durations.

ci-structure.spec.ts holds the measured template counts (puzzle 2, sailing 2, rain 5,
shooter 6) beside the partition and scenario-count guards it already had.
196 passed; the three new pins fail on the pre-change workflow.

* ci(prd-481): a tree that already passed does not run the board twice

The board is chosen from a whole-repo tree SHA rather than from the diff. When the
candidate's tree is the tree a completed, successful `CI` run already tested, `scope`
emits `selection: reused` with that run id, every gate skips, and `ci-required` passes
only after reading the source run's own `ci-required` conclusion through the Actions
API.

- `scripts/ci-change-scope.mjs`: `findReusableRun` lists the recent successful CI runs
  (`gh api`, the job's own `GITHUB_TOKEN`, `actions: read`), compares each run's
  `git rev-parse <head_sha>^{tree}` with the candidate's, and fetches a commit this
  clone cannot see rather than assuming it. A miss, an unreadable or uninterpretable
  listing, `workflow_dispatch`, an explicit `--full`, a non-Actions run, and Sunday's
  nightly all keep the full board; the miss reason is appended so a full run that never
  looked cannot read like one that looked and found nothing.
- `scripts/ci-required.mjs`: a reused verdict resolves the source run by id and fails
  with `CI_REQUIRED_SOURCE_UNKNOWN` / `CI_REQUIRED_SOURCE_NOT_SUCCESS` unless that run's
  own ci-required job concluded success.
- `.github/workflows/ci.yml`: `merge_group` trigger (types `[checks_requested]`); the
  draft guards stay true for a merge group because it carries no pull_request object;
  `lint` and `supply-chain` now name the two selections they admit instead of refusing
  `prose`, which would have read a reused tree as "not prose" and re-run them.
- `AGENTS.md`: "Never cache test verdicts" becomes the 2026-10-02 decision wording, and
  the CLAUDE.md mirror is regenerated.

Verification: `pnpm exec vitest run scripts/__tests__/ci-needs.spec.ts` 25 passed (4 of
the 5 new cases red before the change); the five-spec lane (ci-structure, ci-needs,
ci-efficiency, sync-agent-docs, primary-docs) 218 passed; `pnpm lint` exit 0;
`pnpm typecheck` exit 0; `pnpm check:docs` 2496 links clean; `pnpm sync:agents --check`
in sync. The CI-run boxes stay unticked: nothing here has run on GitHub yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(prd-481): a reduced pass never stands in for a fuller requirement

An identical tree is not the same validation. PRD-380 gives an ordinary pull request a
reduced native matrix and the planner already lets a clean develop pull request skip
`native-platforms`, so the previous contract — identical tree plus a green source
`ci-required` — let a develop pass satisfy a promotion on that tree. Reuse now requires
the source run to cover what this run proves, and any gap fails closed into the full
board.

The profile is a pure function of the run's event, its base branch and whether it owed
the native lane, and it is computed the same way for the source and for this run:

- `validationProfile` returns `{ target, native }`. `target` is `main` for a promotion or
  a main push, `develop` for an ordinary pull request or the merge queue, and `other`
  for anything whose target the API does not report — which ranks lowest, so an
  unproven target never stands in for a proven one. `native` is `none`, `reduced` or
  `full`, following the target.
- A run record in this repository reports no `base_ref`, so `sourceBase` reads a source's
  target from the pull requests its commit belongs to (`commits/<sha>/pulls`); nothing
  readable is no base.
- `currentRun` reads this run's own job graph (`actions/runs/$GITHUB_RUN_ID/jobs`) for its
  required set — every board job and matrix leg its job graph carries, minus the ones the
  plan waived, since a waived job is still listed as a skip — and for the runner class
  its routing chose.
- `coverageMiss` refuses a source whose profile is weaker on either axis, that never ran
  a required leg, whose leg concluded anything but `success`, or whose jobs ran on
  another runner class (`hosted` or `tn-local`, from the labels and `runner_name` the
  jobs API reports). A reusable workflow's `native-platforms` arrives as one check, so
  the lane's own legs stay inside it.
- A `workflow_dispatch` is now excluded as a reuse source as well as a reuse target, and
  a truncated job listing is a miss rather than a quietly shorter requirement set.

`ci-required` repeats the coverage check with its own reading of the source run, so a
reuse it cannot confirm is not a pass; it re-derives only what policy owes each target,
because a reuse plan records nothing about what was required.

Verification: `pnpm exec vitest run scripts/__tests__/ci-needs.spec.ts` 30 passed (5 red
before the change: the four coverage cases and the reused verdict); the five-spec lane
(ci-structure, ci-needs, ci-efficiency, sync-agent-docs, primary-docs) 223 passed;
`pnpm lint` exit 0; `pnpm typecheck` exit 0; `pnpm check:docs` 2496 links clean;
`pnpm sync:agents --check` in sync. The CI-run boxes stay unticked: nothing here has run
on GitHub yet.

* ci(prd-481): one integration workflow, one job per lane

Five workflow files each carried their own triggers, permissions and concurrency group
for one feature's proof. The owner's rule is that no feature gets a workflow file: its
proof is a job in an existing workflow. `integration-csg`, `integration-decals`,
`integration-ik`, `integration-tone` and `integration-vegetation` fold into
`.github/workflows/integration.yml`, one job per feature, each with the paths filter,
draft guard, runner, checkout ref, timeout and artifact names it had before.

- `.github/workflows/integration.yml`: one `paths` gate job reads the pull request's own
  changed paths once with `dorny/paths-filter` (pinned to the v4.0.3 commit) and publishes
  one output per lane, so a lane this commit does not touch is skipped rather than given a
  runner, and editing the workflow still re-runs every lane (each filter names it, as each
  old trigger did). The csg and decals supersession groups become one; every step, timeout,
  runner line and artifact name is unchanged.
- `scripts/__tests__/ci-structure.spec.ts`: the workflow allow-list names one integration
  workflow and rejects a new `integration-*.yml` by name; a second assertion reads the merged
  file and requires one filter per lane, a job behind each gate, and the draft guard on every
  gated job.
- `scripts/workspace-packages.ts`: the hand-listed-package gate counted distinct package names
  per *file*, so merging five files put two unrelated targeted `--filter` builds in one file
  and tripped it. It now counts per `run:` block — one command or one run block naming several
  packages is the drift this gate exists for; two targeted builds in two unrelated jobs are two
  builds — with a case for each direction.

Verification: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-needs.spec.ts scripts/__tests__/ci-efficiency.spec.ts scripts/__tests__/sync-agent-docs.spec.ts scripts/__tests__/primary-docs.spec.ts`
223 passed (131 in ci-structure, whose two new assertions were each red first: a lane without
`needs: paths`, a lane gate without the draft guard, and an added `integration-newlane.yml` for
the allow-list); the whole `scripts/__tests__` suite 1638 passed, 1 skipped; `pnpm lint` exit 0;
`pnpm typecheck` exit 0; `pnpm budgets` ok; `pnpm check:docs` 2496 links clean;
`pnpm sync:agents --check` in sync.

For the owner: three checks change name (`test` becomes `csg`, `ik`, `vegetation` — they were
three indistinguishable checks), a ~30-second gate job now appears on every pull request, and the
three lanes that queued behind a superseded run are cancelled by it instead. Branch protection
naming those checks has to follow.

* ci(prd-481): the integration gate reads changed files itself, no third-party action

dorny/paths-filter was new to the repo for what one gh api call and a regex per
lane do. Each lane's former paths globs translate one-to-one; spot-checked hits
and misses per lane.

* fix(ci): scope fixtures never inherit GITHUB_ACTIONS from the CI job

Run 37071464562, test-unit (3/4): inside CI the workflow-only classification case
turned on the live tree-reuse lookup and its reason grew an Actions API error.
isolatedGitEnvironment now strips GITHUB_ACTIONS; red under GITHUB_ACTIONS=true,
then 131 passed. PRD proof markers moved onto each box first line.

* ci(prd-481): the two legs a shard count cannot reach

Phase 2's third box, measured on run 37071464562: `test-unit (4/4)` ran 481s and
`template-nonvisual (sailing, 2/2)` 467s against a six-minute leg. `rain (3/5)` 358s and `rain
(2/5)` 299s are the rest of it — every other unit and template leg was under 360s. Two changes,
each from a timestamp rather than a preference.

`template-nonvisual`. Sailing's six non-visual scenarios measured 55/247/30/98/28/59 seconds in
classifier order (fail, float-after-the-run-ends, float, sails, touch-controls, win). Two slices
put `float-after-the-run-ends` alone with `sails` and `win` — 404s of scenario work, and the same
run attributes ~63s of overhead to a slice, which is the 467s it measured. Three slices put one
heavy scenario on each and the worst at 275s of work: ~335s predicted, and 3 is the smallest count
that clears it. Six scenarios, three slices, nothing dropped.

`test-unit`. The fourth leg carried `create-threenative/__tests__/template-assets-compile.spec.ts`
at 317s on its own. `vitest --shard` sorts the file list by `sha1(path)` and slices that order: it
divides a list of files, never a file, so no shard count reaches that 317s. The spec is now two
files over the two halves of the template list, with the cases in
`template-assets-compile-cases.ts` beside them — the same 40 tests, the same names, the same
assertions, all 13 templates, and the one whole-suite assertion kept once instead of copied into
both, with `TN_TEMPLATE_ASSETS_HALF_EMPTY` failing closed where a slice could come out empty.
Replaying vitest's own partition over that run's per-file durations, which reproduces its four legs
exactly (261/429/554/630s of test time), predicts 180/240/224/137s across the four legs.

The partition is still blind to duration: the two halves' hashes land them in the two lightest
legs, and renaming either file reshuffles that. Five slices would predict a 223s worst leg for one
more runner; four is what the measurement needed.

- `.github/workflows/ci.yml`: sailing 2 -> 3 slices, with the per-scenario seconds that chose it.
- `packages/create-threenative/__tests__/template-assets-compile-cases.ts` (new): the templates,
  the two halves and the three cases, once.
- `packages/create-threenative/__tests__/template-assets-compile-{1,2}.spec.ts` (new),
  `template-assets-compile.spec.ts` (deleted).
- `scripts/__tests__/ci-structure.spec.ts`: the measured sailing pin, 2 -> 3.
- `docs/PRDs/CI/PRD-481-ci-does-each-piece-of-work-once.md`: the overs from 37071464562 and what
  this commit changes under the shard box, which stays unticked — its proof is a full CI run.

Verification: `pnpm exec vitest run packages/create-threenative/__tests__/template-assets-compile-1.spec.ts
packages/create-threenative/__tests__/template-assets-compile-2.spec.ts` 40 passed in 2 files
(120.8s and 142.7s of file time), against the 40 the unsplit file measured in 270.6s;
`pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts scripts/__tests__/ci-efficiency.spec.ts
scripts/__tests__/ci-needs.spec.ts` 208 passed; `pnpm lint` exit 0; `pnpm typecheck` exit 0. That
run's `test-unit (3/4)` failure was `ci-structure.spec.ts > classifies workflow-only changes as
full CI with no prose skip`, which passes here and is untouched by this change.

* fix(ci): a draft pull request run never cancels a ready one

Pushing then marking ready fires a synchronize run whose payload still says draft;
in the shared 'latest' group with cancel-in-progress it skipped every job and
cancelled the real ready_for_review run (#403, run 37075825121). Draft runs now get
a per-run group. New ci-structure case red first; PRD-481 dist-once box ticked.

---------

Co-authored-by: Joao Paulo Furtado <jfurtado141@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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