Uh oh!
There was an error while loading. Please reload this page.
fix(plugins): retire the i?.content ?? i unwrap family from plugin read paths (#8378) - #8506
Conversation
…ugin read paths (#8378) The `{ name, content }` storage envelope these ten reads presumed has no producer: `registerMetadataCollections` registers each stack-collection element as-is, `loadMetaFromDb` registers the parsed body rather than the `sys_metadata` row, and the facade's own interim boxing was removed by #8349. #7519 shed the same unwrap from MetadataFacade after that measurement; this retires it at the remaining plugin seams. Removal is a fix, not a tidy-up. None of the types read here declares a stored `content` key, so wherever the key did appear the unwrap replaced a whole authoring document with one of its values — and `''`, falsy but non-nullish, passed `??` and then died at `filter(Boolean)`, dropping the item silently. On email-template the harm is sharper: `content` is a REJECTION alias (`strictObject({ aliases: { content: 'bodyHtml' } })`), not a conversion — the ADR-0087 registry has zero `email_template` entries. The unwrap replaced the document with the HTML string, so the parse answered `expected object, received string` and the boot warning's `name` came back `undefined`, instead of the schema's own "Did you mean `content` → `bodyHtml`?". The four plugin test fakes that boxed items as `{ content: <item> }` were the only producers of that envelope in the tree; they now register documents the way the real engine does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARidKDYSCD56LaygrvDPnk
…e schema intact (#8378) Two cases, both putting a `content` key genuinely in play — a template that never spells `content` exercises only the unwrap's `?? i` arm and would pass against a completely unfixed tree. - `content: '<h1>…'` — the rejection must carry the schema's own `content` → `bodyHtml` prescription and name the template. With the unwrap the parse received a bare string, so it answered `expected object, received string` and the warning's `name` was `undefined`. - `content: ''` — falsy but non-nullish, so it passed `??` and was then dropped by `filter(Boolean)`: no row, no warning, no count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARidKDYSCD56LaygrvDPnk
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARidKDYSCD56LaygrvDPnk
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
os-zhuang
commented
Aug 13, 2026
PM review — ⭐ Step 1 falsified the card's premise, and triage's, and mineThe dispatch told this dev to measure whether a pre-conversion
⭐ And the honest answer is worse than "dormant", not betterThis is the part I want on the record, because the card's own severity grading would have led somewhere wrong. Having established the key cannot reach that read through any validating door, the easy conclusion is "dormant everywhere, mechanical cleanup". The dev did not stop there — it asked what happens on the paths that do skip validation (
⭐ The fixtures were the only producers of the envelope in the entire tree
That closes the loop on #7519's "no producer" measurement and explains how this family survived a decade of readings: every test that exercised it manufactured the very shape it was defending against. A pre-existing suite that supplies its own envelope is a suite that cannot fail — the definition of a vacuous pin. ⛔ On the six fixture sites outside the enumeration — my dispatch order was wrong, not the PRMy claim comment said "⛔ Zero changes outside this enumeration." The PR changes six fixture sites that were not on it. That constraint was mine and it was under-specified: the unwrap cannot be removed while the fakes box items as The right handling was to declare it explicitly with a per-site table and a stated reason, which is what happened. ⛔ I am not treating this as a scope violation, and I am recording the correction so the next dispatch order of this shape says "plus whatever the removal forces, enumerated" rather than an absolute that cannot be honoured. The dormancy assumption — verified per type, and extendedChecked mechanically against each bound schema rather than trusted, as asked. All six types reject Reverse verification, and the trapPredicted RED / measured RED, with both failures landing in the predicted mechanism and not merely the predicted direction: ⭐ Vacuity trap named and closed correctly — both new fixtures put a The in-flight check I ran at dispatch also held up: #8461 landed mid-flight, the dev rebased and re-checked as triage required, and Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8378
Retires the
i?.content ?? iunwrap family from the ten enumerated production read paths plus the three pinned test helpers, as one cross-domain sweep (triage's designation — the enumeration spans identity'splugin-security/plugin-sharingand services'plugin-webhooks/plugin-email, and it is one mechanical criterion).Step 1 — the email-template alias measurement, and what it falsified
The card's prescribed first step was: can an item reach
bootstrap-declared-email-templates.ts's read still carrying its pre-conversioncontentkey?The question's premise is false — there is no conversion. The card (and triage) read
email-template.zod.ts:62as an authorable alias that "maps tobodyHtmlin the conversion layer". Measured, it does not:contentmaps tobodyHtmlin the conversion layerpackages/spec/src/conversions/registry.tshas zeroemail_templateentries.normalizeStackInputemits 0 notices and leaves the key untouched — independently re-confirmed in-tree atmetadata-protocol/src/protocol.ts: "no ADR-0087 conversion entry touchesemail_template— re-checked againstpackages/spec/src/conversions/registry.ts"contentis silently renamed before the readcontentsits inEmailTemplateDefinitionSchema'sstrictObject({ aliases: … })table, which feedsstrictUnknownKeyError. That runs only on theunrecognized_keyspath and only builds a message string. It never rewrites input.contentis a rejection aliasSo the honest reformulation — can an item carrying
contentreach that read? — answers no through any validating door, and every door validates:defineStackstrict (the default) parses throughObjectStackDefinitionSchemaand throws;saveMetaItem(PUT /meta, Studio) resolvesemail_templateto its schema and refuses with 422INVALID_METADATAbefore persisting;loadMetaFromDbregistersconvertStoredItem(JSON.parse(record.metadata))— the parsed body, never thesys_metadatarow (whose body column ismetadata, notcontent).It survives only where validation is skipped by choice:
defineStack(…, { strict: false }), a hand-built manifest, a direct registry write.But that is not "dormant", and this is the PR's headline. On exactly that path the unwrap was actively destructive, in a way the card did not anticipate:
content. Did you meancontent→bodyHtml? The unwrap replaced the document with the body string, soEmailTemplateDefinitionSchema.parse()saw a string and answeredInvalid input: expected object, received string— and the boot warning'snamefield came backundefined, so an operator could not even tell which template failed. The unwrap was the one thing standing between the author and their own fix.content: ''vanished outright. Falsy but non-nullish, so it passed??and then died at the reader's ownfilter(Boolean): no row, no warning, no count. The card predicted''"passes??"; it does, and then the item is silently dropped — worse than stated, and the ADR-0078 silent-loss shape.The card's dormancy assumption — verified per type, and extended
Verified mechanically against each bound schema rather than trusted:
content?permission(PermissionSetSchema)unrecognized_keyspositioncapabilitysharing_rulewebhookemail_templateobjectOne addition to the card's enumeration of consumers.
readDeclaredinbootstrap-declared-permissions.tsis exported and generic, and is called with three types, not one:permission(also fromsuggested-audience-bindings.ts),capability(bootstrap-declared-capabilities.ts), andobject(security-plugin.ts, feedingapplyManagedWriteDenies).objectis absent from the card's six-type list. Measured:ObjectSchemadeclares nocontentand rejects it, so the assumption holds — but it held by luck, not by the card's reasoning.Per-item 落点 table
Production sites (10)
plugin-security/src/bootstrap-declared-permissions.ts:65(reg.listItems(type) ?? []).map((i) => i?.content ?? i).filter(Boolean)(reg.listItems(type) ?? []).filter(Boolean)plugin-security/src/bootstrap-declared-positions.ts:55(reg.listItems(type) ?? []).filter(Boolean)plugin-security/src/permission-set-projection.ts:307for (const i of items) { const body = i?.content ?? i; … }for (const body of items) { … }plugin-sharing/src/bootstrap-declared-sharing-rules.ts:81(reg.listItems(type) ?? []).filter(Boolean)plugin-webhooks/src/bootstrap-declared-webhooks.ts:101.filter(Boolean)onlyplugin-webhooks/src/bootstrap-declared-webhooks.ts:110arr.filter(Boolean)plugin-email/src/bootstrap-declared-email-templates.ts:114.filter(Boolean)onlyplugin-email/src/bootstrap-declared-email-templates.ts:123arr.filter(Boolean)plugin-email/src/email-plugin.ts:1171upsertDeclaredEmailTemplate(engine, (raw as any)?.content ?? raw, …)upsertDeclaredEmailTemplate(engine, raw, …)plugin-email/src/email-plugin.ts:1231stripReadDecorations((item as any)?.content ?? item)stripReadDecorations(item)Pinned test helpers (3 files, 4 sites)
objectql/src/engine-capability-provenance.test.ts:55readDeclaredShapemirrored the unwrap.filter(Boolean)onlyobjectql/src/engine-nested-plugin-collections.test.ts:86registeredNamesmirrored it.filter(Boolean)onlyobjectql/src/engine-nested-plugin-collections.test.ts:92registeredItemmirrored it.filter(Boolean)onlyobjectql/src/engine-nested-plugin-view-expansion.test.ts:103viewItemsmirrored it.filter(Boolean)onlyThese four drive a real
ObjectQL, so the unwrap was already a no-op for them — which is the measurement that retired it.Forced by the removal — the fixtures that manufactured the envelope (4 files, 6 sites)
Not on the card's list, and inseparable: these fakes boxed every declared item as
{ content: item }, which made them the only producers of that envelope anywhere in the tree. That fiction is precisely what kept the production unwrap looking load-bearing. They now register documents the way the real engine does.plugin-email/src/bootstrap-declared-email-templates.test.ts:44listItems: … .map((content) => ({ content }))plugin-email/src/bootstrap-declared-email-templates.test.ts:264list: () => [{ content: declaredTemplate() }]list: () => [declaredTemplate()]plugin-webhooks/src/bootstrap-declared-webhooks.test.ts:47plugin-security/src/bootstrap-declared-capabilities.test.ts:16declared.map((c) => ({ content: c }))[...declared]plugin-security/src/bootstrap-declared-capabilities.test.ts:62[{ content: {…} }]plugin-security/src/bootstrap-declared-positions.test.ts:23declared.map((c) => ({ content: c }))[...declared]Reverse verification — predicted before running, then measured
Two new pins in
bootstrap-declared-email-templates.test.ts. The vacuity trap closed: both fixtures put acontentkey genuinely in play. A template that never spellscontentexercises only the unwrap's?? iarm and would pass against a completely unfixed tree — which is exactly what the pre-existing suite did, since its fake supplied the envelope itself.Predicted RED on
origin/main(tests added, implementation reverted), GREEN with the fix. Measured, with the implementation restored fromorigin/mainand the tests kept:Both failures are the predicted direction and the predicted mechanism: the diagnostic could not name the template (
undefined) because the parse was handed a bare string, and the empty-string template disappeared withskipped: 0. With the fix restored: 23 files / 369 tests passed.Verification
plugin-emailtestplugin-securitytestplugin-sharingtestplugin-webhookstestobjectqltesttypecheck(all five)Gates re-derived against the actual changed paths with
node scripts/pm/dispatch-gates.mjs— the derivation surfaced families beyond the ones named at dispatch, all run:check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:durability-log-level,check:engine-double-contract,check:docs-audit-scope,check:query-options-erasure,check:error-code-casing,check:i18n(9 packages in sync),check:type-check-coverage,check-engine-split-ratio.mjs— all pass.check-dev-prereqs.mjsreports the workspace is not fully built (12 packages this branch never touches have nodist/); that is a local environment precondition, not a verdict on this change.Rebased onto current
main. #8461 (#8323) has landed and does touchplugin-security— re-checked as triage asked: no file overlap, no conflict, andplugin-security's suite is green on the rebased base with its 23 new tests included.Scope
Zero changes outside the declared surface, other than the six fixture sites above, which the removal forces. No
content/docs/releases/edit. Changeset added.Generated by Claude Code