Skip to content

Commit 7435353

Browse files
ci(ui-merge): the ui filter covers the roots the ui gates read outside ui/, and Lint UI runs the spec liveness gate (#26278)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoKs8hmc2q8bGsPhpaAWD8EY)_ Part of #25712 Part of objectstack#25677 ## What The root `filter` job's `ui` globs named `ui/**`, the lockfile, the workspace, `turbo.json` and `ci.yml`. Three roots outside `ui/` are read by the ui jobs' gates and tests, and a change confined to one of them started no ui job. This PR (the parked 4b patch, applied onto current `main`) closes that gap and adds one step to Lint UI: - **F21:** `.github/workflows/**` replaces the `ci.yml` entry. Lint UI's pre-install import graph check scans every root workflow, not `ci.yml` alone. - **F16:** `.github/prompts/**` is added. objectui's prompts moved there in #25957, and Lint UI's prompt-keys check reads them. - **`skills/objectui/**`** is added (cutover bill item 35). The skill bundle moved to the root catalog in #26097; Lint UI's skill gates and Test UI's skill readers find it through `ui/scripts/skill-bundle-root.mjs`. - **F26 = B:** Lint UI runs `pnpm --filter @objectstack/spec check:liveness` after its install. Since #26150 the liveness ledger cites the renderer under `ui/`, and Spec Liveness Check neither runs on a ui-only pull request nor is required. The step takes about 3 s. Three ui tests pin the new coverage by path: `check-pre-install-import-graph.test.ts` now requires every root workflow to be matched, and `check-prompt-component-keys.test.ts` and `check-skills-paths.test.ts` pin their Lint UI home and the root they read. ### Measured cost of F21 Over the 211 merges of the 7 days before this change, 3 changed another root workflow and no ui path. Those 3 would newly start the 4 ui lanes (Lint UI, Type Check UI, Build & E2E UI, Test UI). Every other merge is unaffected. Clause-②: no Metadata-protocol: none ## Verification - Head `81471c06`, one commit on `main` `3d50a43b`; 4 files, +82 / −12. - `pnpm --dir ui exec vitest run` on the three pinning tests: 3 files, 120/120. - `pnpm --filter @objectstack/spec check:liveness`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 63 derived gates, 63 exit 0; `--ran`: 63 run, 0 NOT-MEASURED, 0 UNRUN. - Push battery (`GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs`, both tiers): 2182 cases pass, 0 fail (fast 1917, slow 265). - Model tier: dispatch-gates finds no path-derived mandate for this surface. ## 维护者速读(草稿) 1. **改了什么**:根 `ci.yml` 的 `ui` 过滤器补上 ui 门禁实际读取的三处 `ui/` 之外的根路径(全部根 workflow、`.github/prompts/**`、`skills/objectui/**`);Lint UI 增加 spec 活性账本检查一步。 2. **为什么**:只改这三处根路径的 PR 此前不会启动任何 ui 任务,ui 门禁读到的文件无人检查;活性账本自 #26150 起引用 `ui/` 下的渲染器,而 ui-only PR 不跑 Spec Liveness Check。 3. **风险**:近 7 天 211 次合并中有 3 次会因此新启动 4 条 ui 车道;活性检查一步约 3 秒。 4. **验证**:三个钉住测试 120/120,63 个派生门禁全绿,push battery 2182/0。 5. **席位意见**: 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01P33kUswHotSHBLPc6UVn6n Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef025fe commit 7435353

4 files changed

Lines changed: 82 additions & 12 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ jobs:
163163
crosspkg: ${{ github.event_name != 'merge_group' && steps.changes.outputs.crosspkg || 'true' }}
164164
# The objectui tree under `ui/` (objectstack#25677). Three outputs, read
165165
# together by the four `* UI` jobs at the bottom of this file:
166-
# ui — a path under ui/, or a root file the ui jobs build
167-
# from (the lockfile, the workspace, turbo.json, ci.yml);
166+
# ui — a path under ui/, a root file the ui jobs build from
167+
# (the lockfile, the workspace, turbo.json), or a root
168+
# the ui gates read (every root workflow, the prompts,
169+
# the objectui skill bundle; see the filter below);
168170
# ui_consumers — one of the seven packages objectui installs from this
169171
# tree changed, so the Console is compiled (Type Check
170172
# UI) and built (Build & E2E UI) against the new shape in
@@ -446,14 +448,29 @@ jobs:
446448
# narrower audits entry is left as the package that declared it
447449
# spelled it, exactly as the two narrower `skills/` entries were.
448450
- 'docs/**'
449-
# The objectui tree and the root files the ui jobs build from. A
450-
# change to the guard runs the guard, so ci.yml itself is listed.
451+
# The objectui tree, the root files the ui jobs build from, and the
452+
# three roots outside ui/ that the ui jobs' gates and tests read
453+
# (objectstack#25712):
454+
# - every root workflow, not ci.yml alone: Lint UI's pre-install
455+
# import graph check scans all of `.github/workflows/` for ui
456+
# scripts run before an install. ci.yml, the guard itself, is
457+
# one of them. Measured over the 211 merges of the 7 days before
458+
# this entry: 3 changed another workflow and no ui path;
459+
# - `.github/prompts/**`, where objectui's prompts moved in
460+
# objectstack#25957 and which Lint UI's prompt-keys check reads;
461+
# - `skills/objectui/**`, objectui's skill bundle since
462+
# objectstack#26097. Lint UI's skill gates (skills-paths,
463+
# skill-examples, skill-eval-tokens, doc-json,
464+
# shell-escape-residue) and Test UI's skill readers find it
465+
# through `ui/scripts/skill-bundle-root.mjs`.
451466
ui:
452467
- 'ui/**'
453468
- 'pnpm-lock.yaml'
454469
- 'pnpm-workspace.yaml'
455470
- 'turbo.json'
456-
- '.github/workflows/ci.yml'
471+
- '.github/workflows/**'
472+
- '.github/prompts/**'
473+
- 'skills/objectui/**'
457474
# The seven packages objectui installs from this tree (the set its
458475
# `.objectstack-pin` used to pack). A change here compiles and builds
459476
# the Console in the same PR — the check the retired Spec Shape Gate
@@ -3797,10 +3814,11 @@ jobs:
37973814
# when the tree moved under `ui/` (it reads only this repository's
37983815
# `.github/workflows/`), and which objectstack#25712 deleted with the
37993816
# rest of `ui/.github/workflows/`. They judge what objectui ships
3800-
# — `ui/content/docs/**`, `ui/skills/**`, the package READMEs and the
3817+
# — `ui/content/docs/**`, the skill bundle (`skills/objectui/**` at the
3818+
# repository root since objectstack#26097), the package READMEs and the
38013819
# documented component types — so the change most likely to trip them is
38023820
# a docs-only or skills-only PR, which the `filter` job's `ui` globs
3803-
# already include.
3821+
# include.
38043822
#
38053823
# Ported as that workflow ran them, in its order: the install-free gates
38063824
# here, before the install; the gates that read built `dist/*.d.ts` after
@@ -3883,6 +3901,18 @@ jobs:
38833901
if: ${{ !cancelled() }}
38843902
run: pnpm install --frozen-lockfile
38853903

3904+
# The spec liveness ledger cites the renderer in-repo, under ui/
3905+
# (objectstack#26150), so a ui/ change can delete a file a ledger row
3906+
# cites, move a cited line past the end of its file, drop an anchored
3907+
# symbol or stop naming the cited key. Spec Liveness Check does not run on
3908+
# a ui/-only pull request (its `paths:` are the spec side) and is not a
3909+
# required context, so the gate runs here too, where every ui/ change
3910+
# starts it (objectstack#25712 F26). It runs tsx over spec's sources and
3911+
# needs the install alone; about 3 s.
3912+
- name: Spec Liveness Check — Check the liveness ledger's citations, ui/ included
3913+
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
3914+
run: pnpm --filter @objectstack/spec check:liveness
3915+
38863916
- name: Build the @objectstack packages objectui consumes
38873917
run: |
38883918
pnpm exec turbo run build --concurrency=4 \

‎ui/scripts/__tests__/check-pre-install-import-graph.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,13 +410,15 @@ describe('the gate is wired, not merely present', () => {
410410
// Deliberate: its input is the workflows themselves, and a workflow edit is
411411
// the change most likely to break it. A filter would also make the check
412412
// unrequirable (objectui#3523). The root trigger filters no path; the job
413-
// runs on the `filter` job's `ui` globs, which must match the workflow
414-
// whose steps are the population today and the gate itself.
413+
// runs on the `filter` job's `ui` globs, which must match EVERY root
414+
// workflow, not only ci.yml, whose steps are the population today: the scan
415+
// reads them all, so a ui step added before an install in any one of them
416+
// is this gate's subject (objectstack#25712) — and the gate itself.
415417
const on = withoutComments(triggerBlock(ci));
416418
expect(on).not.toMatch(/paths-ignore:/);
417419
expect(on).not.toMatch(/^\s+paths:/m);
418420
const globs = uiFilterGlobs(ci);
419-
const missed = ['.github/workflows/ci.yml', repoPath(SCRIPT)]
421+
const missed = [...rootWorkflowFiles().map((file) => `.github/workflows/${file}`), repoPath(SCRIPT)]
420422
.filter((file) => !globs.some((glob) => globMatches(glob, file)));
421423
expect(missed, `a change to these would skip ${WORKFLOW}`).toEqual([]);
422424
});

‎ui/scripts/__tests__/check-prompt-component-keys.test.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
scanPromptSections,
4646
} from '../check-prompt-component-keys.mjs';
4747
import { INDIRECT_REGISTRATIONS, deriveRegistryKeys } from '../check-doc-component-types.mjs';
48+
import { globMatches, jobBlock, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci';
4849

4950
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
5051
/** The repository root, whose `.github/prompts/` the live prompts sit in (objectstack#25712). */
@@ -312,7 +313,8 @@ describe('wiring', () => {
312313
// this gate. objectstack#25880 ports the `repo-gates.yml` gates (row 24 of
313314
// the objectstack#25697 workflow roster) into the root `Lint UI` job and
314315
// brings these pins back against that job, by path. That case's import-graph
315-
// walk reads no workflow and stays below.
316+
// walk reads no workflow and stays below. The Lint UI home and the filter that
317+
// starts it on a prompts-only change are pinned by the second case below.
316318

317319
it('is exposed as a root package script', () => {
318320
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
@@ -321,6 +323,22 @@ describe('wiring', () => {
321323
expect(pkg.scripts['check:prompt-keys']).toBe(`node ${SCRIPT}`);
322324
});
323325

326+
it('runs in Lint UI, which a prompts-only change starts: the root `ui` filter covers the prompt surface', () => {
327+
// The prompts left `ui/` for the repository root's `PROMPT_DIR`
328+
// (objectstack#25957), where `ui/**` does not reach them, so a prompts-only
329+
// pull request started no ui job until the filter named that root
330+
// (objectstack#25712).
331+
const ci = rootCi();
332+
const lint = jobBlock(ci, UI_JOBS.lint);
333+
expect(lint).toContain(uiRun('check:prompt-keys'));
334+
expect(lint).toContain("needs.filter.outputs.ui != 'false'");
335+
const probe = `${PROMPT_DIR}/any.prompt`;
336+
expect(
337+
uiFilterGlobs(ci).some((glob) => globMatches(glob, probe)),
338+
`no \`ui:\` filter glob covers ${PROMPT_DIR}/`,
339+
).toBe(true);
340+
});
341+
324342
it('needs no install: its import graph reaches no package', () => {
325343
// Walk the WHOLE static import graph, not just this gate's first line: it
326344
// imports a sibling gate, and "needs no node_modules" has to stay true of

‎ui/scripts/__tests__/check-skills-paths.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
readBaseline,
1717
BASELINE_FILE,
1818
} from '../check-skills-paths.mjs';
19+
import { SKILL_BUNDLE } from '../skill-bundle-root.mjs';
20+
import { globMatches, jobBlock, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci';
1921

2022
/**
2123
* objectui#3735 — the test for `scripts/check-skills-paths.mjs`.
@@ -516,7 +518,9 @@ describe('wiring — the gate is reachable by name, beside its baseline', () =>
516518
// the monorepo: GitHub runs the root `.github/workflows/` alone, and no
517519
// workflow there runs this gate. objectstack#25880 ports the `repo-gates.yml`
518520
// gates (row 24 of the objectstack#25697 workflow roster) into the root `Lint
519-
// UI` job and brings these pins back against that job, by path.
521+
// UI` job and brings these pins back against that job, by path. The Lint UI
522+
// home and the filter that starts it on a skills-only change are pinned by
523+
// the last case below.
520524

521525
it('is exposed as a root package script', () => {
522526
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
@@ -526,4 +530,20 @@ describe('wiring — the gate is reachable by name, beside its baseline', () =>
526530
it('ships its baseline next to the script', () => {
527531
expect(fs.existsSync(path.join(repoRoot, BASELINE_FILE))).toBe(true);
528532
});
533+
534+
it('runs in Lint UI, which a skills-only change starts: the root `ui` filter covers the bundle', () => {
535+
// The bundle left `ui/` for the repository root's `SKILL_BUNDLE`
536+
// (objectstack#26097), where `ui/**` does not reach it, so a skills-only
537+
// pull request started no ui job until the filter named that root
538+
// (objectstack#25712).
539+
const ci = rootCi();
540+
const lint = jobBlock(ci, UI_JOBS.lint);
541+
expect(lint).toContain(uiRun('check:skills-paths'));
542+
expect(lint).toContain("needs.filter.outputs.ui != 'false'");
543+
const probe = `${SKILL_BUNDLE}/any`;
544+
expect(
545+
uiFilterGlobs(ci).some((glob) => globMatches(glob, probe)),
546+
`no \`ui:\` filter glob covers ${SKILL_BUNDLE}/`,
547+
).toBe(true);
548+
});
529549
});

0 commit comments

Comments
 (0)