Skip to content

Commit 619a221

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-r9vctw
2 parents 7f5666b + bb60b86 commit 619a221

10 files changed

Lines changed: 450 additions & 23 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): on a narrow window, a toast raised beside the record drawer no longer covers the record's Edit and ⋯ buttons
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
When a right-edge drawer is open and the window is too narrow for a toast beside it (a 720px record drawer on a 1024px window, a split screen, a phone), the console's toaster drops below the drawer's header. It cleared only the drawer's own header, so the success toast raised by creating a record sat on the record header's Edit button and ⋯ menu for its whole four seconds. The toast now also drops below the drawer's first row of controls, measured from the drawer's layout, so it clears the record's actions row for any drawer content. Wide windows keep the toast in the strip beside the drawer, as before.
11+
12+
What to do: nothing.

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,14 @@ export const CEILINGS = new Map([
561561
// 3778 -> 3676: RE-LOCK at the landed count (#19715, card item 4). A 5-line
562562
// table of contents (+154 tokens) landed inside the row's 256 tokens of
563563
// headroom, so no raise; pinned AT the measurement — zero headroom.
564-
['skills/objectstack-data/rules/relationships.md', 3676],
564+
// 3676 -> 3873 (#26297): `sharing.controlledByParent.ownerMayEdit` — the
565+
// owner of a controlled_by_parent detail row may update it by id with read
566+
// access to the master; what it does not widen; refused off a detail and
567+
// one level up — which the file did not teach beside `controlled_by_parent`.
568+
// +201 tokens on a landed 3672, 4 absorbed by headroom, ceiling +197. No
569+
// repeated passage to cut first (measured: no sentence repeats). Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
570+
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
571+
['skills/objectstack-data/rules/relationships.md', 3873],
565572
// NEW FILE (#14296 item 1 = A, condition (b)): the entry's Security & Access
566573
// Control block moved here whole. Pinned at its landed count — no headroom,
567574
// because a split that arrives with budget is a raise wearing a new path.
@@ -591,7 +598,18 @@ export const CEILINGS = new Map([
591598
// first (measured: no sentence repeats in the file). Maintainer ruling
592599
// 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
593600
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
594-
['skills/objectstack-data/rules/security.md', 2956],
601+
// 2956 -> 3693 (#26297): membership keys (`membershipKeys` on the roster
602+
// object — the three members, the refusals, the once-per-request system
603+
// read that fails closed, and the choice rule against owner scoping, a
604+
// position and a sharing rule), the position → `permissionSets` binding
605+
// (a namesake set is reported, never granted) and the `external`
606+
// membership tier for portal users (no baseline, explicit grants only) —
607+
// none of which the file taught, so an author wrote per-row policies and
608+
// expected a position-named set to grant itself. +737 tokens on a landed
609+
// 2956 at headroom 0, ceiling +737. No repeated passage in the file to
610+
// cut first (measured: no sentence repeats). Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
611+
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
612+
['skills/objectstack-data/rules/security.md', 3693],
595613
// 3024 -> 3109 (2026-08-31 app-repo-principles raise, see the block above).
596614
// Severity Levels listed the three values and left the CHOICE unstated: a
597615
// block rests on a judgement a person made, so a machine-inferred signal — a
@@ -756,8 +774,22 @@ export const CEILINGS = new Map([
756774
// ceiling +201; nothing duplicated in the file to cut first. Same ruling as
757775
// the `rules/security.md` row above (2026-10-27, tap A:
758776
// 「A:先削重复,余额按实测提高」).
759-
['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 1095],
760-
['skills/objectstack-data/evals/objects-fields-relationships.json', 946],
777+
// 1095 -> 1404 (#26297): one regression case for the portal membership key
778+
// (a portal user reads only the rows of the accounts they belong to —
779+
// expected: `membershipKeys` on the roster object, `current_user.KEY` in the
780+
// policy, the set bound through a position's `permissionSets`, the
781+
// `external` tier), and the seed case gains the job-owned column declared
782+
// `applyOn: 'create'`. +309 tokens at headroom 0, ceiling +309; nothing
783+
// duplicated in the file to cut first. Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
784+
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
785+
['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 1404],
786+
// 946 -> 1050 (#26297): the invoice-line case gains the owner opt-in
787+
// (`ownerMayEdit` on the controlled_by_parent line) and pins `added_by` as
788+
// `type: 'user'`, never a lookup to a `user` object. +104 tokens at
789+
// headroom 0, ceiling +104; nothing duplicated in the file to cut first.
790+
// Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
791+
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
792+
['skills/objectstack-data/evals/objects-fields-relationships.json', 1050],
761793
['skills/objectstack-formula/evals/cel-predicates-formulas.json', 1046],
762794
// 1004 -> 1451 (2026-10-27, the objectstack-i18n fix card of the skills gap
763795
// audit): two eval prompts for the teaching that card adds — the `reports`,
@@ -815,9 +847,21 @@ export const CEILINGS = new Map([
815847
// absorbed by headroom, ceiling +18. Same maintainer ruling as the SKILL.md
816848
// row above (2026-10-27, verbatim and untranslated): 「A:先削重复,余额按实测提高」.
817849
['skills/objectstack-automation/references/state-machines-and-approvals.md', 5587],
818-
['skills/objectstack-data/references/examples-objects.md', 1779],
850+
// 1779 -> 1769 (#26297): RE-LOCK at the landed count after a net cut — two
851+
// stale `{ type: 'lookup', reference: 'user' }` fields (an object that does
852+
// not exist) became `type: 'user'`; -10 tokens, the row follows the
853+
// measurement down, zero headroom.
854+
['skills/objectstack-data/references/examples-objects.md', 1769],
819855
['skills/objectstack-data/references/lint-rules.md', 970],
820-
['skills/objectstack-data/references/seeds.md', 1299],
856+
// 1299 -> 1566 (#26297): the `columns` dataset key and `applyOn: 'create'`
857+
// — a column a job or an admin owns after the first load is written on
858+
// create and left alone on every `upsert` replay; which modes it touches,
859+
// the refused key, the cross-link to the import door's `applyOn` — which
860+
// the option table lacked, so a replay reverted the app's own writes. +267
861+
// tokens at headroom 0, ceiling +267. No repeated passage to cut first
862+
// (measured: no sentence repeats). Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
863+
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
864+
['skills/objectstack-data/references/seeds.md', 1566],
821865
['skills/objectstack-platform/references/bootstrap.md', 1351],
822866
['skills/objectstack-platform/references/operations.md', 3125],
823867
['skills/objectstack-platform/references/plugin-development.md', 3137],

‎skills/objectstack-data/evals/hooks-security-seeds-datasources.json‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
},
3434
{
3535
"id": 4,
36-
"prompt": "Seed the `country` reference table for every environment, and add demo `opportunity` rows (dev/test only) that close 45 days after install and are owned by whoever installs the package.",
37-
"expected_output": "Uses `defineSeed(Country, { externalId: 'code', mode: 'upsert', records })` for reference data and `defineSeed(Opportunity, { env: ['dev', 'test'], ... })` for demo rows, with lookups referenced by natural key and dynamic values as CEL: `close_date: cel`daysFromNow(45)``, `owner_id: cel`os.user.id``. Never `externalId: 'id'`, never `new Date()` / `Date.now()` (breaks build determinism), never `defineDataset` (the analytics layer), parents before children in the exported array.",
36+
"prompt": "Seed the `country` reference table for every environment, and add demo `opportunity` rows (dev/test only) that close 45 days after install and are owned by whoever installs the package. A nightly job advances each opportunity's `stage`; replaying the seed must not reset it.",
37+
"expected_output": "Uses `defineSeed(Country, { externalId: 'code', mode: 'upsert', records })` for reference data and `defineSeed(Opportunity, { env: ['dev', 'test'], ... })` for demo rows, with lookups referenced by natural key and dynamic values as CEL: `close_date: cel`daysFromNow(45)``, `owner_id: cel`os.user.id``. Never `externalId: 'id'`, never `new Date()` / `Date.now()` (breaks build determinism), never `defineDataset` (the analytics layer), parents before children in the exported array. The job-owned column is declared `columns: { stage: { applyOn: 'create' } }`, so an `upsert` replay leaves it as the row holds it.",
3838
"files": [],
3939
"assertions": {
40-
"must_contain": ["defineSeed", "externalId", "upsert", "env:", "daysFromNow"],
40+
"must_contain": ["defineSeed", "externalId", "upsert", "env:", "daysFromNow", "applyOn: 'create'"],
4141
"must_not_contain": ["new Date(", "defineDataset", "externalId: 'id'"]
4242
}
4343
},
@@ -50,6 +50,16 @@
5050
"must_contain": ["when:", "editable: false", "readable: true", "clm_contract.governing_law"],
5151
"must_not_contain": ["validations:", "readonlyWhen"]
5252
}
53+
},
54+
{
55+
"id": 6,
56+
"prompt": "Customers' staff sign in to our portal and may read only the orders of the accounts they belong to; staff of one account must never see another account's orders.",
57+
"expected_output": "Declares the set on the roster object that defines it: `account_member` (`user` field + `account` lookup) with `membershipKeys: { portal_account_ids: { userField: 'user', valueField: 'account' } }`; a portal permission set whose `rowLevelSecurity` reads it (`using: 'account in current_user.portal_account_ids'`), distributed by a position naming it in `permissionSets`; portal staff are invited on the `external` membership tier, outside the `everyone` baseline. Never a policy per account, never an owner stamp on every order.",
58+
"files": [],
59+
"assertions": {
60+
"must_contain": ["membershipKeys", "current_user.portal_account_ids", "userField", "valueField", "permissionSets", "external"],
61+
"must_not_contain": ["permissions: {"]
62+
}
5363
}
5464
]
5565
}

‎skills/objectstack-data/evals/objects-fields-relationships.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
},
1414
{
1515
"id": 2,
16-
"prompt": "Model invoices with line items, and let invoices be tagged with many tags where each tagging records who added it and when.",
17-
"expected_output": "Line items: `invoice_line` with a `master_detail` field to `invoice`, `required: true`, `deleteBehavior: 'cascade'` (or `'restrict'` — `set_null` is refused on master_detail), `inlineEdit` on that relationship, and a `summary` roll-up field on the parent. Tagging carries attributes, so it is a junction object (`invoice_tag`) with two `lookup` fields plus `added_by` / `added_at` — not a `multiple: true` lookup (an array of ids) and not a `many_to_many` type, which does not exist.",
16+
"prompt": "Model invoices with line items, and let invoices be tagged with many tags where each tagging records who added it and when. A rep who owns a line item but can only read the invoice may still correct that line.",
17+
"expected_output": "Line items: `invoice_line` with a `master_detail` field to `invoice`, `required: true`, `deleteBehavior: 'cascade'` (or `'restrict'` — `set_null` is refused on master_detail), `inlineEdit` on that relationship, and a `summary` roll-up field on the parent. Tagging carries attributes, so it is a junction object (`invoice_tag`) with two `lookup` fields plus `added_by` / `added_at` — not a `multiple: true` lookup (an array of ids) and not a `many_to_many` type, which does not exist. The line object is `sharingModel: 'controlled_by_parent'` with `sharing: { controlledByParent: { ownerMayEdit: true } }` (by-id update only; every other write still needs master edit). `added_by` is `type: 'user'` — never a `lookup` to a `user` object, which does not exist.",
1818
"files": [],
1919
"assertions": {
20-
"must_contain": ["master_detail", "deleteBehavior", "inlineEdit", "summary", "lookup"],
21-
"must_not_contain": ["many_to_many", "deleteBehavior: 'set_null'"]
20+
"must_contain": ["master_detail", "deleteBehavior", "inlineEdit", "summary", "lookup", "ownerMayEdit", "type: 'user'"],
21+
"must_not_contain": ["many_to_many", "deleteBehavior: 'set_null'", "reference: 'user'"]
2222
}
2323
},
2424
{

‎skills/objectstack-data/references/examples-objects.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default ObjectSchema.create({
3939
vat_id: { type: 'text', group: 'billing' },
4040
billing_address: { type: 'address', group: 'billing' },
4141
created_at: { type: 'datetime', readonly: true, group: 'system' },
42-
created_by: { type: 'lookup', reference: 'user', readonly: true, group: 'system' },
42+
created_by: { type: 'user', readonly: true, group: 'system' },
4343
},
4444
});
4545
```
@@ -85,7 +85,7 @@ export default ObjectSchema.create({
8585
]},
8686
account: { type: 'lookup', reference: 'account', required: true },
8787
contact: { type: 'lookup', reference: 'contact' },
88-
assigned_to: { type: 'lookup', reference: 'user' },
88+
assigned_to: { type: 'user' },
8989
due_date: { type: 'datetime' },
9090
},
9191
validations: [

‎skills/objectstack-data/references/seeds.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const SeedData = [statusSeed, categorySeed]; // parents first
5050
| `object` | derived | Auto-set from `objectDef.name` — never write manually |
5151
| `externalId` | `'name'` | Stable business key used for upsert / update lookup |
5252
| `mode` | `'upsert'` | Import strategy (see below) |
53+
| `columns` | — | Per-column write policy, keyed by field: `{ FIELD: { applyOn: 'create' } }` (below) |
5354
| `env` | `['prod','dev','test']` | Environments where the seed loads |
5455
| `records` | — | `Partial<Record<keyof object.fields, unknown>>[]` |
5556

@@ -65,6 +66,26 @@ Full Zod shape: `node_modules/@objectstack/spec/src/data/seed.zod.ts`.
6566
| `ignore` | Insert; silently skip duplicates. | Additive bootstrap |
6667
| `replace` ⚠️ | Delete everything, then insert. **Data loss.** | Cache / lookup tables only — never user data |
6768

69+
### Columns the app owns after the first load — `columns.FIELD.applyOn`
70+
71+
A seed is replayed (every dev-server boot, every re-publish) and an `upsert`
72+
replay re-asserts every column on the row it matched — right for master data,
73+
wrong for a column a job or an admin advances after the first load. Declare it
74+
`applyOn: 'create'`: written when the seed creates the row, left as the row
75+
holds it on every replay (an `update` dataset never writes it; `insert` /
76+
`ignore` / `replace` never update a matched row, so nothing changes there).
77+
Each key must be a field of the object or an injected system column —
78+
`defineSeed` refuses one that is not. Same values and meaning as an import
79+
mapping entry's `applyOn` (objectstack-api).
80+
81+
```typescript
82+
defineSeed(Invoice, {
83+
externalId: 'number',
84+
columns: { status: { applyOn: 'create' } }, // the payment job owns it after the first seed
85+
records: [{ number: 'INV-001', status: 'draft', amount: 1200 }],
86+
});
87+
```
88+
6889
### `externalId` selection
6990

7091
Pick a stable natural key. **Never use `id`** — UUIDs differ across

‎skills/objectstack-data/rules/relationships.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Guide for modeling relationships between objects using `lookup`, `master_detail`
1313
| Type | Lifecycle | Required | Sharing | Roll-ups | Use Case |
1414
|:-----|:----------|:---------|:--------|:---------|:---------|
1515
| `lookup` | Independent | Optional by default | Independent | Supported via `summary` | "Related to" |
16-
| `master_detail` | Coupled (cascade delete) | Forced only under `controlled_by_parent`; else lint-warned | Inherits parent | Supported via `summary` | "Owned by" |
16+
| `master_detail` | Coupled (cascade delete) | Forced only under `controlled_by_parent`; else lint-warned | Inherits parent; owner opt-in `ownerMayEdit` (below) | Supported via `summary` | "Owned by" |
1717
| `tree` | Self-reference | Optional — and so is `reference`, which when written must name this object itself; any other object is refused at parse, not lint-warned | N/A | Not available | Hierarchical |
1818

1919
## When to Use lookup vs master_detail
@@ -92,6 +92,23 @@ export default ObjectSchema.create({
9292
});
9393
```
9494

95+
#### Owner edits under `controlled_by_parent` — `ownerMayEdit`
96+
97+
A `controlled_by_parent` detail inherits the master's access: reads are scoped
98+
to accessible masters and **every write needs edit access on the master**. To
99+
let a detail row's owner (`owner_id`) update that row with only *read* access
100+
to the master, opt in on the detail:
101+
102+
```typescript
103+
sharingModel: 'controlled_by_parent',
104+
sharing: { controlledByParent: { ownerMayEdit: true } }, // by-id update only
105+
```
106+
107+
Nothing else widens — insert, delete, predicate (`multi: true`) updates and
108+
on-behalf-of writes still need master edit; the detail's own row policies
109+
still AND in. Refused at parse under any other `sharingModel` (the owner
110+
already writes there) and one level up (`sharing.ownerMayEdit`).
111+
95112
### Many-to-Many: Junction Object
96113

97114
```typescript

0 commit comments

Comments
 (0)