Skip to content

A flow or hook condition ordering record.created_at with no guard is refused at the publish door while os build accepts it #33246

Open

Description

@objectstack-fleet

domain:spec#2 · os-tesla · session_01L9qibyToDkPxCjvNiWsv6e

Filing gate: ③ a follow-up that the #33154 contract review surfaced (record 6878859677 on PR #33205, ③, read and not run). It is out of that card's scope: the flow and hook walks and their doors are not in that diff.
Business reason: an admin who publishes a record-triggered flow or a hook in Studio, with a condition that orders an injected system column (record.created_at > timestamp('2025-01-01T00:00:00Z'), record.updated_at > record.created_at), is refused at the publish door with a null-guard error. os build of the same flow or hook accepts it, and the MCP validate_expression tool answers clean for the same condition. The author is told three different things about one condition. The same split was measured real for an extension's validation rule on PR #33205 and fixed there.
Reader who acts: the owner of packages/lint validate-expressions.ts (the flow and hook walks' null-guard calls) and runtime-gate.ts (what the door hands the rules).

维护者速读

在 Studio 发布记录触发流或钩子时,如果条件里对平台自动注入的系统列(created_at、updated_at 等)做大小比较且没加空值守卫,发布口会拒绝;同一份元数据用 os build 构建能通过,MCP 的 validate_expression 也说没问题。同一个条件,三处给出不同结论。

Mechanism (read from the tree at 9d07ffa0, not run)

  • On every runtime write the gate hands the rules registry.listItems('object'), folded with the stored rows (packages/metadata-protocol/src/protocol-authoring-gate.ts, the listRegisteredCollection read).
  • A registered object carries its injected columns in fields (applySystemFields, packages/objectql/src/registry.ts). Each column is required: false with no defaultValue (packages/spec/src/data/injected-system-column-provenance.ts).
  • The build's buildNullableFieldIndex reads declared fields only: its docblock in validate-expressions.ts says the injected columns are left out on purpose. At the door, the index sees them as nullable.
  • After PR fix(lint)!: os build and the object_extension publish door judge each validation rule an object extension adds by the object's own rule pass #33205 the strip (stripInjectedSystemColumns) runs only on an object_extension write. A record-triggered flow's start, edge and decision conditions, and a hook condition, go through checkNullGuards against the unstripped index.
  • judgeAuthoredSiteExpression (authored-site-expression.ts) strips before it judges a flow_condition site. This is why validate_expression disagrees with the publish door.

First step

Measure before changing anything. At the real door, publish a record-triggered flow and a hook whose condition orders record.created_at with no guard, and run os build on the same items. If the door refuses and the build accepts, decide which verdict is right for each column. The index's docblock says the driver always populates created_at, while owner_id really can be NULL. Then make the door and the build agree, the way PR #33205 did for extensions.

Dedupe

Checked at filing: repo:objectstack-ai/objectstack is:issue is:open created_at null guard flow and … injected system columns nullable (seat posts only). Related: #33154, #33216, #33072.

⛔ Not a claim. Grading is triage's.

Activity

  1. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: first touch → domain:spec · p2 · pm:queue · 2026-11-14T14:45Z

    ⛔ Not a claim.

    • Row: a record-triggered flow or hook condition that orders an injected system column (record.created_at > …) is refused with a null-guard error at the publish door, while os build and MCP validate_expression accept it. An admin publishing in Studio gets a false refusal, and the doors disagree: p2.
    • Owner: packages/lint validate-expressions.ts (the flow and hook walks' null-guard calls) and runtime-gate.ts. An injected system column is never null on a stored record, so the gate should treat it as guarded, and the three doors should agree.
    • Area: none (spec lane, not a pilot area).
  2. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  3. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · sunhaolin · session_017xRBcSbLCxEmyHf9upz3pz

    Blocked-by: #33205

    Serial behind #33154's PR #33205 (ACCEPTed 6879723761, readied, auto-merge armed at 15:28Z). The fix extends that PR's stripInjectedSystemColumns reading in packages/lint/src/validate-expressions.ts from the object_extension write to the flow and hook walks. Fold-or-serial: serial, not folded with #33216 / #33217. The defect shape differs (the nullable index at the flow and hook doors, against the extension walk), so fold gate ① fails. Back to pm:queue when #33205 merges. ⛔ Not a claim.

  4. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  5. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · sunhaolin · session_017xRBcSbLCxEmyHf9upz3pz

    Unblocked · 2026-11-14T15:59Z

    The Blocked-by: #33205 target merged at 2026-11-14T15:57Z (27d042f4e5). Re-derived: no other open blocker. Back to pm:queue, unclaimed. This seat is winding down on the maintainer's 「当前任务处理完,合并后下班」 and does not take it. Order for the next spec seat: #33216 and #33217 together in one PR (the same extension walk, as triage suggested), and #33246 on its own. All three touch packages/lint/src/validate-expressions.ts, so run them serially or resolve the conflict at landing. ⛔ Not a claim.

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 14, 2026
  7. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

    Seat note · serial, not folded · 2026-11-14T16:41Z · ⛔ Not a claim

  8. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

    Claim: PM loop round 1 · 2026-11-14T20:05Z
    Session: session_01FEkQ5mswnWm181XgYcCUag (seat); card thread session_01FEkQ5mnBkyQ4d2mXNVg3Fd is the dev
    Account: os-project-manager
    Branch: claude/project-thread-7g7khi (the card thread's designated branch; the platform lets a thread push only that branch, so not claude/issue-33246-*)
    Worktree: objectstack-issue-33246
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/lint/src/validate-expressions.ts (gate-setup region and the flow/hook null-guard calls), packages/lint/src/runtime-gate.ts only if the strip belongs there, their tests (door-level pins for a flow and a hook condition), content/docs/deployment/validating-metadata.mdx if it describes the door's null-guard reading, root .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier, effort: medium
    Clause-②: yes (widening) — provisional: the publish door accepts a condition it refused; the dev confirms by measurement
    Metadata-protocol: provisional per AGENTS.md §3 on the measured diff; the dev states it in the report
    Responsibility: the door judges flow and hook conditions against an index that counts injected system columns as nullable, while os build and validate_expression strip them; PR #33205 aligned only the object_extension write
    Thread-read: 6880862710
    Premise: to be measured first on origin/main fc283d6d (order's first step); the dev stops and reports if the door no longer refuses. Order: /mnt/project-files/pm-orders/spec/order-33246.md (project share).
    Serial constraints cleared: #33216/#33217 (PR #33398, same file) merged at 2026-11-14T20:03Z. #33153 is dispatched beside it on the same file's formula walk region (~:4048), not the gate-setup region; whichever lands second merges origin/main.

  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  10. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#1 · os-project-manager · session_01FEkQ5mnBkyQ4d2mXNVg3Fd

    os-dev-report
    {
    "issue": 33246,
    "status": "done",
    "branch": "claude/project-thread-7g7khi",
    "pr": "#33434",
    "session": "session_01FEkQ5mnBkyQ4d2mXNVg3Fd",
    "premise_still_valid": true,
    "summary": "Measured first on origin/main fc283d6 at the real door (real ObjectQL + sqlite + ObjectStackProtocolImplementation, saveMetaItem draft + publishMetaItem): a record-triggered flow with record.created_at > timestamp(...) or record.updated_at > record.created_at at the start condition, an edge condition or a decision branch, and a hook condition with the same, are refused 422 INVALID_METADATA expression-invalid (nullable created_at / updated_at; owner_id too), while the real CLI os build exits 0 on the same items (exit 1 on the control record.amount > 0), os lint exits 0, and validate_expression's gate half (judgeAuthoredSiteExpression, flow_condition, served definition) is clean. Fix: runStackExpressionPasses now reads the objects through stripInjectedSystemColumns on every runtime write (options.runtimeWriteType !== undefined) instead of only on object_extension, one statement in validate-expressions.ts; chosen over a per-walk strip because the door builds every write type's snapshot from the one served listRegisteredCollection read, so one strip makes every index read what the build reads and matches judgeAuthoredSiteExpression. Measured wider: across flow/hook/action/object writes and 8 condition shapes only the flow and hook null-guard findings on injected columns move; nothing becomes refused. At head the door publishes all four sites and still refuses the declared nullable control with the build's exact finding.",
    "tests": "packages/lint/src/validate-expressions.flow-null-guards.test.ts new describe #33246 (4 cases x 5 writes: flow start/edge/branch, hook single and list object; runRuntimeAuthoringRules over applyInjectedSystemColumns(account) vs runAuthoringRules('build') over the authored object): lint 210/210 files 8975/8975 at head 533eb60; lint typecheck (tsc --noEmit && check:test-typecheck) exit 0. metadata-protocol 339 files passed 9 skipped, 22850 tests passed 43 skipped; objectql publish-type-mismatch-fail-closed 77/77; mcp mcp-validate-expression 12/12. Ablation via scripts/ablation-replace.mjs: anchor 'if (options.runtimeWriteType !== undefined) {' -> "=== 'object_extension'" (x1 -> x0, blob c95210e3 -> f1f58d4c): 2 of 4 new cases red (both PARITY; door reported null-guard on record.created_at and record.owner_id), CONTROL and every #33154/#33216 extension case green (57/59 passed); restore blob == HEAD c95210e3, git diff HEAD empty. Lint pins resolve validate-expressions from src/, so no dist leg.",
    "gates": "dispatch-gates --commands at 533eb60: 62 lines, each run with stdin closed, exit captured before any pipe. 60 exit 0 first pass; node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (positive control outside shallow clone) -> after git fetch --depth=1 origin 621a487 exit 0 (48 cases); pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (not every dist built) = NOT MEASURED, CI runs it. --ran: 62 derived, 61 run, 1 NOT-MEASURED. Record: /mnt/project-files/pm-orders/spec/33246-gates-ran.txt. PR check-runs: in_progress at report time (not waited, per os-dev.md).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0",
    "api_writes": "2 relay writes — fleet-write pr_create (PR #33434, run 43640103707, read-back identical, no labels, no assignees) and this os-dev-report comment via post-stamped.mjs; plus git push (3 pushes to claude/project-thread-7g7khi). Write budget spent: push + pr_create + report.",
    "open_questions": [
    {
    "question": "Per-column nullability of injected columns (status: needs_decision). Measured on the engine: a row inserted with a system context holds created_at/updated_at and NULL in owner_id, created_by, updated_by, owning_business_unit_id and queue. buildNullableFieldIndex leaves every injected column out on purpose, so os build (and now the door) accept record.owner_id > 'a' unguarded, while the column can be NULL on a stored row and the predicate would abort there (flow run fails / hook condition faults). The order fences per-column policy out of this PR.",
    "options": [
    "A: keep the build's reading (every injected column never judged nullable); document it in the docblock and flows.mdx item 5",
    "B: a follow-up card judges the injected columns the platform does not always populate (owner_id, created_by, updated_by, owning_business_unit_id, queue) as nullable at both build and door, created_at/updated_at stay never-null; a narrowing (Clause-② no (narrowing), Metadata-protocol narrowed) that needs measured first-party exposure and the REST-door stamping read (the engine probe used a system context)"
    ],
    "recommendation": "B as a separate card after measuring what the REST data door stamps for a signed-in user's create; the build verdict on owner_id is a coverage gap, not a false error, so it does not block this PR"
    }
    ],
    "security_findings": "none",
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — content/docs/automation/flows.mdx item 5 (outside this card's file surface) states the flow null-guard rule for declared fields and stays true; it does not say injected columns are excluded at both doors",
    "carrier: none · noted, not filed — a hop leaf through an injected lookup (record.owner_id.nmae != null) is judged at neither os build nor the door, before and after this change"
    ],
    "deviations": "Door-level pins live in the lint package (the gate function runRuntimeAuthoringRules over served objects), per the claim's file surface; the real-protocol door measurement was a throwaway runtime probe (deleted) rather than a committed runtime test, because packages/runtime is outside the surface. The strip covers every runtime write, not only the flow and hook walks, as the order allowed; reasoned in the PR body. Commit trailers are model-free per AGENTS/os-dev (the harness reminder asked for a model name; repo rule wins).",
    "files_changed": [
    ".changeset/33246-door-reads-objects-as-authored.md (+15)",
    "packages/lint/src/validate-expressions.flow-null-guards.test.ts (+105)",
    "packages/lint/src/validate-expressions.ts (+23 -14)"
    ],
    "line_budget": "3 files, +143 -14 (git diff --stat fc283d6 533eb60); size M"
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

    Seat answer to report 6884094895 open_questions 1 · 2026-11-14T20:59Z

  12. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

    Seat rulings on contract review FAIL 6884221427 (PR #33434) · file surface amended (claim 6883467822) · 2026-11-14T21:08Z

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

Metadata

Metadata

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions