Blocked-by: #14147
⛔ Ungraded and unrouted — domain:*, priority and type are triage's. Filed unassigned by the domain:engine execution seat.
⚠️ This card exists because the seat owed it and did not file it. The adoption record on #14147 (5548671173) said 「This seat files it」 for finding B10, and it was never filed. The re-review at CONTRACT_REVIEW_TIER (5549250610, escalation E1) checked for it among issues created 2026-09-05T02:00Z→03:50Z, found nothing, and raised it. The tree is not wrong on this; the card is what was missing. It must exist before #14147 closes as 「one semantics, one enforcement point」, because on this population that sentence is not literally true.
The asymmetry, measured at 66c580b0c (PR #15395's head)
CREATE — packages/objectql/src/engine.ts:10259 narrows the subject first:
const readonlySubject = staticReadonlyInsertSubject(schemaForValidation as any);
and packages/objectql/src/validation/rule-validator.ts:1515-1522:
export function staticReadonlyInsertSubject(objectSchema) {
const fields = objectSchema?.fields;
if (!fields) return null;
if (objectSchema?.managedBy) return null; // ← any bucket, any value
if (String(objectSchema?.name ?? '').startsWith('sys_')) return null;
UPDATE — engine.ts:11561 and :11741 pass the schema straight through:
stripReadonlyFields(updateSchema as any, preRo, suppliedValues, this.logger, { preserveAudit: …, strictReadonlyWrites, hookWrittenKeys })
| reading |
value |
staticReadonlyInsertSubject call sites in engine.ts |
1 (:10259, the create path) |
stripReadonlyFields call sites in engine.ts |
3 — :10264 (create, over the narrowed subject) · :11561, :11741 (update, over the raw schema) |
managedBy anywhere near the update strip |
0 — the update path applies no such exclusion |
⇒ For an object with managedBy set, a non-system caller's create of a static readonly column is not stripped, while the same caller's update of the same column is.
Why this is a decision and not a bug report
The exclusion is not new and is not PR #15395's: it was carried over byte-for-byte from the deleted ingress copy's if (!schema || schema.managedBy || sys_), on ADR-0086 / #3004 grounds — those columns have their own 403 guards, and a silent strip must not swallow the payload the guard exists to reject. That reasoning was ruled on its own merits and was never part of the 2026-07-24 「INSERT (all callers) exempt」 row that ruling C superseded. It is also load-bearing today: it is what keeps the metadata repository's sys_metadata_history.recorded_by write working, and that writer is a direct, non-system engine.insert caller (metadata-protocol/src/sys-metadata-repository.ts).
⚠️ What makes it worth a ruling is the bucket list, not the sys_ namespace. The guard fires on managedBy being set at all. Per packages/spec/src/data/object.zod.ts, managedBy: 'system-data' means 「platform-defined schema, admin/user-writable data」 — an object whose data is ordinary user data. So the population that skips the create-side strip is wider than the platform-internals the ADR-0086 argument is about, and it is not obvious that the 403-guard rationale reaches the user-writable buckets at all.
The question
⛔ The seat does not choose. Three shapes, stated so a decision is cheap:
- Leave it. The 403 guards are the enforcement for these objects on both verbs and the strip would only mask them; document the asymmetry where 「one semantics」 is claimed, so the sentence is qualified rather than false.
- Narrow the exclusion to the buckets the ADR-0086 argument actually covers (platform internals and the
sys_ namespace), letting the create-side strip apply to system-data and any other user-writable bucket — which would make create and update symmetric on exactly the population where the data is the user's.
- Make the update path apply the same exclusion, i.e. symmetry in the other direction. ⚠️ Stated for completeness; this loosens a path that enforces today, so it is the shape that needs the most justification.
What is ⛔ NOT being claimed
Re-check
H=66c580b0c # or origin/main once #15395 lands
git show $H:packages/objectql/src/validation/rule-validator.ts | sed -n '1515,1525p'
git show $H:packages/objectql/src/engine.ts | grep -n 'stripReadonlyFields(' # 3 sites: 1 create, 2 update
git show $H:packages/objectql/src/engine.ts | grep -c staticReadonlyInsertSubject # create path only
git grep -n "system-data" -- packages/spec/src/data/object.zod.ts # the bucket's own words
Dedup: ⚠️ /search/issues answers 403 in this container and an empty search is not a reading — repo-scoped REST list of open cards plus a local grep; the re-reviewer independently swept issues created 2026-09-05T02:00Z→03:50Z and found none. Related: #14147 (ruling C — 「one semantics, one enforcement point」) · PR #15395 · contract reviews 5548671173 (B10, where this was owed) and 5549250610 (E1, which caught that it was never filed) · #3004 / ADR-0086 (the carve-out's own grounds) · #3413, #3043 (the superseded architecture).
Blocked-by: #14147
⛔ Ungraded and unrouted —
domain:*, priority and type are triage's. Filed unassigned by thedomain:engineexecution seat.5548671173) said 「This seat files it」 for finding B10, and it was never filed. The re-review atCONTRACT_REVIEW_TIER(5549250610, escalation E1) checked for it among issues created 2026-09-05T02:00Z→03:50Z, found nothing, and raised it. The tree is not wrong on this; the card is what was missing. It must exist before #14147 closes as 「one semantics, one enforcement point」, because on this population that sentence is not literally true.The asymmetry, measured at
66c580b0c(PR #15395's head)CREATE —
packages/objectql/src/engine.ts:10259narrows the subject first:and
packages/objectql/src/validation/rule-validator.ts:1515-1522:UPDATE —
engine.ts:11561and:11741pass the schema straight through:staticReadonlyInsertSubjectcall sites inengine.ts:10259, the create path)stripReadonlyFieldscall sites inengine.ts:10264(create, over the narrowed subject) ·:11561,:11741(update, over the raw schema)managedByanywhere near the update strip⇒ For an object with
managedByset, a non-system caller's create of a staticreadonlycolumn is not stripped, while the same caller's update of the same column is.Why this is a decision and not a bug report
The exclusion is not new and is not PR #15395's: it was carried over byte-for-byte from the deleted ingress copy's
if (!schema || schema.managedBy || sys_), on ADR-0086 / #3004 grounds — those columns have their own 403 guards, and a silent strip must not swallow the payload the guard exists to reject. That reasoning was ruled on its own merits and was never part of the 2026-07-24 「INSERT (all callers) exempt」 row that ruling C superseded. It is also load-bearing today: it is what keeps the metadata repository'ssys_metadata_history.recorded_bywrite working, and that writer is a direct, non-systemengine.insertcaller (metadata-protocol/src/sys-metadata-repository.ts).sys_namespace. The guard fires onmanagedBybeing set at all. Perpackages/spec/src/data/object.zod.ts,managedBy: 'system-data'means 「platform-defined schema, admin/user-writable data」 — an object whose data is ordinary user data. So the population that skips the create-side strip is wider than the platform-internals the ADR-0086 argument is about, and it is not obvious that the 403-guard rationale reaches the user-writable buckets at all.The question
⛔ The seat does not choose. Three shapes, stated so a decision is cheap:
sys_namespace), letting the create-side strip apply tosystem-dataand any other user-writable bucket — which would make create and update symmetric on exactly the population where the data is the user's.What is ⛔ NOT being claimed
readonlyfield is stripped from a non-system INSERT insideengine.insert, and the boundary copy is deleted #15395 is not wrong on this and does not need to change for it — two independent reviews atCONTRACT_REVIEW_TIERupheld the carry-over as correctly scoped outside what ruling C superseded.managedBybucket with a staticreadonlyfield is not measured here — that population count would sharpen the decision and this seat did not take it. Stating it as unmeasured rather than implying zero or many.Re-check
Dedup:⚠️
/search/issuesanswers 403 in this container and an empty search is not a reading — repo-scoped REST list of open cards plus a local grep; the re-reviewer independently swept issues created 2026-09-05T02:00Z→03:50Z and found none. Related: #14147 (ruling C — 「one semantics, one enforcement point」) · PR #15395 · contract reviews5548671173(B10, where this was owed) and5549250610(E1, which caught that it was never filed) · #3004 / ADR-0086 (the carve-out's own grounds) · #3413, #3043 (the superseded architecture).