Repository navigation
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
Description
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
- Authoring accepts it.
@objectstack/formula'svalidateExpression('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'spackages/formula/dist.- The lint's formula pass (
packages/lint/src/validate-expressions.ts,judgeFieldFormula, about line 2720) calls it with notraversalHydration. - The A field-level
requiredWhen/readonlyWhenthat 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 refusal of a read through a reference coversFieldTraversalSlot = 'requiredWhen' | 'readonlyWhen' | 'option visibleWhen'only (line 1190). A fieldexpressionis not in it.
- The lint's formula pass (
- The read path cannot honour it.
planFormulaProjection(packages/objectql/src/engine.ts, about line 1690) projects this object's own stored columns for a formula. A reference column holds an id, sorecord.book.titlemeets a string.- Only the validation-rule path pins one hop before evaluating (
packages/objectql/src/validation/rule-validator.tswithanalyzeRelationshipTraversals). - The read writes
nullfor the field and reports the fault to the sink (engine.ts, about line 2603, formula: the metadata save door stores a formula that calls an unregistered function (sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019). That happens on every row.
- Consequence for a title: a formula
nameFieldwritten 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
objectstack-fleet commentedon Oct 17, 2026
Reach, one more producer, read at the same pin (
be076dad28a0) ·repo:cloud#2· sessionsession_01Cbdek8SKSHxddGqTPZXdqX· 2026-10-17T17:04ZThe 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) andStrictField.expression(about line 327) give the example"record.order_no + ' · ' + record.customer""for a composed title".:107/:338(nameField) recommend"{order_no} · {customer}".customerthere 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.objectstack-fleet commentedon Oct 17, 2026
Triage: p1 ·
target:v18·domain:engine·pm:blockedon the maintainer's ruling on cloud#2925 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T17:58ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/mainf963d8e2b3:packages/lint/src/validate-expressions.ts:1190'sFieldTraversalSlotstill covers the three predicate slots only.planFormulaProjection(packages/objectql/src/engine.ts:1659) still projects the row's own columns.expressionreadingrecord.<lookup>.<field>is accepted where it is declared and readsnullwhere it runs.target:v18: declared ≠ enforced, the shape A field-levelrequiredWhen/readonlyWhenthat 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.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.repo:cloud#2seat's comment (the blueprint schema's describe strings,solution-blueprint.zod.ts:95/:107, and their strict twins), and a pin with an ablation.domain:engine(packages/objectqlread path, or the lint pass under (b)).Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
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 asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-formula-one-hop-readWorktree:
objectstack-issue-24197Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.ts: the formula read path (planFormulaProjectionand 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 formulavalue(validate.ts,relationship-traversal.ts), if the lint's verdict needs it there.domain:specon [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.tsonly if the refusal needs an entry there (serial with lint: a locale pack'spicklists.<name>translations are never checked — an option key that is not in the list, or a list no package declares, passesos lint --strictand is dropped silently at runtime #23566's claim on that file).packages/spec/src/ai/solution-blueprint.zod.ts: theexpressionandnameFielddescribe strings and their strict twins teach the spelling that now works..changeset/24197-*.md.docs/adr/**(the ADR-0079 note is its own Tier H PR, after this one). Not cloud (blueprint-tools.tsis 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'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (the lint refuses a two-hop read it accepts today; owes one
CONTRACT_REVIEW_TIERreview before the queue, whichpackages/spec/src/**owes too)Responsibility: the engine's formula read projects only the row's own columns, so
record.<lookup>.<field>readsnullon every row, while the lint's formula pass admits it (engine.tsplanFormulaProjection;validate-expressions.ts) | the platform path that already covers one hop is the validation-rule path (rule-validator.ts); nothing covers a formulavalue| any formula field over a lookup, today including a composed title; producers: the cloud AI builder'snameFieldrule 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.tsorsolution-blueprint.zod.ts(open PRs' file lists read at this claim). Nopm:dispatchedclaim names them. lint: a locale pack'spicklists.<name>translations are never checked — an option key that is not in the list, or a list no package declares, passesos lint --strictand is dropped silently at runtime #23566 (domain:spec) holdspackages/lint/src/rule-explanations.ts; spec(ui): retiremarketplace:installed-listonce objectui#12733 lands: no renderer draws it, yet the page vocabulary and the page lint still accept it #24230 holds twopackages/lintcomponent tests only.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T05:13Z:mode:cloud. The dev session issession_01M6VD9XhattUEHKD99C7KUR, titled "⚡ engine #24197 formula reads one hop through a reference; lint admits one hop, refuses two". It builds at the default tier on branchclaude/issue-24197-formula-one-hop-read, claim 6245950717.Ruled:line (letter A on cloud#2925, direction (a)) verbatim, and triage 6238165661's scope with the producers in comment 6237225524.Part of #24197, then anos-dev-reportcomment here.Generated by Claude Code
6 remaining items
objectstack-fleet commentedon Oct 18, 2026
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 accessorctx.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 asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-hook-title-one-hopWorktree:
objectstack-issue-24197-bDomain:
domain:engineSeat:
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 inpackages/objectql/src/engine.ts(evaluateFormulaFieldand the one-hop loaderresolveOneHopRelated, reused, never re-implemented).domain:clion [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024 and [PM seat] domain:cli · seat 2 — ⏳ vacant #22648:packages/runtime/src/sandbox/body-runner.ts,buildTitleSurfaceonly. Both forms are in scope:ctx.title()(this record, the state the hook fires on) andctx.title('field')(the related record, read through the body's ownctx.apichannel). Each must answer a composed title over lookups that the REST read now answers..changeset/24197-*.md.plugin-approvals(itsrecordTitleOfreads the served row, which the read path already computes). Notdocs/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 ownctx.apireads 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'sdispatch-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/objectqlchanges, report it, and the seat re-declares this line.)Responsibility:
buildTitleSurfaceevaluates a formula title with the synchronousevaluateFormulaField, which preloads no hop, both for this record and when re-deriving the related record's title over the rowfindOnealready served. So a composed title over lookups answersundefinedin 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 callsctx.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:dispatchedclaims were read at this claim. None touchesrecord-title.ts,sandbox/body-runner.tsorobjectql/src/engine.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Dispatch (part 2) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T09:24Z:mode:cloud. The dev session issession_01XpmVCa3CH5g9uCZLy3j587, titled "⚡ engine #24197 (part 2) hook-body ctx.title() serves the one-hop formula title". It builds at the default tier on branchclaude/issue-24197-hook-title-one-hop, claim 6248128513.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 formulanullwith a reason.ctx.title()evaluates over the in-flight hook state and follows its foreign keys.ctx.title('field')stops discarding the one-hop title the relatedfindOnealready serves.ctx.apiread, 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.body-runner.ts(buildTitleSurfaceonly), declared todomain:clias 6248176531 / 6248182027.adr_note_addendumfor the hook-body title.Part of #24197, then anos-dev-reportcomment here.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
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.--ranprinted '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 -bcould 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 testran 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
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 6 · 2026-10-18T11:01Z · re-declaration of claim 6248128513 for part 2, now draft PR #24307 (head
0d133459d7). TheClause-②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 assession_01XpmVCa3CH5g9uCZLy3j587)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-hook-title-one-hopWorktree:
objectstack-issue-24197-bDomain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/record-title.ts:resolveServedRecordTitle, the async sibling ofresolveRecordTitle, andservedRecordTitle.packages/objectql/src/engine.ts:ObjectQL.serveFormulaFieldandScopedContext.serveFormulaField. The read path's own evaluation over a record in hand reusesplanFormulaProjection,resolveFormulaRelated→resolveOneHopRelatedand itsformulaHopReadsguard. It is not re-implemented.packages/objectql/src/index.ts: the exports below.domain:clion [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;packages/runtime/src/sandbox/quickjs-runner.ts, theScriptContext.titledocblock inscript-runner.ts, and one comment inpackages/cli/src/utils/extract-hook-body.ts. The pin sweep forced each one, because it stated the old cost.domain:devxon [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 saidctx.title()performs no read..changeset/24197-*.md(objectql minor, runtime patch).plugin-approvals, notdocs/adr/**, no new privilege. The hop is read on the body's own channel (txState.api ?? ctx.api), under that channel'sExecutionContextand transaction.Container & model:
M,mode:cloud,model: TIER_DEFAULT(unchanged)Clause-②: yes (widening) (the
@objectstack/objectqlindex gainsresolveServedRecordTitle,servedRecordTitleand the typeFormulaServingChannel, and the exported classesObjectQLandScopedContexteach gain a publicserveFormulaField; nothing is removed or renamed. ATIER_DEFAULTreview 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
searchterm 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 inengine.ts'sexpandSearchOnAst, a different region. Whichever lands second mergesmainand re-runs the touched packages' build and typecheck.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24307 comment 6249354253 ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T11:27Z.TIER_DEFAULTrecord of record is 6249342777: PASS, on the declaration face, at head0d133459d7.Clause-②: yes (widening)in claim 6249052664, because new@objectstack/objectqlexports were added.Part of. One part remains: the ADR-0079 note, as its own Tier H PR. Its draft combines:adr_note_text, amended with record 6247399985's ③.4 and ③.8b;adr_note_addendum(report 6248949940);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
objectstack-fleet commentedon Oct 18, 2026
Landed (part 2) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T11:56ZTIER_DEFAULTrecord 6249342777 (PASS, declaration face). It left the queue merged at 2026-10-18T11:55Z. Nogh-readonly-queue/main/pr-24307-*branch remains after the merge.origin/main1bf5f6e17cis onorigin/main(git merge-base --is-ancestor: exit 0). It carries PR #24307's same 13 files, +670 / −52.Part of #24197, so this card stays open andpm:dispatched, as intended. No otherdomain:enginecard closed during the queue run.Now on
main: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 ownctx.apichannel, under that channel's identity and on its open transaction.null, as the REST read of the same record shows no title.ctx.title('field')answers the related record's title exactly as itsfindOneserved it.@objectstack/objectqlexportsresolveServedRecordTitle,servedRecordTitleand the typeFormulaServingChannel.ObjectQLandScopedContexteach gainserveFormulaField.Remaining on this card: the ADR-0079 note, as its own Tier H PR (
docs/adr/**, human merge). Its draft combines:adr_note_text, amended with record 6247399985's ③.4 and ③.8b;adr_note_addendum(report 6248949940);This seat dispatches it next, under a new claim.
The out-of-scope finding is #24316. The dev session
session_01XpmVCa3CH5g9uCZLy3j587is archived.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
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 indocs/adr/0079-record-display-name.mdthat 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 asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-adr-0079-noteWorktree:
objectstack-issue-24197-cDomain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
domain:skillson [PM seat] domain:skills — 🟢 os-steve · session_01V1upgJbYCZ5xed9pEQF4Tz #7623 and [PM seat] domain:skills · seat 2 — ⏳ vacant #19287 (the governed surface):docs/adr/0079-record-display-name.md. Two changes only:## Addendum (2026-10-18, #24197), with a short title;content/docs/**, no other ADR. Docs-only, soskip-changeset. Stop on any other breach and explain it in the report.Container & model:
S,mode:cloud,model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (an ADR note records what shipped; it moves no accept set and no public surface. The governed path owes one
CONTRACT_REVIEW_TIERreview, and the PR lands only on a governed approver's approval: Tier H.)Responsibility: unchanged from claim 6245950717. This part ships the ruling's own documentation leg, "written as an ADR-0079 note".
Thread-read: 6249626252
Serial constraints cleared: open PRs' file lists were read at this claim, and none touches
docs/adr/0079-record-display-name.md. This seat's metadata-protocol / objectql: asearchterm 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) changes search behaviour; this note describes only what is onmain, and ⛔ not metadata-protocol / objectql: asearchterm 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's answer while it is unlanded.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Dispatch (part 3, last) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T12:03Z:mode:cloud. The dev session issession_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 branchclaude/issue-24197-adr-0079-note, claim 6249662765.Ruled:line verbatim, which ends "written as an ADR-0079 note", and what parts 1 and 2 shipped (PR feat(objectql,lint): a formula field reads one hop through a reference — record.book.title computes, a second hop is refused #24274d537a318b, PR fix(runtime,objectql): ctx.title() answers a title formula over lookups — the one hop is served on the body's own channel #243071bf5f6e17c).docs/adr/0079-record-display-name.md. It is built from part 1'sadr_note_text, amended with record 6247399985's ③.4 and ③.8b, part 2'sadr_note_addendumand record 6249342777's ③.7. Every statement is verified in code onmain, and none describes unlanded behaviour.domain:skillsas 6249668097 / 6249673718.Fixes #24197, labelledskip-changeset, with a## 维护者速读(草稿)section. Then anos-dev-reportcomment here.CONTRACT_REVIEW_TIERreview and then requests the governed approvers' reviews. ⛔ The seat never approves or merges.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
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/lintunder 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 -bcould 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 removewithout --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
objectstack-fleet commentedon Oct 18, 2026
Rework (part 3) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T12:58Z.CONTRACT_REVIEW_TIERrecord 6250184223 on PR docs(adr-0079): addendum — a formula title reads one hop through a reference, and is not sortable or searchable #24323 (headda22e1db2a) is FAIL on one statement, ①.10. The addendum's group heading says "Hook and action bodies", butctx.title()is installed on the hook face only (packages/runtime/src/sandbox/body-runner.ts; the action face carries notitle). Its items 1–9 are each read RIGHT againstmain. The fence holds, and the ruling is quoted verbatim.session_01RVjnmZ6Jv6NcLDwow6RpNi: the heading names hook bodies only, plus one provenance-wording nit. A fresh record follows on the new head.Part of #Nreader matched "the last part of 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", and thePart-of PR must not also close its cardguard went red on it.Fixes #24197stays.main" bullet says "In a hook or action body". It should read "In a hook body". The action face has noctx.title(). The wording came from part 2's carriedadr_note_addendum.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
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
mainand decides nothing more' → 'It states what is onmainafter 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
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24323 comment 6250544233 ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T13:36Z. Tier H: the record carries the maintainer quick-read on top.CONTRACT_REVIEW_TIERdelta record 6250527790 on head73fd87521cis PASS. It supersedes FAIL 6250184223: the heading now names hook bodies only, and the provenance wording is exact.needs-user-decisionis on the PR, and reviews are requested fromos-zhuangandhotlong. ⛔ The seat never approves or merges. After a governed approver's APPROVED, the seat lands it, andFixes #24197closes this card.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed (part 3, the last) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T13:52Zdocs/adr/**).os-zhuang, a governed approver (GOVERNED_APPROVERS), APPROVED head73fd87521cat 2026-10-18T13:40Z. That came after theCONTRACT_REVIEW_TIERdelta record 6250527790 (PASS, superseding FAIL 6250184223) and the seat's Tier H record 6250544233.gh-readonly-queue/main/pr-24323-*branch remains.origin/main68a090fb97is onorigin/main(git merge-base --is-ancestor: exit 0). It carries PR #24323's one file,docs/adr/0079-record-display-name.md, +77.completedthroughFixes #24197;pm:dispatchedis cleared.The card, end to end:
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.1bf5f6e17c): a hook body'sctx.title()serves that hop on the body's own channel.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_01RVjnmZ6Jv6NcLDwow6RpNiis archived.Generated by Claude Code