Uh oh!
There was an error while loading. Please reload this page.
fix(service-datasource): stop rendering the unauthorable fields.*.primaryKey in object drafts - #11073
Conversation
…primaryKey` `generateObjectDraft` emitted a field-level `primaryKey: true` and `renderObjectSource` rendered it onto the field line. `primaryKey` is not a key of the spec field schema, so the `*.object.ts` the review-before-commit flow produced was refused by both instruments the file is annotated for: `tsc --noEmit` against `ServiceObject` (`TS2353`) and `ObjectSchema.safeParse` (`unrecognized_keys`). Per the maintainer ruling of 2026-08-22, the key stops being emitted and the introspected value survives as a comment above the `fields` block — information preserved for the reader, zero contract face. The comment names the column(s) the draft was given, says why it is a comment, and does not claim to be a complete key (some drivers report only the first column of a composite key). A table with no reported key renders no comment. With #10712's namespace/`sharingModel` repairs already landed, both paths -- `opts.primaryKey` set and unset -- now clear all three `os build` stages. An authorable spelling on the binding schema is deferred, not rejected; it returns as its own spec change once federated upsert has a runtime consumer. Fixes#11000 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f2713410311d1eea447cd6e72dabef3c0b25cc66 && git checkout f2713410311d1eea447cd6e72dabef3c0b25cc66
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1c3a46f876822d488d3af43189dbc33273b28015 eb16902cc7df3bea8175a8b06e52adea9e28abc6 && git checkout -B drift-repro 1c3a46f876822d488d3af43189dbc33273b28015 && git merge --no-ff eb16902cc7df3bea8175a8b06e52adea9e28abc6
node scripts/docs-audit/affected-docs.mjs --json 1c3a46f876822d488d3af43189dbc33273b28015 |
fields.<f>.primaryKey in object draftsfields.*.primaryKey in object drafts⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32581971774 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11000
generateObjectDraftemittedfields.*.primaryKey: trueandrenderObjectSourcerendered, primaryKey: trueonto the field line.primaryKeyis not a key of the spec field schema, so the*.object.tsthe review-before-commit flow handed the user was refused by both instruments the file is annotated for —tsc --noEmitagainstServiceObject(TS2353) andObjectSchema.safeParse(unrecognized_keys).Per the maintainer ruling of 2026-08-22 (live session, 「同意所有」 item 8 = D): the key stops being emitted, and the introspected value survives as a comment in the generated source — information preserved for the reader, zero contract face. Option C (
external.primaryKey: string[]onObjectExternalBindingSchema) is deferred, not rejected; it returns as its ownpackages/speccard when federated upsert has a live runtime consumer. Nopackages/specfile is touched here.Acceptance — the
os buildpipeline, both paths, before and afterThree stages driven for real:
defineStack()→authoringRulesFor('build')(41 rules) →tsc --noEmitoverdraft.source. Measured ateb16902cc.3e26359a7)eb16902cc)opts.primaryKeyunsetdefineStack()authoringRulesFor('build')tsc --noEmitopts.primaryKeySETdefineStack()objects.0.fields.id: Unrecognized key(s) on this field: `primaryKey`authoringRulesFor('build')tsc --noEmitdraft.ts(10,25): error TS2353: … 'primaryKey' does not exist in typeThe BEFORE column is a live re-measurement on this branch (pre-fix generator checked out, artifact rebuilt), not an inherited claim. It reproduces exactly what #11059's seat reported.
#10712's acceptance — "both paths build" — is only now fully discharged. #11059 landed the namespace/
sharingModelhalf and left theprimaryKey-set row red; this PR clears it. No closing keyword is added for that card: it is already closed by #11059.⭐ The error-ordering hazard #11059 flagged is confirmed here rather than assumed: on the BEFORE/SET path stage 1 aborted on
unrecognized_keys, so nothing downstream of it was measured on that path at all. One path's verdict never covers the other's.What the replacement comment renders
Single key (
opts.primaryKey: ['id'], or introspection reporting one column):// Remote primary key: order_id, line_nofieldsblock, so it has to speak that block's vocabulary or it points at a line that is not there.On #10997 (SQLite introspection returns only the first column of a composite key — unfixed, engine lane): the wording deliberately does not claim to name "the primary key". It says it names the column(s) this draft was given, and states outright that the list is a lower bound. That caveat is pinned, not just written.
The pin flip
The existing suite pinned the invalid emission. It was flipped, not deleted — inverted, it is now the guard that the unauthorable key does not come back — and the explanatory text beside it was rewritten to cite this ruling (the #11046 stale-comment shape).
external-datasource-service.test.ts—expect(fields.order_id).toEqual({ type: 'text', primaryKey: true })→toEqual({ type: 'text' }), plus source-side assertions that the key is gone and the comment is present.toEqualrather thantoMatchObjectis load-bearing and now says so: it is what makes the assertion fail on an extra key.external-object-draft-os-build.test.ts— its fixture docblock said External object-draft renders a field key that is not authorable:fields.<f>.primaryKeyfailstscagainstServiceObjectandObjectSchema.safeParse#11000 was "an open contract question inpackages/spec, deliberately untouched here". Now false; rewritten, with the error-ordering warning kept..changeset/external-object-draft-passes-os-build.md(fix(service-datasource): generate an object draft thatos buildaccepts #11059's, unreleased) ended with "theopts.primaryKeypath still does not build … untouched here". That is a user-visible release note this PR falsifies, same defect class, mechanically corrected — one paragraph, no behaviour.What is pinned — four directions
New file
external-object-draft-primary-key.test.ts(16 cases):['type'], on the option path and the introspection path, and nothing reachesimportObject's metadata store either.ObjectSchema.safeParsesuccess (a value verdict, not an absence-of-unknown-keys check), single and composite. Positive control: the same definition with the key put back is refused, and refused forunrecognized_keys— without that, the green above could be a schema that stopped refusing anything.pk === 1vs SQLite's 1,2,3… numbering) #10997 caveat; says why; and renders nothing when there is no key. An implementation that just droppedopts.primaryKeypasses every other direction and fails these.os buildaccepts #11059 — namespace prefix,sharingModel, and fix(service-datasource): generate an object draft thatos buildaccepts #11059's own still-generates cases all re-run green.Proof
Ablation A — comment renderer removed. Predicted in writing first: reddening (not diagnostics-increase, not inversion),
toContainondraft.source, and — enumerated before running — exactly 8 named cases, with the absence-asserting cases staying green. Observed: exactly those 8,AssertionError: expected '// Generated by `os datasource intros…' to contain '// Remote primary key: order_id'; 54 passed. (The coarser count written before the suite existed said 3; recorded, and wrong.)Ablation B — the key put back on the definition side only, renderer untouched. Predicted 8 named cases red, with a discriminator: the field-line and comment cases must stay GREEN, because the renderer no longer reads
f.primaryKey. Observed: exactly those 8; discriminator held. The two halves of ruling D are independently pinned.Both restores proved byte-identical (
git hash-object→942f2c6ea223e42a173b9b9b8e4375eb50f25ded, matching the pre-ablation hash) and re-run to a real verdict — 62/62 green each time, not trusted to the hash.Zero-hit counter-check, positive control run FIRST. The same
grep -nE "primaryKey: true|, primaryKey: true|primaryKey\?: boolean"matched the pre-fix file at 4 lines — read individually, they are the 4 sites this PR removes (two type annotations, one definition write, one render) — and matches nothing in the fixed file (exit 1). The zero is measured, not assumed.srcvsdist, re-verified from the files rather than inherited — and it is two answers in one PR:'../external-datasource-service.js', a relative specifier that cannot route throughexports, so it measuressrc/. This package'svitest.config.tscarries exactly one alias, anchored/^@objectstack\/core$/, which does not touch it. But the instrument (ObjectSchemafrom@objectstack/spec/data) is a bare specifier → the workspace link →exports['./data']→packages/spec/dist/data/index.mjs, so the schema half needs the closure built. Built and verified present (ObjectSchemain that artifact).os buildharness — loadsExternalDatasourceServicefrompackages/services/service-datasource/dist/index.js, so it measures the artifact and is rebuilt before every measurement. Each run fingerprints it: AFTER/RESTORE 126976 bytes withRemote primary keypresent and, primaryKey: trueabsent; BEFORE 126139 bytes with the inverse. Two instrument defects in the harness itself were caught and fixed before any verdict was believed — an incomplete manifest that failed stage 1 on both paths, and astartsWith('draft.ts')diagnostic classifier that reported 0 for both files (reading as a clean draft and a silent control at once). The tsc positive control was silent until the second was fixed; it now fires on every run.Gates — derived on the final commit, clean tree
node scripts/pm/dispatch-gates.mjswith no path arguments, oneb16902cc(6 paths vs merge base3e26359a7, working tree 0, untracked 0). It named the gates below; the dispatch carried no gate list, so this derivation is the list. Exit codes captured before any pipe; each verdict is the gate's own printed line.check:changeset-gate-self-testscheck:objectui-changesetcheck:slot-lookupcheck:test-source-aliascheck:type-source-resolutioncheck-adr-0087-registrationcheck-changeset-no-majormajorbumpcheck-ci-filter-paritycheck-empty-changesetcheck-plugin-teardown-shapecheck-affected-docscheck:query-options-erasurecheck:type-check-coveragecheck:type-check-debt --re-measurecheck:engine-double-contractcheck:where-matchercheck:nul-bytesNo gate returned a refusal (
PREREQUISITE NOT MET/cannot run/Nothing was checked) — the full workspace closure (70 build tasks) was built first, exactly aslint.ymldoes, so the ratchet re-measured for real. No baseline was touched.check:type-check-debtreports a pre-existing 12-error surplus on@objectstack/plugin-auththat it explicitly calls "not an error"; unrelated to this diff and deliberately left alone.Package tests:
pnpm --filter @objectstack/service-datasource test→ 25 files, 559 tests, all passing.Declared narrowings
check:*farm — CI runs the farm once regardless.tsc --noEmitoverdraft.sourceruns in the PR harness, not the committed suite — following fix(service-datasource): generate an object draft thatos buildaccepts #11059's precedent, which committed no tsc case either. The committed suite pins parse + shape + comment; the compile verdict is the measurement above.primaryKey: ['x']andexcludeColumns: ['x']gets no mention ofxat all — the column is not in the draft to name. A contradictory caller, left as-is rather than widened into. Noted, not filed.Not in scope
#10997 (SQLite composite-PK truncation) and #10998 (
introspectSchemaomitsdialect/introspectedAt) are untouched — engine lane, #10676 ruling. This PR only makes sure the generated comment does not overclaim in the presence of #10997. #10676 itself remains open:generateObjectDraftstill readscol.primaryKeydirectly rather than through this file's ownprimaryKeyReaderunion, which is that card's seam and not widened here.Generated by Claude Code