Repository navigation
Commit 4267484
fix(runtime): AppPlugin registers the forward-converted copy of every collection (#23024)
Fixes #22994
Clause-②: no
Door 2 only, as triage graded it. Door 1 (the conversion window in
`packages/metadata-core`) is #23001, untouched here.
## What changes
`AppPlugin.init()` handed the `manifest` service the RAW bundle. That
service decomposes it into the ObjectQL registry over
`METADATA_ARRAY_KEYS`. The registry is the code layer of the flattened
list that `GET /api/v1/meta/TYPE` serves and that
`getMetaItemsForExecution` hands to the email template loader. On the
artifact boot, the door (`MetadataPlugin`) registered the
forward-converted copy into the metadata service. So the item read
served the converted copy while the list served the raw one.
Now the boot runs ONE ADR-0087 forward conversion (`forwardConverted`,
memoized, `packages/runtime/src/app-plugin.ts`):
- It runs after the D1 handshake (`assertProtocolCompat` is unmoved and
still first).
- Its input is `{ ...manifest, ...collections }`, the definition the
security block has converted since the #12844 work. For a bundle without
`packages[]`, that is byte for byte the payload `init()` always
registered.
- Both registrars take its result: `manifest.register(...)` in `init()`,
and the ADR-0057 security block in `start()`, which no longer converts
on its own. No reader converts again: `readFlattenedMetaItems` and the
email loader are untouched.
Why this input and not `this.bundle`:
- **Converting before the spread would turn the #18034 pin red.** The
`manifest-permissions-string-list-removed` entry would strip
`manifest.permissions` before the registrar sees it. The "2 of 2"
warning would then describe a value that never arrived.
- **The #18034 source decision still reads the RAW inputs.**
`fromManifest` reads `this.collections.manifest` and
`this.collections[field]`, unchanged, because the conversion can rewrite
a collection key (`roles` to `positions`).
- **The artifact-boot branch of the security block is unchanged.** Under
`securityMetadataRegistrar: 'artifact-door'` it registers nothing and
logs the same debug line.
- **A bundle with `packages[]` is handed over as authored, as before.**
Resolving `packages[]` in `init()` is reserved to the `manifest` service
by the posture the plugin-dev #15292 falsifier pins (4/4 green against
the rebuilt `dist`). Also, no conversion reaches package bodies, at the
artifact door either (measured, see Acceptance notes).
## Measured, before and after
The card's door: `os dev -a dist/objectstack.json --fresh` on an
undeclared-engines artifact (`objectstack-ai/objectstack` at BASE
`6a7eca6868`, then at this branch), read over HTTP.
**The second type is `dashboard`, converted by the major-18 retired
entry `dashboard-refresh-interval-to-refresh-interval-seconds`**
(retiredAfter 17.3.0).
| read | before | after |
|---|---|---|
| `GET /api/v1/meta/dashboard`, ops_board | `refreshInterval: 300`,
`_diagnostics.valid: false` (the tombstone's rename prescription) |
`refreshIntervalSeconds: 300`, `valid: true` |
| `GET /api/v1/meta/dashboard/ops_board` | `refreshIntervalSeconds:
300`, `valid: true` | `refreshIntervalSeconds: 300`, `valid: true` (the
same document as the list) |
| `GET /api/v1/meta/email_template`, probe.welcome | carries `category`
/ `isSystem`, `valid: true` | unchanged |
| `GET /api/v1/meta/email_template/probe.welcome` | the door's parsed
copy, `valid: true` | unchanged |
On this tree `category` / `isSystem` are live keys: their retirement is
#22990, which is parked on this card. So the email half was measured on
#22990's head `e4e96505e0` with and without this patch, in a scratch
tree, through the real `createRegistryEmailTemplateLoader` over the
booted kernel's protocol service:
- **Without the patch:**
- the list carries `category: custom`, `isSystem: false` with
`_diagnostics.valid: false`;
- the item read is clean;
- the loader refuses: "email template 'probe.welcome' (en-US) is not a
valid email template, so it cannot be sent: `email_template.category`
was removed…".
- **With the patch:**
- the list is valid with both keys gone;
- the loader sends `{ name: probe.welcome, locale: en-US, subject:
Welcome, body_html, active: true }`.
The list and the item still differ on the PARSE axis: the door's copy
carries the email schema's `variables: []` / `active: true` defaults.
That is route ownership (#12892), not this conversion. The pins assert
"every key the list serves equals the item's", never "one document",
except for the dashboard, where the two really are one document.
## Tests
`packages/runtime/src/app-plugin-converted-collections.test.ts`:
- **A real `createStandaloneStack` boot over the card's artifact.** Four
checks:
- the dashboard list equals the item read, `valid: true`;
- the dashboard execution face equals the conversion's own output;
- every key of the email list equals the item read's, `valid: true`;
- the email execution face equals the conversion's output and passes the
loader's own predicate,
`EmailTemplateDefinitionSchema.safeParse(stripReadDecorations(item))`.
- **A control boot of the same app in current spellings:** the registry
holds the authored items plus stamps, nothing rewritten.
- **Three unit legs:**
- one conversion: the security registrar registers the very object the
manifest payload carries (identity, not equality);
- a current bundle is handed over with its collection arrays by
reference;
- a `packages[]` bundle is handed over as authored.
The email legs are positive controls on this tree. **Measured on
#22990's head:** this file is 8/8 green with the patch and 5 red without
it, both email legs included. So they become live pins the day that
retirement lands, with no edit.
Ablation of the one conversion line in `init()`, run through
`scripts/ablation-replace.mjs`:
- **Mutation landed:** anchor 1 to 0, blob `39d6bb3b6958` to
`891daa76d2b3`.
- **The predicted three went red:**
- the dashboard list pin ("expected undefined to be 300");
- the dashboard execution-face pin;
- the one-conversion unit leg.
- **The other 18 stayed green,** including every #14491 pin (the BASE
behaviour), at `86d2782f09`, before the patch round rewrote five of
those pins.
- **Restore proven:** blob equals HEAD, and `git diff HEAD` is empty.
- **No rebuild was needed:** `app-plugin.ts` reaches the runtime suite
by relative source import. The before and after probe differed without
one.
All on `86d2782f09`:
- **`pnpm --filter @objectstack/runtime test`:** at `0f6556d1c5`, 357 of
357 files and 5141 tests pass, 19 skipped.
- **`pnpm --filter @objectstack/runtime typecheck`:** exit 0; the test
layer held its debt ledger. `tsc --listFiles` compiles the new file with
0 errors in it.
- **Untouched pins, green:**
- #12844 / #12892: `app-plugin-artifact-forward-conversion.test.ts`,
`standalone-stack-security-registrar.test.ts`;
- #18034: `app-plugin.manifest-security-collision.test.ts`.
- **Gates:** all 63 derived by `dispatch-gates.mjs` exit 0.
`check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit
3) and was measured green after building the 10 missing `dist`s. `--ran`
reconciliation: "63 derived famil(ies) accounted for — 63 run, 0
NOT-MEASURED (a DERIVED zero …)".
- **Lint:** narrowed to the two touched sources, with the three proofs:
- `--print-config` puts both in eslint's population;
- `--format json` reports 2 files, 0 errors, 0 warnings;
- no config object enables type-aware linting (every `parserOptions` is
`{ ecmaVersion, sourceType }`), and the four custom rules are AST-local,
so no untouched file's verdict can move.
## #14491's measurement: five conversion-axis pins rewritten (seat
ruling A, `6122651929` → claim `6121395708` widened)
`packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts`
now pins the converged conversion axis. None of the five pins is
skipped, relaxed or deleted; each keeps its subject and asserts the
measured converged values (commit `0f6556d1c5`):
1. permission `probe_agent`: the registry grant map and RLS entry carry
no `allowRestore` / `allowPurge` / `priority`. The three door defaults
are still absent (the parse axis).
2. The stored `sys_permission_set` RLS equals the door's.
3. `share_legacy_deals` carries `sharedWith.type: position` and
`accessLevel: edit` in the registry, equal to the door's.
4. `share_legacy_level` carries `accessLevel: edit` in the registry.
5. **An authorization outcome.** `share_legacy_deals` is now SEEDED,
exactly once, as a position share: `recipient_type: position`,
`recipient_id: sales_mgr`, `access_level: edit`. That gives three rows
where there were two. Before this PR it was dropped at seeding (`role`
is refused by `mapRecipientType`). The changeset says so.
The file's header now records that #22994 closed the conversion axis,
while the parse axis (string `condition`, and the missing `scope` /
`isDefault` / `active` defaults) stays #14491's. Those pins are
unchanged.
Ablating the `init()` conversion line turns exactly the five rewritten
pins red against the raw registry copy (`5 failed | 8 passed`). The
restore is proven against HEAD.
Open PR #22962 (#15204 stage 8) edits the same file and deletes pin 2;
whichever lands second merges `main`. The seat declared this on #15204.
## Acceptance notes (not this PR)
- **The conversion layer does not reach option-B package bodies.**
Measured: the artifact door (`_parseAndRegisterArtifact`) refuses an
artifact whose only dashboard sits in `packages[0].manifest.dashboards`
with `refreshInterval`, with "invalid_type at
packages.0.manifest.dashboards.0.refreshInterval". The conversion walks
a definition's own collections. Same family as #23001, for the seat to
route.
- **Readers of the raw `this.bundle` / `this.collections` after the
seam, answered for assumption 5.** Each reader takes one of three
answers: it needs the converted copy, it keeps the raw one, or it does
not care.
- **Datasources** (`codeDefinedDatasourceOwners`, the in-memory
`datasource` registration, `connectDeclared`): raw. The drivers read
`config.timeoutMs` / `persistence.autoSaveIntervalMs`, so a datasource
still carrying the retired `config.timeout` (turso) or
`autoSaveInterval` (memory) loses the value at connect.
- **Jobs** (`scheduleAppArtifactJobs(ctx, this.bundle)`): raw. The
binder reads `job.timeoutMs`, so a retired `jobs[].timeout` schedules
with no timeout.
- These two are retired keys a consumer reads. Reported, not converted
here, as the dispatch ruled.
- **Does not care:**
- `datasourceMapping`, objects passed to `connectDeclared` (read for
`name` / `datasource` only), seed `data`: no conversion targets what
they read;
- hooks: the binder reads no `timeout` / `timeoutMs`;
- `emitCatalogEvent`, `grantedPermissions`: no conversion touches them.
- **Translations** (`loadTranslations`): NOT MEASURED.
- **Residual of a multi-package config stack**
(`registerUnclaimedTopLevel`): raw, same class.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0ede0f8 commit 4267484
4 files changed
Lines changed: 514 additions & 54 deletions
File tree
- .changeset
- packages/runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
0 commit comments