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 18, 2026
os-dev-report
{
"issue": 24197,
"status": "done",
"branch": "claude/issue-24197-formula-one-hop-read",
"pr": "#24274",
"session": "session_01M6VD9XhattUEHKD99C7KUR",
"premise_still_valid": true,
"summary": "Premise verified on origin/main fb6fb24 and then 53ef752: planFormulaProjection projected only the row's own columns, the lint's formula pass ran with no traversalHydration, and record.LOOKUP.FIELD read null on every row. Implemented the ruling's direction (a). The engine's formula read path (find, findOne, write responses) now preloads one hop through a reference field: one batched read per reference field per page, under the CALLER's own read. It shares the validation path's loader: resolvePredicateRelated's body moved into resolveOneHopRelated(..., authority 'predicate' | 'caller'), and addReferenceTraversals and referenceTraversalConflicts were extracted from rule-validator.ts. The lint's judgeFieldFormula opts into traversalHydration. @objectstack/formula judges the value role with formula wording: one hop passes; a second hop and a bare-and-traversed reference are refused with the prescription. The engine refuses the same two shapes at read time (null plus a reported reason). The spec blueprint's four describe strings now teach record.customer.name and the sort and search answer. H1 confirmed (1 read per reference field per page, pinned, and the same RelatedFieldBinding verdicts). H2 confirmed (caller authority; ablating it reds exactly the 2 permission pins). H3 confirmed except the non-reference hop (see open_questions). H4 measured: a formula value is stored nowhere, so the title is unsortable (400 INVALID_SORT) and unsearchable (400 INVALID_FIELD) where a sort or search names it. That is already implemented and is now pinned.",
"tests": "Head e9c02e6. Live HTTP: examples/app-todo
objectstack dev --freshon this branch, with lib_book, lib_reader and lib_loan saved via PUT /api/v1/meta/object (lookups book and reader, nameField title = record.book.title + ' · ' + record.reader.name). Save with a two-hop title: 422 INVALID_METADATA, 'A formula resolves ONE hop'. One-hop save: 200. POST /api/v1/data/lib_loan record.title = 'Dune · Bo'. GET ?fields=title,book,reader returns 'Dune · Ann', 'Emma · Ann', 'Dune · Bo', and null for an empty reader (the fault is logged once with the optional-read repair); rows keep the FK ids. ?sort=title: 400 INVALID_SORT. ?search=Dune&searchFields=title: 400 INVALID_FIELD. New pins: packages/objectql/src/engine-formula-one-hop.test.ts 17/17 (4 loans over 3 books and 2 readers make 1 lib_book read and 1 lib_reader read, the book read uses where id $in [b1,b2,b3] and fields [id,title], and paging reads only its own ids; projection trim; findOne; insert response; RLS-hidden row reads null with the reason and leaks no name; SYSTEM reads it; FLS-masked column kept out; dangling ref reads null with no fallback; empty ref with .?name.orValue gives 'Emma · —'; undeclared related field; second hop with no related read; own-column formula on the hop read; no second hop through a formula; cycle terminates; orderBy title rejects with INVALID_SORT 400). packages/lint/src/validate-expressions.formula-one-hop.test.ts 10/10. Formula relationship-traversal.test.ts: the flipped pin ('checks nothing in the value role') now asserts code, params and formula wording, 41/41. Suites: formula 47 files / 1330 tests passed; lint 144 files / 6835 tests passed; objectql full suite on the pre-merge head was 437/438 files (the red file, federated-injected-column-readers ledger rows keyed by moved function names, was then fixed in this PR); after the merge the 5 formula, predicate and ledger files were re-run, 82/82. Ablations via scripts/ablation-replace.mjs, each landing proven by anchor count and blob, each restore proven blob == HEAD and git diff HEAD empty: hop preload reds 13 of 17 engine pins; caller authority (read as SYSTEM) reds exactly the 2 permission pins; depth guard reds the second-hop-via-formula pin, and the whole file no longer terminates (cycle recursion; exit 124 at the timeout, restore proven); engine hopConflict reds the second-hop pin; lint traversalHydration dropped reds the 2 lint refusals; formula value arm removed reds the 2 formula refusals. The engine tests import ./engine.js from src, so no dist leg was needed.","gates": "Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on e9c02e6: 117 commands. All 117 ran, each with its exit captured before any pipe: 117 x exit 0.--ranprinted: '117 derived famil(ies) accounted for — 117 run, 0 NOT-MEASURED (a DERIVED zero — all 117 recorded an exit code and none of them is 3)'. An earlier sweep on 1af356d found 3 reds, all fixed before the final sweep: check:system-context-census (anchor moved to resolveOneHopRelated); check:query-options-erasure (the test'sas anyoptions were typed); check:engine-split-ratio and check-plugin-teardown-shape --self-test (clone was shallow; unshallowed). Typecheck exit 0 for each touched package: spec, formula, objectql (check:test-typecheck included) and lint.pnpm --filter @objectstack/spec check:generated: all 14 up to date. CI check-runs on #24274: in_progress at report time, not waited on.","mcp_calls": "2 — mcp__github__issue_read (get) and mcp__github__issue_read (get_comments) on #24197; no MCP write tool",
"api_writes": "2 relay strokes (scripts/pm/with-fleet.sh --via dispatch) plus 1 git push: pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24274/assignees (os-project-manager), body read back 10759/10759 bytes identical; report comment = POST /repos//issues/24197/comments. git push to claude/issue-24197-formula-one-hop-read (not a REST write)",
"line_budget": "none set; diff vs merge base 53ef752 (+/-): .changeset/24197-formula-traversal-subject.md +14/-0 · .changeset/24197-lint-formula-one-hop.md +17/-0 · .changeset/24197-objectql-formula-one-hop.md +19/-0 · .changeset/24197-spec-blueprint-one-hop-title.md +7/-0 · content/docs/permissions/system-context.mdx +1/-1 · content/docs/references/ai/solution-blueprint.mdx +5/-5 (generated) · packages/formula/src/index.ts +1/-0 · packages/formula/src/relationship-traversal.test.ts +38/-6 · packages/formula/src/relationship-traversal.ts +73/-0 · packages/formula/src/validate.ts +21/-7 · packages/lint/src/validate-expressions.formula-one-hop.test.ts +141/-0 · packages/lint/src/validate-expressions.ts +15/-3 · packages/objectql/src/engine-formula-one-hop.test.ts +342/-0 · packages/objectql/src/engine.ts +276/-13 · packages/objectql/src/federated-injected-column-readers.test.ts +13/-5 · packages/objectql/src/validation/rule-validator.ts +87/-24 · packages/spec/src/ai/solution-blueprint.zod.ts +4/-4",
"files_changed": [
".changeset/24197-formula-traversal-subject.md",
".changeset/24197-lint-formula-one-hop.md",
".changeset/24197-objectql-formula-one-hop.md",
".changeset/24197-spec-blueprint-one-hop-title.md",
"content/docs/permissions/system-context.mdx",
"content/docs/references/ai/solution-blueprint.mdx",
"packages/formula/src/index.ts",
"packages/formula/src/relationship-traversal.test.ts",
"packages/formula/src/relationship-traversal.ts",
"packages/formula/src/validate.ts",
"packages/lint/src/validate-expressions.formula-one-hop.test.ts",
"packages/lint/src/validate-expressions.ts",
"packages/objectql/src/engine-formula-one-hop.test.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/federated-injected-column-readers.test.ts",
"packages/objectql/src/validation/rule-validator.ts",
"packages/spec/src/ai/solution-blueprint.zod.ts"
],
"deviations": [
"Two files outside the claim's File surface, both forced by gates on the moved loader and named in the PR body. content/docs/permissions/system-context.mdx row 29b: the anchor moved from resolvePredicateRelated to resolveOneHopRelated (check:system-context-census red otherwise). packages/objectql/src/federated-injected-column-readers.test.ts: the ledger rows were renamed to the moved function names, with dispositions unchanged. content/docs/references/ai/solution-blueprint.mdx is the check:generated --fix output of the describe edit.",
"Worktree: the shared primary checkout was already on the designated branch name at origin/main with no commits, so
git worktree add -bcould not reuse that name. The primary checkout was detached (clean, no work in it) and the branch was recreated in ../objectstack-issue-24197 from origin/main fb6fb24 before any edit.","Attribution: the harness reminder asked for a Co-Authored-By trailer carrying a model name and a different PR footer. AGENTS.md (the user's own instruction, which outranks the reminder) requires the model-free trailer pair and the session-URL 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": "Does a formula refuse a reference read BOTH through and as a plain value (e.g. record.reader == null ? '—' : record.reader.name), or only a second hop? The ruling names two hops only.",
"options": ["A refuse both shapes, as validation predicates do, and prescribe the optional read record.reader.?name.orValue('—') (shipped)", "B refuse only two hops and let a hydrated reference compare against null and ids"],
"recommendation": "A. Business need: the null test is the real authoring pattern, and the optional read serves it (measured: it gives the fallback on an empty reference and the value on a set one). Long-term: one judgment for both seams (findTraversalConflicts with a subject) and no second dialect. AI-error axis: under B,
record.customer == 'acc_1'silently compares a map against an id and reads false, which is the silent-tolerance shape. Startup focus: no new surface; it reuses the existing codes traversal-bare-and-traversed and traversal-multi-hop."},
{
"question": "An EMPTY reference: evaluate (the author decides via the optional read), or refuse like the validation path's no-reference verdict?",
"options": ["A evaluate with the stored empty value; a plain read faults (null plus a reason naming the optional read) (shipped)", "B never evaluate; always null"],
"recommendation": "A. An optional lookup is the common junction case (a loan not yet assigned a reader), and a formula, unlike a rule, has no conditional wrapper to express 'skip when empty'. A dangling or unreadable reference is still NOT evaluated (null with the reason), so no fallback ever stands in for a record that is missing or hidden."
},
{
"question": "A formula on the related object that itself reads a hop (loan reads record.book.shelf_code, where shelf_code = record.shelf.code): is it served?",
"options": ["A not served: a find that is a formula's hop read serves no hop of its own, and the book's formula reads null with the second-hop reason (shipped)", "B serve it recursively"],
"recommendation": "A. The ruling fixes the depth at one hop, and B is two hops plus unbounded recursion (measured: the ablation of this guard makes a ping/pong pair of titles never terminate). The lint cannot see this shape without resolving the related object's formula, so the engine reports it at read time."
},
{
"question": "H3 asked to refuse 'a hop through a non-reference'. Should record.address.city on an object-valued or json field be refused in a formula?",
"options": ["A leave it alone, as validation predicates do (shipped)", "B refuse every member read through a non-reference field"],
"recommendation": "A. It traverses today on object-valued fields and works; refusing it would remove working formulas. The validation path's judgment deliberately leaves it alone ('record.address.city on an object-valued field is left alone')."
}
],
"out_of_scope_findings": [
"class: a · reach: public door, measured on this branch's live backend: GET /api/v1/data/lib_loan?search=zzzz-nothing answers 200 with all 5 rows, the same rows as ?search=Dune, on an object whose fields are two lookups and a formula title (no stored text or select field) · evidence: resolveSearchFieldResolution's auto-default excludes lookup and formula, so the allowed set is empty and the search term is dropped silently instead of refused; a junction object with a formula nameField is exactly this shape · not caused by this PR (independent of the hop) · dedupe words: search empty searchable set silently ignored; ?search returns all rows; auto-default search fields empty; junction object search",
"carrier: none · noted, not filed: evaluateFormulaField (the synchronous hook helper) preloads no hop, so a traversing formula yields null there, as a can() formula already does; documented in its docblock and the PR's Acceptance notes."
],
"adr_note_text": "A formula field reads ONE hop through a lookup or master_detail field (record.LOOKUP.FIELD): the read path preloads the named related fields once per page, under the reader's own permissions. A related record that is missing or not readable leaves the formula null with a reported reason, never a computed fallback. A second hop, and a reference read both through and as a plain value, are refused by the lint and read null at run time. A formula value, a composite title over lookups included, is stored nowhere: it is shown on every surface, but it cannot be sorted by (400 INVALID_SORT) or searched (400 INVALID_FIELD; the auto-default search set never includes it). An object that needs to sort or search by its composite title must store the value in a field of its own, written when its sources change."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 5 · 2026-10-18T07:40Z · code part of this card, re-declared after
os-dev-report6247217617 (the file surface widens by gate-forced and generated rows; the session, the branch and the dev stay)Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud sessionsession_01M6VD9XhattUEHKD99C7KUR)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-formula-one-hop-read(draft PR #24274,Part of)Worktree:
objectstack-issue-24197Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface: as claim 6245950717, plus three rows the gates forced:
domain:devxon [PM seat] domain:devx · seat 2 — 🟢 os-steve · 由 os-steve 账户的项目负责 #20163 and [PM seat] domain:devx @ objectstack — ⏳ vacant #6023:content/docs/permissions/system-context.mdx, one row (29b). Its census anchor moved fromresolvePredicateRelatedtoresolveOneHopRelatedwith the loader;check:system-context-censusis red otherwise.content/docs/references/ai/solution-blueprint.mdx: thecheck:generated --fixoutput of the four describe edits (+5 / −5), with no hand edits.packages/objectql/src/federated-injected-column-readers.test.ts: the ledger rows renamed to the moved function names, with their dispositions unchanged.docs/adr/**. The ADR-0079 note follows as its own Tier H PR, from the report'sadr_note_text.Container & model:
L,mode:cloud,model: TIER_DEFAULT(unchanged; oneCONTRACT_REVIEW_TIERreview before the queue)Clause-②: no (narrowing)
Responsibility: unchanged from claim 6245950717.
Thread-read: 6247217617
Serial constraints cleared: unchanged. No open PR touches the added rows (read at this claim).
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 5 · 2026-10-18T08:00Z · code part of this card, re-declared after the
CONTRACT_REVIEW_TIERrecord 6247399985's ①.9 (the declaration line changes; the file surface, session, branch and dev stay as in claim 6247243624)Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud sessionsession_01M6VD9XhattUEHKD99C7KUR)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24197-formula-one-hop-read(draft PR #24274,Part of)Worktree:
objectstack-issue-24197Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface: as claim 6247243624.
Container & model:
L,mode:cloud,model: TIER_DEFAULT(unchanged; theCONTRACT_REVIEW_TIERreview of record is 6247399985)Clause-②: yes (narrowing) (the lint and the engine narrow what they accept;
@objectstack/formulaalso widens its published index by one optional parameter and one exported type,TraversalSubject. Perclause2-line.mjs, a diff that widens one surface and narrows another readsyes (narrowing). No level moves.)Responsibility: unchanged from claim 6245950717.
Thread-read: 6247243624
Serial constraints cleared: unchanged.
Remaining on this card after PR #24274: the hook-body title accessor
ctx.title()(resolveRecordTitle→ the synchronousevaluateFormulaField, which preloads no hop), which answersundefinedfor a composed title over lookups (record 6247399985 ③.6). Then the ADR-0079 note, as its own Tier H PR.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"issue": 24197,
"status": "done",
"branch": "claude/issue-24197-formula-one-hop-read",
"pr": "#24274",
"session": "session_01M6VD9XhattUEHKD99C7KUR",
"premise_still_valid": true,
"summary": "Declaration correction per CONTRACT_REVIEW_TIER record 6247399985 ①.9 and claim 6247408205 (verified on #24197 before acting). One prose-only commit 7917005 on top of e9c02e6: .changeset/24197-formula-traversal-subject.md line 7 now reads 'Clause-②: yes (narrowing)'. The lint, objectql and spec changesets are unchanged. No code moved.",
"tests": "No code change, so no suite was re-run.",
"gates": "On 7917005, each exit captured before any pipe. (1)
node scripts/check-changeset-no-major.mjs --base origin/main: exit 0. (2)node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0, with all three breaking changesets disposed not-required (no-migration-prescription). The first attempt exited 1 because the re-created worktree had no install; afterpnpm install --frozen-lockfileit exited 0. (3)readClause2Line(scripts/pm/clause2-line.mjs) on the claim 6247408205 line, placed at the start of a line: exit 0 with {kind: 'declared', value: 'yes', arm: 'narrowing'}.","mcp_calls": "1 — mcp__github__issue_read (get_comments) on #24197, to verify claim 6247408205; no MCP write tool",
"api_writes": "1 — POST /repos//issues/24197/comments (this report, via the fleet-write relay); plus git push 7917005 (not a REST write). No PR-body PATCH (see deviations).",
"line_budget": "none set; this push: .changeset/24197-formula-traversal-subject.md +1/-1",
"files_changed": [".changeset/24197-formula-traversal-subject.md"],
"deviations": [
"The PR body was NOT patched, so it still reads 'Clause-②: no (narrowing)'. The seat's message granted one issue_patch, but os-dev.md rule 3 says a dev writes the PR body once, in the pr_create stroke, never PATCHes it, and names the edit for the seat to write. Per the dispatch, os-dev.md wins on conflict. Edit for the seat: in PR #24274's body, replace the line beginning 'Clause-②: no (narrowing)' with the claim 6247408205 line verbatim, at the start of a line: 'Clause-②: yes (narrowing) (the lint and the engine narrow what they accept;
@objectstack/formulaalso widens its published index by one optional parameter and one exported type,TraversalSubject. Perclause2-line.mjs, a diff that widens one surface and narrows another readsyes (narrowing). No level moves.)'. Until then, a gate that reads the body sees 'no (narrowing)' while the formula changeset says yes."],
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24274 comment 6247718723 ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T08:38Z. TheCONTRACT_REVIEW_TIERrecord is 6247399985 (PASS; the schema and narrowing faces), covering the prose-only head move to7917005af. The declaration is corrected toClause-②: yes (narrowing)(claim 6247408205). PR #24274 is marked ready and set to auto-merge through the queue. It saysPart of, so this card stays open for two more parts, which follow in this order:ctx.title()(resolveRecordTitle→ the synchronousevaluateFormulaField), which serves no hop, so a composed title over lookups answersundefinedthere (record ③.6).adr_note_text, amended with the record's ③.4 and ③.8b.The out-of-scope
?search=finding is filed as #24284.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed (code part) ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T09:12ZCONTRACT_REVIEW_TIERrecord 6247399985 (PASS; schema and narrowing faces; the head's move to7917005afwas prose-only). It left the queue merged at 2026-10-18T09:11Z. After the merge, nogh-readonly-queue/main/pr-24274-*branch remains.origin/maind537a318bis onorigin/main(git merge-base --is-ancestor: exit 0). It carries PR #24274's same 17 files, +1074 / −68.Part of #24197, so this card stays open andpm:dispatched, as intended. Since the enqueue, #24252 and #24245 (domain:cli) and #24264 (domain:services) closed, each by its own lane.Now on
main:record.book.title + ' · ' + record.reader.namecomposes. The related fields are read once per page, one batched read per reference field, under the reader's own permissions.nullwith a reported reason, never a fallback.400 INVALID_SORTwhen sorted and400 INVALID_FIELDwhen searched. The blueprint describe strings teach the one-hop title.Remaining on this card:
ctx.title()(resolveRecordTitle→ the synchronousevaluateFormulaField) serves no hop, so a composed title over lookups answersundefinedthere. This seat dispatches it next, under a new claim.adr_note_textwith the record's ③.4 and ③.8b.The out-of-scope
?search=finding is #24284. The dev sessionsession_01M6VD9XhattUEHKD99C7KURis archived.Generated by Claude Code
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 — ⏳ vacant · archive of domain:server seat 1 (#6367) #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 — ⏳ vacant · archive of domain:server seat 1 (#6367) #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-steve · 由 os-steve 账户的项目负责 #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-project-manager #7623 and [PM seat] domain:skills · seat 2 — 🟢 os-warren · 由 os-warren 账户的项目负责 #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