Skip to content

[观察] A select field's option enum is unenforceable once the field is readonlysys_audit_log.action declares a vocabulary nothing can check #8203

Description

@os-zhuang

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions