Skip to content

Commit 86e3766

Browse files
docs(qa): re-point seed-mode-matrix to the ignore default and url-overlay-contract to ?related= (#31516)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbQ1zKyrd5HGvkrwFzZgwXAS)_ Fixes #31497 Fixes #31498 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:平台测试清单里两条过期条目改口:种子默认模式从 `upsert` 改为 `ignore`;记录页标签页的地址参数从 `?tab=` 改为 `?related=对象.字段`。 - 为什么改:QA 第 #31486 轮按清单原文判了两个 FAIL,但产品行为与声明一致,错的是清单(#30152 已把种子改为默认只建不改;记录页早已用 `?related=` 定位标签页)。 - 风险与回滚:只改一个 QA 清单 JSON 文件的两条目,不动产品代码、不发布任何包;回滚即 revert 本 PR。 - 先看:`docs/qa/platform-checklist/areas/platform-core.json` 的 `platform-core.url-overlay-contract`(steps[3]、acceptance[1]/[2]、negative[0]、标题),再看 `platform-core.seed-mode-matrix`(steps[0]、acceptance[0]/[5])。 - 两分钟验证:`pnpm check:platform-checklist`,预期输出 `check-platform-checklist: OK`、退出码 0。 - 需要你做的:无,席位落地。 ## What changes Both items live in `docs/qa/platform-checklist/areas/platform-core.json`; each gets a revision bump and a history row. **`platform-core.seed-mode-matrix`, revision 7 → 8 (#31497).** PR #30152 made a seed create-only by default: an omitted `mode` is `ignore`. - steps[0]: `defaultMode` defaults to 'ignore', not 'upsert'. It also gains the QA run's note that an unprefixed draft fails the whole publish-drafts batch (outcome 'refused', publishedCount 0), because steps[0] already drives that publish. - acceptance[0]: upsert is named as declared explicitly, with ignore named as the default. - acceptance[5]: the seam reads `dataset.mode || config.defaultMode ('ignore')`. - Accepted extension in the same item (seat's answer before the claim): variants[2] drops "(the default)" from upsert, and source[2] reads "defaultMode is SeedSchema.shape.mode, default 'ignore'". Left alone, both would contradict the clauses. - Not added: the runner's note that `POST /packages/ID/discard-drafts` removes a draft-only item. No step deletes a draft item, so the item never reaches that path. Every probe step names its mode, so no verdict moves. **`platform-core.url-overlay-contract`, revision 4 → 5 (#31498).** The record page addresses its tabs with `?related=OBJECT.FIELD` (`RECORD_RELATED_ENLARGE_PARAM` in `ui/packages/app-shell/src/urlParams.ts`; `openEnlargedRelated` / `closeEnlargedRelated` in `RecordDetailView.tsx`). Leaving Overview pushes one entry, moving between tabs replaces it, and Back, Esc or Overview go back to the record in one step. - steps[3]: on a showcase account record, append `?related=showcase_contact.account` (a cold load lands on that tab), switch tabs several times and press Back once. You return to the record's Overview in one step and never step back through tab selections. - acceptance[1]: tab switches add at most one entry (the first leave of Overview), and palette and shortcuts add none. Its verify step reads the record's Overview as the post-Back location. - acceptance[2]: `?related=OBJECT.FIELD` cold-loads the tab. - negative[0]: the same change, for the tab half. - Accepted extension in the same item: the title says record tabs push one entry and then replace, and "?related cold-loads a record tab". ## Verification Facts re-read on `origin/main` `4894cfe1` before editing: - `seed.zod.ts` L207 `mode: SeedMode.default('ignore')`. - `seed-loader.zod.ts` L253 `defaultMode: SeedSchema.shape.mode`. - `metadata-protocol/src/seed-loader.ts` L1303 `const mode = dataset.mode || config.defaultMode;`. - `protocol.ts` publishPackageDrafts pre-flight: the whole batch is refused with `publishedCount: 0`. - `RecordDetailView.tsx` L843–872 and `urlParams.ts` L105. 16 derived gates ran on head `d5d9482c`, all exit 0. `dispatch-gates --ran` reconciled them: 16 derived, 16 run, 0 NOT-MEASURED. After merging `main` (head `bb608509`, which makes no change to this file), `pnpm check:platform-checklist` re-ran with exit 0. <details><summary>Gate commands</summary> ```text node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:platform-checklist :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` </details> ## Acceptance notes - `skip-changeset`: a QA ledger under `docs/`, so nothing publishes. - `?tab=` (`RECORD_DETAIL_TAB_PARAM`) is still read in `RecordDetailView.tsx` and handed to `withPageTabsUrlSync` for a record page that declares its own tabs component. The card's "not read on a columns record page" is accurate as scoped. This item drives the columns page's tab row, so it no longer names `?tab=`. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (n/a) - [x] 2. Every name I added uses the casing for its kind. (n/a) - [x] 3. No formatter ran; `pnpm lint` passes. (no code) - [x] 4. One branch off an up-to-date `main` and one pull request, for two cards batched by the seat. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (n/a) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jR85GTa1SzozbynuXF)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c86d3c9 commit 86e3766

1 file changed

Lines changed: 25 additions & 13 deletions

File tree

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2562,10 +2562,10 @@
25622562
},
25632563
{
25642564
"id": "platform-core.url-overlay-contract",
2565-
"title": "Reserved URL params (ADR-0054 C3): recordId drawer and form overlay push so Back closes them, tab/palette/shortcuts replace so Back never pages through chrome, formObject/formLink pre-link a child create, ?tab cold-loads, the from trail keeps a clickable path back, and a recordId+recordObject mismatch fails closed",
2565+
"title": "Reserved URL params (ADR-0054 C3): recordId drawer and form overlay push so Back closes them, record tabs push one entry on leaving Overview and replace after it, palette/shortcuts replace so Back never pages through chrome, formObject/formLink pre-link a child create, ?related cold-loads a record tab, the from trail keeps a clickable path back, and a recordId+recordObject mismatch fails closed",
25662566
"since": "v16",
25672567
"status": "active",
2568-
"revision": 4,
2568+
"revision": 5,
25692569
"priority": "P1",
25702570
"surface": "browser",
25712571
"personas": ["seeded admin (admin@objectos.ai / admin123)"],
@@ -2582,7 +2582,7 @@
25822582
"record drawer: on an object list, open a record that renders in the drawer — the URL gains ?recordId=<id> as a PUSHED entry; press browser Back — the drawer closes and the list is still there (one history entry consumed, no page change)",
25832583
"form overlay create: trigger a create that uses the global overlay — ?form=new pushes; fill nothing; Back closes the overlay; re-open and confirm ?form=<recordId> (edit) behaves the same",
25842584
"child pre-link: from a parent record, create a subtable child via the overlay — capture ?form=new&formObject=<childObject>&formLink=<fkField>:<parentId>; the FK field arrives pre-linked to the parent; RELOAD the URL cold — the pre-link survives (refresh-safe by construction: the state is the URL)",
2585-
"tab cold-load: copy a record URL, append ?tab=related (or a related:<child> value), open in a fresh tab — the detail lands on that tab; now switch tabs in-page several times and press Back ONCE — you leave the record (or close the prior overlay), you do NOT step back through tab selections (tab REPLACEs)",
2585+
"tab cold-load: copy a showcase account record URL, append ?related=showcase_contact.account, open in a fresh tab (a cold load lands on that tab); switch tabs several times and press Back once: you return to the record's Overview in one step, never back through tab selections",
25862586
"palette/shortcuts chrome: open ?palette=1 and ?shortcuts=1 overlays, close them, and confirm Back never replays them (replace semantics — useUrlOverlay defaults to replace)",
25872587
"drill trail: from a record, drill into a related record by FOLLOWING the link's href (open it as a page): a plain click on a lookup or related-record link opens the record in the peek drawer and adds no from=, while the href carries the from= trail (recordPath in ui/packages/app-shell/src/views/RelatedRecordActionsBridge.tsx; the drawer's open-as-full-page builds the same trail, expandedRecordPath in recordDrawerStack.tsx). The drilled page's URL carries from=…; the drilled record shows an inline '← PARENT' back link above its body (the top bar draws no record crumb since objectui#12461 — objectui packages/app-shell/src/views/RecordDetailView.tsx), and clicking it navigates to the parent, whose own back link carries the rest of the trail",
25882588
"cross-object refusal (#4292): take a real record id of object A; navigate to /forms/<formName>?recordId=<idOfA>&recordObject=<objectB> — the route renders an ERROR state; capture the network: NO /data/ read was issued for the mismatched pair (the refusal precedes the read, so nothing is read and nothing can be written); control: the same URL with the AGREEING recordObject loads and prefills normally",
@@ -2596,13 +2596,13 @@
25962596
"evidence": "the before/Back screenshot pairs with URLs"
25972597
},
25982598
{
2599-
"clause": "passive/chrome state REPLACEs: tab switches, the palette and the shortcuts dialog never stack history — one Back after N tab switches leaves the page rather than stepping through selections",
2599+
"clause": "record tabs and chrome never stack history: tab switches add at most one entry (the first leave of Overview); palette and shortcuts add none — one Back after N tab switches returns to the record's Overview rather than stepping through selections",
26002600
"oracle": "screenshot",
2601-
"verify": "N in-page tab switches + palette/shortcuts open-close, then a single Back — the resulting location proves no chrome entry was on the stack",
2601+
"verify": "N in-page tab switches + palette/shortcuts open-close, then a single Back — the resulting location (the record's Overview) proves at most the one tab entry and no chrome entry was on the stack",
26022602
"evidence": "the switch sequence + the post-Back location"
26032603
},
26042604
{
2605-
"clause": "?tab cold-loads: a fresh navigation to <record>?tab=<value> lands directly on that tab (URL-addressable state, not just an in-page echo)",
2605+
"clause": "?related=OBJECT.FIELD cold-loads the tab: a fresh navigation to <record>?related=<object>.<field> lands directly on that related tab (URL-addressable state, not just an in-page echo)",
26062606
"oracle": "screenshot",
26072607
"verify": "fresh-tab open of the copied URL shows the named tab active",
26082608
"evidence": "the cold-load screenshot"
@@ -2627,7 +2627,7 @@
26272627
}
26282628
],
26292629
"negative": [
2630-
"Back paging through tab switches or re-opening the palette is a FAIL of the push-vs-replace rule — the registry's own table states which params push and which replace",
2630+
"Back paging through tab switches (any entry beyond the one the first leave of Overview pushes) or re-opening the palette is a FAIL of the push-vs-replace rule — the registry's own table states which params push and which replace",
26312631
"a mismatched recordObject EDITING the same-id row of the wrong object is the silent-wrong-write #4292 closed — any /data read or write on the mismatch is a FAIL even if an error eventually renders",
26322632
"a page-scoped param reusing a reserved name (a view that repurposes ?recordId or ?form) is a contract violation — capture and file, the registry exists to make this a review-time offence",
26332633
"drawer/overlay state that survives only in memory (a cold reload of the captured URL losing the overlay or the pre-link) breaks URL-addressable state — FAIL"
@@ -2667,6 +2667,12 @@
26672667
"date": "2026-10-31",
26682668
"change": "steps[5] says how to drive the drill. Checklist-accuracy finding of QA run #27438 (18.3.0 wave 3, parked on anchor #27377; slice S4, #28338): a plain click on a lookup or related link opens the record peek drawer with no from=, and the trail lives on the link's href, so a runner who clicked saw no from= on the URL. acceptance[4] is unchanged. Re-read at 3fd80337",
26692669
"ref": "#27438"
2670+
},
2671+
{
2672+
"revision": 5,
2673+
"date": "2026-11-08",
2674+
"change": "steps[3], acceptance[1] (clause, verify), acceptance[2].clause and negative[0] re-pointed from ?tab= to ?related=, plus the title (an accepted extension in the same item, so it does not contradict the clauses). Stale-spec finding of QA run #31486 (subject 0647dabb, rule R-2): the record page addresses its tabs with ?related=OBJECT.FIELD (RECORD_RELATED_ENLARGE_PARAM in ui/packages/app-shell/src/urlParams.ts; RecordDetailView.tsx openEnlargedRelated/closeEnlargedRelated). Leaving Overview pushes one history entry, moving between tabs replaces it, and Back, Esc or Overview return to the record in one step, so one Back after N switches lands on Overview, not off the page. ?tab= is not read on a columns record page; it still syncs a page that declares its own tabs (withPageTabsUrlSync), which this item does not drive. The capability holds; only the parameter and the one-entry push changed. Re-read at 4894cfe1",
2675+
"ref": "#31498"
26702676
}
26712677
]
26722678
},
@@ -3056,7 +3062,7 @@
30563062
"title": "Seed mode matrix (insert/update/upsert/replace/ignore): each mode does exactly its verb through the ONE SeedLoaderService, no-op replays skip instead of churning, ignore reports skips — and `replace` inserts and deletes nothing, DECLARED and IMPLEMENTED alike (no caller clears the table): its posture is scored as bare insert, never ticked as scoped deletion",
30573063
"since": "v15",
30583064
"status": "active",
3059-
"revision": 7,
3065+
"revision": 8,
30603066
"priority": "P1",
30613067
"surface": "api",
30623068
"personas": ["seeded admin (admin@objectos.ai / admin123)"],
@@ -3073,7 +3079,7 @@
30733079
]
30743080
},
30753081
"steps": [
3076-
"author the scratch object (2 fields + a natural-key `name`, and a declared `sharingModel` — a draft without one is accepted, but its publish is refused (publish-drafts answers outcome 'refused', failed[0] code INVALID_METADATA, rule security-owd-unset) and so is an active save (422 INVALID_METADATA, rule security-owd-unset)) into the writable package and publish, named with that package's namespace prefix: NS_SHORT, where NS is the package manifest's namespace, which a package created without one derives from its id's last segment (com.objectstack.qa.seedmodes gives seedmodes; packages/spec/src/kernel/namespace-prefix.ts#deriveNamespaceFromPackageId). An unprefixed draft is accepted, and its publish is refused 422 NAMESPACE_PREFIX with the spelling to use (packages/spec/src/kernel/namespace-prefix.ts#validateObjectNamespacePrefix, ADR-0129 D2); note `externalId` defaults to 'name' and `defaultMode` to 'upsert' (SeedLoaderRequestSchema)",
3082+
"author the scratch object (2 fields + a natural-key `name`, and a declared `sharingModel` — a draft without one is accepted, but its publish is refused (publish-drafts answers outcome 'refused', failed[0] code INVALID_METADATA, rule security-owd-unset) and so is an active save (422 INVALID_METADATA, rule security-owd-unset)) into the writable package and publish, named with that package's namespace prefix: NS_SHORT, where NS is the package manifest's namespace, which a package created without one derives from its id's last segment (com.objectstack.qa.seedmodes gives seedmodes; packages/spec/src/kernel/namespace-prefix.ts#deriveNamespaceFromPackageId). An unprefixed draft is accepted, and its publish is refused 422 NAMESPACE_PREFIX with the spelling to use (packages/spec/src/kernel/namespace-prefix.ts#validateObjectNamespacePrefix, ADR-0129 D2); the refusal fails the whole publish-drafts batch (outcome 'refused', publishedCount 0), so every healthy draft beside it stays unpublished too; note `externalId` defaults to 'name' and `defaultMode` to 'ignore' (SeedLoaderRequestSchema)",
30773083
"UPSERT: publish a seed draft (mode upsert) with rows A,B → both insert; edit the draft changing B's value and adding C; re-publish → A skipped (no-op replay — only seed-declared fields compared, so runtime edits to OTHER columns never block the skip), B updated in place (same id), C inserted; verify by id stability + values",
30783084
"UPDATE: seed draft (mode update) touching A (changed value) and a nonexistent D → A updates, D is SKIPPED (update never creates); row count unchanged",
30793085
"IGNORE: seed draft (mode ignore) with A (changed value) and new E → A untouched (still the update-mode value; existing rows are never rewritten), E inserted; the skip is visible as A's unchanged value + count",
@@ -3084,7 +3090,7 @@
30843090
],
30853091
"acceptance": [
30863092
{
3087-
"clause": "upsert (the default) matches on externalId and does exactly create-or-update: unmatched rows insert, matched-and-different rows update IN PLACE (id stable), matched-and-identical rows SKIP as no-op replays — comparison covering only seed-declared fields so runtime edits to other columns never block the skip",
3093+
"clause": "upsert (declared explicitly: ignore is the default since PR #30152) matches on externalId and does exactly create-or-update: unmatched rows insert, matched-and-different rows update IN PLACE (id stable), matched-and-identical rows SKIP as no-op replays — comparison covering only seed-declared fields so runtime edits to other columns never block the skip",
30883094
"oracle": "api",
30893095
"verify": "the id/value table across the two upsert publishes from step 1; ids stable, values as authored, A untouched on the replay",
30903096
"evidence": "the before/after row reads keyed by id"
@@ -3114,7 +3120,7 @@
31143120
"evidence": "F's surviving read + G's read + the source excerpt"
31153121
},
31163122
{
3117-
"clause": "the mode vocabulary is the spec's own five and the resolution seam is one line: dataset.mode || config.defaultMode ('upsert'), through the single SeedLoaderService every path shares (boot, publish, dispatcher) — the enumSource pin fails this item when a sixth mode lands",
3123+
"clause": "the mode vocabulary is the spec's own five and the resolution seam is one line: dataset.mode || config.defaultMode ('ignore'), through the single SeedLoaderService every path shares (boot, publish, dispatcher) — the enumSource pin fails this item when a sixth mode lands",
31183124
"oracle": "build",
31193125
"verify": "SeedMode members at seed.zod.ts; the resolution at metadata-protocol/src/seed-loader.ts; the runtime re-export shim proving one implementation",
31203126
"evidence": "the three source excerpts"
@@ -3127,7 +3133,7 @@
31273133
"variants": [
31283134
"mode insert — no matching, always writes (duplicates on replay unless the object's own unique constraint refuses)",
31293135
"mode update — matched rows only; unmatched skipped, never created",
3130-
"mode upsert — create-or-update on externalId with no-op-replay skip (the default)",
3136+
"mode upsert — create-or-update on externalId with no-op-replay skip",
31313137
"mode replace — bare insert, no matching, nothing deleted: DECLARED and IMPLEMENTED alike (the expected-posture clause)",
31323138
"mode ignore — insert-if-absent; existing rows never rewritten"
31333139
],
@@ -3140,7 +3146,7 @@
31403146
"source": [
31413147
"packages/spec/src/data/seed.zod.ts#SeedMode (SeedMode — the five modes with their declared semantics, incl. replace's 'Insert every record, no match, deleting nothing: for a cache table the caller cleared'), (SeedSchema's mode description — replace 'does the same and deletes nothing: it is for a table the caller cleared')",
31423148
"packages/metadata-protocol/src/seed-loader.ts#writeRecord (mode = dataset.mode || config.defaultMode), (existing-records preload for upsert/update/ignore ONLY — insert/replace never match), (writeRecord per-mode switch; the replace arm's bare insert + 'caller should have cleared the table' comment), (decideWriteAction — the batched mirror, replace → insert),+ (isNoOpReplay — seed-declared fields only, conservative on doubt)",
3143-
"packages/spec/src/data/seed-loader.zod.ts#defaultMode (defaultMode default 'upsert')",
3149+
"packages/spec/src/data/seed-loader.zod.ts#defaultMode (defaultMode is SeedSchema.shape.mode, default 'ignore')",
31443150
"packages/metadata-protocol/src/protocol.ts#applySeedBodies+ (applySeedBodies — published seed bodies materialize through the same loader; failures returned, never thrown, surfaced as seedApplied)",
31453151
"packages/runtime/src/seed-loader.ts#SeedLoaderService (the MOVED shim — one SeedLoaderService for every path)",
31463152
"packages/spec/src/kernel/metadata-plugin.zod.ts#allowRuntimeCreate (seed: allowRuntimeCreate true — the scratch-draft fixture path)",
@@ -3188,6 +3194,12 @@
31883194
"date": "2026-11-07",
31893195
"change": "title, steps[5], acceptance[4].clause, variants[3] and source[0] re-worded: replace is now declared = implemented. #31099 (PR #31135) rewrote SeedMode's replace member and SeedSchema's mode description to what the loader does (insert every record, no match, deleting nothing), so the clause's 'declared ≠ implemented' posture went false and source[0] quoted the old delete-all-then-insert wording, which seed.zod.ts no longer holds. Read at base 658d84f5: seed.zod.ts SeedMode and SeedSchema mode describe text (no delete-all wording left), seed-loader.ts writeRecord's replace arm (bare insert, comment 'caller should have cleared the table') and decideWriteAction (replace routes to insert), and no other replace arm on the seed path (packages/cli/src/commands/diff.ts only words a plan line). The probe and its verdicts are unchanged: F survives, G is inserted, nothing is ticked as scoped deletion",
31903196
"ref": "#31140"
3197+
},
3198+
{
3199+
"revision": 8,
3200+
"date": "2026-11-08",
3201+
"change": "steps[0], acceptance[0].clause and acceptance[5].clause re-pointed to the ignore default, plus variants[2] and source[2] (an accepted extension in the same item, so it does not contradict the clauses). Stale-spec finding of QA run #31486 (subject 0647dabb, rule R-1, acceptance[5]): PR #30152 made a seed create-only by default, an omitted mode is ignore. Read at 4894cfe1: packages/spec/src/data/seed.zod.ts SeedSchema mode is SeedMode.default('ignore'); seed-loader.zod.ts SeedLoaderConfigSchema defaultMode is SeedSchema.shape.mode; the seam in packages/metadata-protocol/src/seed-loader.ts writeRecord is unchanged (dataset.mode || config.defaultMode). steps[0] also carries the run's note that an unprefixed draft fails the whole publish-drafts batch (outcome 'refused', publishedCount 0; protocol.ts publishPackageDrafts pre-flight). Every probe step declares its mode, so no verdict moves",
3202+
"ref": "#31497"
31913203
}
31923204
]
31933205
},

0 commit comments

Comments
 (0)