Repository navigation
objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3area:filesFiles — upload, download, signed URLs, access derived from the parent recordFiles — upload, download, signed URLs, access derived from the parent recordand removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-10T03:11Z
Session:session_01Bw3y2DWhT9RPnrmDsNqEVG
Account:os-tesla(the seat's linked user, asget_meanswers it; the card's assignee)
Branch:claude/issue-22593-file-hydration-refused
Worktree:objectstack-issue-22593
Domain:domain:engine
Seat:domain:engine#2(seat post #20966)
File surface (read onorigin/main99801d831f): the loud half only.packages/objectql/src/engine.ts: the file-field hydration'ssys_fileread (thecatchat about:11977–:12020). A refused hydration stops reading as "no file". The shape is the dev's measured choice: the smallest that keeps "refused" distinct from "empty" for the consumers (the console's file renderer, export).- Tests beside it, and a changeset.
- Conditional:
packages/speconly if the served shape needs a declared member. That is a cross-lane path, re-declared before the PR leaves draft.- The consumer side in objectui is a relay, not an edit here.
- ⛔ Not the access half: whether
sys_filemetadata follows the parent record's read. The dev measures it read-only, and this seat files the decision card for the maintainer. - Stop on a breach and explain in the report.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)
Clause-②: yes - The served value of a refused file field changes from a bare id to a refused marker. That is a public-surface change. It owes a contract review at tier before the queue. If the dev's measured shape turns out to change no published surface, the seat amends this line.
Responsibility:objectql's file-field hydration folds a refusedsys_fileread into "no file" | none: no other path hydrates file fields | a record reader withoutsys_fileread, seen on 17.7.0 in hotclm's browser pass (Full browser test on 17.7.0main: drive the whole contract lifecycle as every audience, in en and zh-CN, and report what a real user hits hotclm#87); they cannot tell a refused file from an absent one
Thread-read: none
Serial constraints cleared: at 2026-10-10T03:11Z, 10 open PRs read byfilename. None touchespackages/objectql/src/engine.ts. This seat's in-flight feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 S5 touchesobjectqlonly inplugin.ts's authored-row reads.
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22593,
"status": "done",
"branch": "claude/issue-22593-file-hydration-refused",
"pr": "#22620",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"resumed_from": "73ab084184",
"head": "faf689872c",
"premise_still_valid": true,
"summary": "The objectql file-field hydration (ObjectQL.resolveFileReferences) now marks each held id { id, metadataRefused: true } when the caller's sys_file sub-read is refused with PERMISSION_DENIED. It used to leave the bare id, which an absent file also produces, and logged a storage warn. Every other branch of the seam is unchanged: hydrated for a sys_file reader, empty stays empty, a missing table stays silent and bare, an outage (42501 included) stays bare with the warn. spec declares FileRefusedValueSchema / FileRefusedValue (closed, read-only) and valueSchemaFor(def, 'expanded') accepts it. Changeset: objectql and spec minor, Clause-2 yes. This run merged origin/main d748ae8 (no conflicts), re-measured the symptom on main, ran the ablation and the full battery, and opened draft PR 22620 with assignee os-tesla. No code change was needed after the merge.",
"measured_on_main": "Both pin files were run against main's engine.ts (d748ae8, blob df2300664b, byte-identical by hash-object), with every other file at faf6898, then restored from HEAD (blob 1d1b78f500, git diff HEAD empty). On the real SecurityPlugin + SqlDriver stack, a reader with record read and no sys_file read got attachment === 'f_7cQx2Lm90a' (the bare id): 1 failed and 3 passed (the two controls and the direct sys_file PERMISSION_DENIED/403 probe). The seam pins were 4 failed and 4 passed. The captured warn is exactly the line from the report: 'sys_file lookup failed; file fields keep their raw ids and will render as "no file" for this read'. Reproduces on current main.",
"tests": [
"HEAD faf6898 (origin/main d748ae8 merged in).",
"Build: pnpm --filter '@objectstack/plugin-security...' build (closure covers spec, objectql, plugin-security) VERDICT command-exit 0. pnpm --filter @objectstack/spec check:generated exit 0, all 15 artifacts up to date.",
"Full suites: pnpm --filter @objectstack/objectql test exit 0 (393 files, 7738 tests passed). pnpm --filter @objectstack/plugin-security test exit 0 (194 files, 4079 passed, 45 skipped). pnpm --filter @objectstack/spec test exit 0 (640 files, 19103 passed, 1 todo).",
"Typecheck: spec exit 0. objectql exit 0 when re-run alone (test-typecheck OK, 40 files / 234 errors / 65 pinned signatures held); its first combined run exit 1 is explained under deviations. plugin-security exit 0 (test-typecheck 0 / 0 / 0).",
"Pins green, verbose: objectql engine-file-hydrate-refused.test.ts 8/8 (OBJECTQL_PIN_EXIT=0). plugin-security file-field-hydration-refused.test.ts 4/4 (SECURITY_PIN_EXIT=0). spec field-value.test.ts and type-alias-convention.pin.test.ts 33/33 (SPEC_PIN_EXIT=0).",
"Ablation: node scripts/ablation-replace.mjs, anchor 'if (isReadRefusal(error)) {' replaced by 'if (false && isReadRefusal(error)) {'. Anchor hit 1 time (1 to 0), blob 1d1b78f500 to 1565ec17ca. objectql pins 4 failed / 4 passed (received the bare id). plugin-security 1 failed / 3 passed (received the bare id). Restore: blob == HEAD, git diff HEAD empty, porcelain clean, tool exit 0. Both legs read SOURCE: the objectql test imports ./engine, and plugin-security vitest.config.ts aliases @objectstack/objectql to ../../objectql/src/index.ts, so no dist rebuild was needed.",
"Lint (narrowed, proven): eslint --no-inline-config --format json over the 6 changed TypeScript files reports 6 files, 0 errors, 0 warnings. Population: eslint.config.mjs blocks for packages/**/.{ts,tsx,mts,cts}. Invariance: the config never enables type-aware linting (no parserOptions.project), so the diff cannot move the verdict on an untouched file. Repo-wide pnpm lint is left to CI."
],
"gates": {
"head": "faf689872c",
"derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 119 commands at faf6898; --ran reconciliation: 119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN, exit codes recorded for all 119 (a derived zero)",
"nonzero": [],
"extra": [
"pnpm --filter @objectstack/spec check:generated :: exit 0",
"pnpm --filter @objectstack/objectql run typecheck :: exit 0",
"pnpm --filter @objectstack/plugin-security run typecheck :: exit 0",
"pnpm --filter @objectstack/spec run typecheck :: exit 0"
],
"not_measured_locally": "The 6 path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core, Build Docs), the 4 workspace type-check lanes, and the 6 families that take a value from the workflow, all as the derivation names them. Left to CI.",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:generated :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:quick-reference-counts :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:swallow-census-controls :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
]
},
"access_half": {
"measured_at": "faf689872c, read-only. This PR makes no change to any read rule.",
"sys_file_read_rule_today": [
"Object SystemFile (packages/services/service-storage/src/objects/system-file.object.ts:21) declares no access block and no sharingModel. Tenancy row sys_file: tenant-scoped (packages/objectql/src/tenancy/platform-object-tenancy.ts:136).",
"The read is decided by the object CRUD gate PermissionEvaluator.checkObjectPermission('find', 'sys_file', sets) (packages/plugins/plugin-security/src/permission-evaluator.ts:205). It is allowed only when a resolved permission set grants allowRead through objects.sys_file or the '' wildcard (the spec fold objectPermissionGrants).",
"Shipped sets (packages/plugins/plugin-security/src/objects/default-permission-sets.ts): '' read in admin_full_access (:393), organization_admin (:441) and viewer_readonly (:1234). member_default (:676), the everyone baseline, names no '' and no sys_file, so a plain member has no sys_file read unless an application set grants it.",
"The engine hydration (ObjectQL.resolveFileReferences, packages/objectql/src/engine.ts, one batched sys_file id $in read) runs as the CALLER. The refusal arrives as a throw of PermissionDeniedError (packages/plugins/plugin-security/src/errors.ts:41, code PERMISSION_DENIED, 403) for the whole sub-read, measured on the real stack.",
"Not measured: a reader with object read whose sys_file rows are narrowed at row level would get a short result. Those ids stay bare and are NOT marked by this PR, because telling hidden from uncommitted needs an existence probe, which is itself an access decision."
],
"sys_attachment_parent_gate": [
"Object SysAttachment (packages/platform-objects/src/audit/sys-attachment.object.ts, isSystem, managedBy platform). The object gate is the same checkObjectPermission, and member_default names no sys_attachment either. An application that turns the panel on ships sys_attachment: { allowRead, ... }, as the dogfood fixture attachmentManagerSet models.",
"Row gate: installAttachmentReadVisibility (packages/services/service-storage/src/attachment-access-hooks.ts:758, registered at storage-service-plugin.ts:426). It is an engine middleware on sys_attachment for find/findOne/count/aggregate under a non-system context. computeParentVisibilityFilter (:796) pre-scans the candidate (parent_object, parent_id) pairs under the system context (cap 2000, fail-closed). Then resolveReadableParentIds (:246) runs ONE caller-scoped engine read of the candidate ids per parent object and ANDs { parent_object, parent_id: { $in: visible } } into the AST. A compute failure ANDs the deny-all sentinel.",
"Writes: installAttachmentAccessHooks (:370) requires parent EDIT (sharing checkEdit, else ISecurityService.checkControlledByParentWrite).",
"So attachment ROWS follow the parent record's read, but still behind an object-level grant. sys_file has the object gate only, and nothing derives it from a parent."
],
"files_fileId_resolver": [
"GET /storage/files/:fileId (packages/services/service-storage/src/storage-routes.ts:1355) and its /url sibling (:1305) start with store.getFile (metadata-store.ts:496), a SYSTEM-context by-id read. The caller's sys_file read permission is never consulted.",
"isServableForDownload (:534): committed, or tombstoned and still held via resolveFileHolder.",
"authorizeDownload (:450): acl === 'public_read' allows anonymous download. A file with neither scope === 'attachments' nor field ownership (ref_object + ref_id) needs only a signed-in caller (requireDownloadSession, :408). A gated file goes to authorizeFileRead = buildFileReadAuthorizer (storage-service-plugin.ts:1210).",
"buildFileReadAuthorizer: owner_id === caller allows. A field-owned file goes to a declared fileAccessDelegate service, else a caller-scoped engine.find(ref_object, { where: { id: ref_id } }). Otherwise the sys_attachment links are read (system read, limit 500), and the download is allowed when any parent is caller-readable. AuthzStoreUnavailableError answers 503, and any other throw denies with 403 FILE_DOWNLOAD_DENIED / ATTACHMENT_DOWNLOAD_DENIED.",
"Asymmetry for the decision card: the download door derives access from the owning or parent record and never asks sys_file read. The hydration asks sys_file read and nothing else. The same member can be allowed the bytes of a field-owned file and refused its name, size and type."
]
},
"consumer_relay_objectui": [
"At objectui origin/main 12ff256, packages/fields/src/widgets/file-value.ts readFileValue takes { id, metadataRefused: true } through its object arm. The id is kept and the url is built from the id (fileUrlFromId). The name falls back to the generic "File" and there is no mimeType. metadataRefused is not carried onto FileValueView. So the console already renders a non-empty generic chip, not EmptyValue, but the refusal never reaches the UI.",
"What the file renderers (FileField read-only list, FileCell, ImageField, AvatarField, the grid cell) must show for metadataRefused: true: a distinct restricted state with a lock affordance and a translated label (for example "File (details restricted)"); never EmptyValue; no thumbnail attempt (no MIME type is known); and keep the open/download affordance on the id-derived stable endpoint, which decides on its own (it may answer 403 FILE_DOWNLOAD_DENIED). An editable widget submits only the bare id (fileIdOf already reads id). Once objectui takes a spec carrying FileRefusedValueSchema, it can tell the form apart by shape."
],
"cross_lane_paths": [
"packages/spec (domain:spec): src/data/field-value.zod.ts, src/data/field-value.test.ts, src/type-alias-convention.pin.test.ts, and the generated api-surface/, authorable-surface/, declaration-map/, export-origins/ and json-schema.manifest/ data shards",
"packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts (domain:services, test only)",
"content/docs/references/data/field-value.mdx and content/docs/references/index.mdx (domain:devx, generated by spec build-docs)",
"docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md (generated) and docs/audits/2026-07-unknown-key-strictness-ledger.md (a one-row update on field-value.zod.ts)"
],
"files_changed": [
".changeset/22593-file-field-hydration-refused.md",
"content/docs/references/data/field-value.mdx",
"content/docs/references/index.mdx",
"docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md",
"docs/audits/2026-07-unknown-key-strictness-ledger.md",
"packages/objectql/src/engine-file-hydrate-refused.test.ts",
"packages/objectql/src/engine.ts",
"packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts",
"packages/spec/api-surface/data.json",
"packages/spec/authorable-surface/data.json",
"packages/spec/declaration-map/data.json",
"packages/spec/export-origins/data.json",
"packages/spec/json-schema.manifest/data.json",
"packages/spec/src/data/field-value.test.ts",
"packages/spec/src/data/field-value.zod.ts",
"packages/spec/src/type-alias-convention.pin.test.ts"
],
"deviations": [
"Merged origin/main d748ae8 (7 commits past the base 99801d8) as merge commit faf6898 and pushed it as a fast-forward. No conflicts, and no os-regen path was routed, because main touched none of this diff's paths. The order allows this because spec moved on main.",
"origin/main has moved again since (874a38d and later). Those commits touch spec scripts, plugin-security explain-engine and docs/protocol-upgrade-guide.md, none of them paths in this diff, so the branch was not merged a second time. CI's merge ref covers the joint state.",
"The first combined typecheck (spec + objectql + plugin-security, under the lock) exited 1 in objectql's test layer only: TS7016 "Could not find a declaration file for module @objectstack/metadata" plus test-layer drift in files outside this diff, and engine.ts showed 2 errors. That run overlapped the unlocked gate battery in the same worktree, which reads and rebuilds package dist. objectql typecheck re-run alone exited 0 with the ledger held exactly, and engine.ts had no errors. plugin-security typecheck did not run in that combined call. Its first separate attempt was exit 99 queue-timeout (the #22591 dev held the lock), which is NOT MEASURED, and its retry exited 0.",
"The gate battery (119 check commands) ran unlocked, per the lock rules for check:* gates, at the same time as the locked suites.",
"Lint is a declared, proven narrowing (see tests). The repo-wide pnpm lint is left to CI.",
"No labels were written: the order named none, and skip-changeset does not apply because a changeset exists. label-write was used only for the PR assignee.",
"Cleanup: after the PR opened, the worktree /home/user/objectstack-issue-22593 had its node_modules removed and was then removed with git worktree remove (no --force; it was clean and its HEAD equalled the remote tip faf6898). A patch round recreates it from the remote branch. This report was posted with scripts/pm/post-stamped.mjs from the shared checkout (read-only use)."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, creating draft PR 22620, read back 10144 bytes sent and stored identical; (2) label-write assign, POST /repos//issues/22620/assignees with os-tesla, read back as matching; (3) this os-dev-report comment, POST /repos//issues/22593/comments. Not REST: 1 git push (merge commit faf6898).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (no PR or person will touch the file) · noted, not filed: the header of packages/services/service-storage/src/attachment-access-hooks.ts still says the default member permission sets grant wildcard CRUD on sys_attachment. member_default has named no '*' since the wildcard removal (default-permission-sets.ts:676). This is comment drift, not class a/b/c. It is in the PR's Acceptance notes.",
"carrier: none · noted, not filed: plugin-approvals normalizeActionAttachment maps any object to { id, name?, url? } and would drop metadataRefused. It is dormant: its only caller, listActions, reads sys_approval_action with SYSTEM_CTX, so its hydration never sees a refusal. It is in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22593,
"round": "patch round 1",
"status": "done",
"branch": "claude/issue-22593-file-hydration-refused",
"pr": "#22620",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"resumed_from": "faf689872c596d3170e2fe9913f2ccf90e5e84fe",
"head": "c4a077b5bd72820cea956edb1a77c0ab977b3b43",
"premise_still_valid": true,
"summary": "Docs-only rider for contract review 6094604322 item 3.9. types.mdx, thefileentry: the sentence that gave the expanded read form as the media metadata object alone is rewritten into three forms (file object, bare id,{ id, metadataRefused: true }naming FileRefusedValueSchema), with the stored form stated as unchanged. The JSON example under it now shows what the engine builds (idplus the derived/api/v1/storage/files/:idurl) instead of a CDN url with noid, because the rewritten sentence introduces it as the engine-built object and the dispatch allows no claim engine.ts or field-value.zod.ts does not hold. A docs grep found one more sentence this PR makes false: in attachments-access.mdx, an unheld tombstone "keeps its bare id in a record payload" is no longer true for a reader refused the sys_file read, so a clause now says so. No code, test or changeset change, PR body untouched. origin/main 83b8b80 moved over content/docs/meta.json only, which this PR does not touch, so there was no merge.",
"files_changed": [
"content/docs/protocol/objectql/types.mdx (+27 / -4)",
"content/docs/permissions/attachments-access.mdx (+3 / -1)",
"total vs faf6898: 2 files, +30 / -5; commit c4a077b, pushed as a fast-forward faf6898..c4a077b"
],
"passages_as_written": {
"content/docs/protocol/objectql/types.mdx L1159-1195": "Stored value: an opaquesys_fileid string. What a read returns leaves it\nunchanged: the stored form is the id alone, and the two object forms below are\nread forms only. A record read expands each stored id in place (each element,\nwithmultiple: true) into one of three forms, and none of them fails the\nrecord read that asked for it.\n\nThe file object — the caller'ssys_fileread found the file (a committed\nrow, or a tombstone something still holds).urlis its only required member,\nderived from the id rather than stored:\n\njson\n{\n \"id\": \"abc123\",\n \"name\": \"profile.jpg\",\n \"size\": 1024000,\n \"mimeType\": \"image/jpeg\",\n \"url\": \"/api/v1/storage/files/abc123\"\n}\n\n\naltanddurationSecondsare the other optional members. The keys arenameand\nmimeType— notfilename/content_type.\n\nThe bare id, as stored — the engine had nothing to resolve it against: the\nread found no such file,sys_fileis not registered or its table is not\nprovisioned yet, or the read failed for any reason other than a refusal (an\noutage, which the engine logs as a warning).\n\n**{ \"id\": \"abc123\", \"metadataRefused\": true }** — the caller may read the record\nbut is refused thesys_fileread. The value is the id the record holds plus the\nmarker, nothing else: nourl,name,sizeormimeType, because none was\nread, and it says nothing about whether the file exists. It is distinct from an\nempty field, which holds no file at all, and from the file object, which always\ncarriesurl.\nFileRefusedValueSchema\ndeclares it, closed to exactly those two keys. Download access is judged on its\nown, by the record that owns the file rather than bysys_fileread.",
"content/docs/permissions/attachments-access.mdx L165-170 (clause after the em dash on L168 is new)": " fileGET /storage/files/:idserves is a file a record read expands into\n{ id, name, size, mimeType, url }. The row itself stays tombstoned until a\n sweep tidies it, and a file with no holder left still answers\nFILE_NOT_FOUND(404) and keeps its bare id in a record payload — except to\n a reader refusedsys_fileread, who gets{ id, metadataRefused: true }for\n every file id, held or not (fileread forms)."
},
"claim_check": [
"stored form is the id alone: field-value.zod.ts valueSchemaFor returns FileReferenceIdValueSchema for form stored; FileRefusedValueSchema JSDoc says read-only by construction",
"expanded in place, per element, never failing the record read: engine.ts find and findOne call resolveFileReferences unconditionally (always-on); arrays mapped per element; every catch arm returns records",
"file object: engine.ts toValue builds { id, name?, size?, mimeType?, url: /api/v1/storage/files/ID } from committed rows and tombstones the held-file resolver reports held; FileValueSchema requires url alone, alt and durationSeconds optional",
"bare id: engine.ts returns ids unchanged when sys_file is not registered, on isMissingTableError (silent), on any other non-refusal throw (warn), and when no servable row came back",
"refused marker: engine.ts isReadRefusal (code PERMISSION_DENIED) marks every id token, without reading rows, as { id, metadataRefused: true }; FileRefusedValueSchema is a strictObject of exactly id and metadataRefused true; its describe says distinct from an empty field; JSDoc says no url, name, size or mimeType and that download access is judged by the owning record, not sys_file read"
],
"docs_grep": {
"command": "git grep over content/docs/** excluding content/docs/releases/**: (1) expanded|hydrat|metadataRefused|FileValue|sys_file, 115 hits, 41 files; (2) file field/value/reference, required member, no attachment, no file, reads as empty/absent, bare id, raw id, metadata object, mimeType; (3) file|image|avatar|attachment within 80 chars of url; (4) refused within 60 chars of empty|absent|no file|bare",
"fixed": [
"content/docs/protocol/objectql/types.mdx:1159-1160 "The expanded read form is the media metadata object, whose only required member is url": made incomplete by this PR. Rewritten as the three forms (the ordered target).",
"content/docs/permissions/attachments-access.mdx:168 "a file with no holder left ... keeps its bare id in a record payload": made false by this PR for a reader refused the sys_file read (before it, that reader got the bare id too; now the marker). Fixed with a clause naming the marker, linked to types#file."
],
"not_fixed": [
"content/docs/permissions/attachments-access.mdx:165-166 "a file GET /storage/files/:id serves is a file a record read expands into { id, name, size, mimeType, url }": not made false by this PR. A reader refused the sys_file read already got the bare id before it, so the gap is older than this PR (it belongs to the escalated access half, item 3.1). The new clause on L168 says "held or not", which covers that reader for both sentences.",
"content/docs/permissions/attachments-access.mdx:21-25 "the { id, name, size, mimeType, url } shape is derived at read time, never stored": names the hydrated shape and says nothing about every read returning it. Still true.",
"content/docs/api/plugin-endpoints.mdx:163 "/storage/files/:fileId ... (what file fields carry)": the url member of a hydrated value. Bare-id readers already carried no url before this PR, so it is not made false.",
"content/docs/deployment/cli.mdx:1171-1172 "A file / image / avatar / video / audio field value is an opaque sys_file id": the stored form, which this PR leaves unchanged.",
"content/docs/references/data/field-value.mdx (139-146 FileRefusedValue, FileValue, FileLikeValue): AUTO-GEN, never hand-edited. Round 1 already regenerated it with FileRefusedValue.",
"every other hit (expand of lookups, schema-design collapse, metadata hydration, sys_file upload and storage rows, approvals attachments, translations) is about another subject and makes no claim about the read form of a file field."
]
},
"tests": "All runs on the tree at c4a077b (the final commit), exits captured before any pipe. Named gates: node scripts/check-doc-frontmatter.mjs exit 0 ("2 content root(s) verified ... content/docs 411, content/blog 4"); node scripts/docs-audit/check-affected-docs.mjs exit 0 ("affected-docs self-test: 605 cases pass"; bridge report printed); pnpm check:doc-anchors exit 0 ("477 internal #fragment link(s) across 417 source file(s) all resolve to a real heading", which includes the two new cross-page anchors types#file and field-value#filerefusedvalue); pnpm check:docs-single-h1 exit 0 (411 pages); pnpm check:doc-authoring exit 0 ("418 files clean"). Battery: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands exit 0 derived 119 commands over the branch change set (18 paths vs merge base d748ae8). First pass: 112 exit 0 and 7 exit 3 PREREQUISITE NOT MET, because the recreated worktree had no dist (doc-formula-expressions, doc-security-posture, docs-transcript-drift, skill-examples, dual-build-cjs-loads, i18n, lean-entry-closure). Built under os-verify-lock: spec build (VERDICT command-exit 0, 71s), turbo build formula+lint (VERDICT 0, 4/4 cached), then turbo build --concurrency=2 --filter=!@objectstack/docs (VERDICT 0, 72 tasks, 71 cached; the tree stayed clean). Each of the 7 was re-run with its exact command: all exit 0 (for example "28 ObjectSchema.create example(s) ... carry an os validate-clean security posture", "4 declared transcript value(s) ... equal what the registry derives today", "262 prose examples type-check", "check-i18n-bundles: OK"). --ran over a record of all 119 commands with their final exits: exit 0, "119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). Extra probe: both edited files compile with @mdx-js/mdx 3.1.1 (without the fumadocs plugins). A control-byte grep of both files found no hits. No ablation: this is a docs round.",
"deviations": [
"dispatch-gates printed STALE TREE: HEAD is at least 7 commits behind origin/main 83b8b80, and 4 gate files changed in that range (check-future-spec-major.mjs, check-role-word.mjs, platform-object-tenancy-census.json, regen-artifacts.mjs). Step 3 orders a merge only when main moved over a file this PR touches. It did not (the only docs path main moved is content/docs/meta.json), so there was no merge. The battery therefore reflects this branch tree and not origin/main, and CI on the PR merge ref covers that difference. The conflict between the two instructions is reported here, not resolved silently.",
"ran.list records the FINAL exit of each of the 7 rebuilt gates. Their first-pass exit 3 is in this report and is not in the record.",
"One example change beyond the sentence itself (the JSON under it), explained in the summary. It stays within the passage the dispatch scoped."
],
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #22593 via scripts/pm/post-stamped.mjs (POST issues/22593/comments). Also one git push (not a REST write). Reads only otherwise (gh api GET of review comment 6094604322 and the #22593 comment list).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: PR #22620 (a code rider, seat's call; this round forbade code changes) · noted, not filed: published JSDoc in packages/spec/src/data/field-value.zod.ts still describes the expanded file form as two members. FileLikeValueSchema @deprecated says "the expanded read form is FileValueSchema", and the FILE_REFERENCE_TYPES docblock says valueSchemaFor returns "the id-or-object union only for expanded". Both are now incomplete (three forms, two of them objects), and the d.ts ships the JSDoc. The engine.ts post-process comment "resolve file-field id references to their expanded FileValueSchema form" is internal and the same drift. ADR-0104 section D3 prose is already recorded by review item 1.1."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsRuling pointer: batch #311 item 2 (decision card #22624, this card's access half) · B · maintainer 「同意」 2026-10-10T07:16Z
Director seat, summon #36,
session_019fWAt2renophxLVg5aJXMH(via the relay). The record is 6094990917 on #22624, closedcompleted. Thread-read: 6094830418 (the loud half's latest os-dev-report).- B. A file field's metadata follows the parent-derived verdict the download door already applies (ADR-0104 D3,
buildFileReadAuthorizer): when the caller'ssys_fileread is refused,resolveFileReferencesstill hydrates a file whoseref_object/ref_idname the record being read (the delegate consulted where the owner object declares afileAccessDelegate); a referenced id that does not point at that record keeps PR fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620's refused marker; directsys_filequeries andpublic_readare unchanged. - For the
domain:engineseat (seat 2, which filed [Decision] should a file field's sys_file metadata (name, size, type) follow the holding record's read, as the download door and attachment rows already do? (access half of #22593) #22624): file the execution card,Clause-②: yeswith one contract-review-tier review before the queue; pins for the field-owned file on the record read, a referenced id not field-owned by that record, an owner object with a delegate, andpublic_read, with an ablation. The loud half (PR fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620) does not wait for it. No label change on this card in this act.
Generated by Claude Code
- B. A file field's metadata follows the parent-derived verdict the download door already applies (ADR-0104 D3,
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22593,
"round": "patch round 2",
"status": "done",
"branch": "claude/issue-22593-file-hydration-refused",
"pr": "#22620",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"resumed_from": "c4a077b5bd72820cea956edb1a77c0ab977b3b43",
"head": "056b909d27dff72b83c0b2774c2a75061ff15aed",
"premise_still_valid": true,
"summary": "Comment-only round for the published JSDoc. In packages/spec/src/data/field-value.zod.ts the two named passages (the FILE_REFERENCE_TYPES docblock and FileLikeValueSchema's @deprecated note) now name the three expanded forms that valueSchemaFor(def, 'expanded') admits: the FileValueSchema file object, the bare id, and FileRefusedValueSchema's { id, metadataRefused: true }. The @deprecated note also says that FileLikeValueSchema rejects the refused value, which was checked against the built dist. The engine phrase the order quoted appears twice in packages/objectql/src/engine.ts: the resolveFileReferences JSDoc headline and the find() post-process comment. Both were rewritten. A word check of the same spec file found three more passages with the same drift. They were rewritten under the order's own rule (published text the PR makes incomplete is fixed in the same PR) and are listed under deviations. Every changed line is a comment line. There is no code, type, schema, test or changeset change, and the PR body was not touched. No generated artifact embeds these docblocks: check:generated reported all 15 artifacts up to date, so nothing was regenerated.",
"files_changed": [
"packages/spec/src/data/field-value.zod.ts (+26 / -16, comment lines only)",
"packages/objectql/src/engine.ts (+14 / -7, comment lines only)",
"total vs c4a077b: 2 files, +40 / -23; one commit 056b909, pushed as a fast-forward c4a077b..056b909 (remote tip read back equal)"
],
"regenerated": "none. Spec rebuilt under os-verify-lock, then check:generated returned exit 0 with "All 15 generated artifacts are up to date". The reference docs (content/docs/references/data/field-value.mdx) embed .describe() text only, and no .describe() changed. check:api-surface and check:declaration-map are green. The tree stayed clean after every build and gate.",
"passages_as_written": {
"field-value.zod.ts FILE_REFERENCE_TYPES docblock (named)": " * Media/attachment types. The STORED value of every member is an opaque\n *sys_fileid ({@link FileReferenceIdValueSchema}). TheexpandedREAD\n * value is one of three forms: the inline metadata object\n * ({url, name?, size?, ...}, {@link FileValueSchema}), derived rather than\n * stored, when the id resolves to a file; the bare id, unchanged, when it does\n * not; and{ id, metadataRefused: true }({@link FileRefusedValueSchema})\n * when the reader is refused thesys_fileread that resolves it. ADR-0104 D3\n * wave 2 (file-as-reference) narrowed the stored value, and the classifier\n * below is where that landed:valueSchemaForreturns the id ALONE for\n *form === 'stored'and the union of all three only for'expanded'. Both\n * directions are pinned infield-value.test.ts.",
"field-value.zod.ts FileLikeValueSchema @deprecated (named)": " * @deprecated The stored form is {@link FileReferenceIdValueSchema}. The\n * expanded read form is one of three, {@link FileValueSchema}, the bare id or\n * {@link FileRefusedValueSchema}, andvalueSchemaFor(def, 'expanded')\n * derives that union for a file field. This union admits only a non-empty\n * string or a {@link FileValueSchema} object, so the refused value\n *{ id, metadataRefused: true }, which carries nourl, fails it. Retained\n * for consumers that genuinely need to accept a string or a file object during\n * the migration window, so they say so explicitly rather than by default.",
"engine.ts find() post-process comment (named)": " // Post-process: resolve file-field id references to their expanded\n // read form (ADR-0104 D3), one of three: the FileValueSchema file\n // object, the bare id when nothing resolves it, or\n // FileRefusedValueSchema's{ id, metadataRefused: true }when this\n // caller is refused thesys_fileread. Always-on but free unless a\n // file field holds an id string; dual-mode-safe (blobs pass through).",
"engine.ts resolveFileReferences JSDoc headline (same quoted phrase, second site)": " * Resolve file-field id references to their expanded read form (ADR-0104 D3\n * wave 2). Afile/image/avatar/video/audiovalue stored as an\n * opaquesys_fileid string whose servable row this read finds (committed,\n * or a tombstone still held) is enriched, in place, to theFileValueSchema\n * object{ id, name, size, mimeType, url }—urlderived from the stable\n */files/:fileIdresolver, never stored. That is one of three expanded\n * forms: an id nothing resolves stays the bare id (DUAL-MODE SAFE, and the\n * fail-opencatchbelow), and an id whosesys_fileread is refused to the\n * caller becomesFileRefusedValueSchema(REFUSED IS NOT ABSENT).",
"field-value.zod.ts FileValueSchema docblock (added, see deviations)": " * Wave 2 (file-as-reference) narrows the STORED form to an opaquesys_file\n * id and makes THIS the resolvedexpandedread shape, withurlderived from\n * the/files/:fileIdresolver rather than stored. It is one of three expanded\n * forms, beside the still-unresolved bare id and {@link FileRefusedValueSchema}.",
"field-value.zod.ts FileReferenceIdValueSchema bullet (added, see deviations)": " * - an inline metadata blob is no longer the stored form; it is the\n * resolvedexpandedREAD form ({@link FileValueSchema}), derived rather\n * than stored;",
"field-value.zod.ts RAW_FILE_VALUES_CONTEXT_KEY docblock (added, see deviations)": " * ExecutionContext key that makes a read return file-field values in their\n * STORED form (the baresys_fileid) instead of the expanded form the\n * engine's read resolver derives in place: the{ id, name, url, … }file\n * object, or{ id, metadataRefused: true }for a reader refusedsys_file."
},
"claim_check": [
"valueSchemaFor(def, 'expanded') for a file type returns z.union([FileReferenceIdValueSchema, FileValueSchema, FileRefusedValueSchema]), and the stored form returns FileReferenceIdValueSchema (field-value.zod.ts, valueSchemaFor). Measured on the built dist: for { type: 'file' } expanded, the refused value, a bare id and { url } all parse true. In the stored form the refused value parses false.",
"FileLikeValueSchema = union(z.string().min(1), FileValueSchema). Measured on dist: the refused value parses false, a bare id true, { url } true. That is the 'fails it' sentence.",
"Both directions are pinned in field-value.test.ts: the stored form rejects the refused value (bad(..., { id, metadataRefused: true }) with no form), and the expanded form admits the object, the bare id and the refused marker (lines 353-379 at c4a077b).",
"engine.ts: toValue builds { id, name?, size?, mimeType?, url: /api/v1/storage/files/ID } from committed rows and from tombstones the held-file resolver reports held. isReadRefusal (PERMISSION_DENIED) maps every id token to { id, metadataRefused: true }. Every other branch (no sys_file registered, missing table, any other throw, no servable row, holder-check failure) returns ids unchanged. RAW_FILE_VALUES_CONTEXT_KEY returns before any sys_file read, so neither object form is produced under it."
],
"tests": "All on the tree at 056b909 (the final commit, which the derivation names). Every exit was captured before any pipe (cmd > log 2>&1; EXIT=$?). (1) Spec build under os-verify-lock: VERDICT command-exit 0, 77s. check-dts-emitted 38/38. The new JSDoc is present in packages/spec/dist/*.d.ts (grep hit). (2) pnpm --filter @objectstack/spec check:generated: exit 0, 'All 15 generated artifacts are up to date'. (3) pnpm --filter @objectstack/spec typecheck under the lock: VERDICT command-exit 0 (67s), 'check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. (4) Not ordered, run because engine.ts was touched: pnpm --filter @objectstack/objectql typecheck under the lock. The first try returned exit 2 with TS2307 (dependency dist absent in the fresh worktree, which is a prerequisite and not a finding). After the build: VERDICT command-exit 0, 'check:test-typecheck: OK — 40 file(s) / 234 error(s) / 65 pinned signature(s) held'. (5) Battery: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands returned exit 0 and derived 119 commands over 18 paths vs merge base d748ae8. First pass: 112 exit 0, plus 7 exit 3 PREREQUISITE NOT MET because nothing was built in the fresh worktree: lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:i18n, check:lean-entry-closure. Then pnpm exec turbo run build --concurrency=2 --filter=!@objectstack/docs ran under the lock: VERDICT command-exit 0, 72 tasks, 71 cached. The new comment text was confirmed in spec and objectql dist on disk. All 7 were rerun with their exact commands, and each returned exit 0 ('28 ObjectSchema.create example(s) ... os validate-clean', '262 prose examples type-check', '4 declared transcript value(s) ... equal what the registry derives today', 'check-i18n-bundles: OK', 'Admitted set held exactly (15 packages)'). Selected first-pass verdicts: check-spec-docblock-symbol-anchors '5546 anchors ... resolve', check:api-surface 'unchanged', check:docs '225 generated files in sync', check:nul-bytes OK, check:durability-log-level green, check-issue-citations 'no issue citations added'. (6) --ran over a record of all 119 commands with their final exits: exit 0, '119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero). Control-byte grep of both edited files: no hits. No ablation, because nothing executable changed.",
"deviations": [
"Scope widened inside the same file, comments only. The order named two spec passages, but a word check against valueSchemaFor found three more in field-value.zod.ts that still give the expanded form as the file object alone. These were FileValueSchema's 'makes THIS theexpandedread shape', the FileReferenceIdValueSchema bullet 'it is theexpandedREAD form', and RAW_FILE_VALUES_CONTEXT_KEY's 'instead of the expanded{ id, name, url, … }shape' (the key also suppresses the refused marker). All three ship in the .d.ts, and this PR made them incomplete. They were rewritten under the order's Why rule rather than left for a round 3. If the seat wants the narrower diff, revert those three hunks; nothing else depends on them.",
"The engine phrase the order quoted occurs twice in engine.ts (resolveFileReferences JSDoc L11932 and find() post-process L12681 at c4a077b). Both were rewritten. The findOne post-process comment ('resolve file-field id references (ADR-0104 D3)') makes no claim about the form and is unchanged.",
"dispatch-gates printed STALE TREE: at least 8 commits behind origin/main e8c6666, and 4 gate files changed in that range (check-future-spec-major.mjs, check-role-word.mjs, platform-object-tenancy-census.json, regen-artifacts.mjs). origin/main moved over 68 files since merge base d748ae8, and none of them is among this PR's 18 files, so under step 3 there was no merge. The battery reflects this branch's tree, and CI on the PR merge ref covers the difference. Ancestry: merge-base --is-ancestor d748ae8 to both origin/main and HEAD returned exit 0 (self-proving on a shallow clone).",
"ran.list records the final exit for the 7 gates that were rebuilt and rerun. Their first-pass exit 3 is recorded in this report and not in the ran file."
],
"not_fixed": [
"packages/spec/src/migrations/entries (18.data__FileValue__duration and 18.data-file-value-duration-unit-in-key) and the registry.ts copies call FileValueSchema 'the ADR-0104 D3 wave-2 EXPANDED READ form'. These are released migration text. The claim they carry (a read form, not a stored one, hence no D2 conversion) is still true. Left unchanged.",
"packages/spec/src/data/field-value.test.ts L343 test comment 'it is the expanded read form'. Test files were out of bounds this round."
],
"worktree": "removed: node_modules first, then git worktree remove without --force (exit 0). The tree was clean at removal.",
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #22593 via scripts/pm/post-stamped.mjs (POST issues/22593/comments). One git push (not a REST write). Reads only otherwise: gh api GET of comments 6094830418 and 6093175347.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (read-only, no reach measured): packages/spec/src/contracts/approval-service.ts, ApprovalActionAttachment docblock. It says the read path resolves the id into the expanded FileValueSchema form 'so a consumer gets everything it needs ... without read access to the system sys_file object'. That holds only if the sys_approval_action read runs with sys_file access. A caller refused sys_file read gets { id, metadataRefused: true } after this PR, and got a bare id before it, so the claim is not made false by this PR. The approval read context was not measured. Dedupe words: ApprovalActionAttachment, sys_file read, metadataRefused, attachments expanded."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22593,
"round": "patch round 3",
"status": "done",
"branch": "claude/issue-22593-file-hydration-refused",
"pr": "#22620",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run is identified by its branch)",
"resumed_from": "056b909d27dff72b83c0b2774c2a75061ff15aed",
"head": "4f460762974a6b438b8496ffcac99422cc4cccc6",
"premise_still_valid": true,
"summary": "Fixes the red required context the contract review on 056b909 recorded (comment 6095197443, item 1 under boundary flags). packages/spec/scripts/strictness-ledger.test.ts pinned analyzeSites over data/field-value.zod.ts at 3 sites; FileRefusedValueSchema, added by this PR, is the fourth (a strictObject), so the pin now reads 4 with a comment line in the file's own idiom saying the count is incidental. The z.looseObject idiom assertion on FileValueSchema beside it is unchanged, and no other assertion in that file reads field-value.zod.ts. The optional comment fix at packages/spec/src/data/field-value.test.ts L343 rode in the same commit (comment only). Both spec suites (test and test:repo) and both objectql suites are green locally, and every check-run on the new head concluded: 33 success and 2 skipped. Test Core (1/6) and the Test Core roster are success.",
"pin_as_written": [
" // 3 → 4 at #22593, which ADDEDFileRefusedValueSchema(a strictObject",
" // site) — the count is still incidental.",
" const fv = analyzeSites(at('data/field-value.zod.ts'));",
" expect(fv).toHaveLength(4);"
],
"comment_fix_as_written": " // The inline blob is no longer STORED — it is the resolved expanded read form.",
"files_changed": [
"packages/spec/scripts/strictness-ledger.test.ts (+3 / -1: the count 3 → 4 plus two comment lines)",
"packages/spec/src/data/field-value.test.ts (+1 / -1, comment only)",
"total vs 056b909: 2 files, +4 / -2; one commit 4f46076, pushed as a fast-forward 056b909..4f46076 (git ls-remote reads back 4f46076)"
],
"merge_of_main": "none. git fetch origin main: origin/main is 1b99388. Since merge base d748ae8 it moved over 107 files, and none of them is among this PR's 19 paths (no strictness file, no field-value file, no data shard, no engine.ts). Under step 3 there was no merge.",
"spec_suite_exits": {
"pnpm --filter @objectstack/spec test": "os-verify-lock VERDICT command-exit 0 · Test Files 640 passed (640) · Tests 19103 passed | 1 todo (19104)",
"pnpm --filter @objectstack/spec test:repo": "os-verify-lock VERDICT command-exit 0 · Test Files 54 passed (54) · Tests 915 passed (915). That is the repo project, 54 files, equal to the 54 entries in vitest.repo-tests.json",
"pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2 --reporter=verbose scripts/strictness-ledger.test.ts": "VERDICT command-exit 0 · 16/16 passed, including the case named in the review: '✓ |repo| scripts/strictness-ledger.test.ts › site counting reads the AST, not the source text › knows every object idiom, including z.looseObject('"
},
"check_runs_on_head": {
"head": "4f460762974a6b438b8496ffcac99422cc4cccc6",
"read_at": "2026-10-10T08:21:51Z",
"total": 35,
"pending": 0,
"non_success": [
"Console Pin Gate: skipped",
"Packed-tarball smoke (opt-in): skipped"
],
"conclusions": {
"Auto Label": "success",
"Build Core": "success",
"Build Docs": "success",
"Check Changeset": "success",
"Check Documentation Links": "success",
"Check PR Size": "success",
"Console Pin Gate": "skipped",
"Dogfood Regression Gate": "success",
"Dogfood Regression Gate (1/3)": "success",
"Dogfood Regression Gate (2/3)": "success",
"Dogfood Regression Gate (3/3)": "success",
"Dogfood Verify CLI": "success",
"filter": "success",
"Flag docs affected by code changes": "success",
"Governed Surface Queue Guard": "success",
"Lint & Repo Gates": "success",
"No other open PR may claim the same issue": "success",
"No other open PR may claim the same single-writer path": "success",
"Packed-tarball smoke (opt-in)": "skipped",
"Part-of PR must not also close its card": "success",
"Spec property liveness": "success",
"Temporal Conformance (live PG + MySQL)": "success",
"Test Core": "success",
"Test Core (1/6)": "success",
"Test Core (2/6)": "success",
"Test Core (3/6)": "success",
"Test Core (4/6)": "success",
"Test Core (5/6)": "success",
"Test Core (6/6)": "success",
"The card this PR closes must claim this branch": "success",
"Type Check · consumer gates": "success",
"Type Check · debt ledger": "success",
"Type Check · source gates": "success",
"Type Check · workspace": "success",
"TypeScript Type Check": "success"
}
},
"tests": "Everything ran on the tree at 4f46076, the final commit. Every exit was captured before any pipe (cmd > log 2>&1; EXIT=$?), and every build and test ran through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT set. (1) pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' --filter @objectstack/objectql build: VERDICT command-exit 0. It built 14 packages, spec included. (2) spec test:repo: VERDICT 0, 54/54 files and 915/915 tests. The single-file verbose run shows the formerly red case green. (3) spec test (local project): VERDICT 0, 640/640 files, 19103 passed and 1 todo. (4) spec typecheck: VERDICT 0, 'check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned signature(s) held'. (5) objectql test: VERDICT 0, 393/393 files and 7738/7738 tests. objectql test:repo: VERDICT 0, 1/1 file and 5/5 tests. (6) Battery: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands returned exit 0. It derived 120 commands over 19 paths vs merge base d748ae8; the 120th is new in this round because packages/spec/scripts/strictness-ledger.test.ts is a new path. First pass: 117 returned exit 0, and 3 returned exit 3 PREREQUISITE NOT MET, which is not a measurement: spec check:skill-examples (client-react dist absent), check:dual-build-cjs-loads (65 packages without dist) and check:i18n (CLI build closure). Then pnpm build --concurrency=2 (turbo, docs excluded) ran under the lock: VERDICT 0, 72/72 tasks, 71 cached. All 3 gates were rerun with their exact commands, and each returned exit 0: '262 prose examples type-check across 3 surface(s)', the dual-build provenance line 'this run 107/66/717/1 · floors 90/58/520/1', and 'check-i18n-bundles: OK (9 package(s) — all bundles in sync'. (7) --ran over all 120 commands with their final exits returned exit 0: '120 derived, 120 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero). No ablation and no reverse verification were run locally. CI already measured the red side on 056b909: Test Core (1/6) failed with expected length 3 but got 4, as quoted in the review. The green side is local and is also CI on 4f46076.",
"gate_exits": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:generated :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-governed-merges :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:quick-reference-counts :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:swallow-census-controls :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:i18n :: exit 0"
],
"gate_first_pass_refusals": [
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (PREREQUISITE NOT MET), rerun after the build :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET), rerun after the build :: exit 0",
"pnpm check:i18n :: exit 3 (PREREQUISITE NOT MET), rerun after the build :: exit 0"
],
"deviations": [
"dispatch-gates printed STALE TREE: at least 12 commits behind origin/main 1b99388, with 4 gate files changed in that range (check-future-spec-major.mjs, check-role-word.mjs, platform-object-tenancy-census.json, regen-artifacts.mjs). main moved over none of this PR's paths, so there was no merge (step 3). The battery reflects this branch's tree. CI on the PR merge ref covers the difference, and every check-run there is success or skipped.",
"The first-pass exit 3 results are recorded here and in gate_first_pass_refusals. The ran file given to --ran carries the final exits."
],
"not_fixed": [
"packages/spec/src/data/field-value.test.ts L353 test TITLE 'the EXPANDED media form is the resolved object, or a still-unresolved id' names two of the three expanded forms. The very next test names the third (the refused form). A title is not a comment, and the order said comment only, so it was left unchanged."
],
"boundary_note_for_seat": "Review item 5 under boundary flags (cross-lane paths): this round adds packages/spec/scripts/strictness-ledger.test.ts to the domain:spec path list before undraft, as that record anticipated. A fresh contract record is owed on head 4f46076 (review item 1). This round did not touch the PR body.",
"worktree": "removal follows this post (node_modules first, then git worktree remove without --force); the terminal report records its exit",
"mcp_calls": "0",
"api_writes": "1: this os-dev-report comment on #22593 via scripts/pm/post-stamped.mjs (POST issues/22593/comments). One git push, which is not a REST write. Everything else was a GET: comment 6095197443, comments 6095113347, check-runs on 4f46076 (three reads), check-run 114164297705 with its annotations and job, and pulls/22620. The job-log download was refused (403 from the blob host). Nothing depends on it, because the job steps and the conclusion were read.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (no PR or person touching the file is known) · noted, not filed (comment drift that predates this PR, so not a reproducible defect): packages/spec/scripts/strictness-ledger.test.ts at L387-389 (head 4f46076) says 'the repo has no.strict()on a loose object and noz.looseObject(under a triaged directory'. data/field-value.zod.ts FileValueSchema is a z.looseObject site under the triaged data/ directory, and L136 of the same file asserts it. The drift is not made false by this PR (FileValueSchema was z.looseObject on main), so it was left unchanged. Dedupe words: strictness-ledger.test.ts, looseObject, triaged directory, synthetic posture."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22620 at head
4f46076297. A refusedsys_filehydration marks the file field{ id, metadataRefused: true }instead of a bare id that reads as no filedomain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG(os-tesla), claim 6093175347 · 2026-10-10T08:39Z. Read against GitHub and the branch, not the reports (os-dev-reports 6094498292, 6094830418, 6095113347 and 6095641615).Contract review at
CONTRACT_REVIEW_TIER: 6095738929, PASS on this head. It is owed because of theClause-②: yesline.- The earlier records on the PR are 6094604322 (on
faf689872c) and 6095197443 (FAIL on056b909d27). The FAIL ground was the red requiredTest Core (1/6): the strictness-ledger pin counted 3 sites, and the PR adds a fourth. - The seat's own miss. 6094604322 recorded that shard as pending. The seat then dispatched two rider rounds before reading CI. Patch round 3 fixed the pin. From now on, the seat's reviewers wait for every required context to conclude.
Shape.
- Draft, base
main. Line 1 isFixes #22593and line 2 isClause-②: yes, matching the claim. - 19 files, +737/−61. NOT governed (
check-governed-merges: 798 changed lines). - The head merges clean with
origin/main(git merge-tree).
The change, as read in the diff
- The catch.
ObjectQL.resolveFileReferences(objectql/src/engine.ts) gains a refusal arm, first in the catch. It takes onlyPERMISSION_DENIED, the ADR-0112 code, matched by literal.- Every token-shaped id in a file field becomes
{ id, metadataRefused: true }, per element formultiple. - The record read still succeeds.
- An outage, a missing table or a missing row keeps the bare id, as before.
- Every token-shaped id in a file field becomes
- The new spec export.
FileRefusedValueSchema(@objectstack/spec/data) is closed to exactly those two keys. It joins the file fields' expanded read union. The stored form is unchanged and refuses the marker. - Pins and ablation.
- The pins cover a refused reader seeing the marker on the seam and on the real
SecurityPlugin+SqlDriverstack, an empty field readingnull, and a reader withsys_fileread getting the hydrated file. - The ablation of the arm turns them red.
- The pins cover a refused reader seeing the marker on the seam and on the real
- Docs. The
fileentry intypes.mdxnames the three read forms, andattachments-access.mdxgains one clause. - Changesets.
@objectstack/specand@objectstack/objectql, both minor.
Cross-lane, declared before undraft:
domain:spec: 6094514829 and 6095659956 (the ledger pin);domain:services: 6094517003;domain:devx: 6094519526 and 6095751701 (the two hand-written pages, which the record found missing from the PR's list).
The PR body's list is corrected to match.
CI on
4f46076297. 42 runs: 38 success and 4 skipped, with 0 failure and 0 pending. All seven required contexts are success.- Two of the skips are roster skips: Console Pin Gate and Packed-tarball smoke.
- The other two are Auto Label and Check PR Size in the PR-event re-run after the body edits. Both are success in the push run on the same head.
Out of scope, one line each
- The access half is ruled B on [Decision] should a file field's sys_file metadata (name, size, type) follow the holding record's read, as the download door and attachment rows already do? (access half of #22593) #22624 (6094990917). Its execution card, objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refused
sys_fileread (ruling B on #22624) #22637, waits for this landing and is the lane's next serial dispatch. plugin-approvals'normalizeActionAttachmentwould drop the marker. Its only caller reads under the system context, so no reader meets it today. Noted, not filed.- ADR-0104 §D3's prose still names only
FileValueSchemaas the expanded form. It is governed text, so an erratum is the maintainer's hand. Noted for the access half's execution. strictness-ledger.test.tsL387–389's comment contradicts L136 of the same file. The drift predates this PR; it rides the next card that touches the file.- The objectui consumer already keeps refused and empty distinct, a generic chip versus
EmptyValue. The restricted-state rendering is relayed to objectui on landing.
Next: ready, then auto-merge, in this act.
- The earlier records on the PR are 6094604322 (on
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22620 →
02d9f69e5athrough the merge queue, at 2026-10-10T09:05Z.domain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG· 2026-10-10T09:07Z.- Content on
origin/main: all 19 files are blob-identical to the reviewed head4f46076297.- Among them: the engine's refusal arm,
FileRefusedValueSchema, the pins, the strictness-ledger pin, the docs, the regenerated shards and the changeset. 02d9f69e5ais an ancestor oforigin/main.
- Among them: the engine's refusal arm,
- Card.
Fixes #22593closed it as completed.pm:dispatchedis removed in this act. - Records: ACCEPT 6095763359 and contract review 6095738929 (PASS). The earlier FAIL 6095197443 was answered on this head.
- Access half: objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refused
sys_fileread (ruling B on #22624) #22637 executes ruling B on [Decision] should a file field's sys_file metadata (name, size, type) follow the holding record's read, as the download door and attachment rows already do? (access half of #22593) #22624. Its blocker is now closed, so in this act it moves frompm:blockedtopm:queue. It is next in this lane's serial order, behind feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 stage S5, which is still in flight. - Consumer: the restricted-state rendering of
{ id, metadataRefused: true }is relayed to objectui on deps(v18): move objectui's @objectstack/* dependencies to thenextprereleases (18.0.0-next.N) ahead of 18.0 GA — the maintainer's ruling, since 17.x ships no new release objectui#12030. The marker reaches objectui with the move to the 18nextline.
- Content on
Filing gate ①: a product defect, measured once on 17.7.0 by the
repo:hotclmPM seat in a full browser pass (objectstack-ai/hotclm#87, screenshot036). It was folded into checklist #22590 as item 1. Triage split it out because its landing lane differs (the maintainer's instruction for platform problems found in that pass: 「你遇到的平台问题应该提交issue」). Filed by the triage seat (seat post #6015,session_01AavokzJ5DndAwitDXvKy4U). ⛔ Not a claim.Reader: the
domain:engineseat (packages/objectql).The defect
clm_contract_versionbut not onsys_fileopens a version they just uploaded. The file field renders "no file".sys_file lookup failed; file fields keep their raw ids and will render as "no file" for this read.main5495331fcc:packages/objectql/src/engine.ts, the file-field hydration (the warn at about:12007). Its own comment names this shape: "a fault wearing the appearance of legitimate absent data, indistinguishable from a record that truly holds no file (ADR-0110 D3)". A permission denial is one of the causes it lists.Two halves, decided separately
sys_attachmentreads follow the parent record?sys_file's read boundary, so it goes to the maintainer.sys_fileread rule, and how attachments reach the parent-record gate, and files a decision card with the options.Pins (loud half)
sys_fileread sees the field marked as refused, not empty.sys_fileread gets the hydrated file.Measure on
mainfirst. The report is from 17.7.0.Dedupe (by the filing seat, on #22590):
sys_file read denied file field renders no file hydration record readablereturned 2 hits (#17406, #10702, both closed and unrelated).