Skip to content

fix(scripts): backfill-line-number and backfill-owner-id send query bodies the 17.6.0 door accepts (#1999) - #2002

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-1999-backfill-query-shapes
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-1999-backfill-query-shapes

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #1999
Clause-②: no. These are operator scripts; they touch no published schema and no accept set.

Summary

scripts/backfill-line-number.ts and scripts/backfill-owner-id.ts stopped at their first query on @objectstack/* 17.6.0 and wrote nothing. The 17.6.0 REST query door (POST /api/v1/data/OBJECT/query) checks the body against FindDataRequestSchema from @objectstack/spec/api. It refuses filters: [] (min_items) and sort: 'id asc' (invalid_shape). Both scripts now send the shapes scripts/backfill-contact-mailing-address.ts sends: sort: [{ field: 'id', order: 'asc' }] and no filters key. What each script selects and what it writes are unchanged.

A new test, test/backfill-query-bodies.test.ts, runs every scripts/backfill-*.ts and checks each query body the script sends against the installed schema. The next time the schema gets stricter, pnpm verify fails before an operator hits it.

Changes

file change
scripts/backfill-line-number.ts page body { filters: [], fields, sort: 'id asc', skip, top: 200 } → { fields, sort: [{ field: 'id', order: 'asc' }], skip, top: 200 }
scripts/backfill-owner-id.ts page body: same change. sys_user body { filters: [], fields: ['id'], top: 2000 } → { fields: ['id'], top: 2000 }
test/backfill-query-bodies.test.ts new probe (described below)
.changeset/1999-backfill-query-shapes.md 'hotcrm': patch

.changeset/line-item-line-number-writer.md is unchanged. Its instructions are true now: report, --apply, and a rerun that reports zero were all measured below.

Evidence

All runs used a fresh 17.6.0 boot on port 4819: objectstack dev --no-watch --no-restart --seed-admin --artifact FILE --database file:DB, started from a scratch cwd with no objectstack.config.ts (objectstack#21501). The server log said No objectstack.config.ts found — booting from artifact and named the artifact passed in.

1. Reproduced on f44ab642 (unchanged scripts, report-only), exit 1 for both:

Backfill failed: query crm_opportunity_line_item → 400: Invalid query request
Backfill failed: cannot read sys_user (400)

The door's 400 bodies list query.filters min_items and query.sort invalid_shape for the page queries, and query.filters min_items alone for sys_user.

2. Only these two shapes are refused. I parsed every body against FindDataRequestSchema, wrapped as the door wraps it ({ object, query: { ...body, object } }):

body 17.0.0-rc.2 17.5.0 17.6.0
old line-number page OK filters too_small, sort invalid_union same
old owner page OK same same
old sys_user OK filters too_small same
new line-number page / owner page / sys_user OK OK OK

No other part of either body is refused. top: 2000 is accepted. The new bodies also pass on 17.0.0-rc.2, which was the pin when owner was removed (9f748ab5). So the owner script still works against the old releases it is meant to run on before an upgrade. Paging with the array sort is stable on 17.6.0: reading 73 line items in pages of 10 gave the same 73 ids in the same ascending order as one page of 200.

3. backfill-line-number, at 9b0d597e. To recreate lines from before the #1828 hook, I nulled line_number directly in SQLite on 12 rows: all lines of one prospecting opportunity, the last 2 of a negotiation one, the last 1 of a closed-won one, all 4 of a draft quote, and the last 1 of an accepted quote.

report   exit 0  crm_opportunity_line_item: 73 row(s), 7 without a line number, under 3 parent(s).
                 crm_quote_line_item: 20 row(s), 5 without a line number, under 2 parent(s).
--apply  exit 0  Backfilled 7/7; 0 still without a line number.   Backfilled 5/5; 0 still without a line number.
rerun    exit 0  73 row(s), 0 without a line number ...   20 row(s), 0 without a line number ...

Reading the database afterwards, all 19 rows under those five parents have the same number they had before nulling (0 of 19 differ). The hook numbered each parent's lines in creation order and continued after any number the parent already had.

4. backfill-owner-id, at 9b0d597e. On a 17.6.0 org that is already upgraded, the report says no `owner` column — already upgraded, nothing to read for each of the 12 objects, then No divergence to backfill, exit 0. To test the case the script exists for, I built a scratch artifact (not committed) that adds the old owner lookup back to crm_lead and crm_account, and booted it on a fresh database. I added a second user through /api/v1/auth/admin/create-user. Then I set up four rows, through REST where it allowed and through SQLite where it refused:

  • a lead whose owner is reassigned to the second user;
  • a lead with owner set and owner_id empty;
  • an account with owner set and owner_id empty;
  • an account whose owner is ghost-user-000, a user that does not exist (REST refuses that value with 400, so it went in through SQLite).

The unchanged f44ab642 script still failed on this database: cannot read sys_user (400).

report   exit 0  Scanned 30 record(s) across 2 object(s).  1 ... not a real user — SKIPPED  3 record(s) where the displayed Owner is NOT the access owner
--apply  exit 0  Backfilled 3/3 record(s).
rerun    exit 0  1 ... SKIPPED (the ghost row, as designed)  No divergence to backfill

Reading the database afterwards, owner_id = owner on all three rows that differed, and the ghost row is unchanged.

5. The probe and its ablation. test/backfill-query-bodies.test.ts imports each scripts/backfill-*.ts with fetch stubbed, so each script's own main() sends its real requests. Report-only, the door returns no rows, so nothing reaches a write. The stub accepts or refuses each query body exactly as the door does. A case fails if any body is refused, if a script sends no body, or if a script calls console.error. It captured 16 bodies: 1 from backfill-contact-mailing-address.ts, 2 from backfill-line-number.ts and 13 from backfill-owner-id.ts (sys_user plus 12 objects).

Ablation, from the committed state 2b74bb67, with node scripts/ablation-replace.mjs from objectstack. The predicted direction was red, and the run went red:

ablation-replace: ok mutation landed: anchor 1 -> 0, blob 66b88b75f87f -> 8ca770eed240
  × scripts/backfill-line-number.ts sends only query bodies the door accepts
  + "refused": [ "query.sort: invalid_union" ]   (body: { fields: [...], skip: 0, sort: "id asc", top: 200 })
  Tests  1 failed | 3 passed (4)
os-verify-lock: VERDICT command-exit 1
ablation-replace: ok restored: blob == HEAD (66b88b75f87f) and `git diff HEAD` is empty

Why a probe instead of exporting the bodies: each script starts main() when it loads. Importing it from a test would need an isMainModule guard, and test/script-main-guard.test.ts then requires a symlinked green and red spawn for every guarded script. A backfill script cannot pass the green spawn without a live server. The probe also checks the bodies the scripts actually send, so a body written inline somewhere new is caught too.

Verification

OS_VERIFY_LOCK_SLOT=hotcrm-issue-1999 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm verify' at b721702e:

 Test Files  177 passed (177)
      Tests  3791 passed | 1 skipped (3792)
os-verify-lock: VERDICT command-exit 0 · held the lock 184s (3m04s) · waited 0s

node scripts/check-source-token-ratchet.mjs gave byte-identical output before and after. No src/ file is touched. The src/sales rows are unchanged: business semantics 56,418 / 59,000, interaction layer 27,968 / 31,000, authored total 99,882 / 107,000.

Acceptance notes

  • The doc comment on allContacts() in scripts/backfill-contact-mailing-address.ts says the older backfill scripts use the two refused spellings. After this PR they no longer do, so that sentence is out of date. I left it alone because it is outside this card's declared file surface. Whoever next edits that script can fix it.
  • This PR does not change what the owner script selects (per the dispatch), but two existing behaviours are worth knowing:
    • It recognises an upgraded org by matching the 400's message text (/unknown|no such|not a field/i). It does not check the error's code: 'INVALID_FIELD' / field: 'owner'. On 17.6.0 the message (Unknown field 'owner' on object ...) still matches; that was measured.
    • It reads sys_user in one request (top: 2000). On an org with more users than that, an owner beyond the first 2000 would be listed as SKIPPED. A row would be skipped, never written wrong. This was not tested here.
  • AGENTS.md §3 says tests here cover only this repo's own facts. The new test checks this repo's own operator scripts against the platform's published request schema. It is not a platform diagnostic, and it keeps no roster of its own.

Generated by Claude Code

claude added 3 commits October 3, 2026 09:37
…oor accepts

`scripts/backfill-line-number.ts` and `scripts/backfill-owner-id.ts` sent
`filters: []` and `sort: 'id asc'` to `POST /api/v1/data/OBJECT/query`. The
17.6.0 door validates the body against `FindDataRequestSchema` and refuses
both (`query.filters` min_items, `query.sort` invalid_shape), so each script
failed on its first query and wrote nothing. They now send what
`scripts/backfill-contact-mailing-address.ts` sends: `sort` as
`[{ field: 'id', order: 'asc' }]` and no `filters` key. What each script
selects and writes is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
…d query schema

`test/backfill-query-bodies.test.ts` imports each `scripts/backfill-*.ts` with
`fetch` stubbed, so the script's own `main()` sends its real query bodies. The
stub judges each body as the 17.6.0 REST query door does (`object` merged in,
then `FindDataRequestSchema.safeParse`) and fails the case on any refusal, on a
script that sent no body, or on a script that reported a failure. Nothing is
exported from the scripts for this, so an inline body added later is caught as
well. The next tightening of the query schema turns this red at `pnpm verify`
instead of at an operator's terminal (#1999).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
@vercel

vercel Bot commented Oct 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 3, 2026 9:50am UTC

Request Review

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

Labels

ci/cd CI plumbing and the verification pipeline

Projects

None yet

2 participants