You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec: the FlowSchema @example in flow.zod.ts compares a nullable record.amount unguarded on two edges (sibling of #31334) #31367
Filing gate class ② (a site found outside a landed card's claim). Found by the #31334 card thread (os-dev report 6717074863, out-of-scope finding 1), and confirmed by that PR's contract review on #31358.
What is wrong
packages/spec/src/automation/flow.zod.ts, in the FlowSchema TSDoc @example (around L1070–1071 on main), has two edges that compare amount without a guard:
e2: condition: "record.amount < 500"
e3: condition: "record.amount >= 500"
The publish-time null-guard rule (packages/lint/src/validate-null-guards.ts) refuses an unguarded comparison on a nullable field in a gated position. This example passes today only because its start node names no objectName. Once an author binds it to an object with a nullable amount, as anyone copying it would, the rule refuses it.
either guard the start condition (record.amount != null && …, per the formula skill),
or have the example declare the field required: true, if that is the honest fix.
Give the choice and its reason, and prove it against the real rule: refused before, accepted after. If the example feeds a generated reference page, regenerate the page. Spec example text only: an @objectstack/specpatch, Clause-②: no, Metadata-protocol: none.
p3: an example, not a shipped rule, though agents copy examples. Lane domain:spec (packages/spec). Prove the fix against the real null-guard rule (refused before, accepted after), and regenerate any reference page the example feeds. finding removed, pm:queue.
{
"issue": 31367,
"status": "done",
"branch": "claude/project-thread-ao69dp",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31416",
"session": "session_017ihL2ejruuhep9NDrZsCT7",
"premise_still_valid": true,
"summary": "The FlowSchema TSDoc example now binds its start node to `order` (record-after-update), so the null-guard rule judges it as written. Edge e2 is guarded with `record.amount != null && record.amount < 500`, and edge e3 becomes the default edge (`isDefault: true`), so a missing amount goes to review. I guarded the condition rather than declaring `required: true` because the example defines no object. The default edge replaces a hand-written negation, as the Fable review suggested and the schema's own isDefault doc prescribes. Doc-comment text plus a spec patch changeset.",
"tests": "Scratch vitest file in packages/lint/src (not committed). It evals the @example literal verbatim and runs validateStackExpressions with order.amount {type:'currency'}. Before (origin/main), verbatim: 0 findings, which is the premise: the start was unbound. Before, bound to order: 2 errors (e2 '<', e3 '>=' on record.amount, declared nullable). After, verbatim: 0 null-guard findings, 0 expression issues total, and FlowSchema.safeParse success=true. Re-run on f6f11f22 after merging main, same readings. No permanent test: the guard semantics are pinned in validate-null-guards.test.ts and validate-expressions.flow-null-guards.test.ts. No ablation: this is doc text only.",
"gates": "At f6f11f22: dispatch-gates --commands derived 79; 79 run, all exit 0; --ran: '79 run, 0 NOT-MEASURED (a DERIVED zero)'. Prerequisites: a ./packages/** turbo build (81 tasks) and a fetched teardown positive-control commit. spec gen:schema + gen:docs produced no diff. check:generated: the first run's check:test-typecheck hit a heap-limited tsc ('failed without diagnostics'); rerun alone with a 6144 MB heap gave exit 0 (52 files / 240 errors / 131 pinned held). eslint on flow.zod.ts: exit 0. check-nul-bytes: exit 0. PR check-runs at report time: 5 success, 8 skipped, 20 in_progress.",
"line_budget": "XS: 2 files, flow.zod.ts +8/-3 and a changeset of +12",
"files_changed": [
"packages/spec/src/automation/flow.zod.ts",
".changeset/31367-flow-example-amount-null-guard.md"
],
"deviations": [
"The order's file surface named the two edges. The fix also adds a start-node config to the same @example, because round-1 Fable should-fix: without it the example is never record-triggered and the rule never judges it. It stays inside the @example.",
"Commit trailer is model-free (Co-Authored-By: Claude) per the standing rule; the harness's model-named variant was not used."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"fable_review": "PR comment 6718445025: round 1 PASS with nits (2 should-fix, fixed in 8c1cc372); incremental re-review PASS",
"mcp_calls": "0 GitHub MCP calls",
"api_writes": "2 relay writes (fleet-write dispatch, objectstack-fleet[bot]): pr_create #31416 with assignee marchtian; post-stamped comment 6718445025 on #31416. This report is the 3rd. git push x3 (not REST).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: c · reach: named producer (published skill an agent copies) · skills/objectstack-automation/references/state-machines-and-approvals.md compares record.amount > 500000 / <= 500000 unguarded in a flow condition · dedupe words: state-machines-and-approvals amount null guard, skill flow example nullable",
"carrier: none · packages/services/service-automation/README.md record.amount > 500 unguarded (README, noted, not filed)"
]
}
Contract review at CONTRACT_REVIEW_TIER: round 1 PASS at f6f11f22 (6718493750) with one should-fix (the example's record-after-update start had no start condition, so its own auto-approve write re-launched it); fixed at 0ec8e99a with a start condition per content/docs/getting-started/common-patterns.mdx:337-339; delta PASS 6718517378, 0 open findings.
The fix: e2 guards record.amount != null && record.amount < 500; e3 is the isDefault edge (≥ 500 or null → approval, the conservative branch); the start is bound to order so the null-guard rule judges the example as written. Dev's proof 6718452460: 2 refusals before, 0 after; the review confirms by reading the rule.
Declared Clause-②: no, Metadata-protocol: none, @objectstack/spec patch: confirmed. Path face: packages/spec/src/** (docblock) + one changeset, not governed, 23 lines ⇒ Tier S; lands on green CI through the merge queue.
Accepted deviation: the file surface grew by the start-node config inside the same @example (needed by the review's should-fix).
merged 2026-11-08T07:58:58Z through the queue, merge commit 18ed2db1b770578bb54f6c02aa86d17cd0b229f8, reviewed head 0ec8e99a (contract review PASS 6718493750 + delta PASS 6718517378 + ACCEPT on that head)
card
closed completed by Fixes
State: pm:dispatched removed. Residual: the unguarded record.amount comparisons in skills/objectstack-automation/references/state-machines-and-approvals.md are carried by #31417.
Activity
objectstack-fleet commentedon Nov 8, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p3 ·
domain:spec· 2026-11-08T04:53Z⛔ Not a claim.
FlowSchemaTSDoc@exampleinpackages/spec/src/automation/flow.zod.ts(≈L1070) compares a nullablerecord.amountunguarded on two edges. It passes today only because its start node names no object. This is the site docs: nullablerecord.amountcompared unguarded in gated positions on nine more docs pages and the validation spec example (after #31206) #31334 / PR docs(spec): guard the nullable amount in the docs examples and the validation condition example #31358 found outside its claim.domain:spec(packages/spec). Prove the fix against the real null-guard rule (refused before, accepted after), and regenerate any reference page the example feeds.findingremoved,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:spec#4 · marchtian · session_017ihL2eAG5J8aqAyPZif4BJ
Claim: PM loop round 3 · 2026-11-08T05:49Z
Session:
session_017ihL2ejruuhep9NDrZsCT7(card thread; dispatched by seat sessionsession_017ihL2eAG5J8aqAyPZif4BJ)Account:
marchtianBranch:
claude/project-thread-ao69dpWorktree: the card thread's own container checkout
Domain:
domain:specSeat:
domain:spec#4File surface:
packages/spec/src/automation/flow.zod.ts(theFlowSchemaTSDoc@exampleonly) + one.changeset/*.md+ any regeneratedcontent/docs/references/**page embedding the exampleContainer & model:
XS docs card,mode:thread,model: default tier,effort: default; Fable review of the spec diff before ACCEPT (the maintainer's standing spec rule)Clause-②: no
Metadata-protocol: none
Responsibility: n/a — example text; the sibling of #31334 / PR #31358
Thread-read: 6717448070
Premise: holds (triage 6717448070 read on
main); the dev re-measures againstvalidate-null-guards.tsSerial constraints cleared: none (disjoint from #31344's restriction-profile surface)
objectstack-fleet commentedon Nov 8, 2026
domain:spec#4 · marchtian · session_017ihL2ejruuhep9NDrZsCT7
os-dev-report
{ "issue": 31367, "status": "done", "branch": "claude/project-thread-ao69dp", "pr": "https://github.com/objectstack-ai/objectstack/pull/31416", "session": "session_017ihL2ejruuhep9NDrZsCT7", "premise_still_valid": true, "summary": "The FlowSchema TSDoc example now binds its start node to `order` (record-after-update), so the null-guard rule judges it as written. Edge e2 is guarded with `record.amount != null && record.amount < 500`, and edge e3 becomes the default edge (`isDefault: true`), so a missing amount goes to review. I guarded the condition rather than declaring `required: true` because the example defines no object. The default edge replaces a hand-written negation, as the Fable review suggested and the schema's own isDefault doc prescribes. Doc-comment text plus a spec patch changeset.", "tests": "Scratch vitest file in packages/lint/src (not committed). It evals the @example literal verbatim and runs validateStackExpressions with order.amount {type:'currency'}. Before (origin/main), verbatim: 0 findings, which is the premise: the start was unbound. Before, bound to order: 2 errors (e2 '<', e3 '>=' on record.amount, declared nullable). After, verbatim: 0 null-guard findings, 0 expression issues total, and FlowSchema.safeParse success=true. Re-run on f6f11f22 after merging main, same readings. No permanent test: the guard semantics are pinned in validate-null-guards.test.ts and validate-expressions.flow-null-guards.test.ts. No ablation: this is doc text only.", "gates": "At f6f11f22: dispatch-gates --commands derived 79; 79 run, all exit 0; --ran: '79 run, 0 NOT-MEASURED (a DERIVED zero)'. Prerequisites: a ./packages/** turbo build (81 tasks) and a fetched teardown positive-control commit. spec gen:schema + gen:docs produced no diff. check:generated: the first run's check:test-typecheck hit a heap-limited tsc ('failed without diagnostics'); rerun alone with a 6144 MB heap gave exit 0 (52 files / 240 errors / 131 pinned held). eslint on flow.zod.ts: exit 0. check-nul-bytes: exit 0. PR check-runs at report time: 5 success, 8 skipped, 20 in_progress.", "line_budget": "XS: 2 files, flow.zod.ts +8/-3 and a changeset of +12", "files_changed": [ "packages/spec/src/automation/flow.zod.ts", ".changeset/31367-flow-example-amount-null-guard.md" ], "deviations": [ "The order's file surface named the two edges. The fix also adds a start-node config to the same @example, because round-1 Fable should-fix: without it the example is never record-triggered and the rule never judges it. It stays inside the @example.", "Commit trailer is model-free (Co-Authored-By: Claude) per the standing rule; the harness's model-named variant was not used." ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "fable_review": "PR comment 6718445025: round 1 PASS with nits (2 should-fix, fixed in 8c1cc372); incremental re-review PASS", "mcp_calls": "0 GitHub MCP calls", "api_writes": "2 relay writes (fleet-write dispatch, objectstack-fleet[bot]): pr_create #31416 with assignee marchtian; post-stamped comment 6718445025 on #31416. This report is the 3rd. git push x3 (not REST).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: c · reach: named producer (published skill an agent copies) · skills/objectstack-automation/references/state-machines-and-approvals.md compares record.amount > 500000 / <= 500000 unguarded in a flow condition · dedupe words: state-machines-and-approvals amount null guard, skill flow example nullable", "carrier: none · packages/services/service-automation/README.md record.amount > 500 unguarded (README, noted, not filed)" ] }objectstack-fleet commentedon Nov 8, 2026
domain:spec#4 · marchtian · session_017ihL2eAG5J8aqAyPZif4BJ
ACCEPT → PR #31416 comment 6718517378 · head
0ec8e99a1· 2026-11-08T07:02ZCONTRACT_REVIEW_TIER: round 1 PASS atf6f11f22(6718493750) with one should-fix (the example'srecord-after-updatestart had no start condition, so its own auto-approve write re-launched it); fixed at0ec8e99awith a startconditionpercontent/docs/getting-started/common-patterns.mdx:337-339; delta PASS 6718517378, 0 open findings.record.amount != null && record.amount < 500; e3 is theisDefaultedge (≥ 500 or null → approval, the conservative branch); the start is bound toorderso the null-guard rule judges the example as written. Dev's proof 6718452460: 2 refusals before, 0 after; the review confirms by reading the rule.Clause-②: no,Metadata-protocol: none,@objectstack/specpatch: confirmed. Path face:packages/spec/src/**(docblock) + one changeset, not governed, 23 lines ⇒ Tier S; lands on green CI through the merge queue.@example(needed by the review's should-fix).skills/objectstack-automation/references/state-machines-and-approvals.md(e1, e4, e5) is filed as skills(objectstack-automation): the approval-flow example in state-machines-and-approvals.md still compares a nullable record.amount unguarded on e1, e4 and e5 (residual of #31206) #31417;packages/services/service-automation/README.md:192is named there.objectstack-fleet commentedon Nov 8, 2026
domain:spec#4 · marchtian · session_017ihL2eAG5J8aqAyPZif4BJ
Landed ·
domain:spec#4· 2026-11-08T08:00Z18ed2db1b770578bb54f6c02aa86d17cd0b229f8, reviewed head0ec8e99a(contract review PASS 6718493750 + delta PASS 6718517378 + ACCEPT on that head)FixesState:
pm:dispatchedremoved. Residual: the unguardedrecord.amountcomparisons inskills/objectstack-automation/references/state-machines-and-approvals.mdare carried by #31417.