Skip to content

Commit efed204

Browse files
committed
Merge origin/main into issue-3355 managedBy retirement
Two conflicts, both in the protocol 17 narrative where concurrent retirement PRs each appended a paragraph: - packages/spec/src/migrations/registry.ts — #4509 (unwarnable mapping / contextSelector keys) landed on main with a "Finally, ..." paragraph while this branch had one of its own. Both retirements are real and both belong in the v17 story, so both paragraphs are kept: the managedBy rename reworded to "Separately, ..." and placed first, with #4509 keeping the closing position. No content dropped from either side. conversionIds merged cleanly and carries both sets. - docs/protocol-upgrade-guide.md — generated from the registry above, so it was regenerated rather than hand-reconciled. Verified after the merge: 69/69 turbo test+typecheck tasks across the 8 affected packages, and all 17 spec check gates.
2 parents 77f29a4 + 5966c2a commit efed204

23 files changed

Lines changed: 595 additions & 142 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): declare `doc.tags`, so a book group's `include: { tag }` can finally match something (#4509)
6+
7+
`BookGroup.include` has always accepted two shapes — a glob over doc names, or
8+
`{ tag: '<t>' }`. The tag variant could never match a single doc in any stack,
9+
and not because the matcher was missing. Everything downstream already existed:
10+
11+
- `matchesInclude` compares `doc.tags` against the rule (`book.zod.ts`)
12+
- the book route already forwards `tags: d.tags` into the resolver (`rest-server.ts`)
13+
- `ResolverDoc` already declares `tags?: string[]` — annotated `(P3d; absent today)`
14+
15+
The gap was one line at the *authoring* end: `DocSchema` is `.strict()` and had
16+
no `tags` key, so writing `tags:` on a doc was a parse error. Every doc therefore
17+
reached the resolver with `tags === undefined`, and the variant matched nothing,
18+
forever.
19+
20+
This is the enforce half of ADR-0049 enforce-or-remove. Removal was the
21+
alternative and was rejected on two grounds: a union member has no clean
22+
tombstone (`retiredKey` covers object keys), so authors would have received a
23+
bare union error carrying no prescription — and it would have discarded a
24+
working matcher to fix a declaration.
25+
26+
```ts
27+
defineDoc({ name: 'crm_guide_lead', content: '# Leads', tags: ['tutorial'] })
28+
defineBook({ name: 'crm', groups: [{ key: 'tut', label: 'Tutorials', include: { tag: 'tutorial' } }] })
29+
```
30+
31+
Prefer a name convention (`include: 'crm_guide_*'`) where one exists — tags earn
32+
their place when membership cuts *across* naming, e.g. a `tutorial` tag spanning
33+
several feature prefixes, which no glob can collect.
34+
35+
Additive: `DocSchema` previously rejected `tags`, so nothing that parsed before
36+
parses differently now.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: retire the five keys the advisory lint could never have warned about — mapping `extractQuery`/`errorPolicy`/`batchSize`, contextSelector `includeAll`/`placement` (#4509)
7+
8+
Five authorable keys parsed, stored, and controlled nothing. What groups them is
9+
not the type they sit on but **why they had to go out in a major rather than
10+
after a deprecation cycle**: four of the five carry schema DEFAULTS, and a
11+
default materialises at parse time — so the liveness advisory lint cannot tell a
12+
value the author wrote from one the schema supplied. Marking them would have
13+
warned on every mapping and every selector in existence, which is why the ledger
14+
recorded them as `_authorWarnSkipped` instead. For a key in that state, removal
15+
is not the escalation after a warning. It is the only channel that ever reaches
16+
the author.
17+
18+
**The retirement kit:**
19+
20+
| FROM | TO | Fix |
21+
|---|---|---|
22+
| `mapping.extractQuery` | *(removed)* | Delete the key. Exports run through the ordinary query API (`POST /api/v1/data/:object/query`) — no exporter has ever read a mapping artifact. |
23+
| `mapping.errorPolicy` | *(removed)* | Delete the key. Error handling on the import path belongs to the import REQUEST's own options, not the stored mapping. |
24+
| `mapping.batchSize` | *(removed)* | Delete the key. The write path sizes its own batches. **Do not relocate the value** — see below. |
25+
| `app.contextSelectors[].includeAll` | *(removed)* | Delete the key. Selectors are mandatory-scope; widen `optionsSource.filter` to widen the choices. |
26+
| `app.contextSelectors[].placement` | *(removed)* | Delete the key. Selectors always render in the sidebar header; `'topbar'` placed nothing. |
27+
28+
Run `os migrate meta --from 16` to rewrite existing sources automatically.
29+
30+
**`includeAll` is the one worth reading twice.** It was not unread — it was
31+
deliberately *disobeyed*, and for a security reason. A context selector is a
32+
mandatory scope, so an "All" row would clear the scope on a surface that exists
33+
to be scoped; on Studio's package selector that means listing the platform's own
34+
system/cloud kernel packages to a developer who scoped to their own package. The
35+
renderer never offered an All row regardless of the flag, so `includeAll: false`
36+
hardened nothing and `includeAll: true` unlocked nothing. `STUDIO_APP` shipped
37+
authoring `includeAll: true` against a renderer that ignored it — that authoring
38+
site goes with the key in this change.
39+
40+
**`batchSize` deliberately offers no rename.** `bulkActionDef.batchSize`,
41+
`connector.batchSize`, `sync.batchSize`, `offline.batchSize`, the seed loader's
42+
and the NoSQL driver cursor's are all LIVE and enforced — but each is a
43+
different key on a different type sizing its own path, and none of them sizes a
44+
mapping import. The rejection says so explicitly, because "removed" plus a
45+
familiar name one line away is exactly how a dead setting gets laundered into a
46+
live-looking one. Same trap `datasource.retryPolicy` had to defuse against
47+
`hook`/`job` `retryPolicy` (which spell the delay `backoffMs`) one issue
48+
earlier.
49+
50+
Both schemas are `.strict()`, so the keys are deleted from the shape and
51+
rejected with a `guidance` prescription rather than tombstoned; their liveness
52+
rows are deleted rather than kept. The retired ALIAS spellings (`query`,
53+
`onError`, `errorHandling`, `errorMode`, `batch`, `chunkSize`, `skipErrors`,
54+
`showall`, `location`) route to the same prescriptions instead of suggesting a
55+
rename onto a key that is also gone.
56+
57+
Registered as the ADR-0087 D2 conversion `mapping-inert-keys-removed` and an
58+
extension of `app-dead-authoring-keys-removed`, both wired into the protocol-17
59+
D3 chain step. The mapping conversion is scoped to the `mappings` collection
60+
deliberately — a stack-wide strip would delete an enforced `batchSize` from
61+
connector, sync, bulk-action and offline shapes.
62+
63+
`datasource` reached zero dead keys in #4583; `mapping` reaches zero here.

content/docs/references/data/mapping.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ const result = FieldMappingSchema.parse(data);
6060
| **fieldMapping** | `{ source: string \| string[]; target: string \| string[]; transform: Enum<'none' \| 'constant' \| 'lookup' \| 'split' \| 'join' \| 'javascript' \| 'map'>; params?: object }[]` || |
6161
| **mode** | `Enum<'insert' \| 'update' \| 'upsert'>` || |
6262
| **upsertKey** | `string[]` | optional | Fields to match for upsert (e.g. email) |
63-
| **extractQuery** | `{ object: string; fields?: string[]; where?: any; search?: string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }; … }` | optional | Query to run for export only |
64-
| **errorPolicy** | `Enum<'skip' \| 'abort' \| 'retry'>` || |
65-
| **batchSize** | `number` || |
6663
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
6764
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
6865
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

content/docs/references/system/doc.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const result = DocSchema.parse(data);
6767
| **content** | `string` || Raw Markdown content (CommonMark + GFM) |
6868
| **order** | `number` | optional | Sort key within a book group (ADR-0046 §6) |
6969
| **group** | `string` | optional | Explicit book-group key (ADR-0046 §6); rules usually suffice |
70+
| **tags** | `string[]` | optional | Membership tags matched by a book group's `include: { tag }` rule (ADR-0046 §5) |
7071
| **translations** | `Record<string, { label?: string; description?: string; content: string }>` | optional | Per-locale `{label?,description?,content}` variants; the base doc is the fallback |
7172
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
7273
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

content/docs/references/ui/app.mdx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,8 @@ const result = ActionNavItemSchema.parse(data);
128128
| **label** | `string` || Dropdown label |
129129
| **icon** | `string` | optional | Icon name |
130130
| **optionsSource** | `{ endpoint: string; valueKey: string; labelKey: string; filter?: { key: string; op: Enum<'eq' \| 'ne' \| 'in' \| 'nin'>; value: string \| string[] }[] }` || Option data source |
131-
| **includeAll** | `boolean` || Prepend an "All" option that clears the scope |
132-
| **allValue** | `string` || Template value when "All" is selected (empty = no filter) |
131+
| **allValue** | `string` || Sentinel value meaning "no concrete selection yet" (empty string is almost always right) |
133132
| **persist** | `Enum<'query' \| 'session' \| 'none'>` || Persist selection via URL query, sessionStorage, or not at all |
134-
| **placement** | `Enum<'sidebar_header' \| 'topbar'>` || Render location in the app chrome |
135133

136134

137135
---

0 commit comments

Comments
 (0)