Skip to content

feat(spec,plugin-approvals): enable.approvalsVisibleToReaders, the per-object opt-in for the read-only record-reader approval tier - #22660

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22560-record-reader-opt-in
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22560-record-reader-opt-in

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22560
Clause-②: yes (widening)

What this does

An object's own metadata can now turn on the ruled read-only record-reader approval tier (#8652). Before this, the only switch was the ApprovalsPluginOptions.recordReaderVisibleObjects constructor option. A config-driven app cannot set it, because its host builds the plugin with no options.

  • Spec: ObjectCapabilities (the object's enable block) gains approvalsVisibleToReaders: boolean, default false. Its .describe() states what true grants and to whom: a caller who can read a record of the object sees that record's approval requests and full action history, read-only. This holds on the approvals API and the generic data API alike, on a read that names the record. No approval action is offered.
  • Plugin: ApprovalService.addRecordReaderVisibleIds asks a new private recordReaderTierOn(object) per call. It answers true if the host's constructor set holds the object, or if the object's live registered definition (this.engine.getSchema(object)) declares enable.approvalsVisibleToReaders === true. This is the seat's decision B on the round-1 fork (6094828840), AGENTS.md "Startup registry reads" cure 1.
  • approvals-plugin.ts: doc text only. The constructor option stays for hosts that build the plugin themselves.

The ruling is unchanged (#8652, 5299823744, maintainer 「同意」): "A user with read access to the target business record may view that record's approval requests and full action history, read-only … Enabled by a per-object or plugin-level switch, default OFF … the downstream project opts in."

Why the name approvalsVisibleToReaders

It sits in the enable block beside trackHistory, apiEnabled, files, feeds, activities and clone. Like apiEnabled, it is a flag phrase that says what turning it on does. It names the subject (approvals), the grant (visible, so read-only and not actionable) and the grantee (readers of the record). A bare approvals was rejected. Under enable, it reads as "turn approvals on for this object", and an author or an AI would set it to get approval processes. Approval processes need no such flag, so that reading would silently widen visibility instead.

Where the declaration is read, and why there

The service read the option once, in its constructor, which the plugin calls in start(). A round-1 kernel probe showed that at that moment the registry holds only objects registered in init(). Objects from installed packages (kernel:ready), from a later start(), from Studio edits and from dev reloads arrive afterwards. A set collected at start would leave those declarations inert. It would also keep a removed declaration in force until restart, an exposure the author believes is closed. So the flag is read where it is used, from the registry as it is at the read.

  • Default OFF cost: at most one in-memory registry lookup per read that names a record, and none for an untargeted read (the inbox). The business record is never probed for an object that declares nothing: the size-0 early return could no longer stand alone, so the check moved behind the object/record test.
  • Fails closed: an engine without getSchema (the optional member of ApprovalEngine), an unregistered name, a throwing lookup, or any value but literal true reads as not declared.
  • One visibility definition, both doors: unchanged. requestVisibilitySourceOf hands the same visibleRequestIds to bindRequestReadGate and bindRequestChildReadGates. The flag widens sys_approval_request, sys_approval_action and sys_approval_approver on both doors together. The new pins read all three on both doors.

Declaration debts

  • Liveness row packages/spec/liveness/object.json → enable.children.approvalsVisibleToReaders: live, evidence anchored on approval-service.ts#recordReaderTierOn.
  • Studio: the object form's Capabilities section gets the toggle with a help text (object.form.ts). The metadata-forms bundles are regenerated by node scripts/check-i18n-bundles.mjs --write. The zh-CN, ja-JP and es-ES leaves are hand-written, and object-collapsed-sections-echo-decisions.test.ts carries a decided row for each of the two new leaves, with its counts moved (capabilities 9 → 11 leaves).
  • Generated: authorable-surface/data.json and authorable-defaults/data.json (from the spec build), content/docs/references/data/object.mdx (gen:docs) and liveness/state-counts/object.md (gen:liveness-counts), as check:generated named them. authorable-surface.base.json is untouched.
  • Changesets: @objectstack/spec minor and @objectstack/plugin-approvals minor, each Clause-②: yes (widening); and @objectstack/platform-objects patch, Clause-②: no, for the form row's translated leaves (contract review round 1 6096311080).

Tests

All runs below went through scripts/pm/os-verify-lock.sh, and each quotes the run's own summary line.

  • @objectstack/plugin-approvals, at fe81af4642: vitest run → Test Files 70 passed (70), Tests 1011 passed (1011). typecheck (tsc + scripts + check:test-typecheck) → VERDICT command-exit 0. tsc -p tsconfig.test.json --listFiles includes record-reader-opt-in.integration.test.ts (1 hit).
  • @objectstack/spec, at fe81af4642: vitest run --project local → Test Files 642 passed (642), Tests 19150 passed | 1 todo. typecheck → exit 0.
  • @objectstack/platform-objects, at e0c562f1a9: vitest run → Test Files 69 passed (69), Tests 1082 passed (1082). typecheck → exit 0. At fe81af4642, one pin went red: the catalog-wide translated-label control in object-lifecycle-panel-echo-decisions.test.ts read 668 against 667, because this PR adds one authored label. e0c562f1a9 moves that count. git diff fe81af4642 e0c562f1a9 touches only that file, so the spec and plugin readings above stand for the final head.
  • New pins, record-reader-opt-in.integration.test.ts (8 cases). It uses a real ObjectQL engine over better-sqlite3, the real ApprovalsServicePlugin.start() and the real data-door normalizer. Each pin compares one reading: the approvals door (list, by id, history) plus the data door's list and by-id reads of all three request tables.
    • An object declaring the flag, with the host option empty: a reader of the record sees everything, read-only. viewer is {can_act: false, …}; decide, reassign, comment and recall refuse with FORBIDDEN:; the data-door row carries no viewer.
    • A reader on another record, and a caller with no read: nothing, and 404 RECORD_NOT_FOUND on the data door.
    • A read that names no record: the inbox is not widened.
    • Controls: an undeclared object is unchanged, participants are unchanged, and the host option works alone.
    • Late registration: an object registered after start() with the flag widens; re-registered without it, it stops, with no restart. The registry's own answer flipping is asserted as that pin's control.
  • Spec pins (object.test.ts): the default is false; the key is accepted on ObjectSchema and read back true; a string value is refused with invalid_type.
  • Ablations, through scripts/ablation-replace.mjs (wrap mode; anchor must hit; restore proven by blob equality with HEAD and an empty git diff HEAD). The subject resolves from src by relative import, so no dist/ leg applies. The mutated file's HEAD blob is 8d7983b7e922, the same blob at the final head.
    • (1) The declared-key read replaced by return false: Tests 3 failed | 5 passed (8). Red: the declared reader pin, the read-only pin and the late-registration pin. Green, as expected: the cannot-read, inbox and control pins.
    • (2) The size-0 early return put back before the declared read: Tests 3 failed | 5 passed (8), the same three red. The first attempt at (2) was refused by the tool before any test ran: the replacement contained its own anchor, so the anchor count could not drop. It was re-anchored on the comment line above. Both restores read blob == HEAD (8d7983b7e922) and git diff HEAD is empty.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at e0c562f1a9 derived 115 commands. All were run, plus the 48 artifact-roster commands that need no PR context. Result: 161 exit 0, 1 exit 1, 1 NOT MEASURED. --ran reconciles: 115 derived, 114 run, 0 NOT-MEASURED, 1 UNRUN (the unrun one is check:dual-build-cjs-loads).
    • Exit 1: pnpm check:platform-checklist, red on origin/main 86da194919 too. Its 7 problems are symbol anchors in docs/qa/platform-checklist/areas/access-security.json and attachments-storage.json naming symbols absent from metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts. This diff touches none of those files.
    • check:dual-build-cjs-loads: NOT MEASURED, reason: it needs a whole-workspace build, which this dispatch rules out.
    • Verdict lines: check:generated → ✓ All 15 generated artifacts are up to date. check:liveness → object 54 classified (live 53, planned 1), every path#symbol anchor resolves. check:i18n → OK (9 package(s) — all bundles in sync…). check:i18n-stale-fill → 0 stale-fill. check:api-surface → unchanged ✓. check:nul-bytes → OK. check:type-check-debt → none above its recorded number.

Acceptance notes

  • Kernel probe (throwaway, not committed). ObjectKernel + ObjectQLPlugin, with producers registering through the manifest service in init(), in a start() composed after approvals, and on kernel:ready. Each object declares the flag. The reading was taken inside approvals' start(), after boot, and after the kernel:ready producer re-registered its object without the flag: seenAtStart {init: true, start: false, ready: false}, afterBoot {init: true, start: true, ready: true, plain: false, never_registered: false}, afterRemoval false. An engine with no getSchema answers false. What getSchema returns through that door is the authored literal: the declared object's enable reads {"approvalsVisibleToReaders":true} with no defaults filled in, and an object with no block reads undefined. So an absent block or flag reads as the spec default, false.
  • The option's doc linked ApprovalService.recordReaderVisibleIds, a member that does not exist. It now links addRecordReaderVisibleIds, in the hunk this PR edits anyway.
  • The en label is the extractor's humanize of the key, "Approvals Visible To Readers" (the form declares no label), like its siblings in that block.

Generated by Claude Code

…r-object opt-in for the record-reader approval tier

An object's own metadata can now switch on the ruled read-only
record-reader visibility tier. The approvals service reads the flag from
the object's live registered definition on every read, beside the
host's recordReaderVisibleObjects constructor set; either source
suffices, and the default stays OFF.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
…ey renders into

authorable-surface and authorable-defaults (written by the spec build),
the object reference page (gen:docs) and the object liveness count shard
(gen:liveness-counts), as check:generated named them.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
…oReaders toggle; decide its two leaves

The metadata-forms bundles regenerated through check-i18n-bundles --write,
with the zh-CN, ja-JP and es-ES leaf values written by hand, and the
collapsed-sections echo ledger carrying a decided row for each new leaf.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/spec, touching 9 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-surface/data.json, packages/spec/liveness/object.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class))
  • content/docs/getting-started/quick-reference.mdx (via ObjectCapabilities (symbol, a top-level const object))
  • content/docs/protocol/objectql/security.mdx (via ObjectCapabilities (symbol, a top-level const object))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via ObjectCapabilities (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ApprovalsPluginOptions (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-surface/data.json, packages/spec/liveness/object.json, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ee3ae0360dc5beff655e4feb8f9624c731e306fd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fba0eb17ee69bb834ed4f639037cf414cc13680b — the merge of head b275dc8619fc4fbf3d9a40899d7d2d57f262c30d into base ee3ae0360dc5beff655e4feb8f9624c731e306fd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fba0eb17ee69bb834ed4f639037cf414cc13680b && git checkout fba0eb17ee69bb834ed4f639037cf414cc13680b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee3ae0360dc5beff655e4feb8f9624c731e306fd b275dc8619fc4fbf3d9a40899d7d2d57f262c30d && git checkout -B drift-repro ee3ae0360dc5beff655e4feb8f9624c731e306fd && git merge --no-ff b275dc8619fc4fbf3d9a40899d7d2d57f262c30d

node scripts/docs-audit/affected-docs.mjs --json ee3ae0360dc5beff655e4feb8f9624c731e306fd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ee3ae0360dc5beff655e4feb8f9624c731e306fd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e0c562f1a968281a6e79c04b171d985129df410d
Local-runs: none

Net diff read against the merge base with origin/main (86da194919): 19 files, +597 / −30, six commits. Judged from the diff, the card thread, #8652's ruling 5299823744 and the head's check-runs; the dev's report and the ACCEPT were read as claims to test, not as findings.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong.

  1. @objectstack/spec accept set — RIGHT. ObjectCapabilities (the object's enable block, a strictObject in packages/spec/src/data/object.zod.ts) gains approvalsVisibleToReaders: z.boolean().default(false). On main the key is refused as unknown; at the head it is accepted on ObjectCapabilities and on ObjectSchema.enable, a string is refused invalid_type, and an absent flag parses to false (pins in object.test.ts). The exported type ObjectCapabilitiesParsed gains the key; the block's header comment now lists it among the opt-in flags. No other spec shape changes.

  2. The .describe() — RIGHT, nothing false. Each clause checked against the code at the head:

    • "a caller who can read a record of this object": ApprovalService.addRecordReaderVisibleIds anchors on engine.find(object, where id, fields id, limit 1, context) run AS THE CALLER, so the object's own CRUD, sharing and RLS answer; this hunk is unchanged from main.
    • "that record's approval requests and full action history (comments and attachments included)": the admitted ids are every sys_approval_request on that record inside the caller's tenant; listActions is gated through getRequest, which carries the row's own object_name / record_id anchor; comment text is a column of sys_approval_action; decision attachments go through authorizeFileRead on the same rule.
    • "read-only" and "No approval action is offered": decide and reassign authorize on the pending-approver slate (or override), recall on the submitter (or override), comment on the submitter or a taken slot; none reads the visible set. viewer.can_act is heldSlot on the pending slate, is_submitter an owner check, can_override the admin posture. The new pin asserts all five actions refuse FORBIDDEN: for the reader and viewer is can_act false, can_override false, is_submitter false.
    • "on the approvals API and the generic data API alike": one visibility definition. requestVisibilitySourceOf(service) hands visibleRequestIdsFor, which is visibleRequestIds itself, to bindRequestReadGate (sys_approval_request) and bindRequestChildReadGates (sys_approval_action, sys_approval_approver) in ApprovalsServicePlugin.start(). The flag therefore widens all three tables on both doors together, and nothing in the gates changed.
    • "on a read that names the record": approvals door, listRequests passes filter.object and filter.recordId, loadRequest passes the loaded row's own anchor; data door, requestReadTarget names a record only when object_name and record_id are both pinned by conjunctive equality, or id is pinned (the row's own anchor), and requestChildReadTarget the same through request_id or the child row's id; a pin under $or or $not names nothing. An untargeted read returns from addRecordReaderVisibleIds before any lookup, so the inbox is not widened on either door (pinned).
    • "Default false: only the request's participants (submitter, approvers, past actors) and administrators see it": visibleRequestIds is the override actor (isOverrideActor: system, platform admin, tenant admin inside its org) giving the unrestricted answer, else submitter, current approver by the caller's acting addresses (user id, account email, held position slots), and already-acted (actor_id or acted_as). The sentence's four nouns cover exactly those sources.
  3. The widening is exactly 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652's — RIGHT. A caller who can read the target record, asked as the caller; read-only; on a read that names the record; both doors; all three request tables; no approval action authorizes on it; the inbox untouched; default OFF. The ruling's "per-object or plugin-level switch": the per-object form is taken and the plugin-level constructor option is kept, with either source sufficing (recordReaderTierOn).

  4. Decision B — RIGHT, read per call, nothing frozen, fails closed. recordReaderTierOn(object) is a private synchronous method asked on every addRecordReaderVisibleIds call after the object/record test. It answers recordReaderVisibleObjects.has(object) first, then this.engine.getSchema?.(object) and enable.approvalsVisibleToReaders === true. Nothing is written to an instance field or module binding, so AGENTS.md "Startup registry reads" part 3 is absent and cure 1 is the one applied. ObjectQL.getSchema is this._registry.getObject(name): a registry map read, no I/O. Degenerate engine answers: no getSchema member, an unregistered name, enable absent or null, any value but literal true (a string, a number, a Promise from an async engine), all read false; a throwing lookup is caught, logged at debug and reads false. Every path fails closed. The late-registration pin registers an object after start(), sees it widen, re-registers it without the flag and sees it stop, with the registry's own answer asserted as the control.

  5. Removing the size === 0 early return — RIGHT, one in-memory lookup and nothing else. For a deployment that names no host object and declares nothing, a targeted read now runs: object and record present, one getObject map read, false, return. The anchor find on the business record sits behind the tier test and is never reached; the debug line fires only on a throw; untargeted reads return before the lookup; the sys_approval_request and child-table exclusion still applies after the test, in the same position relative to it that main's has(object) held. The dev's ablation 2 shows the return cannot be reinstated ahead of the declared read without turning the declared, read-only and late-registration pins red.

  6. approvals-plugin.ts — RIGHT. Doc text only; the recordReaderVisibleObjects: this.options.recordReaderVisibleObjects hand-off is unchanged, and the doc now says the set is the host's half only. The dead {@link ApprovalService.recordReaderVisibleIds} in ApprovalServiceOptions now links addRecordReaderVisibleIds, which exists. No exported surface of @objectstack/plugin-approvals changes (the new member is private; ApprovalEngine.getSchema was already optional).

  7. Liveness row — RIGHT. packages/spec/liveness/object.json props.enable.children.approvalsVisibleToReaders: status: live, evidence packages/plugins/plugin-approvals/src/approval-service.ts#recordReaderTierOn, a symbol present at the head, with the note naming the pin file. liveness/state-counts/object.md moves 52 to 53 live, 53 to 54 classified. The "Spec property liveness" check-run is success.

  8. Studio form row and bundles — RIGHT. object.form.ts Capabilities section gains the boolean row with helpText "Readers of a record see its approval requests and history, read-only, with no approval action". The en bundle carries the extractor's label "Approvals Visible To Readers" and that helpText; the zh-CN, ja-JP and es-ES leaves say the same thing (the grantee is a user who can read the record, read-only, no approval action) and the labels do not spend the read-only words. Nothing in any leaf grants wider than the code.

  9. Regenerated artifacts — RIGHT. authorable-surface/data.json gains data/ObjectCapabilities:approvalsVisibleToReaders, authorable-defaults/data.json gains = false, content/docs/references/data/object.mdx gains the row in both tables that render the shape, and the liveness count shard moves. authorable-surface.base.json untouched is correct; its re-anchor is a separate act.

② Semver level

  • @objectstack/spec: minor, Clause-②: yes (widening) — RIGHT. A new authorable key on a published strict schema; AGENTS.md: yes takes at least minor. The body states the key, what true grants and to whom, both doors, the inbox exclusion, the read-only boundary and the Studio row.
  • @objectstack/plugin-approvals: minor, Clause-②: yes (widening) — RIGHT. New behaviour (a declaration the service now honours), an opt-in widening of a read surface, default unchanged, nothing removed. The body states both sources, read-where-used, fail-closed and what is unchanged.
  • @objectstack/platform-objects: NO ENTRY — WRONG; this is the one finding that carries the verdict. Its regenerated bundles publish: the package is public (publishConfig.access: public, no private, files: dist), src/index.ts exports MetadataFormsTranslations from metadata-translations/index.ts, which imports the four *.metadata-forms.generated.ts bundles this PR changes, and the new leaf is a user-visible Studio label and help text in four locales. AGENTS.md Post-Task Checklist 3: "Add a changeset for anything that publishes. Feature, functional improvement or fix." The two closest precedents on main, a new Studio form row shipping its translated leaves, each carried '@objectstack/platform-objects': patch with Clause-②: no (.changeset/21765-object-image-field-form-row.md, .changeset/21863-action-on-success-outcome-messages-form-rows.md); six of the last eight regenerations of that bundle did. The fixed group bumps the version in lockstep regardless, so what is missing is the package's own CHANGELOG entry, not its version; "Check Changeset" is green because it counts only that the PR adds some changeset. Owed: one .changeset/22560-platform-objects-*.md, '@objectstack/platform-objects': patch, Clause-②: no, saying the object form's enable.approvalsVisibleToReaders toggle ships its label and help text translated for zh-CN, ja-JP and es-ES. Nothing else in ① or ③ needs rework, so the re-review on the next head is confined to that file.
  • PR body Clause-②: yes (widening) — RIGHT, and it matches both changesets' lines.

③ Boundary flags

Dev deviations (report 6096215461), each answered:

  1. A safety-check refusal on a stray /build-i18n.pid outside the repo. Nothing of it is in the net diff or the tree; already routed to the maintainer by the seat. No bearing on the contract. Answered.
  2. Two platform-objects test files outside the claim's literal list, put to this review. Verified in the net diff: object-collapsed-sections-echo-decisions.test.ts moves its ledger counts (11 to 13 decisions, 10 label and 3 helpText rows, 39 verdict cells, 9 to 11 capabilities leaves, 105 to 107 panel leaves), adds the two decided rows for the new leaf and the key on its class-(c) boolean list; object-lifecycle-panel-echo-decisions.test.ts moves the catalog-wide translated-label control 667 to 668. Nothing else in either file. Both are the ledger controls that go red by design when the claim's own "regenerated metadata-forms i18n bundles" debt adds a leaf, and the claim's "strictness-ledger counts, as check:generated decides" reaches them. Accepted as owed; not a breach.
  3. The dead {@link} fixed inside the edited hunk. Verified; accepted.
  4. The throwaway probe deleted. No probe file is in the net diff; accepted.
  5. The dev's own suite runner stopped and the suites re-run at the merge head. Process only; no bearing.
  6. Attribution. The five authored commits carry Claude-Session: plus Co-authored-by: Claude, the model-free trailer pair AGENTS.md prescribes, which this repo's instructions say wins over the harness reminder; the merge commit fe81af4642 (made by os-regen-merge.sh) carries no trailer; the PR body ends in the session-URL footer. No model identifier appears in the diff, the changesets or the PR body. Right by the repo's rule; the harness-trailer exemption is reporting-only anyway.
  7. No label written by the dev; size/l is the labeler's. Fine.

Out-of-scope finding (escalated, not answered here): pnpm check:platform-checklist is red on origin/main 86da194919 with 7 absent symbol anchors left by #22515 / #22513; #22594 is closed not_planned. This diff touches none of those files. The "Lint & Repo Gates" check-run is still in progress at this reading; if it concludes failure on that gate alone, the red is main's and the landing pre-check on the settled check-runs decides, as the ACCEPT already says.

Open questions: the report lists none and this review found none.

Check-runs on the head, read at 2026-10-10T09:53Z: 33 check-runs; 23 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 0 failure; 8 still in progress: Lint & Repo Gates, Test Core 1 to 6 of 6, Type Check · workspace. Judged on the concluded ones, every gate verdict is green, among them Check Changeset, Spec property liveness, Build Core, Build Docs, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Temporal Conformance, Governed Surface Queue Guard, Dogfood Regression Gate 1 to 3 of 3, Dogfood Verify CLI, and the three claim guards.

Verdict basis: ① holds in full and ③ carries no breach; the verdict is FAIL on ② alone, for the missing @objectstack/platform-objects changeset entry named there. A head that adds that one file and changes nothing else is expected to PASS.

Implemented-by: claude/issue-22560-record-reader-opt-in
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8adbd88eee94a93042bb27d65e4d9e875191cf70
Local-runs: none

Round 2, scoped. Round 1 (6096311080, head e0c562f1a9) judged the whole diff: ① held in full, ③ carried no breach, and the verdict was FAIL on ② alone, for the missing @objectstack/platform-objects changeset entry. This round judges what moved since and whether that item is cured. Inputs: the card thread (the seat's patch order 6096319885 and the dev's report 6096336189 among them), the PR body and file list, the net diff against origin/main at this head, round 1's record and the head's check-runs. The dev's report and the seat's order were read as claims to test, not as findings.

What moved, verified from the refs, not the report: git diff --name-only e0c562f1a9 8adbd88eee is exactly one path, .changeset/22560-platform-objects-approvals-visible-to-readers-form-row.md; --stat reads 1 file, +7 / −0; the log between the two heads is one commit, 8adbd88eee ("chore(changeset): platform-objects patch for the approvalsVisibleToReaders form row's translated leaves"), no merge of main. The net diff against the merge base 86da194919 is 20 files, +604 / −30: round 1's 19 files plus this one, every other hunk identical to the round-1 head. Nothing else moved.

① Derived judgments

The new file, line by line, against the hunks it describes and the precedents round 1 cited:

  1. Frontmatter '@objectstack/platform-objects': patch — RIGHT. The package publishes (publishConfig.access: public, no private, files: dist), and the four regenerated *.metadata-forms.generated.ts bundles reach consumers through MetadataFormsTranslations in src/index.ts. It is a member of the fixed group in .changeset/config.json, so its version moves in lockstep with spec and plugin-approvals regardless; what this entry adds is the package's own CHANGELOG line, which is what round 1 found missing. The spelling is the precedents'.
  2. The sentence — RIGHT, true of what the regenerated bundles carry. "Studio's object form now offers the approvalsVisibleToReaders row in its capabilities panel, with its label and help text in the metadata-form translation catalogs for en, zh-CN, ja-JP and es-ES." Checked at the head: object.form.ts adds the boolean row, with its helpText, inside the section named capabilities (label "Capabilities") under the enable composite; each of the four bundles gains one "enable.approvalsVisibleToReaders" leaf carrying label and helpText (en: the extractor's "Approvals Visible To Readers" and the form's own help text; zh-CN, ja-JP and es-ES: hand-written leaves, each saying a user who can read the record sees its approval requests and history read-only, with no approval action). Nothing in the sentence grants wider than the leaves. Its first clause names the row, which is @objectstack/spec's hunk; its second clause is this package's own contribution. Precedent 21765 uses the same sentence shape, and the fixed group ships both packages at one version, so the sentence is true of the release its CHANGELOG line will sit in.
  3. Clause-②: no — RIGHT. Bare, at the start of a line of its own, the one spelling clause2-line.mjs reads; no arm, so not the malformed no (widening). True at the package grain: the bundles put no new key on any published payload; they carry a label and a help text for a key that @objectstack/spec declares and grades.
  4. File name and placement — RIGHT. .changeset/22560-platform-objects-*.md, the name round 1 asked for and the seat order spelled; a new file with a non-empty frontmatter, so both rules of check-empty-changeset are untouched (nothing from the merge base is modified or deleted).
  5. Nothing else in ① moved. Round 1's nine judgments — the spec key and its default, the .describe(), the per-call recordReaderTierOn read, the removed size-0 early return, the plugin doc, the liveness row, the Studio row and bundles, the regenerated artifacts — stand on hunks identical to the round-1 head and stay held.

② Semver level

Every package whose packages/**/src/** the net diff moves, against its entry:

③ Boundary flags

Round 1's FAIL item: cured by the one file above. The seat order asked for that file and nothing else, and nothing else moved.

Dev deviations (report 6096336189), each answered:

  1. Two checks beyond the three the seat order named: check-changeset-no-major.mjs re-run with --event carrying the PR payload, so its level axis was measured rather than NOT APPLICABLE, and check:nul-bytes. Both read-only; the first reports the same accounting the "Check Changeset" check-run reached on its own. Accepted.
  2. The stray /build-i18n.pid outside the repo left untouched, as the seat ordered. Not in the diff or the tree; still the maintainer's. Accepted.
  3. Not listed by the dev, found here: the PR body's "Declaration debts" bullet still reads "Changesets: @objectstack/spec minor and @objectstack/plugin-approvals minor, each Clause-②: yes (widening)", naming two entries where the head carries three. Nothing in it became false; no gate reads that prose; the body's own declaration line is right. A reporting debt, not a breach: a body edit naming the third entry closes it with no push (the edited event re-reads the body).
  4. The worktree was rebuilt from the remote head e0c562f1a9, the branch took no merge of main, and GitHub reports mergeable: true (state blocked: a draft with required checks still running). Matches the order.
  5. Attribution on 8adbd88eee: Claude-Session: plus Co-authored-by: Claude, the repo's model-free pair; no model identifier in the commit, the file or the PR body. Right by the repo's rule.

Held from round 1, unchanged by this file: the two platform-objects ledger-control test files as owed, the dead {@link} fix inside the edited hunk, the probe's absence from the diff, and the out-of-scope check:platform-checklist red on origin/main 86da194919 (7 absent symbol anchors left by #22515 / #22513; #22594 is closed not_planned; this diff touches none of those files). If "Lint & Repo Gates" concludes failure on that gate alone, the red is main's, and the landing pre-check on the settled check-runs decides, as round 1 said.

Open questions: the report lists none and this review found none.

Check-runs on 8adbd88eee, read in the minutes before this record was posted (posted 2026-10-10T10:05Z): 32 check-runs; 16 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 0 failure; 14 still in progress: Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Test Core 1 to 6 of 6, Dogfood Regression Gate 1 to 3 of 3, Dogfood Verify CLI, and Temporal Conformance (live PG + MySQL). Judged on the concluded ones, every gate verdict is green, among them Check Changeset, Spec property liveness, Governed Surface Queue Guard, Build Core, Build Docs, Type Check · source gates, Type Check · debt ledger, Check Documentation Links, Check PR Size, Flag docs affected by code changes, and the three claim guards (the card this PR closes must claim this branch; no other open PR may claim the same issue; no other open PR may claim the same single-writer path).

Verdict basis: the one file round 1 owed is present, with the level, the sentence and the clause that round 1's own reasoning and the precedents call for; nothing else moved; ① and ③ stay held with no breach. The check-runs still in progress are the landing pre-check's to settle, not this record's: a FAIL here would need a concluded red or a defect in the diff, and there is neither.

Implemented-by: claude/issue-22560-record-reader-opt-in
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

…tree

The os-regen driver kept the branch's side of the generated shard in the
merge commit and dropped main's two FileRefusedValue entries; gen:schema
over the merged tree carries both main's entries and this branch's
approvalsVisibleToReaders.

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b275dc8619fc4fbf3d9a40899d7d2d57f262c30d
Local-runs: none

Round 3, scoped to the main sync hop. Round 2 (6096398459) held ① ② ③ and passed at 8adbd88eee. The head moved by two commits: 126726f1c5, the merge of origin/main ee3ae0360d (the ort merge that os-regen-merge.sh commits before regenerating), and b275dc8619, the regeneration of packages/spec/authorable-surface/data.json on that merged base. Inputs: the card thread (the seat's sync order 6096552527 and the dev's report 6096640892 among them), round 2's record, the net diff against main at this head, PR #22641's diff on main (3d0eeefa4a), and the head's check-runs. The dev's report was read as a claim to test; every reading below was taken from the refs, not from it. The three-way git merge-file the scope asks for was computed on the four blobs in scratch, a read of git objects and nothing more: nothing was built, run or re-run.

① Derived judgments

1. The union is exact.

  • git merge-file reproduces both hand-written files byte for byte. For packages/plugins/plugin-approvals/src/approval-service.ts, merging ours 8adbd88eee (blob 8d7983b7e9) with base 86da194919 (blob 591687cca4) and theirs ee3ae0360d (blob 39ec14d8d5) exits 0 with no conflict, and its output is identical to the head blob f4370556fe. For approvals-plugin.ts: ours 3aa643b4e7, base 51fd00eb78, theirs 76cfb41fe7; merge-file exits 0, output identical to the head blob 6915ea259f. Nothing in either file was resolved by hand, and nothing from either side was dropped.
  • The hop adds no path of its own. git diff --name-only 8adbd88eee b275dc8619 is 115 paths and equals git diff --name-only 86da194919 ee3ae0360d exactly (comm is empty both ways). Per path, the head blob equals main's blob at ee3ae0360d on 112 of the 115; the three that differ are exactly the paths both sides edited: the two files above and authorable-surface/data.json. So main's side was taken verbatim wherever this PR had no edit, including the governed paths the hop carries (.claude/skills/pm-dispatch/**, docs/adr/0043, docs/adr/0131, skills/objectstack-automation/**).
  • The PR's net delta is unchanged. git diff ee3ae0360d b275dc8619 is the same 20 paths as round 2's git diff 86da194919 8adbd88eee, 20 files, +604 / −30 both times. A line-by-line diff of the two patches, index lines stripped, differs only in five hunk headers' line offsets: four in approval-service.ts (the option doc, the field doc, the addRecordReaderVisibleIds read and the new recordReaderTierOn member, shifted down by 15 or 68 lines because main inserted handleActionPage and its two module-level helpers above them) and one in data.json (shifted by 2 for main's two FileRefusedValue entries). No content line differs. Round 2's hunks are this head's hunks.
  • data.json is the union, by a regeneration, and its gate has already concluded. The merge commit 126726f1c5 held the branch side of this merge=os-regen file: against main it reads +1 / −2, the approvalsVisibleToReaders entry present and main's two FileRefusedValue entries absent, exactly the silent drop AGENTS.md §11 says the driver produces and the reason the merge is committed apart. b275dc8619 is one file, +2, adding data/FileRefusedValue:id and data/FileRefusedValue:metadataRefused. Against ee3ae0360d, and against origin/main's current tip 6a3fe2517b too, the head's data.json differs by the one data/ObjectCapabilities:approvalsVisibleToReaders line. A textual union of a generated file is not the generator's verdict; here the file was regenerated by gen:schema per the report, the sharding keeps the two sides' entries disjoint, and check:authorable-surface runs in the Type Check · source gates job (lint.yml), which has concluded success on this head. So the shard's currency is judged by its own gate, not only by the dev's local check:generated.
  • main has moved one commit past the merge base, 6a3fe2517b (feat(spec,service-storage,client)!: one upload-scope vocabulary for the upload requests, the sys_file select, the upload doors and the SDK (#22470) #22647, 16 paths); none of them is among the PR's 20, GitHub reports mergeable: true, and the merge base of the head and origin/main is ee3ae0360d.

2. The interaction: confirmed from the code, in both directions.

What PR #22641 (3d0eeefa4a) added to approval-service.ts: the import of renderConfirmPage and renderResultPage from action-link-pages.ts; the module-level ACTION_PAGE_PATH constant and actionPageResponse(html); actionLinkUrl spelled on that constant; and handleActionPage(request). To approvals-plugin.ts: one ctx.logger.info line in the raw-app mount. Plus its own test file, changeset and one dependency. None of it names the visible set.

Does the action page read the record-reader tier? No. The chain at this head: handleActionPage (about :4999) on GET calls peekActionToken (:4937), on POST redeemActionToken (:4947); both call resolveActionToken (:4909), which reads sys_approval_token by hash with SYSTEM_CTX (about :4915) and then this.getRequest(token.request_id, SYSTEM_CTX) (:4924). getRequest (:7242) is loadRequest(requestId, context, true); with visibility enforced, loadRequest calls visibleRequestIds with the row's own anchor (:7264). visibleRequestIds (:6873) opens with if (this.isOverrideActor(context, tenantOrg)) return null;, and isOverrideActor (:1623) answers true at if (context.isSystem) return true; for SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } (:848). So the set is null, loadRequest's if (visible && ...) is skipped, and addRecordReaderVisibleIds (:6941, the last statement of visibleRequestIds and its only call site) is never reached; recordReaderTierOn (:7077) has exactly one caller, inside addRecordReaderVisibleIds (:7008), so it is never asked. On POST, redeemActionToken then calls decide (:3944) with { ...SYSTEM_CTX, userId: person } or SYSTEM_CTX; decide is decideNode (:3418) plus the resume, and decideNode reads the row with SYSTEM_CTX (:3431), takes isOverrideActor (:3441, true on isSystem) and takenSlot(pendingApprovers, actorId, context) (:3442), and refuses only when both are false. The actor is the token's approver_id, which resolveActionToken has just confirmed in pending_approvers (about :4928). No visible set is consulted anywhere on that path. personForTokenSlot (:5043) reads sys_user under SYSTEM_CTX. The two renderers are module functions importing only types from @objectstack/spec/contracts; they hold no reference to the service.

Does the tier reach the action page the other way? No. handleActionPage reads request.method, the token query parameter and the token form field, and nothing else: no ExecutionContext, session, cookie or Authorization header, as its own doc block says and its body confirms. The token stays the only credential. And the tier's widening is bounded to the one definition round 2 held: visibleRequestIds is the only reader of addRecordReaderVisibleIds; its four callers are visibleRequestIdsFor (the data-door source, :1451), the two list paths (:7140, :7183) and loadRequest (:7264); and the child set it extends is APPROVAL_REQUEST_CHILD_OBJECTS = ['sys_approval_action', 'sys_approval_approver'] (request-read-gate.ts:117). sys_approval_token is not in it, so a reader admitted by the flag does not see token rows through either door, and the approvals door's can_act: false for such a reader (round 1's pins, unchanged) is contradicted by nothing #22641 added. Under enable.approvalsVisibleToReaders, the action page neither widens nor narrows; the dev's report on this point is right in every particular checked here.

3. Held from round 2, on hunks now proven identical: the spec key and its false default, the .describe(), the per-call recordReaderTierOn read through engine.getSchema, the removed size-0 early return and its replacement comment, the fail-closed arms, the plugin doc text, the liveness row and its anchor, the Studio row and the four bundles with their ledger-control tests, the regenerated artifacts, the eight integration pins and the three spec pins, and round 2's four changeset judgments. main's hop added one new consumer of an object's enable block (service-analytics/src/api-exposure-door.ts, #22634); it reads apiEnabled and apiMethods through apiExposureDenialReason and never names the new key, and git grep approvalsVisibleToReaders at the head finds the key nowhere outside this PR's own 20 files. No interaction.

② Semver level

Unchanged by the hop, and the hop could not change it: the three changesets are byte-identical to round 2's, main's ten new .changeset/*.md are main's bytes, and the PR body's Clause-②: yes (widening) line is unchanged. @objectstack/spec minor yes (widening), @objectstack/plugin-approvals minor yes (widening), @objectstack/platform-objects patch no, each for the reason round 2 gave. Check Changeset is success on this head. plugin-approvals's package.json moved on main (one dependency, #22641); that is main's, and the PR does not touch it.

③ Boundary flags

Governed surfaces: the branch now contains .claude/**, docs/adr/** and skills/** content that arrived with the merge, each blob equal to main's, so the PR's file list against its base ee3ae0360d carries none of them (20 paths, as above). Governed Surface Queue Guard is success on this head. Size unchanged at +604 / −30; Check PR Size success.

Dev deviations (report 6096640892), each answered:

  1. os-regen-merge.sh exited 1 at its commit step while the os-regen shard was stale, and the dev followed its printed prescription: regenerate, stage, inspect, commit. The result is the two-commit shape AGENTS.md §11 prescribes, the merge apart from the regeneration, and that is what let this record read "what main brought" (112 blobs equal to main's) apart from "what the change produces" (one file, +2). Accepted.
  2. pnpm install --frozen-lockfile re-run after the merge (main moved pnpm-lock.yaml). Local, no effect on the diff. Accepted.
  3. check:nul-bytes and check-changeset-no-major with the PR's event payload, beyond the order's list. Both read-only. Accepted.
  4. The PR body untouched; the stray /build-i18n.pid untouched, as ordered. Accepted.
  5. The report's own claims re-measured here: the 115-path equality holds; the merge commit's data.json is the branch side (+1 / −2 against main) and the regen commit restores exactly the two FileRefusedValue lines; the report's "proof by delta" on the two source files is replaced here by the stronger blob identity of the three-way merge. Every claim held.

Still open from round 2, not a breach: the PR body's "Changesets" bullet names two entries where the head carries three. The seat's order 6096552527 says the seat corrects it in the landing act; it is unchanged at this read and stays a reporting debt. The out-of-scope check:platform-checklist red held from rounds 1 and 2: the hop moves neither the two checklist areas it names nor the two source files whose symbols went missing, so whatever Lint & Repo Gates concludes on it here is main's, as before; on the round-2 head the seat's order read that job as success (33 success, 2 skipped).

Open questions: the report lists none and this review found none.

Check-runs on b275dc8619, read at 2026-10-10T10:43Z: 32 check-runs; 15 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 0 failure; 15 in progress: Build Core, Dogfood Regression Gate 1 to 3 of 3, Lint & Repo Gates, Temporal Conformance (live PG + MySQL), Test Core 1 to 6 of 6, Type Check · consumer gates, Type Check · debt ledger and Type Check · workspace. Judged on the concluded ones, every gate verdict is green: Check Changeset, Spec property liveness, Governed Surface Queue Guard, Build Docs, Dogfood Verify CLI, Type Check · source gates (the lane carrying check:authorable-surface and check:generated --reconcile-only), Check Documentation Links, Check PR Size, Flag docs affected by code changes, the three claim guards and the part-of guard. The required contexts still running are the landing pre-check's to settle; Test Core is the one that runs this PR's eight pins beside main's new action-page-member pins on one tree.

Verdict basis: the hop is a clean three-way union on the two files both sides edited, proven by blob identity; the PR's delta against main is content-identical to the delta round 2 passed; the regenerated shard differs from main by the one line this PR exists to add, and its gate has concluded green; and the one new member main brought into the same file never reaches the record-reader read, in either direction. Nothing round 2 held is moved. A FAIL here would need a concluded red or a defect in the union, and there is neither.

Posted 2026-10-10T10:49Z.

Implemented-by: claude/issue-22560-record-reader-opt-in
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at b275dc8619, by the owning seat: every check green, queued

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T11:08Z · holder of claim 6095200466 on #22560.

  • The review chain:
  • The seat's own check of the hop: git merge-file of the round-2 head, the old merge base 86da194919 and ee3ae0360d equals each head blob of approval-service.ts and approvals-plugin.ts. authorable-surface/data.json differs from main by the one approvalsVisibleToReaders line.
  • The round-2 residue, fixed in this act's run-up: the PR body's "Changesets" bullet now names all three entries. The body edit's Check Changeset re-run is green.
  • CI on this head: 42 check-runs: 38 success, 4 skipped. check-expected-skips --pr 22660 reads every skip in the roster.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22660 reads NOT governed; +604 / −30.
  • Closing keywords: the body carries Fixes #22560 alone, and no commit message carries one.
  • main drift since the merge base ee3ae0360d: main (now 07ee6f6e58) moved none of the PR's 20 paths, generated ones included. GitHub reports mergeable: true (clean).

pr_ready and automerge_enable follow.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 11:09
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 11:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit a00cf99 Oct 10, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22560-record-reader-opt-in branch October 10, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants