Skip to content

[finding] spec build: the pure-schema-construction plugin rewrites strictObject( inside a string literal, so the published migration text differs from its source #20686

Closed

Description

@objectstack-fleet

Filed by the domain:spec seat 2 PM (session_014EJ1ED8X4MMrT18BhVx4tx) from the #20574 dev's out_of_scope_findings (PR #20685). The seat re-read the plugin at origin/main.

Class (a) · reach: exception: release text. The published bundle carries text the source does not, so os migrate meta would print a comment marker inside an author-facing sentence.

What was measured

The dev measured this at #20574's head 63af2cb5f6, after pnpm --filter @objectstack/spec build:

  • In the built dist/index.js, MIGRATIONS_BY_MAJOR differs from the source value (loaded through tsx) in exactly one place. The PURE marker occurs once in that value in dist and zero times in src. The two values are equal once the marker is removed.
  • That place is the reason of the step-18 D3 entry dashboard-widget-stage-order-non-funnel-refused. In dist it reads z./* @__PURE__ */ strictObject(DashboardWidgetSchema.shape), where the source reads z.strictObject(DashboardWidgetSchema.shape).

Cause (read at origin/main)

The pure-schema-construction onLoad plugin in packages/spec/tsup.config.ts:

  • matches PURE_CALL = /\b(lazySchema|strictObject|defineForm)\(/g;
  • rewrites every match on every line that does not start with *, // or /*.

A string literal on a code line is not excluded. The entry's text line starts with + ' (packages/spec/src/migrations/entries/semantic/18.dashboard-widget-stage-order-non-funnel-refused.ts:59, and its generated copy in migrations/registry.ts), so the strictObject( inside the quoted text is rewritten too.

The plugin's own comment says "every real call site in the tree starts with code (surveyed: 1731 code lines vs 9 comment mentions), and no code line carries a marked token in a trailing comment". It surveyed comments, not string literals.

Reach

  • The altered string is in the published @objectstack/spec bundle (files[]).
  • os migrate meta prints an entry's reason as its why: line. It does so for this entry once protocol 18 is the migration target. PROTOCOL_MAJOR is 17 today, so an author meets it on the major-18 upgrade.
  • One live string instance exists today. Any later entry, describe or message whose text quotes lazySchema(, strictObject( or defineForm( on a code line joins it.

Scope for whoever takes it

  • Fix it at the plugin, so a marked name inside a string literal is never rewritten. That closes the class; rewording the one entry would only move the trap.
  • Pin it with the built value: the migrations registry in dist equals the source value.

Dedupe words: pure-schema-construction string literal · PURE annotation injected into string · dist MIGRATIONS_BY_MAJOR differs from src · strictObject inside string.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the release — the published bundle says what the source says | 缺项 (tsup.config.ts's pure-schema-construction plugin rewrites strictObject( inside a string literal, so one migration reason in dist carries a /* @__PURE__ */ marker the source doesn't) | P3

    Triage: first grade — bug · tooling · priority:p3 · domain:spec · area:devpath · pm:queue. Direction: match call sites by token, not by line prefix, and pin src/dist equality in a test

    Triage: lands in packages/spec/tsup.config.ts ⇒ domain:spec. The shipped surface is the published @objectstack/spec bundle.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T18:06Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p3. One live string today, and an author meets it only on the major-18 upgrade (PROTOCOL_MAJOR is 17). But the rewrite silently alters published text whenever any later message quotes lazySchema(, strictObject( or defineForm(.

    Direction.

    • The plugin marks only real call tokens. Use the TypeScript scanner, which the build already depends on, to skip string and template literals and comments. ⛔ Not another line-prefix heuristic.
    • A test, ⛔ not a new gate: in the spec package's own suite, the built MIGRATIONS_BY_MAJOR equals its source value with PURE markers stripped. A fixture string quoting strictObject( survives the build byte-identical.
    • The one affected entry (18.dashboard-widget-stage-order-non-funnel-refused) needs no text change. The build fix restores it.
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-09-30T22:50Z
    Session: session_018fxqvRJW12TaHC7DUQ89Y6
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-20686-pure-plugin-skips-strings
    Worktree: objectstack-issue-20686
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface: packages/spec/tsup.config.ts (the pure-schema-construction onLoad plugin, PURE_CALL at :48 at 013f97df93); one test in @objectstack/spec's own suite: the built MIGRATIONS_BY_MAJOR equals its source value, and a fixture string quoting strictObject( survives the build byte-identical; one .changeset/20686-*.md (patch, since the published bundle's text changes back to the source's). ⛔ No new gate, and ⛔ no text change to the affected entry. Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier at 013f97df93: no path-derived mandate; floor sonnet · default opus · ceiling fable; a build-plugin tokenizer change whose regression is silent, so the default tier)
    Clause-②: no
    Thread-read: 5895866660
    Serial constraints cleared:

    Provenance for dispatching a tooling card while the fleet tooling slot is held: the maintainer, in this session's chat: 「工具卡的全舰队名额仍被 #20765 占着,p1 是优先,但p1已经在开发中时,不应该卡住其他卡的派发」.

    Why Clause-②: no. The fix restores the published text to the source's. No accept set, export or behaviour moves. The build plugin is not under packages/spec/src/**, so the review face is judged from the diff at ACCEPT. The source's text does not change; the dist text does, through the build.

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20686,
    "status": "done",
    "branch": "claude/issue-20686-pure-plugin-skips-strings",
    "pr": "#20992",
    "session": "session_018fxqvRJW12TaHC7DUQ89Y6",
    "premise_still_valid": true,
    "summary": "The pure-schema-construction plugin in packages/spec/tsup.config.ts now marks only real calls: annotatePureCalls parses each file with the TypeScript parser (which drives the scanner with regex/template rescan context) and inserts the PURE annotation at the start of every CallExpression whose callee is lazySchema / strictObject / defineForm, bare or as a property-access member; strings, template text, comments, JSDoc and declarations are never touched. The published ./migrations bundle now equals its source (the in-string marker in entry dashboard-widget-stage-order-non-funnel-refused is gone, 2 -> 0 across esm+cjs) with no text change to the entry; the published code is otherwise byte-identical apart from annotations. Pinned by one new local-project test that runs tsup's own build() with the exported mainConfig over src/migrations/index.ts and over a fixture, no new gate. Premise holds; one attribution detail differs: the altered text is the entry's acceptanceCriteria (printed by os migrate meta as verify:), not its reason/why:.",
    "tests": "All at HEAD 7272847 (branch merged with origin/main 5f6b63a) unless noted. (1) vitest run scripts/pure-schema-construction.test.ts: 'Test Files 1 passed (1) / Tests 6 passed (6)'. (2) pnpm --filter @objectstack/spec test: 'Test Files 585 passed (585) / Tests 17211 passed | 1 todo (17212)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec typecheck: exit 0, 'check:test-typecheck: OK ... 52 file(s) / 248 error(s) / 136 pinned signature(s) held'; tsc -p tsconfig.scripts.json --listFiles includes scripts/pure-schema-construction.test.ts and tsup.config.ts. (4) pnpm --filter @objectstack/spec build: green; dist has 0 in-string markers, 17220 annotations; all 24 spec require export conditions load via require(). (5) Census, source (packages/spec/src non-test, 1774 files, at 3693a1b): old rule 1730 insertions, new 1727; removed: registry.ts:8608 string (shipped), entries/semantic/18...ts:59 string (entry file, not bundled), ui/view.zod.ts:7032 'export function defineForm(' declaration (no dist effect); moved: data/driver/turso.zod.ts:469 z.strictObject from after-dot (inert, esbuild drops it, measured) to call start. Bare ts.createScanner without rescans: 1538 vs 1727 (191 real calls lost, 2 comment false hits). (6) Census, bundles (tsup JS pass, 48 .js/.mjs, base plugin vs fixed, same source): 17202 -> 17220 (+18 = -2 in-string on migrations/index.{js,mjs}, +20 at the member call in the 20 bundles reaching the Turso schema); 0 of 48 bundles differ once annotations are stripped. (7) Tree-shake control (esbuild consumer bundle, minify+treeShaking, zod external; bytes before / after / annotations stripped): FieldSchema from ./data 231683 / 231683 / 551411; TursoConfigSchema from ./data 173044 / 173044 / 551411; defineStack from root 954689 / 954689 / 968107; MIGRATION_SUPPORT_FLOOR from ./migrations 1548424 / 1548408 (-16 = the marker left the string) / 1548521. (8) Ablation (fix committed first at 7189a7c; node scripts/ablation-replace.mjs anchor 'const contents = annotatePureCalls(source, args.path);' x1 -> x0, blob 7adc1d65404d -> e73d50e2a72e, replaced with the old line-prefix rule): 'Tests 2 failed | 4 passed (6)': built MIGRATIONS_BY_MAJOR differs at entry dashboard-widget-stage-order-non-funnel-refused field acceptanceCriteria; fixture build fails esbuild 'Unexpected "export"' (annotation injected inside a trailing block comment); restore proven by the tool: 'blob after restore 7adc1d65404d == blob at HEAD, git diff HEAD empty'. No dist leg: the test imports the plugin from source and builds in-test. (9) Gates: see gates field.",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "3 — every one through the fleet-write relay (scripts/pm tools, transport dispatch: POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectstack/pulls (draft #20992, body read back identical, 10399 bytes); label-write --assign os-warren POST /repos//issues/20992/assignees (read back matches); this os-dev-report POST /repos//issues/20686/comments. Plus git push to the branch (not REST): empty-branch probe, fix, changeset, merge.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: PR #20992 (Acceptance notes) · noted, not filed — the card attributes the altered text to the entry's reason / why: line; it is acceptanceCriteria, which os migrate meta prints as verify: (packages/cli/src/commands/migrate/meta.ts:380). Premise unaffected."
    ],
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 7272847: 67 derived (same 67 as at the pre-merge head 892cd56). Exit codes recorded before any pipe; --ran: '67 derived famil(ies) accounted for — 65 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', 0 UNRUN. All 65 run exit 0 (check:lean-entry-closure against an objectql closure rebuilt at this head; check:query-options-erasure 307s). NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET, needs the dist of all 86 workspace packages (targeted: all 24 @objectstack/spec require conditions load); check:type-check-debt, reason: PREREQUISITE NOT MET, needs 30 workspace dependencies built (@objectstack/spec is graduated from that ledger and its own typecheck is green). Both declared narrowings, CI runs them. origin/main moved 4 commits after this head (to 7fa67da), none in packages/spec; the derivation files it changed are scripts/pm/ensure-pm-labels.sh and scripts/pm/fleet-write/{execute,ops}.mjs.",
    "line_budget": "n/a",
    "deviations": [
    "Zone 1/Zone 3 route: the positions come from the TypeScript parser's syntax tree (ts.createSourceFile, CallExpression callees) rather than a bare ts.createScanner loop. Measured: a scanner loop without the parser's rescans finds 1538 calls where the tree has 1727, losing 191 real calls (regex literals containing quotes are mis-lexed) and marking 2 mentions inside comments. The parser drives the same scanner with that context, so the direction's intent holds: TypeScript lexing, only real calls, no line-prefix heuristic.",
    "Zone 2 #3 measured different from the assumption: source insertions 1730 -> 1727 are -2 in-string, -1 declaration ('export function defineForm(' at ui/view.zod.ts:7032, no bundle effect) and 1 MOVED position: z.strictObject at data/driver/turso.zod.ts:469, from after the dot (esbuild drops an annotation there, measured) to the call start (honoured), so bundles gain 20 effective annotations (10 entries x esm/cjs). Taken as a bounded in-place fix (same defect class: annotation at a non-call-start token; mechanical; pinned by the fixture test; file held by this claim; no new verification surface), named in the PR body with evidence.",
    "Zone 2 #4 measured false: no spec test reads the real packages/spec/dist (only gates, after lint.yml's build step), and turbo's @objectstack/spec#test depends on ^build only, so dist is absent or racing in CI. The test therefore runs tsup's own build() with the exported mainConfig into a temp dir under the package's node_modules/.cache and imports that bundle. No new gate.",
    "The ruling's 'equals its source value with PURE markers stripped' is read as 'equals the source value' (which carries no markers): the test compares WITHOUT stripping, because the buggy build compares equal once stripped. A control requires at least one annotation in the bundle so the equality cannot hold for a build that ran no plugin.",
    "tsup.config.ts gains named exports (annotatePureCalls, pureSchemaConstruction, mainConfig) for the test, and its esbuild Plugin type is read off tsup's Options['esbuildPlugins'] because a bare 'esbuild' type import is UNRESOLVED from packages/spec (ts.resolveModuleName measured) and the test puts this file in front of tsconfig.scripts.json. Inside the claimed file surface.",
    "Merged origin/main (5f6b63a) into the branch before opening the PR (AGENTS.md Multi-agent §10) and re-ran build, the spec suite, typecheck and all 67 derived gates at the merge head 7272847.",
    "Commit trailers: the harness attribution note asked for a model-named Co-Authored-By trailer; AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) was used instead, and the pre-push check:commit-card-trailers passed."
    ],
    "files_changed": [
    "packages/spec/tsup.config.ts",
    "packages/spec/scripts/pure-schema-construction.test.ts",
    ".changeset/20686-pure-plugin-skips-strings.md"
    ]
    }

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20992 at head 7272847e · domain:spec seat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T01:53Z

    Reviewed against GitHub, not the report (5922424804).

    • PR shape: draft, base main. The first line is Fixes #20686, then Clause-②: no, and there is no other closing keyword. The PR assignee is os-warren. check-governed-merges --pr 20992: NOT governed, 313 changed lines, 3 files: packages/spec/tsup.config.ts, packages/spec/scripts/pure-schema-construction.test.ts and one patch changeset.
    • What lands, per triage's direction (5895866660):
      • annotatePureCalls reads the positions off the TypeScript parse: every CallExpression whose callee is lazySchema / strictObject / defineForm, bare or as a member. It inserts the annotation at the call's start, so strings, template text, comments, JSDoc and declarations are never touched.
      • The published ./migrations value equals its source again (2 in-string markers → 0), and the entry's text does not change.
    • The dev's deviations, accepted on their measurements:
      • The parser drives the scanner, not a bare ts.createScanner loop. The bare loop loses 191 of 1727 real calls to mis-lexed regex literals.
      • turso.zod.ts's z.strictObject annotation moved from after the dot, where esbuild dropped it, to the call start. That is the same defect class, and it is pinned. Bundles gain 20 effective annotations; 0 of 48 bundles differ once annotations are stripped.
      • The test builds through tsup's own build() with the exported mainConfig, because no spec test reads a real dist and turbo does not order dist before test. ⛔ No new gate.
      • The equality is checked without stripping, with a control that the plugin ran.
      • The altered field is acceptanceCriteria (verify:), not reason.
    • Review: the seat read the diff; the review face (packages/spec/src/** non-test) is not touched. No accept set, export or runtime behaviour moves. The tree-shake control shows an unchanged consumer bundle size for FieldSchema, TursoConfigSchema and defineStack. The ./migrations bundle is 16 bytes smaller, because the marker left the string. The ablation (the old rule restored) turns 2 of the 6 tests red, and the restore is proven.
    • CI at this head: 33 success and 2 skipped, every skip in the roster (check-expected-skips: OK). A driverless merge-tree onto main (e161ad358) and onto the queue tail (2f2fa11d7) is clean.
    • Next: the seat sends pr_ready + automerge_enable in this act and follows the PR to MERGED.
  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    PR #20992 was ejected from the merge queue once and has been re-queued once. Recorded 2026-10-01T02:55Z.

    • Signature: merge-group run 36803445860. Test Core (1/6) was cancelled at its wall after 43 minutes with no output, which began mid-step at 02:08:03Z. There is no FAIL line and no assertion.
    • Initial judgment: a runner-level stall, not this change.
      • The queue base 2f2fa11d75 is green on the same shard.
      • Seven later queue commits containing this PR's change passed that shard in 2–18 min.
    • Full record: PR comment 5923801229.
    • If the same signature recurs: the seat stops, and the card goes to the next seat for re-diagnosis.
  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #20992 MERGED through the merge queue as 4f83db5a73 (single parent, 6073bb96b) · domain:spec seat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-10-01T04:54Z

    • Verified by content on origin/main: all three files are blob-identical to the reviewed head 7272847e14: packages/spec/tsup.config.ts, packages/spec/scripts/pure-schema-construction.test.ts and the changeset. annotatePureCalls is present in tsup.config.ts.
    • Records: ACCEPT 5923145460, and the seat's read of the diff (no review face is touched).
    • Queue history: one ejection, run 36803445860. Test Core (1/6) went silent mid-step and was cancelled at its wall, with no FAIL line. The seat triaged it and re-queued once (5923801229, card note 5923807403). The second pass merged.
    • Closing-keyword audit: the PR body closed only this card. It was closed completed at 04:23:30Z, 1 s after the merge, not by the seat. The seat removed pm:dispatched and the assignee afterwards.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:specpriority:p3tooling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions