Skip to content

Commit 769ac80

Browse files
tooling(ui-wiring): inert workspace, turbo and CI wiring for the ui/ tree (objectstack#25697 PR A) (#25710)
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.com/claude-code) https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f6ba134 commit 769ac80

12 files changed

Lines changed: 1019 additions & 35 deletions

‎.github/workflows/ci.yml‎

Lines changed: 624 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/spec/scripts/check-duration-unit-keys.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ export const ROOT_DIR_WATCH_HINTS = [
313313
'packages/connectors/*/src/**',
314314
'apps/*/src/**',
315315
'examples/*/src/**',
316+
// The objectui tree, imported under `ui/` (objectstack#25677 / #25697): four
317+
// workspace roots pnpm-workspace.yaml declares ahead of the import. Each is a
318+
// live workspace root (the self-test holds both directions), and each walks
319+
// nothing until `ui/` arrives.
320+
'ui/src/**',
321+
'ui/packages/*/src/**',
322+
'ui/apps/*/src/**',
323+
'ui/examples/*/src/**',
316324
];
317325

318326
/** Canonical unit → every spelling the describe prose or a key token may use. */

‎packages/spec/scripts/check-llms-txt.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ export const DECLARED_WATCH_HINTS = [
227227
'packages/**/package.json',
228228
'apps/*/package.json',
229229
'examples/*/package.json',
230+
// The objectui tree's workspace roots (`ui`, `ui/packages/*`, `ui/apps/*`,
231+
// `ui/examples/*`, objectstack#25677). This gate opens every manifest under
232+
// them once the tree is imported; until then the globs match nothing.
233+
'ui/**/package.json',
230234
];
231235

232236
/** A domain directory with no `*.zod.ts` under it is not a schema domain. */

‎pnpm-workspace.yaml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,23 @@ packages:
1010
- packages/connectors/*
1111
- apps/*
1212
- examples/*
13+
# The objectui tree, once it is imported under ui/ (objectstack#25677). Until
14+
# then these globs match no directory and pnpm resolves exactly the workspace
15+
# above: the lockfile, `pnpm install` and `pnpm -r ls` are unchanged. `ui`
16+
# itself is a member because objectui's root package.json carries the scripts
17+
# its CI runs (`lint:root`, `type-check:e2e`, the `check:*` gates). objectui's
18+
# own `docs` workspace entry is not carried: it names no package.
19+
# objectui's pnpm overrides and its patchedDependencies entry arrive with the
20+
# import, not here: an unused patch fails `pnpm install` and an override
21+
# rewrites this lockfile.
22+
- ui
23+
- ui/packages/*
24+
- ui/apps/*
25+
- ui/examples/*
1326

27+
# objectui declares no onlyBuiltDependencies of its own (neither in its
28+
# pnpm-workspace.yaml nor in its package.json `pnpm` field), so the union of
29+
# the two allowlists is this list unchanged.
1430
onlyBuiltDependencies:
1531
- better-sqlite3
1632
- esbuild

‎scripts/check-cross-package-test-inputs.mjs‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
426426
// turbo.json declares, one per recognised name -- so the pin moves when either
427427
// set does. That is the point: a directory dropping out of turbo.json is a
428428
// declaration disappearing, which is worth a red.
429-
'the TOP-LEVEL WHITELIST, reconciled against turbo.json (#18342)': 26,
429+
'the TOP-LEVEL WHITELIST, reconciled against turbo.json (#18342)': 30,
430430
});
431431

432432
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -1435,6 +1435,22 @@ export const TOP_LEVEL_DIRECTORIES_PENDING = Object.freeze({
14351435
'.github': 'over-collection census (#18342): two literals, both non-reads; disposition reserved to the seat',
14361436
});
14371437

1438+
/**
1439+
* Top-level directories a `$TURBO_ROOT$` input declares AHEAD of their arrival in
1440+
* the tree, each with the reason. A name here is excused from the "tracked as
1441+
* neither file nor directory" case ONLY while git tracks nothing under it; the
1442+
* moment it is tracked it lands in `directories` and owes a place in
1443+
* `RECOGNISED_TOP_LEVEL_DIRECTORIES` or `TOP_LEVEL_DIRECTORIES_PENDING` like any
1444+
* other, so the import commit decides how this collector treats it.
1445+
*
1446+
* `ui`: the objectui tree, imported under `ui/` (objectstack#25677). turbo.json's
1447+
* `type-check` and `lint` tasks carry objectui's repo-root inputs re-rooted there.
1448+
* Retire the entry with the import.
1449+
*/
1450+
export const TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT = Object.freeze({
1451+
ui: 'the objectui import (objectstack#25677): turbo.json declares its type-check and lint inputs ahead of the tree',
1452+
});
1453+
14381454
/** Repo-root FILES declared as `$TURBO_ROOT$` inputs -- the fourth shape, pinned, not collected. */
14391455
export const TURBO_ROOT_FILE_INPUTS = Object.freeze(['sdui.manifest.json']);
14401456

@@ -4353,7 +4369,27 @@ function selfTest() {
43534369
// Non-vacuity first: a reading of zero would green every per-directory case
43544370
// below by having none of them run.
43554371
ok('turbo.json still declares $TURBO_ROOT$ inputs at all', live.directories.size > 0);
4356-
ok('no declared top-level segment is tracked as neither file nor directory', live.unknown.size === 0);
4372+
const awaitingImport = new Set(Object.keys(TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT));
4373+
const unexcused = [...live.unknown].filter((name) => !awaitingImport.has(name));
4374+
ok(
4375+
`no declared top-level segment is tracked as neither file nor directory (unexcused: ${unexcused.join(', ') || 'none'})`,
4376+
unexcused.length === 0,
4377+
);
4378+
// The excuse is narrow in both directions: it covers only the names it lists,
4379+
// and only while git tracks nothing under them.
4380+
ok(
4381+
'an unknown segment the awaiting-import map does not list is still unexcused',
4382+
[...new Set(['zzqq-25697', ...live.unknown])].filter((name) => !awaitingImport.has(name)).includes('zzqq-25697'),
4383+
);
4384+
ok(
4385+
'an awaiting-import name git already tracks is NOT excused here (it must be recognised or pending)',
4386+
[...awaitingImport].every((name) => !live.directories.has(name) || recognised.has(name) || pending.has(name)),
4387+
);
4388+
ok('the awaiting-import map holds exactly the measured arrivals', [...awaitingImport].sort().join() === 'ui');
4389+
ok(
4390+
'every awaiting-import entry carries a reason',
4391+
Object.values(TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT).every((r) => typeof r === 'string' && r.length > 0),
4392+
);
43574393
for (const dir of [...live.directories].sort()) {
43584394
ok(
43594395
`turbo.json declares "${dir}/" and the collector accounts for it (recognised, or pending with a reason)`,

‎scripts/check-dispatcher-error-vocabulary.mjs‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@
288288
* accept set, deliberately not taken here.
289289
*/
290290

291-
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
291+
import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
292+
import { tmpdir } from 'node:os';
292293
// [#15487] The SHARED half of the regex-literal recogniser: the 14-word
293294
// `REGEX_AFTER_KEYWORD` set, `IDENT_CHAR`, and the literal WALK (character
294295
// class, backslash sequences, line-terminator refusal, flags) live ONCE, in
@@ -493,12 +494,46 @@ function expandWorkspaceMembers(root, glob) {
493494
* passes {@link expandWorkspaceMembers} — production and `--self-test` reading
494495
* two different populations is the defect #17145 carded, not a spare knob.
495496
*/
496-
export function emptyWorkspaceGlobs({ readGlobs, expand }) {
497+
export function emptyWorkspaceGlobs({ readGlobs, expand, awaiting = new Set() }) {
497498
return readGlobs()
498499
.filter((glob) => !isExclusionGlob(glob))
500+
.filter((glob) => !awaiting.has(glob))
499501
.filter((glob) => expand(glob).length === 0);
500502
}
501503

504+
/**
505+
* Workspace globs `pnpm-workspace.yaml` declares AHEAD of the tree they match:
506+
* the objectui import under `ui/` (objectstack#25677, wired by #25697). Each
507+
* is excused from the empty-glob refusal above ONLY while
508+
* {@link AWAITING_IMPORT_MARKER} is absent; the moment the import lands, every
509+
* one of them must expand to members or it is the finding it always was. The
510+
* self-test holds the map against the live workspace (every key is a declared
511+
* glob) and against both states of the marker. Retire the map with the import.
512+
*/
513+
export const WORKSPACE_GLOBS_AWAITING_IMPORT = Object.freeze({
514+
ui: 'objectui import (objectstack#25677): the `ui` root manifest itself (`object-ui-monorepo`)',
515+
'ui/packages/*': 'objectui import (objectstack#25677): the @object-ui/* packages',
516+
'ui/apps/*': 'objectui import (objectstack#25677): the Console and the docs site',
517+
'ui/examples/*': 'objectui import (objectstack#25677): objectui example apps',
518+
});
519+
520+
/** The file whose presence ends the excuse: the imported tree's root manifest. */
521+
export const AWAITING_IMPORT_MARKER = 'ui/package.json';
522+
523+
/**
524+
* The globs excused from the empty-glob refusal on the tree at `root`: the
525+
* whole {@link WORKSPACE_GLOBS_AWAITING_IMPORT} key set while the marker is
526+
* absent, nothing once it exists.
527+
*
528+
* @param {string} root
529+
* @returns {Set<string>}
530+
*/
531+
export function awaitingImportGlobs(root) {
532+
return existsSync(join(root, AWAITING_IMPORT_MARKER))
533+
? new Set()
534+
: new Set(Object.keys(WORKSPACE_GLOBS_AWAITING_IMPORT));
535+
}
536+
502537
/**
503538
* [#16649] Every published workspace member's `src/`, as repo-relative prefixes.
504539
*
@@ -6145,8 +6180,50 @@ function selfTest() {
61456180
ok(emptyWorkspaceGlobs({
61466181
readGlobs: () => readWorkspaceGlobs(ROOT),
61476182
expand: (g) => expandWorkspaceMembers(ROOT, g),
6183+
awaiting: awaitingImportGlobs(ROOT),
61486184
}).length === 0,
61496185
'the LIVE workspace has a glob that expands to nothing — the production run refuses on this too');
6186+
// [#25697] The awaiting-import excuse, narrow in every direction: only the
6187+
// globs it names, only while the marker is absent, and only globs the live
6188+
// workspace really declares (so the map cannot outlive the import unnoticed).
6189+
ok(emptyWorkspaceGlobs({
6190+
readGlobs: () => ['packages/*', 'ui/packages/*', 'zz-gone/*'],
6191+
expand: (g) => (g === 'packages/*' ? ['a'] : []),
6192+
awaiting: new Set(['ui/packages/*']),
6193+
}).join(',') === 'zz-gone/*',
6194+
'an empty glob the awaiting set names is excused; an empty glob it does not name is still reported by name');
6195+
ok(emptyWorkspaceGlobs({
6196+
readGlobs: () => ['ui/packages/*'],
6197+
expand: () => [],
6198+
}).join(',') === 'ui/packages/*',
6199+
'with no awaiting set the same empty glob is a finding — the excuse is opt-in at the call site, never a default');
6200+
{
6201+
const liveGlobs = new Set(readWorkspaceGlobs(ROOT));
6202+
const keys = Object.keys(WORKSPACE_GLOBS_AWAITING_IMPORT);
6203+
ok(keys.every((g) => liveGlobs.has(g)),
6204+
`every awaiting-import glob is one pnpm-workspace.yaml declares — stale keys: ` +
6205+
`${keys.filter((g) => !liveGlobs.has(g)).join(', ') || 'none'}`);
6206+
ok(keys.every((g) => typeof WORKSPACE_GLOBS_AWAITING_IMPORT[g] === 'string' && WORKSPACE_GLOBS_AWAITING_IMPORT[g].length > 0),
6207+
'every awaiting-import entry carries a reason');
6208+
ok(keys.sort().join() === 'ui,ui/apps/*,ui/examples/*,ui/packages/*',
6209+
'the awaiting-import map holds exactly the four ui roots and nothing else');
6210+
// Both states of the marker, on a scratch root: absent → the whole key
6211+
// set is excused; present → nothing is, and the import commit meets the
6212+
// refusal with no further edit here.
6213+
const scratch = mkdtempSync(join(tmpdir(), 'dispatcher-awaiting-'));
6214+
try {
6215+
ok(awaitingImportGlobs(scratch).size === keys.length,
6216+
'with the marker absent, every awaiting-import glob is excused');
6217+
mkdirSync(join(scratch, 'ui'), { recursive: true });
6218+
writeFileSync(join(scratch, 'ui', 'package.json'), '{"name":"object-ui-monorepo","private":true}\n');
6219+
ok(awaitingImportGlobs(scratch).size === 0,
6220+
'with the marker present, nothing is excused — the excuse voids itself at the import');
6221+
} finally {
6222+
rmSync(scratch, { recursive: true, force: true });
6223+
}
6224+
ok(existsSync(join(ROOT, AWAITING_IMPORT_MARKER)) === (awaitingImportGlobs(ROOT).size === 0),
6225+
'the live reading agrees with the live marker');
6226+
}
61506227
// [#17145] The same expansion, shown to be the MANIFEST-filtered one and not
61516228
// the raw directory listing. Without this the two could drift apart again
61526229
// silently, which is the whole defect: a glob resolving only to category
@@ -6516,6 +6593,7 @@ function main() {
65166593
const emptyGlobs = emptyWorkspaceGlobs({
65176594
readGlobs: () => readWorkspaceGlobs(ROOT),
65186595
expand: (glob) => expandWorkspaceMembers(ROOT, glob),
6596+
awaiting: awaitingImportGlobs(ROOT),
65196597
});
65206598
if (emptyGlobs.length > 0) {
65216599
throw new Error(

‎scripts/check-published-files.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,13 @@ const ROOT_DIR_WATCH_HINTS = [
458458
'packages/triggers/*',
459459
'apps/*',
460460
'examples/*',
461+
// The objectui tree's roots (objectstack#25677). `ui/*` stands for the bare
462+
// `ui` member (objectui's root manifest), spelled with a separator for the
463+
// reason above; the other three match its own `<dir>/*` globs.
464+
'ui/*',
465+
'ui/packages/*',
466+
'ui/apps/*',
467+
'ui/examples/*',
461468
];
462469

463470
/**

‎scripts/check-test-source-alias.mjs‎

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,21 @@ const WORKSPACE_PARENT_GLOBS = [
633633
'packages/triggers/*',
634634
'apps/*',
635635
'examples/*',
636+
// The objectui tree's package roots (objectstack#25677); they match nothing
637+
// until it is imported under ui/.
638+
'ui/packages/*',
639+
'ui/apps/*',
640+
'ui/examples/*',
636641
];
637642

643+
/**
644+
* Workspace members pnpm-workspace.yaml names as a BARE directory (no `/*`):
645+
* the one package AT that directory, declared by its manifest path so the entry
646+
* keeps the separator the dispatch deriver needs. `ui` is objectui's root
647+
* manifest (objectstack#25677); absent until the import, like the globs above.
648+
*/
649+
const WORKSPACE_BARE_MEMBER_MANIFESTS = ['ui/package.json'];
650+
638651
/** The parent directories those globs enumerate — each glob minus its leaf. */
639652
const WORKSPACE_PARENT_DIRS = WORKSPACE_PARENT_GLOBS.map((glob) => glob.replace(/\/\*$/, ''));
640653

@@ -650,22 +663,24 @@ function readJson(file) {
650663

651664
function listWorkspacePackages(root) {
652665
const out = [];
666+
const candidates = [];
653667
for (const parent of WORKSPACE_PARENT_DIRS) {
654668
const abs = join(root, parent);
655669
if (!existsSync(abs)) continue;
656-
for (const name of readdirSync(abs)) {
657-
const dir = join(abs, name);
658-
const manifest = join(dir, 'package.json');
659-
if (!existsSync(manifest)) continue;
660-
let json;
661-
try {
662-
json = readJson(manifest);
663-
} catch {
664-
continue;
665-
}
666-
if (!json.name) continue;
667-
out.push({ name: json.name, dir, rel: relative(root, dir), json });
670+
for (const name of readdirSync(abs)) candidates.push(join(abs, name));
671+
}
672+
for (const manifestPath of WORKSPACE_BARE_MEMBER_MANIFESTS) candidates.push(dirname(join(root, manifestPath)));
673+
for (const dir of candidates) {
674+
const manifest = join(dir, 'package.json');
675+
if (!existsSync(manifest)) continue;
676+
let json;
677+
try {
678+
json = readJson(manifest);
679+
} catch {
680+
continue;
668681
}
682+
if (!json.name) continue;
683+
out.push({ name: json.name, dir, rel: relative(root, dir), json });
669684
}
670685
return out;
671686
}
@@ -3322,7 +3337,7 @@ function selfTest() {
33223337
// docblock (#9955). Asserted here rather than left to review because the
33233338
// regression is a tidy-up nobody would flag.
33243339
battery('the declared population must stay READABLE by the dispatch deriver');
3325-
for (const glob of WORKSPACE_PARENT_GLOBS) {
3340+
for (const glob of [...WORKSPACE_PARENT_GLOBS, ...WORKSPACE_BARE_MEMBER_MANIFESTS]) {
33263341
expect(
33273342
glob.replace(/^(?:\.\.?(?:\/|$))+/, '').includes('/'),
33283343
`workspace parent ${glob} carries no path separator, so scripts/pm/dispatch-gates.mjs refuses it as too generic and every package under it drops out of the derived gate list`,
@@ -3346,7 +3361,12 @@ function selfTest() {
33463361
// the declaration stays and the live parse becomes its CHECK, in both
33473362
// directions, the shape check-published-files.mjs already uses.
33483363
battery('the declaration must still BE the workspace (#11510)');
3349-
const declaredParents = WORKSPACE_PARENT_GLOBS.map((g) => g.replace(/\/\*+$/, ''));
3364+
// A bare-directory member is declared by its manifest path; both spellings
3365+
// reduce to the directory pnpm-workspace.yaml names.
3366+
const declaredParents = [
3367+
...WORKSPACE_PARENT_GLOBS.map((g) => g.replace(/\/\*+$/, '')),
3368+
...WORKSPACE_BARE_MEMBER_MANIFESTS.map((m) => m.replace(/\/package\.json$/, '')),
3369+
];
33503370
const liveParents = readWorkspaceGlobs(REPO_ROOT)
33513371
.filter((g) => !isExclusionGlob(g))
33523372
.map((g) => g.replace(/\/\*+$/, ''));

0 commit comments

Comments
 (0)