Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/NORTH-STAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- records · 写入规则在服务端兑现,撞车响亮、后悔得了、改过什么看得见 · records-forms · validation-rule-type-matrix, object-hook-lifecycle, delete-behavior-matrix, concurrent-edit-conflict, record-edit-undo, field-history-tracking
- records · 记录上的动作与协作:按钮在声明的位置、参数契约在派发时兑现、讨论与 @ 提醒 · records-forms · action-location-matrix, action-param-widgets, upload-guard-blocks-confirm, record-discussion-mentions
- records · 搜得到:跨字段、字段限定、权限一致、拼音、即时新鲜;全局搜索与命令面板同一条路 · search · cross-field-object-search, field-scoped-narrowing, rls-both-personas, pinyin-flag-both-sides, freshness-and-empty, console-global-search, command-palette-navigation
- records · 共享选项集:多个对象复用同一份选项,客户按组织追加值而不改应用源码 · records-forms · picklist-shared-across-objects, picklist-org-append
- access · 行级与字段级权限两边都对:受限成员只看自己的,该只读的只读、该看不见的不回给前端 · access-security · rls-both-sides, scope-depth-asymmetry, fls-mask-and-strip
- access · 增删改查逐格兑现,改完权限立刻换脸,自查接口与服务端一致 · access-security · crud-permission-matrix, permission-matrix-edit-loop, me-permissions-aggregation-parity
- access · 写入路径挡得住:只读剥离、伪造与转移 owner 被拒、默认可见度只能收紧 · access-security · write-path-guards, owd-save-gate
Expand Down
98 changes: 98 additions & 0 deletions docs/qa/platform-checklist/areas/records-forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -4177,6 +4177,104 @@
"ref": "claude/issue-18682-predicate-relationship-traversal"
}
]
},
{
"id": "records-forms.picklist-shared-across-objects",
"title": "One shared picklist on two objects: both are served the same resolved options, a package extension adds to both, a value outside the set is refused, and a locale switch relabels",
"since": null,
"status": "planned",
"revision": 1,
"priority": "P2",
"surface": "mixed",
"personas": [
"seeded admin (admin@objectos.ai / admin123)"
],
"fixtures": {
"app": "showcase",
"requires": [
"a picklist declared once (`*.picklist.ts`) and referenced by `Field.select({ picklist })` on two showcase objects, a `picklistExtensions` entry that adds one value to it, and `picklists.NAME` labels for a second locale. Stock showcase has none of these, and nothing serves a picklist until the runtime layer (#19519) lands, so the fixture arrives with it"
]
},
"acceptance": [
{
"clause": "both objects' fields are served with the SAME resolved `options` (the picklist's own values plus the extension's) beside the `picklist` name, and each object's form offers exactly that set",
"oracle": "api",
"verify": "read both objects' field metadata: each field parses as packages/spec/src/data/picklist.zod.ts#PicklistServedFieldSchema and the two option lists are equal; then enumerate the options each object's form offers",
"evidence": "both served field bodies + both form option enumerations"
},
{
"clause": "the write door judges both objects against the resolved set: the extension's value persists on object A, and a value outside the set is refused on object B, the refusal naming the picklist",
"oracle": "api",
"verify": "POST the extension's value to object A and re-read the row; POST an out-of-set value to object B and capture the refusal envelope",
"evidence": "the re-read + the refusal envelope"
},
{
"clause": "switching the locale relabels the options on both objects from the picklist's `picklists.NAME.options.VALUE` translations, and the stored values do not change",
"oracle": "api",
"verify": "read both objects' field metadata under each locale; a picklist-bound field takes its option labels through packages/spec/src/system/i18n-resolver.ts#translateObject, and the values are identical across locales",
"evidence": "both served bodies under each locale"
}
],
"negative": [
"a picklist-bound field served WITHOUT `options` is a FAIL: renderers, the record validator and filter pickers read `options`, so they would have nothing to offer or judge",
"a value outside the set accepted with 200 on either object is a FAIL: hiding it in the form is a client courtesy, and the write door is the boundary",
"a field naming a picklist that does not exist, served as an empty option list, is a FAIL: it must be refused loudly at load, naming the field and the package",
"an extension that duplicates, replaces or removes a base value is a FAIL: extensions are additive only, and a duplicate is refused loudly rather than last-wins"
],
"source": [
"#18164 design of record (comment 5715762696), items 1 to 6: the kind, the field reference, server resolution, the additive package extension, the translation face, and the verification sentence that this item and records-forms.picklist-org-append split between them",
"#19518, the spec layer (landed as addbbf02ab): packages/spec/src/data/picklist.zod.ts#PicklistSchema",
"#19518, the extension shape: packages/spec/src/data/picklist.zod.ts#PicklistExtensionSchema",
"#19519, the runtime layer this item waits on (load before `object`, additive merge, resolve at serve time, write validation against the resolved set); its acceptance sentence is this item's three clauses"
],
"history": [
{
"revision": 1,
"date": "2026-09-30",
"change": "initial — planned. The spec layer landed (#19518), but no runtime reads a picklist yet (packages/spec/liveness/picklist.json grades every key `planned`), so there is nothing to drive. The acceptance clauses are drafted from the design of record and #19519's acceptance; steps arrive with #19519, in the edit that promotes this item after a run in which it passes. The locale relabel sits on this item rather than on the organization item because it is phase-1 behaviour, and a promotion needs the whole item to pass",
"ref": "#19518"
}
]
},
{
"id": "records-forms.picklist-org-append",
"title": "An organization appends a value to a shared picklist without changing the app source: that organization can write it, and another organization is refused",
"since": null,
"status": "planned",
"revision": 1,
"priority": "P2",
"surface": "mixed",
"personas": [
"an admin of organization A (appends the value, then writes it)",
"a member of organization B (the same app and picklist, without A's value)"
],
"fixtures": {
"app": "showcase",
"requires": [
"two organizations on one environment, each with its own signed-in persona, sharing one app whose object has a picklist-bound field"
],
"knownGaps": [
"NOT DECLARED: the per-organization overlay is phase 2 of the picklist design. The picklist row of packages/spec/src/kernel/metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRY carries `allowOrgOverride: false`, the one opt-in ADR-0005 allows for an organization overlay, and ruling 5904864936 on #18164 defers the overlay to its own sub-issue, filed when a customer requirement measures it. Nothing here can run until that sub-issue lands the overlay. Package-level extension (phase 1) is records-forms.picklist-shared-across-objects"
]
},
"negative": [
"organization B writing the value that only organization A appended, accepted with 200, is a FAIL: the appended value belongs to A's overlay alone",
"an organization overlay that removes or renames a value the owning package declared is a FAIL: the overlay adds values only, and removal or rename stays with the owning package",
"an append that needed an edit to the app source or to the owning package does not demonstrate this item: the capability is appending without touching either"
],
"source": [
"#18164 design of record (comment 5715762696), item 4 (a per-organization overlay of the picklist shape, additive only, whose allowance was left to the maintainer as an open point) and item 6 (the verification sentence: org A adds a value, A writes it, B is refused)",
"#18164 ruling 5904864936 (the overlay stays deferred to its own sub-issue on a measured customer requirement) and #19518 Scope 5 (not declared in the spec layer)",
"docs/adr/0005-metadata-customization-overlay.md (`allowOrgOverride` on the type registry is the only org-overlay opt-in)"
],
"history": [
{
"revision": 1,
"date": "2026-09-30",
"change": "initial — planned. The docs/NORTH-STAR.md feature-map line names per-organization append as half of the shared-picklist feature, and the design of record's verification sentence names this behaviour, so the ledger holds it as a planned requirement rather than leaving it off. It carries no acceptance clauses: neither the overlay's schema nor its write door is designed yet, and a clause would be written against a design nobody has settled",
"ref": "#19518"
}
]
}
]
}
3 changes: 3 additions & 0 deletions docs/qa/platform-checklist/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@
"api-backend.api-methods-verb-gate"
]
},
"picklist": {
"waived": "WAIVED 2026-09-30 — there is no runtime behaviour to drive yet, measured rather than asserted. The spec layer landed (#19518, addbbf02ab): PicklistSchema, `Field.select({ picklist })`, PicklistServedFieldSchema, `picklistExtensions` and the `picklists.NAME` translation face. No runtime reads a picklist: packages/spec/liveness/picklist.json grades every key `planned`, and scripts/check-stack-collection-maps.mjs carries `picklists` and `picklistExtensions` as rows PENDING the runtime layer (#19519) in the METADATA_ARRAY_KEYS and ARTIFACT_FIELD_TO_TYPE registration maps. No code resolves a picklist onto a field or judges a write against one, so an item written to RUN today could only assert absence. The two items that will carry this kind are authored as `planned`: records-forms.picklist-shared-across-objects (phase 1: two objects, the package extension, the write door, the locale relabel) and records-forms.picklist-org-append (phase 2: the per-organization overlay, deferred by ruling 5904864936 on #18164). They are not listed under `items`, because a kind whose only items are planned is UNMAPPED and an entry cannot hold a waiver and items together. WHAT RETIRES THIS WAIVER — #19519 lands resolve-at-serve, the additive merge and write validation; records-forms.picklist-shared-across-objects gains its steps, runs, passes and is promoted to `active`, and the same edit replaces this waiver with `items` naming it, the planned organization item beside it. ⚠️ Re-audit it every sweep, as SWEEP.md asks of every waiver."
},
"position": {
"items": [
"access-security.scope-depth-asymmetry",
Expand Down
14 changes: 7 additions & 7 deletions scripts/check-platform-checklist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
[BATTERY_SYMBOL_ANCHORS]: 42,
// New with the `planned` status. Set at its landed count (headroom 0, the
// convention every entry above uses). The load-bearing third of it is the
// coverage direction: the live ledger carries ZERO planned items today, so
// coverage direction: no coverage.json entry maps a planned item today, so
// nothing but these fixtures can tell a working ratchet rule from a deleted
// one — the unreferenced-recipe argument, applied to a rule whose subject
// population is empty on purpose rather than by luck.
Expand Down Expand Up @@ -2277,10 +2277,10 @@ function selfTestSymbolAnchors() {
* ratchet. If a planned item ever counted as coverage, "凡是有的能力, 都要测试"
* would become "凡是有的能力, 都要打算测试", and the ratchet would go green on
* a kind nothing runs against. So the ratchet direction is pinned BOTH ways,
* on fixtures, not on the tree: the live ledger carries zero planned items and
* is expected to for a while, which means the real data cannot tell "this rule
* works" from "this rule was deleted" — the same silent-success argument the
* unreferenced-recipe battery above makes.
* on fixtures, not on the tree: no coverage.json entry maps a planned item,
* which means the real data cannot tell "this rule works" from "this rule was
* deleted" — the same silent-success argument the unreferenced-recipe battery
* above makes.
*/
function selfTestPlannedStatus() {
const failures = [];
Expand Down Expand Up @@ -2538,7 +2538,7 @@ if (process.argv.slice(2).includes('--self-test')) {
' and the `/meta` call-spelling refusal reads its vocabulary out of the live generated contract, fires on every folded spelling a `call` can instruct, and stays silent on the canonical singular, on parameter placeholders, and on the `why`/`expect`/`source`/`requires` prose that narrates the fold;' +
' and the line-citation limb DETECTS NOTHING ITSELF EITHER: the last forked grammar in this file went into the shared core at #18592, so what is pinned here is the BINDING — the corpus declaring `pathlessLineCitations`, a source read finding no citation regex and no detector while the same read DOES find the declaration, the binding driven ON and OFF against ONE text so the green is the declaration working rather than a text that would have matched anyway, the DARK case that a citation both grammars already agreed on keeps its verdict either way, the refusal to over-fire on this ledger\'s own HTTP statuses, config literals, URL ports, clock times and quoted JSON, and the live zero with the control that says it is a reading;' +
' and the symbol-anchor limb DETECTS NOTHING AND RESOLVES NOTHING ITSELF: it is a registered corpus (#18107), so the grammar, the walk and the verdict are all `scripts/symbol-anchors.mjs`\'s, pinned here by a source read that finds no local extension set, no anchor regex and no detector while the same read DOES find the registration, by the anchorable-extension vocabulary being the shared OBJECT rather than a copy of it, by the `runs/` exclusion driven three ways on the live corpus (the subtree holds files, none is swept, the areas beside it still are, and dropping the exclusion puts them back), and by the #16898 binding re-taken through the registration — a call site / import / local parameter / string-substring all reading ABSENT, the positive control that a declaration and a complete quoted token still resolve, a `.json` key resolving where a `.json` value does not, an INLINE object-literal key reading absent where one at the start of a line resolves — with the closed, grow-never residual and the per-file anchor floor held in both directions beside it;' +
` and the \`planned\` status is driven on fixtures rather than on a ledger that carries none of it — the accept set widened without losing its closure, \`since: null\`/no-steps/personas relaxed for planned alone while the ${plannedStatus.liveItems} live items are judged exactly as before, and the coverage ratchet held BOTH ways: a planned item beside an active one is silent, a kind whose only items are planned is UNMAPPED, and the bearing set is pinned NOT to contain \`planned\`; and the two CALL SITES those rules ride on are pinned by a source read over comment-MASKED source driven ON and OFF, because severing either one — by deletion OR by commenting it out in place — left this very self-test green; \u26d4 that pin is a TEXT pin and G12 records the three semantic severings it cannot see.`,
` and the \`planned\` status is driven on fixtures — the accept set widened without losing its closure, \`since: null\`/no-steps/personas relaxed for planned alone while the ${plannedStatus.liveItems} live items are judged exactly as before, and the coverage ratchet held BOTH ways: a planned item beside an active one is silent, a kind whose only items are planned is UNMAPPED, and the bearing set is pinned NOT to contain \`planned\`; and the two CALL SITES those rules ride on are pinned by a source read over comment-MASKED source driven ON and OFF, because severing either one — by deletion OR by commenting it out in place — left this very self-test green; \u26d4 that pin is a TEXT pin and G12 records the three semantic severings it cannot see.`,
);
process.exit(0);
}
Expand Down Expand Up @@ -2619,7 +2619,7 @@ if (symbolAnchorControl.failures.length) {
}
// And for the `planned` status. Its schema half is exercised by the tree the
// moment anyone authors a planned item; its COVERAGE half is not, and will not
// be for as long as the ledger's planned count is the 0 this gate prints. A
// be while no coverage.json entry maps a planned item. A
// deleted ratchet rule and an honest ledger print the same green, so the
// fixtures below it are the only thing that can tell them apart.
const plannedStatusControl = selfTestPlannedStatus();
Expand Down
20 changes: 10 additions & 10 deletions skills/objectstack-upgrade/references/examples-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ The shape in a protocol-16 project:
}
```

The chain deletes the key (`field-mapping-transform-removed`) and the schema
tombstones it, so the parse error *is* the prescription: the union had five
members and **no runtime ever executed any of them**. The customer wrote it
because they wanted a transformation, and that need is real regardless.
The chain deletes the key (`field-mapping-transform-removed`), then all of
`fieldMappings` (`connector-sync-keys-removed`), whose tombstone *is* the
prescription: **nothing ever ran any connector field mapping**. The customer
wrote it because they wanted a transformation, and that need is real regardless.

The prescription names one live target; the rest is the business decision:

Expand All @@ -29,10 +29,10 @@ The prescription names one live target; the rest is the business decision:
| multi-source, multi-stage transformation | **nothing** — the L2 ETL layer retired at 17, unexecuted. Do it where it runs: warehouse ELT, a `flow`, a job. |
| nothing — the value was already correct | delete the key and record that the transformation never ran. |

That third row is frequently the truth: the member never executed, so the
connector has been landing raw values for as long as it has been running.
Whether the downstream data is wrong is a question only the owner can answer —
exactly the kind of finding the report exists to surface.
That third row is frequently the truth: no connector sync ever ran, so no value
ever passed through this mapping. Where the data really comes from, and whether
it is right, only the owner can answer — exactly the kind of finding the report
exists to surface.

### 3.4 The report — the human half

Expand All @@ -44,7 +44,7 @@ maintainer can read in five minutes and a year from now. Write
# Protocol 16 → 17 upgrade — <project>

**Status:** complete | complete with N open decisions
**Spec:** <installed @objectstack/spec version> · **Chain:** 16 → 17
**Spec:** <installed @objectstack/spec version> · **Chain:** 16 → 18
**Verified:** `os validate` green · `tsc --noEmit` green · replay-from-17 applies 0 mechanical changes

## 1 · Mechanical (applied by the chain)
Expand All @@ -64,7 +64,7 @@ _N sites, M conversions. Ported into sources from `os migrate meta --out`._
- **Options:** import-mapping `transform` · ETL step · delete
- **Decision:** delete — owner confirmed the values arrive pre-scaled.
_Decided by: <who>, <date>._
- **Verified:** `os validate` green; connector sync run against staging, 200 rows, values unchanged.
- **Verified:** `os validate` green; nothing ever read the key, so no data changes.

## 3 · Open decisions

Expand Down
Loading