Skip to content

test(runtime): pin the multi-value cascade-delete path on PostgreSQL, not only SQLite - #18732

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-18617-cascade-delete-pin-postgres-face
Sep 17, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-18617-cascade-delete-pin-postgres-face

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #18617

Clause-②: no

What moved

packages/runtime/src/cascade-delete-multivalue-lookup-real-driver.integration.test.ts was the only real-stack pin of the multi-value cascade-delete path — ObjectQL over a real SqlDriver through the protocol data-plane delete — and it hard-coded client: 'better-sqlite3'. That literal is the named mechanism by which #18172 reached two published releases: every DELETE of an object targeted by a multiple: true reference answered 500 on PostgreSQL, always, and this pin stayed green throughout.

The file now declares a driver axis: the embedded SQLite cell plus a live PostgreSQL cell provisioned by OS_TEST_POSTGRES_URL. Everything below the one newDriver() seam is written once and measured on both.

packages/runtime/src/expected-read-refusal-noise.ts had to move with it, and that is declared here rather than buried — see In-place repair below.

The measurement — this pin was shown to go RED before it was allowed to be green

The acceptance bar was not coverage. Ablation: SqlDriver.applyJsonMembership made to answer false unconditionally behind a runtime-evaluated marker, so the cascade probe's $contains falls through to the pre-#17590 substring emitter. driver-sql rebuilt each leg (runtime's tests resolve @objectstack/driver-sql through exports, i.e. dist/), and scripts/ablation-dist-preflight.mjs used to prove the mutation reached the artifact before any colour was read.

mutate  -> on-disk anchor 1, marker 1; blob d25e03f4 vs HEAD 2a17fc14
        -> rebuild -> preflight: marker present in 2 built files
        -> vitest: 6 failed | 9 passed (15)
           ALL SIX failures in "(live postgres)" — ZERO in "(better-sqlite3)"
restore -> git checkout HEAD -- THE_PATH; git diff HEAD empty; marker 0 on disk
        -> rebuild -> preflight --absent: marker absent from all 6 built files,
                      working tree clean against HEAD
        -> vitest: 15 passed (15)
        -> final blob 2a17fc14 == HEAD blob

The fault reproduced byte for byte, on a live PostgreSQL 16.13 with timezone=Asia/Shanghai:

[sql-driver] DATABASE_ERROR - the backend refused a read on 'zz_field_zoo' (42883).
  ... select * from "zz_field_zoo" where "f_lookups" LIKE $1 ESCAPE $2
      - operator does not exist: json ~~ text
Serialized Error: { code: '42883', file: 'parse_oper.c', routine: 'op_error' }

That asymmetry is the whole card. A multiple: true lookup is a TEXT column on SQLite and a real json column on PostgreSQL — measured through information_schema on the live server, and now asserted as this cell's non-vacuity guard. On TEXT the membership lowering and the substring lowering are indistinguishable, so no assertion written against SQLite can separate them. Only the live cell can, and now it does.

Without a server

The unprovisioned cell is a NAMED skip that says which variable would run it, never a silent omission — the suite is emitted either way, because a cell that emits nothing at all is not a skip and vitest's summary counts would read as coverage. Under OS_EXPECT_LIVE_DIALECT_MATRIX=1 the missing URL is a failure instead, so a runner that declared it provisioned a server cannot quietly degrade to SQLite-only. Measured: the full @objectstack/runtime suite without a URL reports 3661 passed | 1 skipped, and that one skip is this cell naming itself.

⚠️ Read the limit honestly: a named skip is a report, not coverage — see Not done here.

Isolation

The live cell owns a PostgreSQL schema named from this file's repo-relative path (os_lv_cascade_delete_multivalue_lookup_r_b7005a0a8835), recreated per test and dropped in afterAll. Object names here are short and generic (zz_account, zz_guard) and CI provisions ONE server for every live leg, so a shared schema would not be contention but destruction. Same derivation as driver-sql's live-dialect-matrix.testkit.ts and metadata-protocol's live-mysql-database.testkit.ts — same os_lv_ prefix, same 34-character slug cap, same 12 hex of sha256 over the workspace-relative path — so the copies stay jointly injective on that one server. pnpm check:live-db-isolation green.

In-place repair, declared: the noise capture recognised only SQLite

expected-read-refusal-noise.ts matched its reason half against the literal no such table: TABLE_NAME, which is SQLite's phrasing and only SQLite's. On PostgreSQL the driver writes relation "sys_organization" does not exist, so the fail-soft tenancy probe's refusal was never recognised: the capture printed the noise it exists to withhold AND silentChannels() reported the channel silent. Measured — all eight cases of the live cell red on that assertion, with the driver's (42P01) line sitting in the log immediately above it. Relaxing the pin is explicitly forbidden by its own docblock, and the pin was not wrong; the recogniser was SQLite-shaped.

So the reason half became the caller's: MissingTableReason, defaulting to SQLite's sentence so all eighteen existing call sites are byte-unchanged, with the PostgreSQL spelling exported beside it and this matrix picking per cell through a Record keyed on the cell id — a new cell cannot be added without answering the question. Neither half is widened: the envelope still pins the table positively and the reason must still be that dialect's missing-table sentence about that same table, so a permission denial, a dropped connection or a syntax fault still fails it on every dialect.

⚠️ The first cut reached for isMissingTableError in @objectstack/types instead, and that was measured and withdrawn. This module is imported by RELATIVE path out of trigger-record-change and plugin-approvals, so a bare workspace specifier added to it lands in THEIR resolution domain: check:test-source-alias went red naming @objectstack/trigger-record-change: NEW unaliased artifact import(s) ... @objectstack/types, and green again on the reverted tree (controlled revert, both directions run). The gate's prescribed repair is an alias in those packages' vitest configs — outside this card's file surface and a different defect class — so the design changed instead of the surface.

MySQL is deliberately absent from that vocabulary: its sentence is database-qualified (Table 'db.t' doesn't exist), the plain fragment form cannot carry it, and no fixture here rigs MySQL. Declaring an entry nothing exercises is the declared-not-enforced trap. It arrives with the fixture that needs it.

Not done here: the CI leg (outside this card's file surface)

⚠️ The Temporal Conformance (live PG + MySQL) job runs driver-sql, the non-SQL temporal backends, and metadata-protocol. It does NOT run @objectstack/runtime, so nothing in CI hands this cell a URL and the cell will report itself un-run on every job until that changes. The honest wiring is two steps in .github/workflows/ci.yml, in the job that already provisions the postgres service:

      - name: Build runtime and its dependencies
        run: pnpm exec turbo run build --filter=@objectstack/runtime... --concurrency=4

      - name: Run the runtime cascade-delete matrix against live PostgreSQL
        env:
          OS_TEST_POSTGRES_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres
          OS_EXPECT_LIVE_DIALECT_MATRIX: '1'
        run: |
          pnpm --filter @objectstack/runtime exec vitest run --project local \
            cascade-delete-multivalue-lookup-real-driver

The positional is a SUBSTRING, not a glob — the same reading the metadata-protocol step records for itself, where the glob form matched zero files. .github/workflows/** is outside the declared surface for this card, so no workflow is edited here; this is reported for the seat to place.

Does this fold into #18200?

Measured, and the reading is no. #18200 is about WHICH FILES a local green covers — a driver-sql run blind to 11 of its 188 files. This card is about WHICH DIALECT one file runs on. Running all 188 driver-sql files would not have caught #18172, because the only real-stack pin of the cascade path does not live in driver-sql at all; it lives in packages/runtime and it ran, green, on the one dialect that cannot fail. Two different mechanisms with the same symptom. #18200 is not addressed here and remains open.

Scope and grading

  • Clause-②: no — measured, not assumed. Both changed files live in packages/runtime/src/ and neither is reachable from src/index.ts, which is tsup's single entry. Grepping the built artifact across the package's whole files[] (dist, README.md, CHANGELOG.md): captureExpectedReadRefusals, POSTGRES_MISSING_TABLE_REASON, MissingTableReason, zz_field_zoo, os_lv_, OS_TEST_POSTGRES_URL — 0 files each. Positive control on the same built tree: AppPlugin 6 files, DriverPlugin 4. Nothing published moves.
  • skip-changeset — same measurement. No released package ships anything this diff touches.
  • needs:contract-review is the seat's label; this PR neither hangs nor removes it.

Verification

Run on bb71ba4526 (this branch merged with origin/main through scripts/pm/os-regen-merge.sh), every heavy run through scripts/pm/os-verify-lock.sh.

what reading
@objectstack/runtime suite (no PG URL) 265 files, 3661 passed | 1 skipped
the matrix file, live PG + OS_EXPECT_LIVE_DIALECT_MATRIX=1 15 passed (15) — 7 SQLite, 8 live postgres
ablation, live PG 6 failed | 9 passed — all six in the live cell, none in SQLite
@objectstack/runtime typecheck green (test layer 27 files / 191 errors / 69 pinned signatures, unchanged)
@objectstack/trigger-record-change test 101 passed (101) — cross-package consumer of the changed helper
@objectstack/plugin-approvals test 764 passed (764) — same
@objectstack/spec check:generated all 15 generated artifacts up to date (post-merge)
derived gate families (scripts/pm/dispatch-gates.mjs) 50 derived, 49 run green, 1 NOT MEASURED
pnpm check:live-db-isolation green (outside the derivation; run anyway)
eslint . --no-inline-config exit 0 over the whole population — 6822 files, 0 findings, on bb71ba4526

pnpm check:dual-build-cjs-loads is the one NOT MEASURED: it exits 3 with PREREQUISITE NOT MET because it reads built output for ~30 packages this worktree never built. That is not a pass and not a finding; CI builds the repo and measures it there.

Acceptance notes

Observations found on the way, filed nowhere and fixed nowhere, per Prime Directive #10:

  • 29 other test files under packages/runtime/src/ also hard-code client: 'better-sqlite3'. noted, not filed — none of them carries [finding] The only real-stack pin of the multi-value cascade-delete path hard-codes better-sqlite3 — the named mechanism by which #18172 (p1, PostgreSQL) shipped in two releases #18617's receipt (a dialect-specific defect that shipped), most pin behaviour with no dialect axis at all, and converting them wholesale would be exactly the scope expansion the directive forbids. The carrier for the next one is whichever card names a dialect-specific defect on that path.
  • expected-read-refusal-noise.ts is imported by RELATIVE path from two packages outside packages/runtime (trigger-record-change/src/record-change-integration.test.ts, plugin-approvals/src/status-mirror-cascade.integration.test.ts), which is what makes any bare workspace import added to it a change to their resolution domains. noted, not filed — it is a coupling, not a defect, and it is now recorded in the module's own header where the next author will meet it.

Generated by Claude Code

… not only SQLite

`cascade-delete-multivalue-lookup-real-driver.integration.test.ts` was the only
real-stack pin of the multi-value cascade-delete path, and it hard-coded
`client: 'better-sqlite3'`. That single literal is the named mechanism by which
#18172 shipped in two published releases: every DELETE of an object targeted by
a `multiple: true` reference returned 500 on PostgreSQL, always, and the pin
stayed green throughout because SQLite stores the lookup in a TEXT column where
the membership lowering and the substring lowering are indistinguishable.

The file now declares a driver axis: the embedded SQLite cell, plus a live
PostgreSQL cell provisioned by `OS_TEST_POSTGRES_URL`. An unprovisioned cell is
a NAMED skip and a failure under `OS_EXPECT_LIVE_DIALECT_MATRIX=1`, so a run
that did not measure PostgreSQL says so rather than counting as coverage. The
live cell runs in a per-file schema derived from the file's repo-relative path
(never a literal — `scripts/check-live-db-isolation.mjs`), recreated per test
and dropped afterwards, and opens with a non-vacuity case that asserts the
server really is PostgreSQL and that the driver really gave the lookup a JSON
column there.

`expected-read-refusal-noise.ts` had to move with it: its reason half matched
the literal `no such table:`, which is SQLite's phrasing alone, so on any other
dialect a fail-soft probe's refusal was never recognised — printed to the log
while `silentChannels()` reported the channel silent. It now asks
`isMissingTableError` from `@objectstack/types`, the repo's one vocabulary for
that question, keeping the "envelope and reason name the same table" property.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
…ntence

The first cut of this reached for `isMissingTableError` in `@objectstack/types`,
which is the right shape for an error OBJECT and the wrong one here: this module
is imported by RELATIVE path out of `trigger-record-change` and
`plugin-approvals`, so a bare workspace specifier added to it lands in THEIR
vitest resolution domain. Measured — `check:test-source-alias` went red naming
`@objectstack/trigger-record-change: NEW unaliased artifact import(s) …
@objectstack/types`, and green again on the reverted tree — and the gate's own
prescribed repair is an alias in those packages' configs, which is outside this
card's file surface and a different defect class from the one it is about.

So the reason half becomes the CALLER'S: `MissingTableReason`, defaulting to
SQLite's sentence so all eighteen existing call sites are byte-unchanged, with
the PostgreSQL spelling exported beside it and the cascade-delete matrix picking
per cell through a `Record` keyed on the cell id — a new cell cannot be added
without answering the question. Neither half of the predicate is widened: the
envelope still pins the table, the reason must still be that dialect's
missing-table sentence about that same table.

MySQL is deliberately absent — its sentence is database-qualified, so the plain
fragment form cannot carry it and no fixture here rigs MySQL. It arrives with
the fixture that needs it.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 6 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/deployment/cli.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/deployment/seed-tenancy-repair.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/administrator-guide.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectql/schema.mdx (via sys_organization (literal, a string literal in a comment on a changed line))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-1.mdx (via sys_organization (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-4.mdx (via sys_organization (literal, a string literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 26 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 080688b561417fcbe6f3d28f88bac73a0c06b953 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e76e3d54a3bcd89ce6bfe48b4f21f3f080fa0262 — the merge of head bb71ba4526415b16d2adc1cbb0201ec6a8383b46 into base 080688b561417fcbe6f3d28f88bac73a0c06b953, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e76e3d54a3bcd89ce6bfe48b4f21f3f080fa0262 && git checkout e76e3d54a3bcd89ce6bfe48b4f21f3f080fa0262
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 080688b561417fcbe6f3d28f88bac73a0c06b953 bb71ba4526415b16d2adc1cbb0201ec6a8383b46 && git checkout -B drift-repro 080688b561417fcbe6f3d28f88bac73a0c06b953 && git merge --no-ff bb71ba4526415b16d2adc1cbb0201ec6a8383b46

node scripts/docs-audit/affected-docs.mjs --json 080688b561417fcbe6f3d28f88bac73a0c06b953

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 080688b561417fcbe6f3d28f88bac73a0c06b953 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@huangyiirene
huangyiirene marked this pull request as ready for review September 17, 2026 17:35
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 41f9f46 Sep 17, 2026
40 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-18617-cascade-delete-pin-postgres-face branch September 17, 2026 18:00
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ve PostgreSQL URL (objectstack-ai#18772)

Fixes objectstack-ai#18734

Clause-②: no

## What moved

Two steps into `Temporal Conformance (live PG + MySQL)`, the job that
already provisions a health-checked PostgreSQL 16 and already runs
driver-sql, the non-SQL temporal backends and metadata-protocol against
it.

PR objectstack-ai#18732 gave
`packages/runtime/src/cascade-delete-multivalue-lookup-real-driver.integration.test.ts`
a live PostgreSQL cell and **showed it red** before allowing it green
(ablation of `SqlDriver.applyJsonMembership`: `6 failed | 9 passed`, all
six in the live cell, zero in SQLite, reproducing `operator does not
exist: json ~~ text` / `42883` byte for byte). Nothing in CI handed that
cell a URL, so it reported itself un-run on every job. "There is a pin
that can go red" and "that pin will be run" are two different claims,
and only the first was true.

**The two steps are byte-identical to the derivation in PR objectstack-ai#18732's
body** — same md5 over the ten lines, not retyped from memory and not
re-derived here. The only thing this PR adds around them is the comment
block that records why they exist and what must not be done to them.

## The positional is a SUBSTRING, not a glob — measured on THIS file

The card and objectstack-ai#18732 both carry this reading from metadata-protocol. It
was re-measured here, on the file this step actually selects, rather
than carried over:

| form | reading |
|:--|:--|
| `cascade-delete-multivalue-lookup-real-driver` (the substring, as
shipped) | 1 test file selected — `Test Files 1 passed (1)`, `Tests 7
passed \| 1 skipped (8)` |
| `src/cascade-delete-*.integration.test.ts` (the "improvement") | **`No
test files found, exiting with code 1`** |

⇒ The step runs tests, stated as a **count and not an exit code**: **8
tests across 1 file**, 7 of them the SQLite cell and 1 the live cell
naming itself as un-provisioned. On CI, where the URL is supplied, that
cell expands to the 8 live tests objectstack-ai#18732 measured, for 15 total.

`packages/runtime`'s vitest config also runs `runFilterPreflight`
(objectstack-ai#17853/objectstack-ai#17978), which speaks up when a named path selects nothing — a
second net, and the comment says explicitly that it is not a licence to
change the form.

## Both env lines are load-bearing — positive control

`OS_EXPECT_LIVE_DIALECT_MATRIX: '1'` with the URL withheld:

```
FAIL |local| ... (live postgres) > is provisioned — set OS_TEST_POSTGRES_URL to run this cell
AssertionError: OS_TEST_POSTGRES_URL is unset while OS_EXPECT_LIVE_DIALECT_MATRIX=1: this
runner declared it provisions a live server, so the live postgres cell ... must not be skipped.
Tests  1 failed | 7 passed (8)
```

⇒ If the URL line is ever dropped while the flag stays, this step reds
instead of quietly degrading to SQLite-only coverage. That is the same
vacuous-pass guard the driver-sql and metadata-protocol legs above
already carry, and it is scoped to the STEP rather than the job for the
same reason theirs is: it asserts "the runner provisioned what THIS step
needs", so it stays honest if a later step's server is ever dropped. The
card listed the step-versus-job question as Not measured; this is the
answer and the reason.

## Job wall-clock — the cost on every PR, priced

The seat asked to hear this plainly rather than discover it later. It is
**not** four minutes.

| reading | value |
|:--|:--|
| the job today, run 35263914432 (`95b21b33b`) | **345s (5.8 min)** |
| the job today, run 35262334045 (`09e16a574`, colder cache) | **432s
(7.2 min)** |
| its `timeout-minutes` | **30** |
| build tasks in `@objectstack/runtime...` | 31 |
| build tasks the job's three existing legs already cover | 18 |
| ⇒ **packages this build step newly adds** | **13** |
| the new build step, warm Turbo cache | **71ms — `FULL TURBO`, 30/30
cached** |
| the new build step, fully cold on this container | 4m20s for all 30
tasks (only 1 cached) |
| the new test step, SQLite-only shape | **8.00s** |

The warm figure is the common path, and it is what CI actually shows: in
run 35263914432 all three existing build steps took 0–1s, because this
job restores the Turbo cache from the Build Core namespace, which builds
every package. The cold figure bounds the other end — and note the 4m20s
is for the **whole 30-task closure**, of which 18 tasks are ones the job
already pays for today; the marginal 13 sit inside the band CI's own
colder run shows for the sibling steps (driver-sql's closure 106s, the
non-SQL one 42s).

⇒ **Honest estimate: a few seconds on the common path, roughly 1–2
minutes when the Turbo cache misses**, against a job that runs 5.8–7.2
min with about 23 minutes of headroom under its own timeout. No job name
and no matrix shape changed, so the merge-queue required set is
untouched (`check:required-contexts` green).

## Scope and grading

- **Clause-②: no** — measured, not assumed. The diff is one file.
Predicate: is a changed path inside any workspace package's shipping set
(package dir joined with each entry of its `files[]`, private packages
excluded)? Enumerated **217 shipping roots** across the workspace;
`.github/workflows/ci.yml` is inside **0** of them. Positive control on
the same predicate: `packages/adapters/hono/dist/index.js` resolves to 1
package (`@objectstack/hono`). No accept set moves, no schema, no
exported symbol, no door.
- **skip-changeset** — same measurement, and it is also this workflow's
own prescription: `pr-automation.yml` lists `.github/` under "It
releases nothing ... apply the `skip-changeset` label. PREFERRED".
- Not a governed surface: `scripts/pm/check-governed-merges.mjs --test
.github/workflows/ci.yml` exits **0**, 0 of 1 path hits the register.
- `needs:contract-review` is the seat's label; this PR neither hangs nor
removes it.

## Verification

Base `95b21b33be`; final commit measured below. Heavy runs through
`scripts/pm/os-verify-lock.sh`.

| what | reading |
|:--|:--|
| derived gate families (`scripts/pm/dispatch-gates.mjs --repo ...
--ran`) | **44 derived, 44 accounted — 42 green, 2 NOT MEASURED** |
| — of those, the workflow-reading ones | `check:ci-filter-parity`,
`check:required-contexts`, `check:workflow-step-name-quoting`,
`check:workflow-status-functions`, `check:step-collectors`,
`check:self-test-workflow-commands`, `check:stall-guard-budget`,
`check:stall-guard-headroom`, `check:pnpm-filter-targets` — all
**green** |
| `pnpm check:nul-bytes` | green (8835 files); plus a direct
control-byte scan of the diff — 0 hits |
| workflow parses | `yaml.parse` — steps land in job
`temporal-conformance`, 16 steps, env and run bodies as intended |
| the two steps vs PR objectstack-ai#18732's derivation | **md5 identical**
(`26e85b39...`) |
| eslint | the changed file is **outside eslint's population** —
`isPathIgnored('.github/workflows/ci.yml') = true`, 1 result object with
0 errors (the ignored-file warning); positive control
`eslint.config.mjs` returns `isPathIgnored = false`. `eslint.config.mjs`
is not in this diff, so no untouched file's verdict can move. The
repo-wide scan is CI's run, not one this PR narrows away from. |

The 2 NOT MEASURED, named:

- `pnpm check:dual-build-cjs-loads` — **exit 3, `PREREQUISITE NOT
MET`**: it reads built output for ~30 packages this worktree never built
and says so itself ("This is NOT a pass: nothing was measured"). CI
builds the repo and measures it there.
- `pnpm check:pm-dispatch-gates` — **exit 124**, killed by this
container's foreground wall at 540s. Not a refusal and not a red: **1768
self-test assertions printed green and zero failed** before the kill. CI
measures it.

## Acceptance notes

- ⚠️ **A stray remote branch this round created and could not delete:
`claude/issue-18734-workflow-scope-probe`** (commit `f22c6321`, a
throwaway comment appended to `ci.yml`, no PR). The dispatch required
proving a `.github/workflows/**` push is accepted before spending the
round, and it is — but **deleting the probe afterwards is blocked on
both channels**: `git push origin --delete` returns `RPC failed; HTTP
403` from the egress proxy, and `DELETE /repos/.../git/refs/heads/...`
returns `403 {"message":"Write access to this GitHub API path is not
permitted through this proxy."}`. It needs a hand with delete rights.
The lesson is cheap and worth writing down: **probe on the branch you
are going to keep**, because a probe branch is easier to create than to
remove here.
- The **29 other** `better-sqlite3` literals under
`packages/runtime/src/` — **noted, not filed**, unchanged from objectstack-ai#18732's
reading: none carries a shipped dialect-specific defect, and this card
explicitly does not reopen that. Carrier: whichever card next names a
dialect-specific defect on one of those paths.
- Nothing here repairs anything the newly-powered cell may turn red. If
it goes red on this PR's own CI run, that is a finding for the seat, not
a repair to smuggle into a workflow PR.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants