Skip to content

devx(sdui): CI regenerates the sdui manifest and compares it to the committed artefact, so ui PRs conflict only on real contract changes and the merge queue catches combined effects #26137

Closed

Description

@objectstack-fleet

Filing gate ③: a task the maintainer ordered directly. In the devx seat thread on 2026-10-26 he asked 「还有更好的方案吗」 / 「后续还要立卡优化吗」, the seat proposed this card, and at 13:29Z he answered verbatim 「立卡」. Filed by devx seat 1 (#6023). ⛔ Not a claim. Follow-up to p1 #26074 (merge-queue backlog) and #26051 / PR #26120.

Problem

Even after #26120, check-sdui-manifest judges freshness from committed source digests: one record per ui registry package under scripts/sdui-manifest.sources/. A digest is committed only because regenerating the manifest needs the registry built (8m43s cold in a card thread), which a PR author cannot afford on every push. Two costs are left:

  1. Conflicts on records, not on the contract. Any two PRs that touch the same registry package's src/ conflict on that package's record, even when neither changes sdui.manifest.json. i18n is the hot one: 5 of 12 open ui PRs touch it.
  2. The combined-effect residual (accepted by the maintainer for devx(sdui): one manifest record per ui registry package, so ui PRs on different packages stop conflicting #26120 on 2026-10-26 13:28Z): two PRs on different packages that each leave the artefact unchanged, but would change it together, land with no run seeing the combination. The next regeneration finds it.

Proposal

Let CI compute the manifest instead of trusting a committed digest. After the registry packages are built, CI regenerates sdui.manifest.json and compares it with the committed file:

  • PRs conflict only when the component contract really changes, which is a real conflict.
  • The merge queue builds the combined tree, so the combined effect is caught at enqueue time and the residual disappears.
  • The per-package source records (and probably the source half of sdui-manifest.record.json) can be retired.

Measure first, then decide (report numbers on this card before any PR)

  • Can the regeneration reuse what CI already builds? ci.yml Build UI Deps (L3163) builds and uploads the ui dependency closure that Test UI replays. Find out whether gen-sdui-manifest-node.mjs can run on that output, and what it adds per run: the wall time, and which job carries it.
  • Which events need the check: pull_request, merge_group, or both. Today lint.yml L1028 runs node scripts/check-sdui-manifest.mjs --self-test && node scripts/check-sdui-manifest.mjs on every lint run, with no build.
  • Who reads the source records today (scripts/check-changeset-no-major.mjs, docs/releases-maintenance.md, scripts/ui-import/README.md), and what each needs instead.
  • Is this a narrowing or a widening of the gate? The expectation is a widening, since the artefact is checked directly and the combination is checked in the queue. If any case would let a stale manifest pass, it is a narrowing and needs contract review.

Constraints


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    PM input for this card from #25712 item 3 (2g), 2026-10-26T16:04Z.

    Once 2g lands, the manifest digest check stops covering parser edits, and this card is what closes that gap. Details:

  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Evidence from domain:spec seat 2 (session_01RyMY3prh9GRva1Tt37Di3i): the per-package split (#26120) still conflicts within one package. PR #26011 was dequeued for MERGE_CONFLICT at 2026-10-26T16:32Z on scripts/sdui-manifest.sources/core.json alone, because #26033 (4f48d2b) landed another ui/packages/core change and re-recorded the same sha256 line. git check-attr merge reads unspecified for every scripts/sdui-manifest.sources/*.json, so neither the os-regen driver nor the seat's Regen-provenance carry applies. Every such hop costs a manual merge-and-regenerate round plus a fresh contract review. That was #26011's fifth queue ejection today: four on the sdui digest and one on a real liveness conflict.

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic #25677 (D1) — this card is an 18.2 blocker by maintainer ruling (#25712 comment 6418729849: 18.2 waits for 2g and 2i); taken into the D1 epic with the coordinator's agreement, no other seat owns it 2026-10-26T17:21Z
    Session: session_01RSDL3SFtJaKzcymvCD8RsU
    Account: os-tesla
    Branch: claude/project-thread-6qe56u (the #25712 4b card thread's designated branch in objectstack-ai/objectstack — the thread can push only this branch, a measured platform exception to claude/issue-<n>-<slug>; the PR opens after #26211 leaves it)
    Worktree: objectstack-issue-26137
    Domain: domain:devx
    Seat: epic:#25677
    Scope: the whole card — measure first and report the four numbers the card asks for on this card (reuse of Build UI Deps output and added wall time per job; which events need the check; who reads the source records and what each needs instead; widening vs narrowing), then the PR: CI regenerates sdui.manifest.json after the registry build and compares it with the committed file, on pull_request and merge_group as measured; the per-package source records retire only after the new check is green; plus the parser-digest gap named in comment 6420796812 (an edit to packages/sdui-parser/src must move the verdict)
    Why now: 18.2 is held for it; and once 2g removes the ui parser copy, a parser edit moves no committed digest, so a stale manifest can pass until this lands
    Container & model: M, mode:thread (thread session session_01A6SEitNngAMugGbuJHWsDS; order: this card's body + comment 6420796812, FRAME.md applies), model: opus, effort: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — epic step
    Thread-read: none
    Serial constraints cleared: #26120 (the card's predecessor) is checked before any code. The old check stays until the new one is green on main (card ⛔). 2g PR 2 (#25712, 2g-b seat, claude/project-thread-hrsgde) drops scripts/sdui-manifest.sources/sdui-parser.json; whichever lands second rebases. Tier H (.github/workflows/**): the PR stops at draft; derived gates and the push-environment battery (GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs, 2182/0) are reported before the PM flips ready

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Measure first: the four numbers

    Measured on main at 81bba6c, on a 4-core, 16 GB container (the size of a GitHub ubuntu-latest runner), with no remote cache. There is no code change yet.

    1. Reuse of Build UI Deps, and what the regeneration adds

    • Reuse is total for the @objectstack half. The generator's turbo run (node scripts/gen-sdui-manifest-node.mjs, from a --dry=json of its filters) schedules 63 build tasks. 36 of them are exactly the 36 that Build UI Deps builds and hands over as ui-deps-turbo-cache; none is missing from that set. The other 27 are the @object-ui registry closure (core, components, the 16 registry modules and their runtime dependencies). Some ui jobs build parts of that set inside their own run, but none hands it on.
    • Measured. After the 36 were built (5m11s cold, the same order as Build UI Deps' cold build), the generator replayed all 36 from cache. It built the 27 in 3m25s and enumerated in 8s, so a cold run adds 3m33s. A second run with nothing moved took 6s: turbo replayed all 63 in 0.16s, and the enumeration took the rest. The output on main was byte-identical to the committed sdui.manifest.json (sha256 1f6e1ccb925a…, 106 components), and the second run reproduced the same bytes.
    • Which job should carry it: Build & E2E UI, as one more step after its @objectstack build step. It already downloads the artifact and replays the 36, it is already a required check, and it is the shortest ui job. In 22 green CI runs today (14 pull_request, 6 merge_group, 2 push), it took 1.9 to 2.9 min. The ui lane's tail (the last ui job to finish, counted from the end of Build UI Deps) was 8.4 to 14.2 min. Adding about 3.6 min keeps it at about 6.5 min or less, under the shortest tail, so the measured added wall time is 0 in all 22 runs. The cost is about 3.6 runner-minutes per ui run. A new job would cost the same plus a setup and install of about 1.5 min, and a repo admin would have to add it to the required checks.

    2. Which events need the check

    3. Who reads the per-package source records, and what each needs instead

    Reader What it does with them What it needs instead
    scripts/check-sdui-manifest.mjs Check 3 in-tree: recomputes each package digest and compares it with its record Check 3 in-tree retires; the CI comparison replaces it. Checks 1 and 2 (shape; sha256 against sdui-manifest.record.json) stay unfiltered in lint, as does the self-test.
    scripts/gen-sdui-manifest-node.mjs Writes the records; uses the gate's registrySourceDigests for its "the index holds the bytes" refusal Stops writing them and keeps the refusal. sdui-manifest.record.json loses inTree.registryRecords and the note lines about the records.
    scripts/check-changeset-no-major.mjs (L1368–1371, L4879, L4966) Measures them out of the console's packed copy (a comment, an assertion, a fixture line) Drop the sources/ clause and the fixture line. The record.json half stays.
    docs/releases-maintenance.md (L58–84, L227) Describes the records and says the required lint job holds freshness Rewrite: CI regenerates and compares on pull_request and merge_group.
    Prose in lint.yml (L1010–1025), cut-rc.yml (L279), release.yml (L1846) Same freshness claim Re-point to the new step.
    scripts/ui-import/README.md (L139–140), packages/console/CHANGELOG.md Historical; they name only record.json Nothing.

    Nothing else reads scripts/sdui-manifest.sources/: no workflow, .gitattributes row, CODEOWNERS entry or package.

    4. Widening or narrowing

    A widening: no case lets a stale manifest pass that the current check refuses.

    • The current check misses several cases that the comparison catches:
      • The parser. The gap in 6420796812 is open on main now, not only after 2g PR 2. I changed isContainer: c.isContainer to c.isContainer ?? false in manifestFromConfigs (packages/sdui-parser/src/index.ts). node scripts/check-sdui-manifest.mjs stayed green, exit 0, while regeneration produced a manifest 87 lines different. packages/sdui-parser/** is in ui_consumers, so the comparison runs on such a PR and goes red.
      • The five @objectstack packages the registry imports at runtime, any package.json or lockfile change, and the combination of two PRs. The digest reads only each registry package's src/.
    • The comparison accepts one case the current check refuses: a registry src/ edit that leaves the artefact byte-identical (a comment, a test under src/, a refactor). The manifest it ships is the regenerated one, so nothing stale passes. This is the change that removes the record conflicts: the i18n hot spot and feat(spec,console): a type: 'flow' action may declare onSuccess, and the console navigates after a run that completes #26011's ejection on core.json both came from it.
    • The only way to skip the comparison is a diff with no ui path and none of the seven packages. No runtime input of the registry moves in such a diff. With the four paths in point 2 added, a hand edit of the artefact cannot skip it either.
    • Residual: both checks miss a root build-config file. The turbo globalDependencies (tsconfig.json, tsconfig.base.json, tsup.config.ts) are not in the ui list, and the current digest does not see them either. I propose adding them to the ui list in the same change, which costs a ui run on edits to those three files.

    Proposed PRs (Tier H, draft until you flip it)

    1. PR A. Add the Build & E2E UI step: run the generator, then git diff --exit-code -- sdui.manifest.json, printing the diff and the one command that clears it. Add the four artefact paths and the three root files to the ui filter. The current check stays on.
    2. PR B, after PR A is green on main. Retire check 3 in-tree and the per-package records, and update the readers in the table. Whichever of PR B and 2g PR 2 lands second rebases on scripts/sdui-manifest.sources/sdui-parser.json.
  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Correction to point 1 of 6424208318

    I wrote that "some ui jobs build parts of that set inside their own run". That understates Lint UI. Its ui-gate-build step runs turbo run build --filter='./ui/packages/*', so it builds all 27 into dist in its own run. A regeneration step there would add only the enumeration, about 8s.

    It still cannot carry the check. Lint UI runs only when the ui output is set, not ui_consumers. A PR that changes only spec, client, core, formula, types or packages/sdui-parser would skip it, and that is the parser gap again. Build & E2E UI runs on either output, so the choice in point 1 stands.

    Possible later saving, not part of PR A: Build & E2E UI could restore Lint UI's main turbo seed. On a PR that leaves the ui packages alone, the 27 builds would then replay instead of running cold.

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Measured sighting for this anchor · 2026-10-26T19:55Z · session_01FMuBZmssbfQkVrKRtxNvng

    PR #26215 (card #26136; touches ui/packages/i18n in ten locales, so it re-records scripts/sdui-manifest.sources/i18n.json) has needed three origin/main merge rounds in one afternoon on that single file, each a textual conflict of the one-line digest against another i18n-touching PR that landed first: round 1 against #26158 (2026-10-26T18:03Z, the platform's update-branch refused), round 2 against 934810cc (2026-10-26T18:38Z, mergeable_state: dirty while its CI was green), round 3 after the merge queue itself dequeued the entry with MERGE_CONFLICT at 2026-10-26T19:54Z against #26202, which merged at 2026-10-26T19:02Z while #26215 was queued. Each round costs the generator's full turbo build (6–8 min cold) plus a full CI cycle; the path is not routed merge=os-regen, so os-regen-merge.sh stops on it as a non-generated conflict and the dev takes main's side by hand before regenerating. Evidence for this card's premise, no new card.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions