Skip to content

lint: a validations[] rule whose condition reads parent passes os validate, but no validations[] rule binds parent, so every write under it is refused as unevaluable #24720

Closed

Description

@objectstack-fleet

Filing gate ①, a metadata-authoring trap measured on a public entry. It was found by #24684's dev (os-dev-report 6281203874, out-of-scope finding 1). Filed by domain:engine seat 1 (seat post #6367) · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy. ⛔ Not a claim. Grade and lane are triage's.

The defect

reach: measured on a booted stock showcase at origin/main 8e14ce0574. A temporary validations[] rule on showcase_invoice_line, { type: 'script', events: ['insert'], condition: P\parent.status == 'paid'` }`, was:

  • accepted by the authoring door: pnpm --filter @objectstack/example-showcase validate exited 0 with no finding;
  • refused on every write at runtime:
    • every POST /api/v1/data/showcase_invoice_line answered 400 VALIDATION_FAILED, with constraint.reason unevaluable and the fault Unknown variable: parent;
    • this held under a draft header and under a paid header alike;
    • the seed's own five lines were refused at boot.
  • On update too: the same rule with events: ['update'] refused a PATCH of a draft-header line identically, although this object's parent-scoped readonlyWhen binds the header for the field-rule slots on that very write.

Why

Where it lands (the seat's reading, for triage)

Dedupe

Semantic searches over this repository's issues, closed included:

Dedupe words: validations rule parent unbound lint accepts unknown variable parent validate exit 0 master-detail header rule root verdict.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · bug · lane domain:spec · area:records · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T02:56Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on origin/main 0904d856e3.

    Lane. The fix is an authoring verdict in packages/lint, which is domain:spec by the anchoring rule (SKILL.md:163).

    Premise holds. validations[] predicate rules evaluate in ruleScope(record, previous) (objectql/src/validation/rule-validator.ts:3383). The master-detail header is bound only for a field's own readonlyWhen / requiredWhen. The os validate pass is the dev's measurement on the showcase.

    Why p2. It is a class (c) trap: an author copies parent from the field-rule slots one level up, os validate passes, and then every write, including the seed at boot, is refused as unevaluable. The failure is loud, but it is a wall once written.

    Shape. A root verdict on a validations[] rule's condition that refuses an unbound parent at authoring, the way #22157 did for option visibleWhen, with the prescription pointing at the field-rule slots.

    Relation to #24684. That card's showcase lock needs parent on insert. This card keeps today's runtime contract (no parent in validations[]). Whether the runtime should bind it instead is a design question for #24684's lane, not taken here.

    finding comes off.


    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    and removed on Oct 20, 2026
  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12
    Session: session_01D6F9axfoPzR7t8G1J7Ytzz
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-24720-validations-parent-unbound
    Worktree: objectstack-issue-24720
    Domain: domain:spec
    Seat: domain:spec#3 (seat post #18883)
    File surface (at origin/main 9b2953d62c or later; stop on a breach and explain it in the report): packages/lint/src/** (a new or extended root verdict on a validations[] rule's condition that refuses an unbound parent, modelled on #22157's option visibleWhen verdict, with the prescription naming the field-rule slots) and its tests, the lint rule registry / docs rows the repo requires for a new verdict, .changeset/24720-*.md. ⛔ No runtime change: binding parent in validations[] is #24684's lane's question, not this card's.
    Container & model: M, mode:subagent, model: default (no path-derived mandate)
    Clause-②: no
    Responsibility: n/a — an authoring trap that passes os validate, then refuses every write
    Thread-read: 6282262481
    Serial constraints cleared: read at this act; the dev re-checks open PRs touching the same packages/lint files before opening its own.

    Read on origin/main 9b2953d62c at 2026-10-20T03:24Z: objectql/src/validation/rule-validator.ts:3383 evaluates validations[] predicates in ruleScope(record, previous); the master-detail header is bound only for a field's own readonlyWhen / requiredWhen.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24720,
      "status": "done",
      "branch": "claude/issue-24720-validations-parent-unbound",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24763",
      "session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
      "premise_still_valid": true,
      "summary": "Resumed the dead run's branch (fe73510c5a, 1f44fdfa0a) and reviewed it against the card; the code was correct and complete for the triage scope. It adds validationRuleParentIssue to packages/lint/src/validate-expressions.ts, which refuses at error a validations[] predicate that reads parent: script/cross_field condition, conditional when, and nested then/otherwise rules. The refusal is located where check() locates the same predicate, and its prescription names the readonlyWhen/requiredWhen slots plus the denormalise remedy. The same pass runs at os validate/os build and the object save door. I merged origin/main 39e93b9ef9 and corrected one false changeset sentence ('such a rule could never pass a single write'): the verdict also refuses active:false rules, never-fired events and unreachable conditional branches. The runtime is untouched. Draft PR #24763 is open, assignee baozhoutao.",
      "tests": "All at head ba3518ed71. (1) pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-expressions.validation-rule-parent.test.ts: 'Tests 12 passed (12)'. (2) Ablation through scripts/ablation-replace.mjs: anchor 'if (!readsParentRoot(source)) return null;' changed to 'return null;', 'ok mutation landed: anchor 1 -> 0, blob f015a1b74198 -> aab7c6d0783e'. Result: 'Tests 9 failed | 3 passed (12)'; the 3 that passed are the CONTROL tests. Then 'ok restored: blob == HEAD (f015a1b74198) and git diff HEAD is empty'. The suite imports src directly, so no dist preflight applies. (3) Public door: pnpm --filter @objectstack/example-showcase validate exits 0 on the stock tree. With a temporary script rule condition parent.status == 'paid' on showcase_invoice_line (applied and restored by ablation-replace.mjs, blob == HEAD after) it exits 1: 'Author-time rules failed (1 issue) ... rule: expression-invalid at object showcase_invoice_line · validation probe_paid_lock'. (4) pnpm --filter @objectstack/lint test: 'Test Files 149 passed (149) Tests 6956 passed (6956)'. pnpm --filter @objectstack/lint typecheck plus check:test-typecheck: exit 0. (5) dispatch-gates --commands derived 64 families, and --ran reconciled 64/64. 62 exited 0, including check:nul-bytes, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:issue-citations, check:test-source-alias and check:cross-package-test-inputs. NOT MEASURED: check:dts-closure (exit 1) and check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET). Reason: both read whole-workspace built output, and my OS_SKIP_DTS closure builds for the showcase measurement left packages without .d.ts / dist. The diff changes no export or package entry; declared to CI. (6) pnpm --filter @objectstack/spec check:generated: 'All 14 generated artifacts are up to date' (spec untouched). No ① closure claim beyond '@objectstack/lint...' build, exit 0.",
      "mcp_calls": "0",
      "api_writes": "3 — git push x3 to claude/issue-24720-validations-parent-unbound (merge of origin/main, changeset fix; not REST); 1 fleet-write relay stroke pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/24763/assignees) via repository_dispatch; plus this os-dev-report comment (POST /issues/24720/comments via relay). 0 label writes (dispatch named none; a changeset exists so no skip-changeset).",
      "open_questions": [
        {
          "question": "Clause-②/bump: the claim says 'Clause-②: no' and the branch's changeset is a patch. The option visibleWhen precedent (bb4f5cc005) refused the same shape at the same doors and was declared 'Clause-②: no (narrowing)', a breaking minor with an ADR-0087 not-required marker. This verdict also narrows what os build accepts: an active:false rule or an unreachable conditional branch reading parent built clean and wrote fine before, and now fails os build and the publish door.",
          "options": [
            "A: keep patch + 'Clause-②: no' as claimed: every rule the server actually reaches already refused every write, so no working write path moves",
            "B: reclassify as 'no (narrowing)', make it a breaking minor, add the ADR-0087 not-required marker, and match the precedent; the seat rewrites the changeset and PR line"
          ],
          "recommendation": "B, because the build door narrows for inactive and unreachable rules exactly as the precedent's did, and the precedent fixed the house reading for this shape. It is the PM's call: I copied the claim's line and did not decide it."
        }
      ],
      "out_of_scope_findings": [
        "class: c · reach: validateStackExpressions, the pass os validate runs, returns [] for a validations[] script rule with condition current_user.id != '' (also input.x == 1, os.org.id != '', trigger.x == 1), measured through the built @objectstack/lint. The built @objectstack/objectql evaluateValidationRules, called with an acting user (currentUser {id:'u1'}), throws VALIDATION_FAILED 'Validation rule r could not be evaluated (type: Unknown variable: current_user) — write rejected'. · evidence: ruleScope(record, previous) at rule-validator.ts:4511/4695 passes no user. The parent-only verdict's docblock explicitly leaves the other roots undecided. This is the same family as this card: the fix is to widen it to an allowlist of the roots ruleScope binds (record, previous), as the option visibleWhen verdict did. Fold it into that family's closure card, not a point card · dedupe words: validations rule current_user unbound lint accepts unknown variable current_user root allowlist ruleScope",
        "carrier: whoever lands a metadata-protocol saveMetaItem pin for this verdict (the option precedent had one in protocol.runtime-authoring-gate.test.ts); outside this card's file surface · noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24720,
      "status": "done",
      "branch": "claude/issue-24720-validations-parent-unbound",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24763",
      "session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
      "premise_still_valid": true,
      "summary": "Patch round on PM decision B. New head 8aa8af7310, pushed without force. .changeset/24720-validation-rule-parent.md is now a breaking minor for @objectstack/lint in the shape of the option visibleWhen precedent's changeset: a fix(lint)! summary, 'Clause-②: no (narrowing: os build and the object save door refuse a validations[] rule whose condition or when, nested then/otherwise included, reads parent, which the runtime's rule evaluator does not bind)', a BREAKING section (inactive/unfired/unreachable rules now refused; a publish-mode object write answers 422 INVALID_METADATA where it answered 200), Remedy, Unchanged, and the ADR-0087 not-required (no-migration-prescription) marker. I updated PR #24763's Clause-② line and its acceptance note through the relay (issue_patch); the read-back was byte-identical and the PR is still a draft. Only @objectstack/lint is bumped, unlike the precedent's extra @objectstack/metadata-protocol entry, because this diff changes code in no other package. The verdict was not widened to current_user/input/os/trigger. Code and tests are unchanged from ba3518ed71.",
      "tests": "At head 8aa8af7310, each exit code captured before any pipe: exit 0 :: node scripts/check-changeset-no-major.mjs --self-test; exit 0 :: node scripts/check-changeset-no-major.mjs --base origin/main; exit 0 :: node scripts/check-empty-changeset.mjs --self-test; exit 0 :: node scripts/check-empty-changeset.mjs --base origin/main; exit 0 :: node scripts/check-adr-0087-registration.mjs --self-test ('548 assertions'); exit 0 :: node scripts/check-adr-0087-registration.mjs --base origin/main ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. .changeset/24720-validation-rule-parent.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)'); exit 0 :: pnpm check:changeset-gate-self-tests; exit 0 :: pnpm --filter @objectstack/spec check:spec-changes; exit 0 :: node scripts/check-changeset-fixed.mjs; exit 0 :: pnpm check:pm-changeset-deadline-census; exit 0 :: pnpm check:nul-bytes; exit 0 :: pnpm check:doc-authoring; exit 0 :: pnpm check:issue-citations. The change set's paths are the same three files as at ba3518ed71, so dispatch-gates derives the same 64 families. The code and test readings from the previous report still hold, because no code changed: 12/12 new tests, ablation 9 red / 3 controls green, lint 6956/6956, typecheck 0, and showcase os validate 0 stock / 1 with the probe. check:dts-closure and check:dual-build-cjs-loads stay NOT MEASURED (whole-workspace built-output prerequisite), declared to CI.",
      "mcp_calls": "0",
      "api_writes": "This round: 1 git push (8aa8af7310, not REST). 1 fleet-write relay stroke issue_patch = PATCH /repos/objectstack-ai/objectstack/issues/24763 (body), read back identical. Plus this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/24720/comments via the relay. 0 label writes. Earlier round: pr_create (POST /pulls + POST /issues/24763/assignees) and comment 6283720708.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: validateStackExpressions, the pass os validate runs, returns [] for a validations[] script rule with condition current_user.id != '' (also input.x == 1, os.org.id != '', trigger.x == 1), measured through the built @objectstack/lint. The built @objectstack/objectql evaluateValidationRules, called with an acting user, throws VALIDATION_FAILED 'Unknown variable: current_user — write rejected'. · evidence: ruleScope(record, previous) at rule-validator.ts:4511/4695 passes no user. This is the same family as this card; the fix is to widen it to an allowlist of the roots ruleScope binds. Fold it into that family's closure card · dedupe words: validations rule current_user unbound lint accepts unknown variable current_user root allowlist ruleScope",
        "carrier: whoever lands a metadata-protocol saveMetaItem pin for this verdict (the option precedent had one); outside this card's file surface · noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat hold · domain:spec seat 3 (#18883) · baozhoutao · session session_01D6F9axfoPzR7t8G1J7Ytzz · 2026-10-20T05:03Z · ⛔ Not a claim.

    PR #24763 (head 8aa8af7310) is reviewed and stays a draft.

    Why it is held. This is a narrowing of what os build and the object save door accept: an active: false rule, or an unreachable conditional branch that reads parent, built clean before. Rule 6261447528 therefore applies, and the 「是否提前」 question goes to the maintainer.

    • Triage 6282262481 did not mark the protocol class. The seat saw it only when the dev reported the bump question.
    • The seat put the question to the maintainer in its session chat at 2026-10-20T05:03Z. The answer is recorded here when it comes.

    Out of scope, the same family, for triage: validateStackExpressions accepts a validations[] condition reading current_user / input / os / trigger, and ruleScope(record, previous) binds none of them. Every such write is refused as Unknown variable. The dev's dedupe words: validations rule current_user unbound lint accepts unknown variable current_user root allowlist ruleScope.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision filed · domain:spec seat 3 (#18883) · baozhoutao · session session_01D6F9axfoPzR7t8G1J7Ytzz · 2026-10-20T10:39Z · ⛔ Not a claim.

    Under rule 6261447528, this card's 「是否提前」 question is now on decision card #24854, together with the other two pending domain:spec protocol cards. Nothing on this card moves until the maintainer answers there. The answer is recorded here.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling applied · domain:spec seat 3 (#18883) · baozhoutao · session session_01D6F9axfoPzR7t8G1J7Ytzz · 2026-10-20T14:24Z · ⛔ Not a claim.

    18.0.0 has shipped. The maintainer's standing ruling 「甲」 (#6015 6292005854), applied on #24854, decides this card: no ADR-0087 conversion can rewrite a rule that reads parent.

    The seat confirms triage's derivation.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24720,
      "status": "done",
      "branch": "claude/issue-24720-validations-parent-unbound",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24763",
      "session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
      "premise_still_valid": true,
      "summary": "Patch round on the maintainer ruling 甲 (#6015 comment 6292005854). New head b784487c01: merge of origin/main a7199ef974 (670b2433b7, no rebase or force), then 669007ba72 and b784487c01. validationRuleParentIssue now reports at severity 'warning'. That fails neither os validate nor os build, and the object save door admits the write and lists the finding in advisories. The docblock's 'Why it is refused' became 'Why it is reported' plus 'Why a warning, and when it becomes a refusal': the refusal is planned for 19.0 under 甲, and the ⛔ withdrawal note is kept. The message adds 'This is a warning today; os build and the object save door will refuse it from 19.0.' The tests assert warning, no error findings at either command, an empty errors list at the door, and parity in advisories; the controls are unchanged. The changeset is a patch with 'Clause-②: no', no bang, no BREAKING section and no ADR-0087 marker; it states the 19.0 deferral. A new warning on a fix is a patch under AGENTS.md Post-Task step 3. The PR #24763 body was rewritten to match through the relay (issue_patch, read back identical). It is still a draft and carries the 19.0 deferral note. Runtime untouched; no widening to current_user/input/os/trigger.",
      "tests": "At head b784487c01. exit 0 :: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-expressions.validation-rule-parent.test.ts ('Tests 12 passed (12)'). Ablation A, verdict returns null ('ok mutation landed: anchor 1 -> 0'): 'Tests 9 failed | 3 passed (12)'; the 3 are the controls; 'ok restored: blob == HEAD (eff83c96b2eb)'. Ablation B, severity back to 'error': 'Tests 7 failed | 5 passed (12)', restored blob == HEAD, git diff HEAD empty. exit 0 :: pnpm --filter @objectstack/lint test ('Test Files 151 passed (151) Tests 7000 passed (7000)'). exit 0 :: pnpm --filter @objectstack/lint typecheck. Public door: showcase with the temporary probe rule parent.status == 'paid' on showcase_invoice_line, pnpm --filter @objectstack/example-showcase validate exit 0, 'Validation passed', with the finding printed as a warning 'rule: expression-invalid at object showcase_invoice_line · validation probe_paid_lock'. The probe was restored to blob == HEAD. Changeset gates: exit 0 :: check-changeset-no-major --base origin/main; exit 0 :: check-empty-changeset --base origin/main; exit 0 :: check-adr-0087-registration --base origin/main ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'); exit 0 :: check:changeset-gate-self-tests; exit 0 :: spec check:spec-changes; exit 0 :: check-changeset-fixed; exit 0 :: check:pm-changeset-deadline-census. dispatch-gates --commands at b784487c01 derived 65 families, all run: 62 exit 0. NOT MEASURED: check:dts-closure (exit 1, declaration files absent) and check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET). Both read whole-workspace built output, and this fresh worktree has only the OS_SKIP_DTS closure builds. Also NOT MEASURED: check:type-check-debt (exit 124 at 400s, and again at 580s): a whole-workspace tsc ratchet over the foreground cap. All three are declared to CI.",
      "mcp_calls": "0",
      "api_writes": "This round: 2 git pushes (669007ba72 with the merge, b784487c01; not REST). 1 relay stroke issue_patch = PATCH /repos/objectstack-ai/objectstack/issues/24763 (PR body), read back identical. 1 relay stroke comment = POST /repos/objectstack-ai/objectstack/issues/24720/comments (this os-dev-report). 0 label writes.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: c · reach: validateStackExpressions, the pass os validate runs, returns [] for a validations[] script rule with condition current_user.id != '' (also input.x == 1, os.org.id != '', trigger.x == 1), measured through the built @objectstack/lint. The built @objectstack/objectql evaluateValidationRules, called with an acting user, throws VALIDATION_FAILED 'Unknown variable: current_user — write rejected'. · evidence: ruleScope(record, previous) at rule-validator.ts passes no user. This is the same family as this card; fold it into that family's closure card (it falls under 甲 too: warning first, refusal in 19.0) · dedupe words: validations rule current_user unbound lint accepts unknown variable current_user root allowlist ruleScope",
        "carrier: the 19.0 refusal row for this verdict (severity warning -> error at the line marked in runStackExpressionPasses) is the seat's to file under 甲 rule 2 · noted in PR body and changeset, not filed",
        "carrier: whoever lands a metadata-protocol saveMetaItem pin for this verdict; outside this card's file surface · noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 3 · baozhoutao · session session_01D6F9axfoPzR7t8G1J7Ytzz · 2026-10-20T18:40Z

    Claim 6282610979 ends here; pm:dispatched is removed in this stroke.


    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

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions