Repository navigation
Description
domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB
Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
立卡门类别: ① 产品缺陷 — 具名落点 (SeedLoaderService.loadExistingRecords, packages/metadata-protocol/src/seed-loader.ts; the seed save / publish probe) + 复现
reach: 公开入口实测一次错误结果 — PUT /api/v1/meta/seed/... with externalId naming no field is saved valid:true, and each POST /api/v1/packages/ID/publish-drafts inserts a duplicate row (two independent boots at 7fa9873)
业务理由: a package author who mistypes the upsert key (or renames the field it names) gets a duplicate of every seeded row on every publish, with a success receipt and no warning. Users then see doubled reference data. Not doing it: silent data duplication on each deploy, discovered only by end users.
动手的读者: the domain:server seat (metadata-protocol seed loader and publish probe); triage first touch confirms the lane, then os-dev.
查重: seed externalId → 46 hits, 0 open (#26674, #24624, #24562 closed, different seams); seed-loader externalIdKey → 3 hits, 0 open. No duplicate.
QA-source: #28942 · platform-core.seed-mode-matrix · observed beside the clauses
What happens. A seed dataset with mode: 'upsert' and an externalId that is not a field of its object is accepted at save (it reads back _diagnostics: {valid: true}) and at publish. Every POST /api/v1/packages/ID/publish-drafts of the unchanged seed then answers seedApplied {"success":true,"inserted":1,"updated":0} with no probe issue and no log line, and the table gains one identical row per publish. The upsert has silently become insert — the replay duplication the build-time lint seed-insert-mode-duplicates-on-replay (validate-seed-replay-safety.ts, framework#3434) exists to flag, with no warning here because the mode reads upsert.
Mechanism. SeedLoaderService.loadExistingRecords (packages/metadata-protocol/src/seed-loader.ts) keys each existing row by externalIdKey(record, externalId), and an empty key is never registered. With a field the object lacks, every existing and incoming key is empty, so decideWriteAction finds no match and returns insert. Nothing between the save door and the loader checks SeedSchema.externalId against the object's declared fields, and neither does the publish probe pass (probes.checked.seeds).
Reproduction (fresh showcase boot at 7fa9873, admin bearer; reproduced on two independent boots and DBs):
POST /api/v1/packages {"manifest":{"id":"com.objectstack.qa.seedrepro","name":"R","version":"1.0.0","scope":"project","type":"app","engines":{"protocol":"^18"}}}→ 201.PUT /api/v1/meta/object/seedrepro_other?mode=draft&package=com.objectstack.qa.seedrepro {"name":"seedrepro_other","label":"R","sharingModel":"public_read_write","fields":{"name":{"type":"text","label":"Name"},"code":{"type":"text","label":"Code"}}}→ 200, thenPOST /api/v1/packages/com.objectstack.qa.seedrepro/publish-drafts {}→ published.- Three times:
PUT /api/v1/meta/seed/r_ghost?mode=draft&package=com.objectstack.qa.seedrepro {"object":"seedrepro_other","mode":"upsert","externalId":"no_such_field","records":[{"name":"G1","code":"g"}]}→ 200, thenPOST …/publish-drafts {}. GET /api/v1/data/seedrepro_other.
- Expected: the save or the publish refuses (or at least diagnoses) an
externalIdnaming no field of the seed's object; failing that, replays converge on one G1 row. - Actual: each publish answers
inserted:1with 0 probe issues; step 4 returns 3 rows named G1 with three ids. - Control: the same seed with
externalId: "name"→inserted:1, theninserted:0, updated:0on replay.
Fix direction: refuse or diagnose at the seed save / publish probe when externalId (each member, for a composite) is not a declared field of the seed's object; the loader could also warn when every key of a keyed dataset evaluates empty.
Full evidence: QA run record #28942.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: platform-core.seed-mode-matrix
Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:00Z
⛔ Not a claim.
SeedLoaderService.loadExistingRecords(packages/metadata-protocol/src/seed-loader.ts) and the seed save / publish probe, so it isdomain:server. An upsert seed whoseexternalIdnames no declared field silently turns into insert and duplicates rows on every publish, which is a user-visible behaviour defect, so it is p2 under 「协议新增定 p1,其余 p2」. The location, repro and control are named and nothing needs the maintainer, so it goes topm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim: PM loop round 1 · card thread · 2026-11-02T05:20Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5pEjzdJdM36Fgh7YyY(a new card thread, fresh frommain)Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-ncjmkt(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage#1 graded the card p2 (05:00Z). Route (the dispatching seat's call): the seed save door and the publish probe refuse an
externalId(each member of a composite) that names no field of the seed's object; the boot loader never refuses and names the dataset once atwarnwhen every key of a keyed dataset is empty. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).File surface: the seed save door and the publish probe in
packages/metadata-protocol, andSeedLoaderService(packages/metadata-protocol/src/seed-loader.ts); their tests and changesets. ⛔ Nopackages/spec/**, no new error code, no new gate.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no (narrowing) (expected: a seed save or publish that was accepted with a dangling
externalIdis refused; the dev measures)Metadata-protocol: per AGENTS.md, measured by the dev
Responsibility: the code is the seed save, publish probe and loader of
@objectstack/metadata-protocol· the platform path is an upsert seed whose key names no field, inserting a duplicate row on every publish · reached by every package author who mistypes or renames the upsert keyThread-read: none
Premise: holds on
main7fa9873f(the card's two boots)Serial constraints cleared: No open server card edits
seed-loader.tsor the seed save door.Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01FEkQ5pEjzdJdM36Fgh7YyY
os-dev-report
{
"issue": 28952,
"status": "done",
"branch": "claude/project-thread-ncjmkt",
"pr": "#29066",
"session": "session_01FEkQ5pEjzdJdM36Fgh7YyY",
"premise_still_valid": true,
"summary": "Step 1 measured 0 dangling externalIds over 47 keyed seeds (showcase 19, crm 5, todo 1, hotcrm d4ffe99 22; platform packages and app-multi-package ship none). New seedExternalIdSaveRefusal (protocol-save-door-refusals.ts) refuses a seed in upsert/update/ignore mode whose externalId member (undeclared = default name) is not a field of its object, 400 VALIDATION_ERROR with one issue per member at externalId / externalId.N, asked in saveMetaItem (draft and publish; migrate-stored exempt), promoteDraftForPublish (batch object drafts judged from request.pending.objects) and the restore doors. Object unresolvable at save (still a draft) is left for its publish; neither registered nor drafted is not judged. SeedLoaderService warns once per keyed dataset whose every record keys empty and loads as before: a replay of such a stored seed on this head still inserts every record again (upsert, ignore) or skips it (update). Changeset minor on metadata-protocol, Clause-2 no (narrowing), Metadata-protocol narrowed.",
"tests": "metadata-protocol full suite at b8d8083: 299 files / 22137 tests passed. New protocol.seed-external-id-refusal.test.ts 22/22, seed-loader-empty-key-warn.test.ts 5/5, restore-doors family 36/36 (2 new refusal pins + 2 controls), dogfood seed-external-id-refusal 4/4. Red on base 62b3ec3: in-process 16 failed / 47 passed (all reds refusal or warn pins); dogfood on a base build 3 failed / 1 passed (control inserted 2, the card's duplication). Ablation of the saveMetaItem check via ablation-replace (blob 96642a86ae58 to 18dc7463b075), rebuilt, preflight marker in 2 dist files: dogfood 3 failed / 1 passed, unit 8 failed / 14 passed; restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent 0 of 24 files, tree clean, dogfood 4/4. First ablation attempt used a void-literal marker the bundler dropped; redone, only the second quoted.",
"gates": "dispatch-gates --commands on the final diff: 73 families, all exit 0; --ran: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN. First pass: check:engine-double-contract exit 1 (new doubles unpinned; --write committed d920028, then 0), check-plugin-teardown-shape --self-test exit 3 (shallow; fixture fetched, then 0), check:dual-build-cjs-loads exit 3 (prereq; full turbo build 118/118, then 0). 62 families ran at b8d8083, the 11 re-run/new at d920028. Typecheck metadata-protocol + dogfood exit 0; eslint scoped exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "b8d8083b · VERDICT command-exit 0 · Test Files 384/384 passed (3 skipped) · Tests 3167 passed (28 skipped) · no failures. d920028 changes only scripts/engine-double-contract.pinned.json.",
"mcp_calls": "0",
"api_writes": "3 — git push (not REST); relay pr_create #29066; relay os-dev-report comment on #28952",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: an object_extension drafted in the same package batch is not in the batch closure, so a seed keyed on a field only that pending extension adds would be refused at batch publish (no shipped seed does); sibling unknown-datasource refusal answers 422 INVALID_METADATA while the order fixed 400 VALIDATION_ERROR here; branch not merged with origin/main (9 commits, none in touched packages)"
]
}
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Release: server seat 1 shift close · 2026-11-02T07:43Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat that claimed this card); card threadsession_01FEkQ5pEjzdJdM36Fgh7YyY(paused)Who / what / where: Leehom (
os-project-manager) in the server-lane project chat — 「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」 (2026-11-02T07:11Z), 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z), 「我说的是pm-dispatch 中车道下班的流程,都要走完」 (2026-11-02T07:33Z). Shift close returns every claim to the queue (seat-post-protocol.md 交接收尾清单 step 2); no stay-behind.Resume-from:
claude/project-thread-ncjmkt@9127f436332169631f997380f1a74079598e612f— PR #29066, ACCEPTed by the seat on this head. ACCEPT 6571560317 (after the fixture rename to sku); auto-merge armed, checks pending.Successor: read the handoff 6571543819 on seat post #6367 first; land PR #29066 on green, write the landing record, close this card. ⛔ Do not re-dispatch the card from scratch.
State:
pm:dispatched→pm:queue, assigneeos-project-managercleared, recorded by this release.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Claim: PM loop round 1 · takeover of a released card · 2026-11-02T08:18Z
Session:
session_01ADHkmQjPiXgNoSaEpqeGvYAccount:
os-steve(the card's assignee, set in the same round)Branch:
claude/project-thread-ncjmktWorktree: the predecessor card thread's (paused); no new dev is dispatched
Domain:
domain:serverSeat:
domain:server#1File surface: unchanged from the predecessor's claim 6569727725 (seed save door, publish probe,
packages/metadata-protocol/src/seed-loader.ts); the open PR #29066 is the surface.Container & model:
S,mode:thread, landing only (no new build)Clause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: as in the predecessor's claim (unchanged)
Thread-read: 6571815096
Premise: holds (PR #29066 ACCEPTed by the predecessor seat)
Serial constraints cleared: none new; the seven PRs this seat inherited are its only in-flight surface.
Resume-from:
claude/project-thread-ncjmkt@9127f436332169631f997380f1a74079598e612fTakeover record: the predecessor (
os-project-manager,session_01R1q2xdtYgU6FKrZ7LgjPCC) released this card in 6571815096 at its shift close (Leehom: 「自动合并中的也交给下一任跟进」). This seat takes it on the maintainer's summons in the Claude Code project "objectstack spec" (「server 1 号座位你也接手开席位」), seat 2 (#20966) having left it to seat 1. Last pushed head:9127f43633(PR #29066). In the merge queue since 2026-11-02T07:50Z; follow to MERGED.objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Landed · 2026-11-02T09:25Z
3cf7f61e9f, through the merge queue (queued by the predecessor seat)Fixes #28952pm:dispatchedis removed. Seat records: review FAIL 6571500081 (one CI red on a fixture column), fixture rename at9127f436, ACCEPT 6571560317 (predecessor); this seat took the card over to land it.9127f436(claim 6573033881 on the PR). If it FAILs, the finding goes to a follow-up card; the closed PR is not reopened. Still owed from the predecessor review: a seed keyed on a field only a pendingobject_extensiondraft adds is refused (should-fix, not filed yet).