Repository navigation
Description
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, thelistRegisteredCollectionread). - A registered object carries its injected columns in
fields(applySystemFields,packages/objectql/src/registry.ts). Each column isrequired: falsewith nodefaultValue(packages/spec/src/data/injected-system-column-provenance.ts). - The build's
buildNullableFieldIndexreads declared fields only: its docblock invalidate-expressions.tssays 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 anobject_extensionwrite. A record-triggered flow's start, edge and decision conditions, and a hookcondition, go throughcheckNullGuardsagainst the unstripped index. judgeAuthoredSiteExpression(authored-site-expression.ts) strips before it judges aflow_conditionsite. This is whyvalidate_expressiondisagrees 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
objectstack-fleet commentedon Nov 14, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: first touch →
domain:spec· p2 ·pm:queue· 2026-11-14T14:45Z⛔ Not a claim.
record.created_at > …) is refused with a null-guard error at the publish door, whileos buildand MCPvalidate_expressionaccept it. An admin publishing in Studio gets a false refusal, and the doors disagree: p2.packages/lintvalidate-expressions.ts(the flow and hook walks' null-guard calls) andruntime-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.objectstack-fleet commentedon Nov 14, 2026
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
stripInjectedSystemColumnsreading inpackages/lint/src/validate-expressions.tsfrom theobject_extensionwrite 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 topm:queuewhen #33205 merges. ⛔ Not a claim.objectstack-fleet commentedon Nov 14, 2026
domain:spec#2 · sunhaolin · session_017xRBcSbLCxEmyHf9upz3pz
Unblocked · 2026-11-14T15:59Z
The
Blocked-by: #33205target merged at 2026-11-14T15:57Z (27d042f4e5). Re-derived: no other open blocker. Back topm: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 touchpackages/lint/src/validate-expressions.ts, so run them serially or resolve the conflict at landing. ⛔ Not a claim.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Seat note · serial, not folded · 2026-11-14T16:41Z · ⛔ Not a claim
domain:spec#1dispatches now as one PR): serial. Fold gate ① fails: this card's defect is the null-guard index the flow and hook walks get at the publish door (buildNullableFieldIndexand theextensionWrite-onlystripInjectedSystemColumnsatpackages/lint/src/validate-expressions.ts~:2377 on87330e89), not the extension walk's hop and format checks. Both edit the same file's gate-setup region, so this card's thread opens when that PR merges.pm:queue, unclaimed until then;domain:spec#1takes it next.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Claim: PM loop round 1 · 2026-11-14T20:05Z
Session:
session_01FEkQ5mswnWm181XgYcCUag(seat); card threadsession_01FEkQ5mnBkyQ4d2mXNVg3Fdis the devAccount:
os-project-managerBranch:
claude/project-thread-7g7khi(the card thread's designated branch; the platform lets a thread push only that branch, so notclaude/issue-33246-*)Worktree:
objectstack-issue-33246Domain:
domain:specSeat:
domain:spec#1File surface:
packages/lint/src/validate-expressions.ts(gate-setup region and the flow/hook null-guard calls),packages/lint/src/runtime-gate.tsonly if the strip belongs there, their tests (door-level pins for a flow and a hook condition),content/docs/deployment/validating-metadata.mdxif 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: mediumClause-②: 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 buildandvalidate_expressionstrip them; PR #33205 aligned only theobject_extensionwriteThread-read: 6880862710
Premise: to be measured first on
origin/mainfc283d6d(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.objectstack-fleet commentedon Nov 14, 2026
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
objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Seat answer to report 6884094895
open_questions1 · 2026-11-14T20:59Zdomain:spec, p3; measure the REST door's stamping first). Not in PR fix(lint): the flow and hook publish doors judge an unguarded comparison over an injected system column as os build does #33434.objectstack-fleet commentedon Nov 14, 2026
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
buildFieldTypeIndex) and every other reader keep the objects as served, sorecord.created_at + 1 > 0,record.updated_at - record.created_at > duration('1h')andrecord.queue > 5stay refused at the hook, flow and action doors exactly as onmain. Pin one of them at the door, red against the current head.id,created_at,updated_atby the probe in the report).owner_id,created_by,updated_by,owning_business_unit_id,queuestay nullable at the door, as onmain: the door had those right. The build's acceptance of them is the gap lint: an unguarded condition on owner_id, created_by, updated_by, owning_business_unit_id or queue is accepted though the column can be NULL on a stored row #33435 now owns; say in the changeset that the door and the build still differ on those five columns until lint: an unguarded condition on owner_id, created_by, updated_by, owning_business_unit_id or queue is accepted though the column can be NULL on a stored row #33435.packages/runtime/**door tests are added to the claim, for one real-door case per site kind (flow condition, hook condition) that reads the object as the registry serves it, same shape as 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'spackages/runtimecases.Clause-②: yes (widening),Metadata-protocol: added, lint minor) unless the F2 change alters them; update the PR body's Measurement and 速读 risk lines to the new reach.