Observation surfaced while implementing #8147 (PR #8200). Filing rather than fixing: it is out of that card's scope, and its right disposition may depend on the import ruling #8147 is waiting on.
The measurement
validateRecord skips readonly and system fields outright on both the insert and the update branch (packages/objectql/src/validation/record-validator.ts, insert branch):
if (SKIP_FIELDS.has(name)) continue;
if (def.system || def.readonly) continue;
The invalid_option check that enforces a select field's declared options therefore never runs for such a field.
sys_audit_log declares every field readonly: true (its own docblock says so: "Every field is readonly: true — audit logs are written only by internal system hooks"). So sys_audit_log.action declares an eight-value vocabulary that nothing anywhere validates. engine.insert('sys_audit_log', { action: 'anything_at_all' }) succeeds.
Why it is worth recording
The skip itself is defensible — the engine owns readonly columns, and validating what the engine just wrote is mostly redundant. What is not obvious, and what cost #8147 a full measurement pass to establish, is the consequence:
On a readonly field, a declared option set is documentation, not a contract. There is no mechanical detector of divergence between the declared vocabulary and what writers actually write, in either direction:
For an ordinary readonly column (created_at, a computed total) this is harmless. For a compliance surface whose whole product value is that the vocabulary is true, it means the declaration cannot be trusted without a manual writer census every time it changes.
Not proposing the fix here
Several directions exist and they are not equivalent — enforce options even for readonly fields (blast radius: every system object, every engine write path); a build-time census that cross-checks declared action vocabularies against literal writer sites; or accept it and rely on pin tests per object (what #8147 did — packages/plugins/plugin-audit/src/objects/sys-audit-log-retired-actions.test.ts). Triage should pick.
Refs
Generated by Claude Code
Observation surfaced while implementing #8147 (PR #8200). Filing rather than fixing: it is out of that card's scope, and its right disposition may depend on the
importruling #8147 is waiting on.The measurement
validateRecordskipsreadonlyandsystemfields outright on both the insert and the update branch (packages/objectql/src/validation/record-validator.ts, insert branch):The
invalid_optioncheck that enforces aselectfield's declared options therefore never runs for such a field.sys_audit_logdeclares every fieldreadonly: true(its own docblock says so: "Every field isreadonly: true— audit logs are written only by internal system hooks"). Sosys_audit_log.actiondeclares an eight-value vocabulary that nothing anywhere validates.engine.insert('sys_audit_log', { action: 'anything_at_all' })succeeds.Why it is worth recording
The skip itself is defensible — the engine owns readonly columns, and validating what the engine just wrote is mostly redundant. What is not obvious, and what cost #8147 a full measurement pass to establish, is the consequence:
On a readonly field, a declared option set is documentation, not a contract. There is no mechanical detector of divergence between the declared vocabulary and what writers actually write, in either direction:
export/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 found. Retiringimportthere would have made the schema deny a valueplugin-auth's admin user-import writes on every run, and no gate, test or runtime check would have gone red. It was caught by reading the writer, not by any instrument.For an ordinary readonly column (
created_at, a computed total) this is harmless. For a compliance surface whose whole product value is that the vocabulary is true, it means the declaration cannot be trusted without a manual writer census every time it changes.Not proposing the fix here
Several directions exist and they are not equivalent — enforce options even for readonly fields (blast radius: every system object, every engine write path); a build-time census that cross-checks declared action vocabularies against literal writer sites; or accept it and rely on pin tests per object (what #8147 did —
packages/plugins/plugin-audit/src/objects/sys-audit-log-retired-actions.test.ts). Triage should pick.Refs
export/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 / PR refactor(plugin-audit)!: retireexport/permission_changefrom the sys_audit_log action enum (#8147) #8200 — where this was measured; carries the writer census forsys_audit_logpackages/objectql/src/validation/record-validator.ts— the skippackages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts— the unenforceable enumGenerated by Claude Code