Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/retire-generated-paths-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@objectstack/spec": patch
---

Retire ci.yml's `generated` paths filter and its `Check Generated Artifacts` job; every
spec artifact gate now runs in lint.yml's unfiltered, required TypeScript Type Check job.

The filter was a hand-maintained duplicate of each gate's input set, and nothing
reconciled the two. It drifted three times on record, each found by accident and written
up in a comment rather than gated:

- #2584 moved a generated page and the filter kept watching the old path, so hand-edits
to the generated block went unchecked for months.
- #3855 listed specific spec paths but no schema dirs, so `check:authorable-surface` went
dormant on exactly the PRs that remove an authorable key.
- `packages/spec/json-schema.manifest.json` — the #2978 ratchet, and the only durable
record of every schema ever emitted since `json-schema/` is gitignored — was never
watched at all, so a PR retiring a key from it skipped its own verifier.

Six of the ten gates had already escaped to the typecheck job one at a time, each with a
comment explaining that the filter had failed them. This moves the last four
(`check:skill-docs`, `check:spec-changes`, `check:upgrade-guide`,
`check:authorable-surface`) and deletes the filter, so there is no second ledger to keep
in sync — the failure mode #4255 fixed for the `check:generated` ledger, removed at the
source here rather than gated.

Affordable because the work was already being done: `check:docs` in that job runs
`gen:schema` — the same `scripts/build-schemas.ts` that backs `check:authorable-surface` —
and that entire step measures 4s in CI, against a 5-minute job dominated by the workspace
build. All four read source via tsx and need no build.
96 changes: 0 additions & 96 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ jobs:
outputs:
docs: ${{ steps.changes.outputs.docs }}
core: ${{ steps.changes.outputs.core }}
generated: ${{ steps.changes.outputs.generated }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
Expand All @@ -39,37 +38,6 @@ jobs:
- 'content/**'
- 'pnpm-lock.yaml'
- '.github/workflows/ci.yml'
# Inputs AND outputs of every generated artifact checked by the
# `check-generated` job. A path missing here makes that gate dormant on
# exactly the PRs that can break it, so keep the two in lockstep.
generated:
# check:skill-docs — catalog source is skills/*/SKILL.md; the derived
# listings are skills/README.md + the reference page below. (That page
# was content/docs/guides/skills.mdx until #2584 moved it; this filter
# kept watching the old path, so hand-edits to the generated block went
# unchecked from then until now.)
- 'skills/**'
- 'content/docs/ai/skills-reference.mdx'
- 'packages/spec/scripts/build-skill-docs.ts'
# check:spec-changes / check:upgrade-guide — derived from the ADR-0087
# registries and the protocol version, none of which the skills paths
# above cover. Both gates were dormant on spec-only PRs: a protocol bump
# or a new migration could not trigger the job that verifies them.
- 'packages/spec/scripts/build-spec-changes.ts'
- 'packages/spec/scripts/build-upgrade-guide.ts'
- 'packages/spec/src/migrations/**'
- 'packages/spec/src/conversions/**'
- 'packages/spec/src/kernel/protocol-version.ts'
- 'packages/spec/api-surface.json'
- 'packages/spec/spec-changes.json'
- 'docs/protocol-upgrade-guide.md'
# check:authorable-surface — the ratchet's input is every Zod schema
# in the spec, so the filter has to be that broad or the gate goes
# dormant on exactly the PRs that remove an authorable key (#3855).
- 'packages/spec/src/**'
- 'packages/spec/scripts/build-schemas.ts'
- 'packages/spec/authorable-surface.json'
- '.github/workflows/ci.yml'
core:
- 'packages/**'
- 'examples/**'
Expand Down Expand Up @@ -728,67 +696,3 @@ jobs:

- name: Build Docs
run: pnpm --filter @objectstack/docs build

# Generated artifacts that are committed to the tree: regenerate, fail on drift.
# Named for the class, not for skill docs alone — it has checked the ADR-0087
# protocol artifacts for a while, and a name that hid them is part of why their
# paths went missing from the filter above.
#
# The generated reference docs (content/docs/references/**) are deliberately NOT
# here: they need to gate spec-only PRs, and this whole job is filter-gated, so
# they live in lint.yml's required, unfiltered "TypeScript Type Check" job.
check-generated:
name: Check Generated Artifacts
needs: filter
if: needs.filter.outputs.generated == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Enable Corepack
run: corepack enable

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check skill docs are generated from SKILL.md frontmatter
run: pnpm --filter @objectstack/spec check:skill-docs

- name: Check spec-changes.json is regenerated with the ADR-0087 registries
run: pnpm --filter @objectstack/spec check:spec-changes

- name: Check the protocol upgrade guide is regenerated with the ADR-0087 registries
run: pnpm --filter @objectstack/spec check:upgrade-guide

# The authorable KEY surface — what a metadata author may write, which for
# this platform is the third-party API. `api-surface.json` records exported
# names and `api-surface-signatures.json` hashes factory types as TypeScript
# PRINTS them (a reference, never structurally expanded), so neither sees a
# key added to or removed from a schema. #3883 removed three authorable keys
# with every witness green; #3733 did it by accident. ADR-0059 §5 deferred
# this gate until a narrowing actually slipped both — it has.
- name: Check the authorable key surface is recorded and nothing vanished
run: pnpm --filter @objectstack/spec check:authorable-surface
61 changes: 52 additions & 9 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,55 @@ jobs:
# while the wrapper AGENTS.md prescribes exited red on `main`, running zero
# gates. Twice in three days: #4177 (`check:variant-docs`, fixed only when
# #4194 collided with the same wall) and #4232 (`check:strictness-ledger`,
# caught while wiring this step). Not in ci.yml's `check-generated` job:
# its `generated` paths filter does not watch packages/spec/package.json —
# the one file every offending PR must touch — so both offenders skipped
# that job entirely. This job is unfiltered and required, so the meta-gate
# cannot go dormant. Reads package.json only; no build, sub-second.
# caught while wiring this step). It could not live in ci.yml's
# `check-generated` job — that job was gated on a `generated` paths filter
# that never watched packages/spec/package.json, the one file every
# offending PR must touch. That job is gone now (see the four gates below);
# this job is unfiltered and required, so the meta-gate cannot go dormant.
# Reads package.json only; no build, sub-second.
- name: "Check every check:/gen: script is classified in the check:generated ledger"
run: pnpm --filter @objectstack/spec check:generated --reconcile-only

# The last four artifact gates, moved here from ci.yml's `check-generated`
# job when that job and its `generated` paths filter were deleted (#4291).
#
# The filter was a hand-maintained duplicate of each gate's input set, and
# nothing reconciled the two. It drifted three times on record, each found
# by accident and written up in a comment rather than gated: #2584 moved a
# generated page and the filter kept watching the old path, so hand-edits
# went unchecked for months; #3855 listed specific spec paths but no schema
# dirs, so `check:authorable-surface` went dormant on exactly the PRs that
# remove an authorable key; and `json-schema.manifest.json` — the #2978
# ratchet that is the ONLY durable record of every emitted schema, since
# json-schema/ is gitignored — was never watched at all, so a PR retiring a
# key from it skipped its own verifier. Six gates had already escaped to
# this job one at a time, each with a comment saying the filter had failed
# it; these four finish the migration and retire the duplicate ledger.
#
# Affordable because the work was already being done here: `check:docs`
# below runs `gen:schema` — the same scripts/build-schemas.ts that backs
# `check:authorable-surface` — and that whole step measures 4s in CI. All
# four read source via tsx and need no build, so they run before the
# workspace build, as the gates below already do.
- name: Check skill docs are generated from SKILL.md frontmatter
run: pnpm --filter @objectstack/spec check:skill-docs

- name: Check spec-changes.json is regenerated with the ADR-0087 registries
run: pnpm --filter @objectstack/spec check:spec-changes

- name: Check the protocol upgrade guide is regenerated with the ADR-0087 registries
run: pnpm --filter @objectstack/spec check:upgrade-guide

# The authorable KEY surface — what a metadata author may write, which for
# this platform is the third-party API. `api-surface.json` records exported
# names and `api-surface-signatures.json` hashes factory types as TypeScript
# PRINTS them (a reference, never structurally expanded), so neither sees a
# key added to or removed from a schema. #3883 removed three authorable keys
# with every witness green; #3733 did it by accident. ADR-0059 §5 deferred
# this gate until a narrowing actually slipped both — it has.
- name: Check the authorable key surface is recorded and nothing vanished
run: pnpm --filter @objectstack/spec check:authorable-surface

# Generated-docs gate: content/docs/references/** is generated from the spec
# by `gen:schema && gen:docs` and committed. Nothing regenerated it in CI, so
# it drifted silently — #3076 added RowCrudActionOverride to the spec and the
Expand All @@ -278,10 +319,12 @@ jobs:
# drift is served straight to consumers' agents — 6 of 113 schema pointers named
# files the spec had already deleted or renamed.
#
# Not in ci.yml's `check-generated` job for the reason above: its `generated`
# filter lists specific spec paths (migrations/, conversions/, protocol-version)
# but no schema dirs, so a PR touching src/data/** or src/ui/** — exactly what
# drives these two artifacts — never triggers it. It is not required, either.
# These moved here early, when ci.yml still had a filter-gated
# `check-generated` job: its `generated` filter listed specific spec paths
# (migrations/, conversions/, protocol-version) but no schema dirs, so a PR
# touching src/data/** or src/ui/** — exactly what drives these two
# artifacts — never triggered it, and it was not required either. That job
# and its filter are gone (#4291); every artifact gate now lives here.
#
# Both read packages/spec/src via tsx and need no build (verified with every
# workspace dist/ removed), so they run before the workspace build. check:skill-refs
Expand Down
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,12 @@ Root also exports: `defineStack`, `composeStacks`, `defineView`, `defineApp`, `d
### Touched `packages/spec`? Regenerate its artifacts BEFORE pushing

`packages/spec` has **eight** checked-in generated artifacts, each with its own CI gate.
They live in two different jobs (`TypeScript Type Check` in `lint.yml`, `Check Generated
Artifacts` in `ci.yml`), and each job runs its gates **sequentially** — so the first
stale artifact masks every one behind it, and you get one red build per artifact instead
of one for all of them. Match the change to the gate and regenerate up front:
All of them live in one job — `TypeScript Type Check` in `lint.yml`, which is required and
has no paths filter, so no gate can go dormant on the PR that breaks it (#4291 retired the
filtered `Check Generated Artifacts` job for exactly that reason). That job runs its gates
**sequentially**, so the first stale artifact masks every one behind it, and you get one
red build per artifact instead of one for all of them. Match the change to the gate and
regenerate up front:

| You changed | Gate that fails | Regenerate with `pnpm --filter @objectstack/spec …` |
|:---|:---|:---|
Expand Down
15 changes: 8 additions & 7 deletions packages/spec/scripts/check-generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* Run **every** generated-artifact gate and report **all** stale artifacts in one
* pass.
*
* CI runs these gates as separate sequential steps across two jobs, so the first
* stale artifact masks the rest: you fix it, push, and discover the next one on
* CI runs these gates as separate sequential steps, so the first stale artifact
* masks the rest: you fix it, push, and discover the next one on
* the following run. That happened twice on #4040 (`check:docs`, then
* `check:api-surface`) and twice again on #4161 (`check:spec-changes`, then
* `check:upgrade-guide`) — four pushes spent learning something one local run
Expand Down Expand Up @@ -153,11 +153,12 @@ const fix = process.argv.includes('--fix');
// individual steps, so an unclassified `check:`/`gen:` script kept every CI gate
// green while this wrapper exited red on `main` before running a single gate.
// Twice in three days — #4177 (fixed only by colliding with #4194) and #4232
// (caught wiring this flag in). ci.yml's `check-generated` job cannot host the
// fix: its `generated` paths filter does not watch packages/spec/package.json,
// the one file every offending PR must touch — both offenders skipped that job
// entirely. So lint.yml's unfiltered, required "TypeScript Type Check" job runs
// this mode instead. Reads package.json and the arrays above; no build, <1s.
// (caught wiring this flag in). It could not go in ci.yml's `check-generated`
// job: that job was gated on a `generated` paths filter that never watched
// packages/spec/package.json, the one file every offending PR must touch, so
// both offenders skipped it entirely. #4291 deleted that job and its filter and
// moved every gate to lint.yml's unfiltered, required "TypeScript Type Check"
// job, which runs this mode too. Reads package.json and the arrays above; <1s.
const reconcileOnly = process.argv.includes('--reconcile-only');
const scripts = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).scripts ?? {};
reconcileLedger(scripts);
Expand Down
Loading