Skip to content

A formula field that reads through a reference (record.book.title) passes validateExpression, then reads null on every row: the read path projects only the row's own columns #24197

Closed

Description

@objectstack-fleet

Ruled: 6245544505 · letter A on objectstack-ai/cloud#2925 (batch #362 item 4) · direction (a) · 2026-10-18T04:15Z
The formula read path preloads one hop of the referenced fields a formula names (page-batched, as validation rules do); lint admits one hop and refuses two; a title formula over lookups sorts and searches by its stored value or is declared unsortable — written as an ADR-0079 note. p1 target:v18 stands. pm:blocked → pm:queue in this act.

Source

Read on the framework pin cloud consumes (.objectstack-sha be076dad28a0, frozen tree), while the repo:cloud#2 seat checked the premise of objectstack-ai/cloud#2925 before dispatch. That card asks the AI builder to point a junction object's nameField at a formula composed from its two lookups' titles (「{图书} · {读者} · {借出日期}」). ADR-0079 D3 prescribes a text formula as the composite-title mechanism. Nothing tells an author that such a formula cannot read the other side.

What happens

  1. Authoring accepts it. @objectstack/formula's validateExpression('value', "record.book.title + ' · ' + record.reader.name", { objectName: 'loan', fields, fieldTypes: { book: 'lookup', reader: 'lookup', … }, scope: 'record' }) answers 0 errors and 0 warnings at the pin. This was measured by a probe against the pin's packages/formula/dist.
  2. The read path cannot honour it.
  3. Consequence for a title: a formula nameField written this way shows no title on any record. An AI that follows ADR-0079 D3 literally for a junction object (a loan, a booking, an enrolment) produces it.

Declared ≠ enforced

The expression is accepted where it is declared and never honoured where it runs. This is the same shape #20078 closed for field-level predicates. Which direction closes it is the open question on objectstack-ai/cloud#2925 (its decision analysis follows on that card):

  • (a) the formula read path pins one hop the way validation rules do, so the expression works;
  • (b) the formula pass refuses a read through a reference loudly, with the prescription that names the alternative.

Either way, today's silent null goes.

Reach

Any formula field (not only a nameField) whose expression names record.<lookup>.<field>. The AI builder in cloud is a producer: its prompt tells the model to compose titles as formulas (blueprint-tools.ts, the nameField rule).

Dedupe

Read: #20078 (the predicate slots, closed), #22019 (unregistered function → null, closed), #20082 (can() in a formula, closed), #6674 (formula in searchableFields, closed). None covers a formula expression read through a reference.

Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX). ⛔ Not a claim.

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Reach, one more producer, read at the same pin (be076dad28a0) · repo:cloud#2 · session session_01Cbdek8SKSHxddGqTPZXdqX · 2026-10-17T17:04Z

    The spec's own AI blueprint schema teaches the composed title with a reference in it:

    • packages/spec/src/ai/solution-blueprint.zod.ts:95 (BlueprintFieldSchema.expression) and StrictField.expression (about line 327) give the example "record.order_no + ' · ' + record.customer" "for a composed title".
    • :107 / :338 (nameField) recommend "{order_no} · {customer}".

    customer there is a lookup. The bare spelling reads the stored id, so the title carries a raw key. The traversal spelling (record.customer.name) reads null, as this card's body states. Neither gives the customer's name. Whichever direction this card takes, these describe strings are model-facing and should change with it. Not measured: what a model actually emits from them.

  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 · target:v18 · domain:engine · pm:blocked on the maintainer's ruling on cloud#2925 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T17:58Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Fresh read on origin/main f963d8e2b3:
      • packages/lint/src/validate-expressions.ts:1190's FieldTraversalSlot still covers the three predicate slots only.
      • planFormulaProjection (packages/objectql/src/engine.ts:1659) still projects the row's own columns.
      • So a formula expression reading record.<lookup>.<field> is accepted where it is declared and reads null where it runs.
    • Why p1 + target:v18: declared ≠ enforced, the shape A field-level requiredWhen / readonlyWhen that reads through a lookup (record.account.tier) is accepted at authoring, but the runtime never hydrates it, so since ADR-0137 D2 every write that reaches it is refused #20078 closed for predicates. Either direction is a metadata-protocol change: (a) widens the formula read path, (b) narrows what the lint accepts.
    • Waits on the direction. It is the open question on objectstack-ai/cloud#2925, with the maintainer, seat lean (a). ⛔ No Blocked-by: line is written: under (a), cloud#2925 itself waits on this card, so a closure-gated line would deadlock. Triage releases this card when the ruling is recorded.
    • Scope after the ruling: the chosen direction, the producers named in the repo:cloud#2 seat's comment (the blueprint schema's describe strings, solution-blueprint.zod.ts :95 / :107, and their strict twins), and a pin with an ablation.
    • Lane: domain:engine (packages/objectql read path, or the lint pass under (b)).

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-18T05:07Z · on the Ruled: line (6245544505, letter A on objectstack-ai/cloud#2925, direction (a)) and triage 6238165661's scope. This is the code part; the ruling's ADR-0079 note follows as its own PR (docs/adr/** is Tier H).
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24197-formula-one-hop-read
    Worktree: objectstack-issue-24197
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/objectql/src/engine.ts: the formula read path (planFormulaProjection and the evaluation it feeds) preloads one hop of the referenced fields a formula names, page-batched, as the validation-rule path does (validation/rule-validator.ts, read; a helper it shares may be extracted, not re-implemented).
    • packages/formula/src: the one-hop / two-hop reading of a formula value (validate.ts, relationship-traversal.ts), if the lint's verdict needs it there.
    • Cross-lane, declared to domain:spec on [PM seat] domain:spec — ⏳ vacant #6017: packages/lint/src/validate-expressions.ts, the field-formula pass (judgeFieldFormula): one hop admitted, two hops refused loudly with the prescription. packages/lint/src/rule-explanations.ts only if the refusal needs an entry there (serial with lint: a locale pack's picklists.<name> translations are never checked — an option key that is not in the list, or a list no package declares, passes os lint --strict and is dropped silently at runtime #23566's claim on that file). packages/spec/src/ai/solution-blueprint.zod.ts: the expression and nameField describe strings and their strict twins teach the spelling that now works.
    • The sort and search answer for a title formula over lookups, as the ruling words it (by its stored value, or declared unsortable): measured, then implemented in the engine.
    • Pins beside each, and one changeset per package touched, .changeset/24197-*.md.
    • ⛔ Not docs/adr/** (the ADR-0079 note is its own Tier H PR, after this one). Not cloud (blueprint-tools.ts is cloud's). No new public method, no new error code beyond the lint refusal's rule, no enforcement change elsewhere. Stop on any other breach and explain it in the report.
      Container & model: L, mode:cloud (the read is measured on a live backend over a junction object with two lookups), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
      Clause-②: no (narrowing) (the lint refuses a two-hop read it accepts today; owes one CONTRACT_REVIEW_TIER review before the queue, which packages/spec/src/** owes too)
      Responsibility: the engine's formula read projects only the row's own columns, so record.<lookup>.<field> reads null on every row, while the lint's formula pass admits it (engine.ts planFormulaProjection; validate-expressions.ts) | the platform path that already covers one hop is the validation-rule path (rule-validator.ts); nothing covers a formula value | any formula field over a lookup, today including a composed title; producers: the cloud AI builder's nameField rule and the spec blueprint's own describe strings (comment 6237225524)
      Thread-read: 6238165661
      Serial constraints cleared: no open PR touches objectql/src/engine.ts, packages/formula/src, validate-expressions.ts or solution-blueprint.zod.ts (open PRs' file lists read at this claim). No pm:dispatched claim names them. lint: a locale pack's picklists.<name> translations are never checked — an option key that is not in the list, or a list no package declares, passes os lint --strict and is dropped silently at runtime #23566 (domain:spec) holds packages/lint/src/rule-explanations.ts; spec(ui): retire marketplace:installed-list once objectui#12733 lands: no renderer draws it, yet the page vocabulary and the page lint still accept it #24230 holds two packages/lint component tests only.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T05:13Z: mode:cloud. The dev session is session_01M6VD9XhattUEHKD99C7KUR, titled "⚡ engine #24197 formula reads one hop through a reference; lint admits one hop, refuses two". It builds at the default tier on branch claude/issue-24197-formula-one-hop-read, claim 6245950717.

    • Carried: the Ruled: line (letter A on cloud#2925, direction (a)) verbatim, and triage 6238165661's scope with the producers in comment 6237225524.
    • This dispatch (code part):
      • the formula read path preloads one hop, page-batched, through the validation-rule path's mechanism, under the caller's own read permissions;
      • the lint's field-formula pass admits one hop and refuses two, loudly;
      • the blueprint describe strings teach the spelling that works;
      • sort and search for a title formula over lookups are measured, then implemented as the ruling words them.
    • After it: the ADR-0079 note, as its own Tier H PR. The dev's report carries the proposed text.
    • Delivery: a draft PR saying Part of #24197, then an os-dev-report comment here.

    Generated by Claude Code

  5. 6 remaining items

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · 2026-10-18T09:15Z · part 2 of this card, after PR #24274 (d537a318b) landed the code part: the hook-body title accessor ctx.title() serves the one-hop formula title the read path now serves (record 6247399985 ③.6)
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24197-hook-title-one-hop
    Worktree: objectstack-issue-24197-b
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/objectql/src/record-title.ts (resolveRecordTitle, and a sibling if one is needed) and, only if the title needs it, the formula seam it calls in packages/objectql/src/engine.ts (evaluateFormulaField and the one-hop loader resolveOneHopRelated, reused, never re-implemented).
    • Cross-lane, declared to domain:cli on [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024 and [PM seat] domain:cli · seat 2 — ⏳ vacant #22648: packages/runtime/src/sandbox/body-runner.ts, buildTitleSurface only. Both forms are in scope: ctx.title() (this record, the state the hook fires on) and ctx.title('field') (the related record, read through the body's own ctx.api channel). Each must answer a composed title over lookups that the REST read now answers.
    • Pins beside each, and one changeset per package touched, .changeset/24197-*.md.
    • ⛔ Not plugin-approvals (its recordTitleOf reads the served row, which the read path already computes). Not docs/adr/** (the ADR-0079 note is this card's last part, as its own Tier H PR). No new privilege: every hop is read on the caller's own read channel, transaction-scoped as the body's own ctx.api reads are. Stop on any other breach and explain it in the report.
      Container & model: M, mode:cloud (measured live through a hook body on a junction object), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
      Clause-②: no (a declared capability honoured: the accessor is documented to name the record by its declared title. If a published export of @objectstack/objectql changes, report it, and the seat re-declares this line.)
      Responsibility: buildTitleSurface evaluates a formula title with the synchronous evaluateFormulaField, which preloads no hop, both for this record and when re-deriving the related record's title over the row findOne already served. So a composed title over lookups answers undefined in a hook body while every REST read shows it | the platform path that already serves the hop is the engine read path (PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274) | every hook or action body that calls ctx.title() on a junction object; the exemplar app's bodies compose titles this way (body-runner.ts's own docblock: four of five measured title pointers are formulas)
      Thread-read: 6248094478
      Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim. None touches record-title.ts, sandbox/body-runner.ts or objectql/src/engine.ts.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch (part 2) · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T09:24Z: mode:cloud. The dev session is session_01XpmVCa3CH5g9uCZLy3j587, titled "⚡ engine #24197 (part 2) hook-body ctx.title() serves the one-hop formula title". It builds at the default tier on branch claude/issue-24197-hook-title-one-hop, claim 6248128513.

    • Carried: the Ruled: line verbatim, and what PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274 (d537a318b) shipped: one hop, never two; the caller's own permissions; a missing or hidden related record leaves the formula null with a reason.
    • This dispatch: both forms of the hook-body title accessor. ctx.title() evaluates over the in-flight hook state and follows its foreign keys. ctx.title('field') stops discarding the one-hop title the related findOne already serves.
    • Measured first: which channel serves the hop. It must be the body's own transaction-scoped ctx.api read, with no second connection on SQLite. Also measured: a caller who may not read one related record, and a second hop reached through the related object's own title.
    • Cross-lane: body-runner.ts (buildTitleSurface only), declared to domain:cli as 6248176531 / 6248182027.
    • After it: the ADR-0079 note, as its own Tier H PR. This dev's report adds adr_note_addendum for the hook-body title.
    • Delivery: a draft PR saying Part of #24197, then an os-dev-report comment here.

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24197,
    "status": "done",
    "branch": "claude/issue-24197-hook-title-one-hop",
    "pr": "#24307",
    "session": "session_01XpmVCa3CH5g9uCZLy3j587",
    "premise_still_valid": true,
    "summary": "Premise verified at d537a31: buildTitleSurface evaluated both forms with the synchronous evaluateFormulaField, which preloads no hop, so a title over lookups answered null in a hook body while findOne served it. Added ObjectQL.serveFormulaField(objectName, record, field, context), the read path's own evaluation over a record in hand (planFormulaProjection, resolveFormulaRelated → resolveOneHopRelated(..., 'caller') with its formulaHopReads one-hop guard, resolveFormulaPermissions, formulaFaultSink), and ScopedContext.serveFormulaField, which calls it with the channel's own ExecutionContext (identity, permissions, transaction). record-title.ts gains resolveServedRecordTitle (the async sibling) and servedRecordTitle (reads a served row's title). body-runner's ctx.title() now serves the hop on the channel the installer hands in per call (txState.api ?? ctx.api), and ctx.title('field') reads the title its findOne served instead of re-deriving it. All five hypotheses were confirmed by measurement on real SQLite through QuickJS.",
    "tests": "Head 0d13345 (after merging origin/main): pnpm build --concurrency=2 76/76 exit 0. objectql pnpm test: 439 files / 8280 tests passed. runtime pnpm test: 418 files / 6574 passed, 19 skipped (run on pre-merge head 650581b; see deviations). On the final head, rebuilt: runtime hook-ctx-title-one-hop + hook-ctx-title 20/20, objectql engine-formula-one-hop + record-title 47/47. New pins: packages/runtime/src/sandbox/hook-ctx-title-one-hop.integration.test.ts, 11 tests on a real engine with the real SqlDriver (better-sqlite3, one pooled connection) and real QuickJS, using a lib_loan junction titled record.book.title + ' · ' + record.reader.name. The pins: beforeUpdate 'Dune · Ann' equal to findOne for the same caller. H1: a moved key names 'Emma · Ann'; beforeInsert names the payload's book; afterUpdate names the after-state. MEASUREMENT: +2 reads (lib_book, lib_reader) vs a body that never asks; the book read is where id $in [b1], fields [id,title]. CONTROL: an own-column title costs 0 reads. H2 owned transaction: both hop reads ran on the body's transaction handle. H2 joined: every read ran on the after-hook unit of work's one handle. H4: an RLS-hidden reader gives ctx.title() null and findOne title null, with no partial string and no hidden name; the SYSTEM control gives 'Dune · Hidden Person' from both. H5: a second hop answers null from both, with no lib_shelf read. H3: ctx.title('book') answers 'Dune @ A-1', equal to findOne. Also 8 serveFormulaField pins in packages/objectql/src/engine-formula-one-hop.test.ts. Ablations used scripts/ablation-replace.mjs; each landing was proven by an anchor count of 1 → 0 and a blob move, and each restore by blob == HEAD with git diff HEAD empty. A1 (served path → sync resolveRecordTitle) reds 8 of 11 runtime pins. A2 (channel → the hook's base engineCtx.api) deadlocks the owned-transaction H2 pin: 'Acquire connection error: aborted', test timeout at 30000ms. A3 (related form re-derives) reds H3: 'null' vs 'Dune @ A-1'. A4 (the engine hop read as SYSTEM) reds exactly the caller-permission pin. A5 (ScopedContext drops transaction) reds exactly the transaction pin. A1-A3 mutate runtime src, which the suite imports directly; A4-A5 ran on the objectql suite, which imports ./engine.js from src, so no dist leg was owed.",
    "gates": "Local, on 0d13345: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 102 commands. All 102 ran with the exit captured before any pipe: 102 x exit 0. --ran printed '102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED (a DERIVED zero — all 102 recorded an exit code and none of them is 3)'. The first sweep, on 650581b before the full build, had 5 exit-3 PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity: CLI and dist not built). Those were not counted as measurements; after the full build all were exit 0. Typecheck exit 0 for each touched package: objectql, runtime and cli (each script runs check:test-typecheck over tsconfig.test.json). check:platform-checklist was not run, since no exported symbol was renamed or removed. CI check-runs on PR #24307 head 0d13345 at report time: 22 in_progress, 2 completed (success/skipped). Not waited on.",
    "mcp_calls": "0 — no MCP GitHub tool was called; reads used gh api (REST GET)",
    "api_writes": "2 relay writes plus 1 git push. (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24307/assignees (os-project-manager); read-back 9777/9777 bytes identical. (2) this os-dev-report: POST /repos//issues/24197/comments. git push to claude/issue-24197-hook-title-one-hop (not a REST write).",
    "line_budget": "none set; diff vs origin/main (+/-): .changeset/24197-objectql-serve-formula-field.md +13/-0 · .changeset/24197-runtime-hook-title-one-hop.md +13/-0 · content/docs/automation/hook-bodies.mdx +16/-9 · packages/cli/src/utils/extract-hook-body.ts +2/-1 · packages/objectql/src/engine-formula-one-hop.test.ts +84/-3 · packages/objectql/src/engine.ts +66/-0 · packages/objectql/src/index.ts +3/-1 · packages/objectql/src/record-title.ts +65/-0 · packages/runtime/src/sandbox/body-runner.ts +48/-28 · packages/runtime/src/sandbox/hook-ctx-title-one-hop.integration.test.ts +339/-0 · packages/runtime/src/sandbox/hook-ctx-title.integration.test.ts +8/-3 · packages/runtime/src/sandbox/quickjs-runner.ts +4/-1 · packages/runtime/src/sandbox/script-runner.ts +9/-6",
    "files_changed": [
    ".changeset/24197-objectql-serve-formula-field.md",
    ".changeset/24197-runtime-hook-title-one-hop.md",
    "content/docs/automation/hook-bodies.mdx",
    "packages/cli/src/utils/extract-hook-body.ts",
    "packages/objectql/src/engine-formula-one-hop.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/index.ts",
    "packages/objectql/src/record-title.ts",
    "packages/runtime/src/sandbox/body-runner.ts",
    "packages/runtime/src/sandbox/hook-ctx-title-one-hop.integration.test.ts",
    "packages/runtime/src/sandbox/hook-ctx-title.integration.test.ts",
    "packages/runtime/src/sandbox/quickjs-runner.ts",
    "packages/runtime/src/sandbox/script-runner.ts"
    ],
    "deviations": [
    "Published export of @objectstack/objectql widened (the claim asks this to be reported). index.ts adds resolveServedRecordTitle, servedRecordTitle and the type FormulaServingChannel. The exported classes ObjectQL and ScopedContext each gain a public serveFormulaField. Nothing is removed or renamed. The PR body carries the claim's Clause-② line verbatim ('Clause-②: no (...)') and names this widening for the seat to re-declare; see open_questions. The objectql changeset is graded minor and the runtime one patch.",
    "Files outside the claim's File surface. All are comment-only or doc-only, and the pin sweep forced each because it stated the old cost: content/docs/automation/hook-bodies.mdx (three table rows and one bullet pair said ctx.title() performs no read), packages/runtime/src/sandbox/quickjs-runner.ts (VM-wire comment), packages/runtime/src/sandbox/script-runner.ts (ScriptContext.title docblock), packages/cli/src/utils/extract-hook-body.ts (one comment). Also the objectql test file engine-formula-one-hop.test.ts (pins beside the engine change) and packages/objectql/src/index.ts (the exports). No behaviour changed outside record-title.ts, engine.ts and body-runner.ts.",
    "The fix landed where the dispatch expected, with one addition: ScopedContext.serveFormulaField in engine.ts. The engine's loader is reached with the channel's own ExecutionContext, which is private to ScopedContext, so the method lives on the channel. Reading the hop through the channel's object(...).find instead would rebuild the query object and lose the identity-keyed formulaHopReads marker, so the related object's formulas would take a second hop.",
    "Worktree: the shared primary checkout was already on the designated branch at origin/main with no commits, so git worktree add -b could not reuse the name. The primary checkout was detached (clean, no work), the empty local branch was deleted, and the branch was recreated in ../objectstack-issue-24197-b from origin/main d537a31 before any edit. Pushed empty first.",
    "Runtime full pnpm test ran on the pre-merge head 650581b. The merge of origin/main (2 commits: spec ui/page and metadata-protocol changes, none in objectql/runtime/cli sources) was followed by a full build, the touched suites (20/20 and 47/47), the three typechecks and the full 102-gate sweep on 0d13345, but not a second full runtime suite. The merge commit was also amended once locally (no content change) before its first push.",
    "Attribution: the harness reminder asked for a Co-Authored-By trailer carrying a model name and a different PR footer. AGENTS.md requires the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the session-URL PR footer, so those were used.",
    "Not subscribed to PR activity, per os-dev.md (the dispatch contract outranks the platform's auto-subscribe posture)."
    ],
    "open_questions": [
    {
    "question": "H3: how does ctx.title('field') keep the minimal-embedder guard without discarding a served one-hop title?",
    "options": [
    "A read the title the channel's findOne served off the row, never re-derive (shipped: servedRecordTitle)",
    "B re-derive asynchronously with serveFormulaField over the returned row (one more hop read per traversed reference, the same answer)",
    "C prefer the served value when the key is present, re-derive when it is absent"
    ],
    "recommendation": "A. Business need: every in-repo channel computes formulas (ScopedContext → engine.findOne, ql.object, ql.createContext, and the repo facade over ql.find), so the guard defended a hypothetical embedder; measured, the re-derivation was what answered null. Long-term: one evaluation on the read path, no second pass. AI-error axis: C reads an absent key as 'not computed', but field-level security deletes the key (field-masker.ts), so C would compute a title FLS removed, a silent leak shape; B doubles the hop cost for the same answer. Startup focus: A is the smallest surface."
    },
    {
    "question": "ctx.title() with no argument now reads when the title formula reads a hop. Should it still need no capability?",
    "options": [
    "A no capability (shipped): the reads are the declared title's, made on the body's own channel under the caller's permissions",
    "B require api.read whenever the object's title formula traverses a reference"
    ],
    "recommendation": "A. Business need: the accessor exists so naming a record is cheaper than writing record.id; under B a body's required grant would change when another object's title formula changes, which is a run-time refusal far from its cause. Long-term: the capability gates the body's own reach for another record (ctx.title('field')), not a declaration's evaluation. AI-error axis: B makes the CLI extractor's static inference (bare ctx.title() needs nothing) wrong in a way no static check can see. Startup focus: no new gate."
    },
    {
    "question": "A channel without serveFormulaField (the UnscopedHookApi a runAs:'user' hook gets with no user, or a foreign api): what does ctx.title() answer?",
    "options": [
    "A the synchronous path (shipped): an own-column title resolves, a title over a reference answers null",
    "B throw"
    ],
    "recommendation": "A. That channel cannot read at all, and the title is exactly what the record's own columns give. This keeps today's behaviour for those channels, so it is not a new tolerance. B would refuse a body that never asked for a read."
    },
    {
    "question": "Clause-② for this PR: the claim's line is 'no', and the objectql published index widens by three exports plus a public method on two exported classes. What should the line read?",
    "options": [
    "A 'Clause-②: yes' (widening, no narrowing; the objectql changeset is already minor)",
    "B keep 'no'"
    ],
    "recommendation": "A, per clause2-line.mjs's question ('does this card widen the accept set or the public surface?') and AGENTS.md ('no (widening)' is malformed). The seat re-declares; the dev did not PATCH the PR body (os-dev rule 3). Edit for the seat: replace the PR body line beginning 'Clause-②: no' with the re-declared line."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed: a hook body's ctx.title() hop is the engine's read, so a title formula reading through a lookup to a stored-metadata family table is not refused by the body api's family-read refusal (refuseStoredMetadataBodyReads). It is read under the caller's own permissions, the same value REST serves, and no in-repo object composes such a title. Recorded in the PR's Acceptance notes."
    ],
    "adr_note_addendum": "In a hook or action body, ctx.title() evaluates the object's title formula over the in-flight record state the hook fires on, and serves its one hop through a reference the way a read does: one batched read per reference field, on the body's own ctx.api channel (the caller's permissions, and the open transaction's connection). So a title over lookups names the related records the in-flight foreign keys point at, and a related record the caller may not read, a missing one, or a second hop leaves the title null, as the REST read of the same record shows no title. ctx.title('field') answers the related record's title exactly as its findOne served it, its own one-hop title included."
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · 2026-10-18T11:01Z · re-declaration of claim 6248128513 for part 2, now draft PR #24307 (head 0d133459d7). The Clause-② line changes and the file surface widens, both as the dev's report 6248949940 names them. The branch, the session and the scope are unchanged.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev ran as session_01XpmVCa3CH5g9uCZLy3j587)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24197-hook-title-one-hop
    Worktree: objectstack-issue-24197-b
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/objectql/src/record-title.ts: resolveServedRecordTitle, the async sibling of resolveRecordTitle, and servedRecordTitle.
    • packages/objectql/src/engine.ts: ObjectQL.serveFormulaField and ScopedContext.serveFormulaField. The read path's own evaluation over a record in hand reuses planFormulaProjection, resolveFormulaRelated → resolveOneHopRelated and its formulaHopReads guard. It is not re-implemented.
    • packages/objectql/src/index.ts: the exports below.
    • Cross-lane, declared to domain:cli on [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024 and [PM seat] domain:cli · seat 2 — ⏳ vacant #22648:
      • packages/runtime/src/sandbox/body-runner.ts, buildTitleSurface;
      • comment-only edits to the VM-wire comment in packages/runtime/src/sandbox/quickjs-runner.ts, the ScriptContext.title docblock in script-runner.ts, and one comment in packages/cli/src/utils/extract-hook-body.ts. The pin sweep forced each one, because it stated the old cost.
    • Cross-lane, declared to domain:devx on [PM seat] domain:devx · seat 2 — 🟢 os-sales · session_016PVjjuVPte5XEJvaQx1E4t · serial (maintainer) #20163 and [PM seat] domain:devx @ objectstack — ⏳ vacant #6023: content/docs/automation/hook-bodies.mdx, the rows and bullets that said ctx.title() performs no read.
    • Pins beside each, and two changesets, .changeset/24197-*.md (objectql minor, runtime patch).
    • ⛔ Unchanged: not plugin-approvals, not docs/adr/**, no new privilege. The hop is read on the body's own channel (txState.api ?? ctx.api), under that channel's ExecutionContext and transaction.
      Container & model: M, mode:cloud, model: TIER_DEFAULT (unchanged)
      Clause-②: yes (widening) (the @objectstack/objectql index gains resolveServedRecordTitle, servedRecordTitle and the type FormulaServingChannel, and the exported classes ObjectQL and ScopedContext each gain a public serveFormulaField; nothing is removed or renamed. A TIER_DEFAULT review of record by this lane seat is owed before the queue.)
      Responsibility: unchanged from claim 6248128513.
      Thread-read: 6248949940
      Serial constraints cleared: open PRs' file lists were read at this re-declaration, and none touches a file above. This seat's metadata-protocol / objectql: a search term on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 dev (claim 6248247765, no PR yet) works in engine.ts's expandSearchOnAst, a different region. Whichever lands second merges main and re-runs the touched packages' build and typecheck.

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24307 comment 6249354253 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T11:27Z.

    • Review: the TIER_DEFAULT record of record is 6249342777: PASS, on the declaration face, at head 0d133459d7.
    • Declaration: re-declared as Clause-②: yes (widening) in claim 6249052664, because new @objectstack/objectql exports were added.
    • Queue: PR fix(runtime,objectql): ctx.title() answers a title formula over lookups — the one hop is served on the body's own channel #24307 is marked ready and set to auto-merge through the queue.
    • This card stays open: the PR says Part of. One part remains: the ADR-0079 note, as its own Tier H PR. Its draft combines:
      • part 1's adr_note_text, amended with record 6247399985's ③.4 and ③.8b;
      • part 2's adr_note_addendum (report 6248949940);
      • record 6249342777's ③.7.

    The out-of-scope finding from this part is filed as #24316, a security card that names only the category and the position.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (part 2) · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T11:56Z

    Probe Reading
    Queue PR #24307 entered the merge queue at 2026-10-18T11:28Z, after ACCEPT 6249354253 and the TIER_DEFAULT record 6249342777 (PASS, declaration face). It left the queue merged at 2026-10-18T11:55Z. No gh-readonly-queue/main/pr-24307-* branch remains after the merge.
    origin/main 1bf5f6e17c is on origin/main (git merge-base --is-ancestor: exit 0). It carries PR #24307's same 13 files, +670 / −52.
    Cards closed The PR says Part of #24197, so this card stays open and pm:dispatched, as intended. No other domain:engine card closed during the queue run.

    Now on main:

    • In a hook or action body, ctx.title() evaluates the object's title formula over the in-flight record state. It serves the formula's one hop through a reference the way a read does: one batched read per reference field, on the body's own ctx.api channel, under that channel's identity and on its open transaction.
    • A related record the caller may not read, a missing one, or a second hop leaves the title null, as the REST read of the same record shows no title.
    • ctx.title('field') answers the related record's title exactly as its findOne served it.
    • @objectstack/objectql exports resolveServedRecordTitle, servedRecordTitle and the type FormulaServingChannel. ObjectQL and ScopedContext each gain serveFormulaField.

    Remaining on this card: the ADR-0079 note, as its own Tier H PR (docs/adr/**, human merge). Its draft combines:

    • part 1's adr_note_text, amended with record 6247399985's ③.4 and ③.8b;
    • part 2's adr_note_addendum (report 6248949940);
    • record 6249342777's ③.7.

    This seat dispatches it next, under a new claim.

    The out-of-scope finding is #24316. The dev session session_01XpmVCa3CH5g9uCZLy3j587 is archived.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · 2026-10-18T12:00Z · part 3, the last part of this card, after PR #24307 (1bf5f6e17c) landed part 2. It writes the ruling's ADR-0079 note: one dated addendum in docs/adr/0079-record-display-name.md that states what parts 1 and 2 shipped.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24197-adr-0079-note
    Worktree: objectstack-issue-24197-c
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch (part 3, last) · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T12:03Z: mode:cloud. The dev session is session_01RVjnmZ6Jv6NcLDwow6RpNi, titled "⚡ engine #24197 (part 3) ADR-0079 addendum: a title formula over lookups reads one hop, is stored nowhere". It builds at the default tier on branch claude/issue-24197-adr-0079-note, claim 6249662765.


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24197,
    "status": "done",
    "branch": "claude/issue-24197-adr-0079-note",
    "pr": "#24323",
    "session": "session_01RVjnmZ6Jv6NcLDwow6RpNi",
    "premise_still_valid": true,
    "summary": "Claim 6249662765 names this branch (verified before any edit). Appended one section to docs/adr/0079-record-display-name.md, '## Addendum (2026-10-18, #24197) — a formula title reads one hop through a reference, and is not sortable or searchable', plus one '- Addenda:' pointer line in the header block (ADR-0048's spelling). The addendum names ruling 6245544505 (cloud#2925 letter A) and PRs #24274 / #24307, quotes the Ruled line verbatim, and states nine facts in three groups (read path; hook and action bodies; sort and search), each checked in code on main at the merge da22e1d and mapped to file and symbol in the PR body's verification record. It qualifies the Alternatives first bullet and D1's 'real STORED' in its own text; nothing above it is edited. 'Shown on every surface' is not carried: the addendum names the two verified surfaces and makes no statement about other title consumers. It names neither #24284 nor #24316 and describes neither path.",
    "tests": "Docs-only; no package touched, so no build closure, test suite or typecheck is owed. The one build run was a gate prerequisite: pnpm exec turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint under os-verify-lock, 'VERDICT command-exit 0', 4/4 tasks. Heading slug: github-slugger's slug of the new heading equals the pointer's fragment (computed). No ablation (no tested behaviour changed).",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 2950277 and again on the merged head da22e1d: the same 19 commands both times (dispatch's count: 19). On da22e1d, each exit captured before any pipe: 19 x exit 0 — check-adr-links.mjs (+ --self-test), check-adr-symbol-anchors.mjs (+ --self-test), check-ci-filter-parity.mjs, check-closing-keyword-parity.mjs (+ --self-test), check-comment-mask-corpus.mjs, lint check:doc-formula-expressions, check:adr-anchors, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:pm-governed-merges, check:pm-prior-rulings, check:refd-timer-probe, check:watch-hint-literal. --ran (exit 0): '19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)'. On the first sweep (2950277), check:doc-formula-expressions exited 3 (PREREQUISITE NOT MET, formula and lint unbuilt in the fresh worktree); not counted as a measurement; exit 0 after the build, and 0 on da22e1d. Pre-push check:commit-card-trailers passed. CI check-runs on PR #24323: not read at report time (in_progress is the honest value); not waited on.",
    "mcp_calls": "5 — read-only: mcp__github__issue_read (get) and mcp__github__issue_read (get_comments) on #24197; mcp__github__pull_request_read (get_comments) on #24274 and on #24307; mcp__github__pull_request_read (get) on #24323 (label/draft/assignee read-back). No MCP write tool.",
    "api_writes": "2 relay strokes plus 1 git push. (1) pr_create via scripts/pm/fleet-write/dispatch.mjs (run 39126957245, success): POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24323/assignees (os-project-manager) + POST /repos//issues/24323/labels (skip-changeset); body read back 8919/8919 bytes identical; labels ['skip-changeset'], assignees ['os-project-manager'], draft true read back via MCP get. (2) this os-dev-report: POST /repos//issues/24197/comments. git push to claude/issue-24197-adr-0079-note (empty branch first, then 2950277, then da22e1d; not REST writes).",
    "line_budget": "none set; diff vs merge base 2b7fc52: docs/adr/0079-record-display-name.md +77/-0 (1 header pointer line + 76 lines of appended addendum, separator included)",
    "files_changed": ["docs/adr/0079-record-display-name.md"],
    "deviations": [
    "Worktree: the shared primary checkout was already on the designated branch name at an older origin/main with no commits of its own, so git worktree add -b could not reuse the name. The primary checkout was detached (clean), the empty local branch deleted, and the branch recreated in ../objectstack-issue-24197-c from origin/main 2ce2e36 before any edit; the empty branch was pushed first.",
    "PR bodies of #24274 and #24307 were not read in full: their records (6247399985, 6249342777), the seat's ACCEPTs (6247718723, 6249354253), both reports and both landing comments were read in full, and every statement was verified against code on main, not against those bodies.",
    "Attribution: the harness reminder asked for a 'Co-Authored-By: Claude Opus 5.5' trailer and a different PR footer. AGENTS.md requires the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the session-URL PR footer, so those were used.",
    "Not subscribed to PR activity, per os-dev.md (the dispatch contract outranks the platform's auto-subscribe posture).",
    "Worktree removed after the PR opened (node_modules deleted, git worktree remove without --force, exit 0); the branch tip da22e1d equals origin."
    ],
    "open_questions": [
    {
    "question": "'Shown on every surface': verify every title surface, or name the verified ones?",
    "options": ["A name the two surfaces verified (engine read path; hook-body ctx.title()) and state that the addendum makes no statement about other title consumers (shipped)", "B verify global search hits and plugin-approvals recordTitleOf too and keep 'every surface'"],
    "recommendation": "A. Business need: the junction-object title is read through the data read and ctx.title(), both verified. Long-term: a decision record claiming more than its verification invites the declared-not-enforced drift this card closed. AI-error axis: recordTitleOf reads ctx.previous[field], whose formula materialisation was not established here, so 'every' could be false and an AI would rely on it. Startup focus: no new verification surface; the two unverified consumers are named in Acceptance notes only."
    },
    {
    "question": "The addendum also qualifies D1 ('real STORED field' includes formula) in its own text, beyond the mandated Alternatives qualification. Keep?",
    "options": ["A keep one bullet: a formula is a declared field but no column holds its value (shipped)", "B qualify the Alternatives bullet only"],
    "recommendation": "A. The claim fence permits a qualification in the addendum's own text; D1's 'STORED' contradicts item 9 ('stored nowhere') on the page an AI reads first, and leaving the contradiction unmarked is the trap. No D1 text is edited."
    },
    {
    "question": "Two details from the sources were left out as not re-verified at this head: the metadata save door's 422 on a two-hop formula, and the optional-read fallback/plain-read fault split on an empty reference. Add them?",
    "options": ["A leave out (shipped): the addendum says the lint refuses and an empty reference is evaluated", "B add both after a fresh measurement"],
    "recommendation": "A. Every sentence that stays is checked in code; the omitted details are pinned in #24274's suites and do not change the decision. Startup focus: fewer claims to keep true."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed: other consumers of a record's title (global search hit titles in metadata-protocol searchAll; plugin-approvals recordTitleOf over ctx.previous) were not verified for a title over lookups; recorded in the PR's Acceptance notes, and the addendum makes no statement about them."
    ]
    }


    Generated by Claude Code

  15. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Rework (part 3) · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T12:58Z.


    Generated by Claude Code

  16. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24197,
    "status": "done",
    "round": "rework 1 (record 6250184223 FAIL on ①.10)",
    "branch": "claude/issue-24197-adr-0079-note",
    "pr": "#24323",
    "session": "session_01RVjnmZ6Jv6NcLDwow6RpNi",
    "head": "73fd87521c8e5898b93c08aab6ab71deb28fc3d0",
    "summary": "Record 6250184223 read in full. ①.10 verified at main before editing: buildTitleSurface is called once, on the hook face (body-runner.ts line 371); the hook context's title carries the comment '[#11293] Hook face only. The action face …' (line 1171); quickjs-runner.ts installs ctx.title only for 'hook bodies; the action face does not' (lines 845–848). Fix commit 6d6ecfe, then merge of origin/main 73fd875 (the merge touched none of the files the addendum's statements rest on). Items 5–8 re-read: none says 'action' (the only substring hit is 'transaction'). PR body not touched.",
    "changed_lines": [
    "heading: 'Hook and action bodies (packages/runtime/src/sandbox/body-runner.ts):' → 'Hook bodies (packages/runtime/src/sandbox/body-runner.ts, the hook face):'",
    "provenance (①.11 nit): 'It states what those two changes put on main and decides nothing more' → 'It states what is on main after those two changes and decides nothing more'"
    ],
    "gates": "On 73fd875: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 19 commands, identical to the previous round's list. All 19 ran (after a fresh install and the formula+lint build under os-verify-lock, VERDICT command-exit 0), each exit captured before any pipe: 19 x exit 0. --ran (exit 0): '19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)'. Pre-push check:commit-card-trailers passed. CI on the new head: not read, not waited on.",
    "line_budget": "none set; net diff vs origin/main unchanged in size: docs/adr/0079-record-display-name.md +77/-0; this round +2/-2 on that file",
    "files_changed": ["docs/adr/0079-record-display-name.md"],
    "mcp_calls": "This round 1 — mcp__github__pull_request_read (get_comments) on #24323, to read record 6250184223. No MCP write tool.",
    "api_writes": "This round 1 — POST /repos//issues/24197/comments (this report, via the fleet-write relay); plus one git push to claude/issue-24197-adr-0079-note (73fd875; not a REST write).",
    "deviations": [],
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  17. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24323 comment 6250544233 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T13:36Z. Tier H: the record carries the maintainer quick-read on top.

    • Review: the CONTRACT_REVIEW_TIER delta record 6250527790 on head 73fd87521c is PASS. It supersedes FAIL 6250184223: the heading now names hook bodies only, and the provenance wording is exact.
    • Waiting on a person: needs-user-decision is on the PR, and reviews are requested from os-zhuang and hotlong. ⛔ The seat never approves or merges. After a governed approver's APPROVED, the seat lands it, and Fixes #24197 closes this card.

    Generated by Claude Code

  18. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (part 3, the last) · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T13:52Z

    Probe Reading
    Approval Tier H (docs/adr/**). os-zhuang, a governed approver (GOVERNED_APPROVERS), APPROVED head 73fd87521c at 2026-10-18T13:40Z. That came after the CONTRACT_REVIEW_TIER delta record 6250527790 (PASS, superseding FAIL 6250184223) and the seat's Tier H record 6250544233.
    Queue It entered the merge queue at 2026-10-18T13:40Z and left it merged at 2026-10-18T13:51Z. No gh-readonly-queue/main/pr-24323-* branch remains.
    origin/main 68a090fb97 is on origin/main (git merge-base --is-ancestor: exit 0). It carries PR #24323's one file, docs/adr/0079-record-display-name.md, +77.
    Card Closed completed through Fixes #24197; pm:dispatched is cleared.

    The card, end to end:

    1. PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274 (d537a318b): a formula reads one hop through a reference field; the lint admits one hop and refuses two; a title formula over lookups is stored nowhere, so it is not sortable or searchable.
    2. PR fix(runtime,objectql): ctx.title() answers a title formula over lookups — the one hop is served on the body's own channel #24307 (1bf5f6e17c): a hook body's ctx.title() serves that hop on the body's own channel.
    3. PR docs(adr-0079): addendum — a formula title reads one hop through a reference, and is not sortable or searchable #24323 (68a090fb97): the ruling's ADR-0079 addendum, which states both.

    Filed out of this card: #24284 (landed as PR #24320) and #24316 (security, in build). The dev session session_01RVjnmZ6Jv6NcLDwow6RpNi is archived.


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions