Skip to content

tooling(ui-wiring): inert workspace, turbo and CI wiring for the ui/ tree (objectstack#25697 PR A) - #25710

Merged
os-zhuang merged 5 commits into
mainfrom
claude/project-thread-dwhq6r-25697
Oct 25, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/project-thread-dwhq6r-25697

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #25697 (PR A of two; PR B, which retires the two-repo pin gates, is drafted later and merges in the cutover window). Part of #25677.

What

The objectstack side of importing objectui under ui/, landed inert: until ui/package.json exists, nothing in this PR changes what pnpm installs, what turbo builds, or what CI runs.

  1. Workspace. pnpm-workspace.yaml adds ui, ui/packages/*, ui/apps/*, ui/examples/*. objectui declares no onlyBuiltDependencies, so the union with ours is our list unchanged. objectui's overrides and its @radix-ui/react-focus-scope patch arrive with the import, not here.

  2. Root turbo. turbo.json adds type-check, lint, test:dist, test:watch with objectui's inputs, outputs, dependsOn and cache settings. Its repo-root inputs are re-rooted under $TURBO_ROOT$/ui/. Two things are not carried over:

    • the //#lint:root and //#type-check:e2e dependsOn edges. Turbo refuses a whole run over a package-named dependsOn whose package does not exist yet, so those two programs are steps in the ui jobs instead.
    • its globalDependencies (pin plumbing).
  3. CI jobs in ci.yml, ported from objectui's ci.yml and lint.yml:

    • Type Check UI
    • Test UI (n/16)
    • Lint UI
    • Build & E2E UI

    The filter job gains three outputs: ui (ui/** plus the root files the jobs build from), ui_consumers (the seven packages objectui installs: spec, client, core, formula, lint, sdui-parser, types) and ui_tree (whether ui/package.json exists, set by a step with no if:). Every job's if: follows THE FILTER CONTRACT: !cancelled() && needs.filter.outputs.ui_tree != 'false' && PATH_OUTPUTS != 'false'. With ui/ absent, every job is skipped on every event. None of them is a required context or an aggregator member.

  4. A spec change compiles the Console. Type Check UI and Build & E2E UI also run on ui_consumers. On pull_request and merge_group, Type Check UI uses turbo --filter=...[base], so a spec change selects every ui package that depends on spec.

  5. Roster audit of objectui's 28 workflows: see the table below.

Gates that had to learn about a tree that has not arrived yet

Several repo gates treat "config describes something absent" as a defect. That reading is correct except for a tree that is about to be imported. Each gate below now declares the ui roots, and each hold is narrow:

  • check-turbo-task-graph: the generic keys type-check, lint and test:dist have no holder until the import. GENERIC_TASKS_AWAITING_UI_IMPORT holds them only while ui/package.json is absent. A held key that turbo.json does not declare is itself a finding. 7 new self-test cases.
  • check-cross-package-test-inputs: ui is a $TURBO_ROOT$ segment that git does not track yet. TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT excuses it only while it is untracked. Once tracked, it owes a recognised or pending entry.
  • check-published-files, check-test-source-alias, check-undeclared-dep-imports, check-llms-txt: each holds a hand copy of the workspace globs, and each now declares the four ui roots. check-undeclared-dep-imports also learned the bare DIR workspace entry; before, it made the whole sweep fatal. 2 new cases.
  • check-expected-skips: the four * UI names are declared as by-design skips.

Why objectui commands are spelled pnpm --dir ui run ALIAS

This repo's workflow gates (check-self-test-wired, the dispatch-gates derivation and others) key scripts/NAME and pnpm ALIAS against this repo's root, and none of them reads working-directory. Under working-directory: ui, objectui's scripts/check-entry-guard.mjs was credited to objectstack's file of the same name. scripts/check-lint-coverage.mjs, which only objectui has, read as a missing script.

Test UI shard count

N = 16, chosen the way scripts/partition-test-shards.mjs chooses Test Core's N: max/mean ≤ 1.3, from ui/scripts/test-shard-timings.json (4979 files, 325.7 summed minutes).

  • By package, @object-ui/app-shell alone is 104.2 minutes, so the bound admits at most 4 legs.
  • By file, objectui's own LPT sequencer gives max/mean = 1.000 at any N, so the job timeout sets N. 16 legs carry 20.4 summed minutes each.

The suite must run with cwd = ui/, because the dataset keys are relative to the vitest root.

Verification (local, on issue-25697 at 597471c)

Check Command Result
Lockfile byte-identical pnpm install --frozen-lockfile --prefer-offline && git diff --exit-code pnpm-lock.yaml exit 0 / exit 0 (also with --lockfile-only)
Workspace unchanged pnpm -r ls --depth -1 with the base workspace file vs the new one identical (84 members + root)
Same build tasks turbo run build --dry=json, before vs after 84 = 84 taskIds. Hashes were identical with the turbo/workspace commit alone. At head, 78 hashes move only because packages/spec/scripts/check-llms-txt.ts is an input of @objectstack/spec#build.
New tasks on today's tree turbo run type-check / lint / test:dist / test:watch --dry=json before: exit 1 "Could not find task"; after: exit 0. No root script and no CI step invokes them while ui/ is absent.
Workflow syntax actionlint 1.7.7 -shellcheck= .github/workflows/ci.yml (shellcheck not installed) exit 0, before and after
Derived gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run in full, plus pnpm check:pm-dispatch-gates 121 commands, all exit 0. --ran reconciliation: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN (all with recorded exit codes).
Ledger ratchet pnpm check:type-check-debt (under scripts/pm/os-verify-lock.sh) VERDICT command-exit 0
Lint on changed scripts eslint --no-inline-config on the 7 changed scripts exit 0

Throwaway import (local, never pushed, deleted afterwards). Built from this branch plus the #25678 spike's adapted ui/ tree, overrides and patch, then a lockfile recompute. Results:

  • pnpm install: 48 s.
  • pnpm -r ls --depth -1 lists 45 @object-ui/* packages plus object-ui-monorepo.
  • turbo run type-check --filter='./ui/packages/types' --dry=json resolves @objectstack/spec#build → @object-ui/types#type-check.
  • The full-scope filters select 46 ui type-check tasks. They select no @objectstack/* type-check and not the ui root (which would recurse).
  • A probe edit in packages/spec/src selects 44 ui packages under the affected filter.
  • pnpm --dir ui run type-check:coverage, lint:coverage, check:phantom-deps and check:entry-guard --self-test all exit 0.

Not verified here, owed by the CI runs on #25697 (a DO NOT MERGE branch = this head + the subtree import):

  • every new job running green with ui/ present;
  • the spec counter-probe turning Type Check UI red on a ui/ file;
  • cold and turbo-warm minutes for Type Check UI and Build & E2E UI against today's ~13 min hourly Spec Shape Gate;
  • the live required-set sweep (check-required-contexts --verify-required-set), which cannot run from a dev seat. The new names are not in REQUIRED_CONTEXTS.

Acceptance notes: what the import commit will turn red (measured on the throwaway tree)

None of these is reachable while ui/ is absent. Each needs a decision in the cutover PR (sub-issue 5) or in sub-issue 2:

  • check-turbo-task-graph and check-workspace-manifest-cycles self-tests: their DECLARED_WATCH_HINTS do not cover ui/**/package.json. They cannot declare it today, because both also red on a pattern that covers zero manifests.
  • check-published-files self-test: EXPORTS_CENSUS_FLOOR (50) must rise. The live publishable count goes 73 → 112.
  • check-undeclared-dep-imports: 9 findings. @object-ui/console imports @objectstack/spec and @objectstack/client but declares them only as devDependencies.
  • check-type-check-coverage: it cannot parse ui/tsconfig.json, which is JSONC.
  • check-cross-package-test-inputs self-test: the tracked ui/ segment must be recognised or pending, as designed.
  • Not measured, but likely: the root pnpm build (turbo run build) would include object-ui-monorepo#build, which is itself turbo run build, and the root pnpm lint (eslint .) would sweep ui/.

Roster audit (item 5)

objectui workflow Disposition
ci.yml: Type Check · Test (shard n/16) · Build & E2E ported in PR A → Type Check UI · Test UI (n/16) · Build & E2E UI
ci.yml: Test (dist pins) · coverage lane · Build Docs kept, follow-up port
ci.yml: Test (aggregate) dropped until the cutover required set
ci.yml: Changeset Fixed Group Check → sub-issue 4
lint.yml ported in PR A → Lint UI (closer-contract step → sub-issue 4)
live-e2e, lockfile-dedupe, lockfile-integrity, node-esm-load-gate, performance-budget, published-dist-gate, shadcn-check kept, follow-up port
spec-range-floors dropped (workspace:^ makes the floor the workspace version)
governed-surface-guard, half-state-patrol, hook-selftests, labeler, required-check-set-patrol covered by an objectstack workflow → sub-issue 4
board-snapshot, changelog, changeset-guard, changeset-presence, changeset-release, check-links, cross-repo-issue-closer, dependabot-auto-merge, docs-deploy, line-citation-gate, merge-queue-head-patrol, repo-gates, test-shard-timings-refresh → sub-issue 4

The full table, one line per workflow with notes, goes on #25697 as a comment (the seat posts it).

维护者速读

改了什么 —— objectstack 这一侧为导入 objectui(ui/ 子目录)预先铺线,全部惰性:在 ui/package.json 出现之前,pnpm 装什么、turbo 建什么、CI 跑什么,一个字节都不变。具体是 workspace 四条 ui 通配、根 turbo 四个任务名(type-check/lint/test:dist/test:watch)、ci.yml 四个新 job(Type Check UI / Test UI 16 片 / Lint UI / Build & E2E UI,都挂在"ui/ 树存在"这一个开关上,不存在就 skipped,且都不是必查项),以及 7 个仓内门禁学会这四个还不存在的根目录。附 objectui 28 个 workflow 的去向表。

为什么改 —— 让后面那个 2.59 M 行的导入提交只做一件事:git subtree add。接线先行、并且先在今天的树上过一轮 CI,导入那天就不用边导入边改 CI。spec 一改就编译 Console(Type Check UI 对 ui_consumers 也触发)是把"objectui 被 spec 改坏"从发版后发现提前到 PR 上发现。

风险与代价(含回滚) —— 对现有流水线零影响的证据:锁文件逐字节不变(--frozen-lockfile 过)、turbo run build --dry=json 前后同为 84 个任务、121 条派生门禁全部 exit 0、actionlint 过。代价是 ci.yml 多了 624 行当前全部 skipped 的 job;导入后每个碰 ui/ 的 PR 多跑 16 个测试 runner(与 objectui 今天一样)。回滚 = revert 一个 squash 提交,无发布、无数据副作用。未在本地量到的只有导入后 Type Check UI / Build & E2E UI 的冷热分钟数,靠下一步的 DO NOT MERGE 验证分支量。

席位意见 —— 建议批准。这是子单 3 的 PR A;PR B(退役两仓 pin 门禁)单独起草、只在切换窗口合。

你要做的(一个动作) —— 在本 PR 上留一条 APPROVED review;之后由派发席翻 ready、挂 auto-merge 走合并队列。

Landing

Tier H: .github/workflows/**. Draft until an APPROVED review from GOVERNED_APPROVERS is on record; then the dispatch seat lands it through the merge queue (maintainer ruling C). No seat flips it ready before that.
No changeset: the diff touches workflows, root config, scripts/** and packages/spec/scripts/** (not in spec's files[]), so nothing published changes.
Changed lines: 964 (931 additions, 33 deletions), under the human-merge threshold.

Clause-②: no
Metadata-protocol: none

🤖 Generated with Claude Code

https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU

…tree (objectstack#25697)

pnpm-workspace.yaml gains ui, ui/packages/*, ui/apps/* and ui/examples/*.
They match no directory until objectui is imported under ui/, so
`pnpm install --frozen-lockfile` and `pnpm install --lockfile-only` leave
pnpm-lock.yaml byte-identical and `pnpm -r ls --depth -1` is unchanged.
objectui declares no onlyBuiltDependencies, so the union is this list.

turbo.json gains the four task names objectui's packages use and the root
lacks: type-check, lint, test:dist, test:watch, with objectui's inputs,
outputs, dependsOn and cache settings. Its repo-root inputs are re-rooted
under $TURBO_ROOT$/ui/. Two dependsOn edges are not carried: //#lint:root
and //#type-check:e2e bind objectui's ROOT manifest, which is the `ui`
member here, and a package-named dependsOn on a package that does not exist
yet makes turbo refuse the whole run. The ui CI jobs run those two programs
as their own steps instead. Its globalDependencies (pin plumbing) are not
ported. `turbo run build --dry=json` lists the same 84 tasks with the same
84 hashes before and after.

Two gates read "turbo.json describes something absent" as a defect, which
is right except for a tree that is about to arrive:
- check-turbo-task-graph: type-check, lint and test:dist are held by no
  member until the import. GENERIC_TASKS_AWAITING_UI_IMPORT holds them
  only while ui/package.json is absent; the hold is void once it exists,
  and a held key turbo.json does not declare is itself a finding.
- check-cross-package-test-inputs: `ui` is a top-level segment git does
  not track yet. TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT excuses it only
  while untracked; once tracked it owes a recognised or pending entry.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
… (objectstack#25697)

ci.yml's filter job gains three outputs: `ui` (ui/** and the root files
the ui jobs build from), `ui_consumers` (the seven packages objectui
installs from this tree: spec, client, core, formula, lint, sdui-parser,
types) and `ui_tree` (ui/package.json exists, from an unconditional step).

Four jobs, ported from objectui's ci.yml and lint.yml:
- Type Check UI: objectui's coverage guard, its check:* source gates and
  root tsc programs, then `turbo run type-check` over the ui packages,
  affected-only on pull_request / merge_group. Runs on `ui` OR
  `ui_consumers`, so a spec change compiles the Console in its own PR.
- Test UI (n/16): objectui's vitest root, sharded by its own LPT
  sequencer. N stays 16: by package the 1.3 max/mean bound admits at
  most 4 legs (app-shell is 104.2 of 325.7 summed minutes); by file the
  bound never binds and 16 legs carry 20.4 summed minutes each.
- Lint UI: objectui's lint job, with //#lint:root as its own step.
- Build & E2E UI: the Console build and Playwright chromium, on `ui` OR
  `ui_consumers`.

Every job's `if:` is `!cancelled() && ui_tree != 'false' && <paths>`,
so with ui/ absent each one is skipped on every event, never failed and
never started. None is a required context or an aggregator member. The
.objectstack-pin plumbing is replaced by a turbo build of the seven
@objectstack packages; objectui root scripts run with
working-directory: ui, turbo runs from the root with directory filters.

check-expected-skips declares the four names as by-design skips.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
…o not misread them (objectstack#25697)

This repo's workflow gates key `scripts/<x>` and `pnpm <alias>` against
the repository root and do not read `working-directory`. Under
`working-directory: ui`, objectui's `node scripts/check-entry-guard.mjs`
was credited to objectstack's file of the same name, and
`scripts/check-lint-coverage.mjs`, which only objectui has, read as a
missing script (check-self-test-wired --self-test went red on it).

Every objectui command in the ui jobs is now `pnpm --dir ui run <its
alias>` or `pnpm --dir ui exec ...`. Test UI drops objectui's per-file
timings reporter and its upload: both feed the dataset refresh workflow,
which is in the governance-workflow audit, and the sequencer reads the
committed dataset meanwhile.

check-llms-txt opens every manifest under the workspace globs, so the
ui roots are declared as `ui/**/package.json` beside the others.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
…s (objectstack#25697)

Three gates hold a hand-written copy of pnpm-workspace.yaml's `packages:`
block and red when the live file names a root they do not declare. Each
now declares the ui roots, which match nothing until the import:

- check-published-files: ROOT_DIR_WATCH_HINTS gains ui/*, ui/packages/*,
  ui/apps/*, ui/examples/* (ui/* stands for the bare `ui` member, spelled
  with a separator as the declaration requires).
- check-test-source-alias: WORKSPACE_PARENT_GLOBS gains the three `/*`
  roots; the bare `ui` member is declared as WORKSPACE_BARE_MEMBER_MANIFESTS
  (`ui/package.json`), walked as the one package at that directory and
  reconciled against the live file like the globs.
- check-undeclared-dep-imports: its parser refused any entry that is not
  `<dir>/*`, so the bare `ui` entry made the sweep fatal. A bare `<dir>`
  is now the one package at that directory, absent contributes nothing;
  two new refusal-battery cases drive both directions, and `ui/**` joins
  ROOT_DIR_WATCH_HINTS.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 04273b9b2cdcadcc90c3fb44861625bddc91816e → packageMentionDocs.

@os-zhuang
os-zhuang marked this pull request as ready for review October 24, 2026 23:06
@os-zhuang
os-zhuang enabled auto-merge October 24, 2026 23:06
…roots (objectstack#25697)

PR A's first CI run turned two gates red that the derived gate list for
its paths did not name, both because pnpm-workspace.yaml now declares
four globs that match nothing until the import:

- check-dispatcher-error-vocabulary refuses any workspace glob that
  expands to no member (#16649), in production and in its self-test.
  WORKSPACE_GLOBS_AWAITING_IMPORT excuses the four ui globs ONLY while
  ui/package.json is absent (awaitingImportGlobs); the self-test holds
  the map against the live workspace, pins the exact key set, and drives
  both marker states on a scratch root so the excuse voids itself at the
  import. The excuse is opt-in at the call site, never a default.
- check-duration-unit-keys (packages/spec) mirrors the workspace roots
  as ROOT_DIR_WATCH_HINTS literals and holds them against the live file
  in both directions; the four ui roots are declared as `ui/src/**`,
  `ui/packages/*/src/**`, `ui/apps/*/src/**`, `ui/examples/*/src/**`.

Both gates and their self-tests exit 0 on the PR merge commit; every
gate the dispatch derivation names for these two files exits 0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 24, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI on 597471c344: the Lint & Repo Gates lane was red on two gates, everything else green (the four * UI jobs skipped as designed, Test Core still running at the time).

gate why red fix in 7ca50a8a7b
Dispatcher error-code vocabulary guard (check:dispatcher-error-vocabulary) refuses any pnpm-workspace.yaml glob that expands to no member (#16649); the four ui globs match nothing until the import WORKSPACE_GLOBS_AWAITING_IMPORT excuses exactly those four, only while ui/package.json is absent (awaitingImportGlobs); opt-in at the call site, self-test pins the key set, holds every key against the live workspace, and drives both marker states on a scratch root
Duration-shaped spec keys carry their unit in the key name (packages/spec check:duration-unit-keys) mirrors the workspace roots as ROOT_DIR_WATCH_HINTS literals and holds them against the live file in both directions the four roots declared as ui/src/**, ui/packages/*/src/**, ui/apps/*/src/**, ui/examples/*/src/**

Neither gate is in the dispatch-gates --commands derivation for this PR's paths, which is why the local sweep did not catch them. Reproduced on the PR merge commit (refs/pull/25710/merge), both exit 1 before and 0 after; the 80 gate commands the derivation names for the two edited files: 76 exit 0, 4 exit 3 NOT MEASURED (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content need a built dist/; they do not read either edited file and were green in CI on the previous head). eslint on both files exit 0.

The approval already on record is unaffected by the push (unpinned since 2026-09-04); auto-merge stays armed. Changed lines are now 1,019 (+1,019 / −33 → 1,052 total), still under the human-merge line.

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

Labels

ci/cd size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants