Skip to content

docs(data-modeling): field-types and validation-rules describe expression under formula only — once PR #24673 lands, a stored field's declared expression is computed on every write and the hand-written pages never say so #24716

Closed

Description

@objectstack-fleet

Blocked-by: #24673

Filing gate ①, class (a): a shipped-doc gap with a named producer. Filed by domain:devx seat 1 (os-warren, session_01FM29ZqLTrnPNXxijioZwMp, seat post #6023), from domain:engine seat 1's addendum 6280927836 on #6023. The addendum offers it to this lane: "A gap for domain:devx, not this PR's … a docs follow-up for that lane to take up or not". ⛔ Not a claim. The lane is self-labelled under rule B; grade and area:* are triage's.

Measured (origin/main after 73d4cc7391)

Wanted (after PR #24673 merges, on its landed describe)

Why it is worth a card

An author reading only these pages, an authoring agent included, learns that expression belongs to formula. They then either never use it on a stored field or are surprised by a refused predicate update. Declared-but-undocumented behaviour on an authoring surface is the AI-error axis: the contract tightens, and the docs should say so.

Dedupe

The engine seat's declaration and addendum (6279265790, 6280927836 on #6023) are the only mentions. A search for "stored field expression computed on write docs field-types validation-rules" finds no card.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 15 · unblocked by PR #24673 merging (c0803b04d2); runs beside objectstack-ai/objectui#13108 under the maintainer's 「并发2」
    Session: session_01FM29ZqLTrnPNXxijioZwMp
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-24716-stored-field-expression-docs
    Worktree: objectstack-issue-24716
    Domain: domain:devx
    Seat: domain:devx#1
    File surface: content/docs/data-modeling/field-types.mdx and content/docs/data-modeling/validation-rules.mdx. Each gets a stored-field expression meaning in the landed describe's terms (packages/spec/src/data/field.zod.ts expression), with a link to the generated references/data/field.mdx row. Stop on breach and explain in the report. ⛔ No package code, and no generated page (references/** is gen:docs output).
    Container & model: S (two docs passages), mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate)
    Clause-②: no
    Responsibility: two hand-written data-modeling pages describe expression under formula only, while a stored field's expression is now computed on every write (PR #24673) | none: the generated reference carries the meaning | every author reading the data-modeling guide, authoring agents included
    Thread-read: none
    Serial constraints cleared: no open PR touches the two pages (read at 2026-10-20T02:37Z). PR #24673 merged at 2026-10-20T02:14:19Z. objectstack-ai/objectui#13108 (in flight) is objectui-only.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24716,
      "status": "done",
      "branch": "claude/issue-24716-stored-field-expression-docs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24734",
      "session": "session_01FM29ZqLTrnPNXxijioZwMp — the parent PM seat's id (mode:subagent); the relay envelope carried the same id",
      "premise_still_valid": true,
      "summary": "Identity: the newest Claim (6282040610) names branch claude/issue-24716-stored-field-expression-docs, which matches. No second claim was posted, the card's assignee was not written, and the PR assignee is os-warren. BASE ec2694dc194c27357deb2f0a26fda0d1b9e684e1; the FieldSchema.expression describe (field.zod.ts:1840) is byte-identical to the dispatch's d20bf9b09b reading. Files changed: content/docs/data-modeling/field-types.mdx (+9 lines: a short subsection, `expression` on a stored field, under `### formula`, with one Field.currency example) and content/docs/data-modeling/validation-rules.mdx (+2 lines: one paragraph under its `formula` section). Total +11/-0, docs only, one commit 9dc3aba0af. PASSAGE field-types BEFORE: the only expression text is the formula row at :411 ('CEL calculation expression') and the example at :415. AFTER (:418-:425): '`expression` is not only a `formula` key. On a field of any other type, the engine computes it from the row each write stores and stores the result in the field's column, so a summary can roll the column up. It is computed on every insert and every update by id (the stored row merged with the patch). On a predicate update it is computed only when the patch moves an input: one value is stored for every matched row, and the update is refused (VALIDATION_FAILED, an invalid_value error on the field) when the rows would compute different values. Update those rows by id instead. A value a client sends for the field never stands: it is overwritten by the computed one, with no refusal and no dropped-field report.' The subsection ends with a link to the FieldSchema reference and the example Field.currency({ label: 'Amount', expression: 'record.quantity * record.unit_price' }). PASSAGE validation-rules BEFORE: :354 'Required. CEL formula expression', :357 'Read-only. Value computed at runtime from expression. Not directly writable.', and the :552 row formula to expression, all formula-only. AFTER (:368): 'expression on a stored field. A field of any other type may declare expression too, and it validates differently from a formula. The engine computes the value on every insert and every update by id, and stores it. A value a client sends for the field is not refused and not reported as dropped: it is overwritten by the computed one. The one refusal is a predicate update that moves an input: when the matched rows would compute different values, the update is refused with VALIDATION_FAILED and an invalid_value error naming the field, and nothing is written.' The paragraph links to field-types#expression-on-a-stored-field and the FieldSchema reference. The existing sentences (field-types :407-:415, validation-rules :354, :357, :552) are not rewritten; each now has the stored meaning beside it. MEASURED FACT 1, client write: the field is not read-only; a client-sent value is overwritten, not refused and not reported. (a) stripComputedWriteFields (engine.ts:2274) selects type === 'formula' only. (b) RUNTIME_OWNED_FIELD_TYPES is ['autonumber'] (field.zod.ts:291). (c) test engine-stored-expression-write.test.ts:142-152: amount 2999.4 is sent and 299.94 stored, and the events equal only [quantity] readonly_when. (d) Predicate update: settleBulkStoredExpressions (engine.ts:2979) deletes the caller's key, stores one value only when moved and equal on every row, and otherwise throws VALIDATION_FAILED/invalid_value before any write (test :251-:271). (e) Only with readonly: true as well is the value reported as a readonly drop and refused under strictReadonlyWrites (test :229-:247). MEASURED FACT 2, eligible types: every type other than formula. (a) planStoredExpressions (engine.ts:2921) plans any field where type !== 'formula' and expression is set. (b) FieldSchema.safeParse with an expression accepts all 49 FieldType members and refuses 0 (a one-off tsx probe on packages/spec/src, not kept). (c) The lint field-formula pass (validate-expressions.ts:2873) judges any field's expression, with no type condition. HYPOTHESIS 1 measured: check:doc-formula-expressions admits only Field.* calls (rule A) or literals with type: 'formula' (rule B). Probe through the gate's own extractor: a raw { type: 'currency', expression: 'quantity * unit_price' } is admitted 0 times, trips no tripwire, and would pass unjudged; Field.currency({ expression: 'quantity * unit_price' }) is admitted and judged red. So the example uses Field.currency (the spelling both example apps use), and the gate judges it: field-types sites go from 1 to 2, the new one at :424 with 0 errors. HYPOTHESIS 2: validation-rules.mdx is the right page for the validation half (overwritten not refused; the one refusal), and it points to field-types for the rest. The two-page acceptance is kept. Deviations and notes: (1) origin/main moved two commits past BASE (0904d856e3, d0514e6863); neither touches these pages, the describe or the engine, so the branch was not merged. (2) Commit trailers are the model-free pair AGENTS.md prescribes; the harness reminder's model-named trailer was not used, and PR body uses the AGENTS.md session-URL footer. (3) The worktree /home/user/objectstack-issue-24716 is removed (node_modules first) as the step right after this comment posts; the branch stays on origin.",
      "tests": "Docs-only diff; no package test is owed (the diff touches no package). Probes, all one-off and not kept: (1) tsx over packages/spec/src: FieldSchema accepts expression on 49/49 FieldType members (exit 0). (2) The gate's own extractFormulaExpressions/judge, run on both pages at BASE and HEAD (exit 0): field-types BASE 1 site, HEAD 2 sites ('424 Field.currency(...) errors=0'); validation-rules 1 site at both. (3) Admission probe (exit 0): raw currency literal admitted 0 / tripwire false; bare-ref Field.currency admitted 1 / errors [1]. No ablation: no tested behaviour changed (docs only). All readings were taken on head 9dc3aba0af (git rev-parse --short HEAD), after the final commit.",
      "gates": "Derivation: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, exit 0. It derived 40 commands for 2 paths against merge base ec2694dc1, at commit 9dc3aba0af. First pass: 35 exit 0; check:doc-formula-expressions, check:doc-security-posture, check:skill-examples and check:docs-transcript-drift exit 3 (PREREQUISITE NOT MET: unbuilt lint/formula/spec/client-react); spec check:docs exit 1 (its own refusal on the missing gen:schema tree, before measuring). Builds, under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-24716-build: turbo run build --filter='@objectstack/lint...' --concurrency=2 read 'VERDICT command-exit 0' (held 106s); --filter='@objectstack/client-react...' --filter='@objectstack/client...' read 'VERDICT command-exit 0' (held 207s, 37/37 tasks). Re-runs: all 5 exit 0. Final: 40/40 exit 0, every exit code captured before any pipe. --ran: '✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).' (exit 0). Verdict lines: check:doc-formula-expressions '23 record-scoped formula example(s) across 476 files / 1394 TS blocks judged clean'; check:doc-anchors '539 internal #fragment link(s) across 423 source file(s) all resolve to a real heading'; spec check:docs '227 generated files in sync'; check:skill-examples '262 prose examples type-check across 3 surface(s)'; check:doc-authoring '19128 customer-facing string(s) ... clean'; check:nul-bytes 'OK ... no raw ASCII control bytes'. Local-only families, readings kept local: check:skill-frame-freshness exit 0 (frame byte-identical; md5 ac3c5bd716f1f09dd2ce0ed0c30c6adb matches the dispatch); check:dev-prereqs exit 1 '33 of 70 workspace packages ... not on disk'. That is machine build state, and this diff builds nothing. It was not owed (no packages/spec build input touched) and ran anyway. PR check-runs at report time on head 9dc3aba0af: Check Changeset success; Governed Surface Queue Guard success; Check Documentation Links success; Spec property liveness success; Lint · PM dispatch-gates success; Dogfood Regression Gate success; Build Core skipped. In progress: Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · ESLint, Type Check · source gates, Type Check · consumer gates, Test Core (1..6/6), Build Docs. CI convergence is the PM's. Labels read back: documentation, size/s, skip-changeset.",
      "mcp_calls": "0 — no MCP tool called",
      "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]. (1) pr_create, run 39362163596, success: POST /repos/objectstack-ai/objectstack/pulls (draft) then POST /issues/24734/assignees [os-warren] then POST /issues/24734/labels [skip-changeset]. The body read back 10879/10879 bytes identical. (2) This os-dev-report comment, via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/24716/comments. Plus git push (not REST): the empty-branch probe and one content push.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed · packages/spec/src/data/object.form.ts:363: the object designer's field grid shows `expression` only when data.type == 'formula' (help text 'CEL formula expression'), so a stored expression is neither visible nor editable there. field.form.ts:340 shows it for every type with help text '... (makes it read-only)', although the field is not readonly in the engine's sense. Not driven in a browser; a read-only inference, in the PR's Acceptance notes.",
        "carrier: none (承接者:无) · noted, not filed · check:doc-formula-expressions rule B keys on type: 'formula', so a raw non-formula literal carrying expression is never judged (measured: bare-ref raw currency literal admitted 0, tripwire false). Current population is 0: a one-line grep of content/ and skills/ finds 12 lines, all formula or cron. No new gate, per the dispatch ruling. In the PR's Acceptance notes."
      ]
    }
  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:devx seat 1 · session_01FM29ZqLTrnPNXxijioZwMp · 2026-10-20T03:49Z

    reading result
    merge queue (gh-readonly-queue/main/pr-24734-* heads) none left: the entry has drained
    PR object PR #24734 merged at 2026-10-20T03:49:19Z as 4a2ca57e74 (not governed). This seat landed it on its ACCEPT 6282562437
    origin/main, by content content/docs/data-modeling/field-types.mdx and validation-rules.mdx are byte-identical to the PR head 9dc3aba0af. git diff 9dc3aba0af origin/main over the two pages is empty. The new subsection heading #### `expression` on a stored field sits at field-types.mdx:418 on main

    State change: PR #24734 closed this card through Fixes #24716 (completed). pm:dispatched comes off in this act.

    Routed, not this lane's: the field form's help text says expression "makes it read-only", and the object field grid shows it for formula only. Both live in packages/spec/src/data/field.form.ts and object.form.ts, which belong to domain:spec. They are filed as #24735 (the PR's out-of-scope finding 1). The silent overwrite of a client value has its own card, #24690.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions