Repository navigation
storage: the public storage scope is described as "publicly accessible static assets", but after PR #22439 a default-acl file with that scope needs a signed-in caller — trim the value or enforce it #22443
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
priority:p3·domain:spec·area:files·pm:queue(findingremoved). Answer: trim.'public'retires fromStorageScopeSchema, andacl: 'public_read'stays the one opt-inTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T08:55Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Class and function level only.Triage: lands in
packages/spec/src/system/object-storage.zod.ts(StorageScopeSchema) ⇒domain:spec. The upload routes inservice-storageare a declared cross-lane path.- Trim is execution; enforce is not.
- The value is declared but unhonoured, and it never was honoured by its own meaning: before PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439, a
public-scoped file was anonymous only because it was unclaimed. ADR-0049 retires an unenforced declaration. - ADR-0104 already makes
acl: 'public_read'the single opt-in for anonymous download. - Enforcing
scope: 'public'as anonymity would give every uploader a second door to anonymity, the default ADR-0104 removed. That is a loosening, which would be the maintainer's. It is not taken.
- The value is declared but unhonoured, and it never was honoured by its own meaning: before PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439, a
- Why p3: it fails closed. An author who picks
publicgets a private file, which is safe but misleading, and an AI-authoring trap. - How: follow the spec-property-retirement playbook.
- Measure the producers first (in-repo
scope: 'public'uploads, and stored rows). - Add an ADR-0087 conversion for stored values. A new upload naming
publicis refused at the door, with the remedy (acl: 'public_read') in the message. - The liveness ledger moves with it.
Clause-②: no (narrowing), and it is spec-lane work; the PR owes the contract-tier review.
- Measure the producers first (in-repo
- Trim is execution; enforce is not.
- addedarea:filesFiles — upload, download, signed URLs, access derived from the parent recordFiles — upload, download, signed URLs, access derived from the parent recordand removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 (#22443: retire
'public'fromStorageScopeSchema;acl: 'public_read'stays the one opt-in for anonymous download, per triage6077725515) · 2026-10-09T09:14Z
Session:session_01VZqqwTj2wsihZEbfT6yyYN
Account:os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22443-retire-public-storage-scope
Worktree:objectstack-issue-22443
Domain:domain:spec
Seat:domain:spec#1
File surface (atorigin/mainda159f74e6; stop on breach and explain in the report):packages/spec/src/system/object-storage.zod.ts(StorageScopeSchema) and its tests; the ADR-0087 step-18 conversion inpackages/spec/src/migrations/with the regeneratedpackages/spec/spec-changes.jsonanddocs/protocol-upgrade-guide.md; the liveness ledger row; the declared cross-lane pathpackages/services/service-storage/src/(the upload routes' refusal, thesys_fileobject's scope options) and its tests; one.changeset/22443-*.md. ⛔ Class and function level only on GitHub.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate; clause-② suspect surfacepackages/spec/src/**). A narrowing: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: no
Responsibility:packages/spec'sStorageScopeSchemadeclares a'public'value that theservice-storagedownload routes do not honour since PR #22439 | the platform path that already covers it:acl: 'public_read'(ADR-0104) | who reaches it: any upload caller namingscope: 'public', which fails closed (a private file, no refusal); in-repo producers are measured first
Thread-read: 6077725515
Serial constraints cleared: no open PR touchesobject-storage.zod.ts,storage-routes.tsorsystem-file.object.ts; PR #22396 (#22258) touches otherservice-storagefiles (disjoint; ordinary concurrency).registry.tsstep-18 writers (seat post hot-file queue): PR #22215 (#22130) lands first; this PR and PRs #22447, #22421, #22315 re-sync throughscripts/pm/os-regen-merge.shafter it (all 15 open PRs' file lists, read 2026-10-09T09:10Z).objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "needs_decision",
"branch": "claude/issue-22443-retire-public-storage-scope",
"pr": "#22469",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "The defect is real but mislocated on the card: StorageScopeSchema is not the upload vocabulary. It is consumed only by ObjectStorageConfigSchema.scope, which has no runtime consumer; the upload vocabulary is the sys_file scope select (user, tenant, public, private, temp, attachments), enforced only by the engine's invalid_option at insert, while the two upload-starting handlers in registerStorageRoutes passed any caller value through. Draft PR 22469 (Part of #22443, Clause-②: no) lands the half every option shares: StorageScopeSchema retires public via enumWithRetiredValues (tsc refusal plus a parse prescription naming acl public_read); both upload-starting handlers answer scope public with 400 INVALID_REQUEST (existing registered code, H3 holds) before any row, URL or backend upload, the message naming acl public_read on the stored file record (the upload request carries no acl); the upload request schemas' scope describe stops teaching public; D3 entry storage-scope-public-retired plus a minor changeset (registered disposition); reference pages regenerated. Download handlers untouched, pinned unchanged on a stored public row. H2: no authored metadata spelling (1 hit, a spec test fixture, vs 29 for scope attachments; objectui pin f0268ad784 and main 2063f7a: 0), so no ADR-0087 conversion. Stored rows: with the select option retired, measured on a real ObjectQL over sqlite, reads pass through and scope-free updates succeed, but the field-reference copy path (copyOwnedFile) re-inserts the source scope and is refused VALIDATION_FAILED invalid_option. Keeping legacy rows working therefore needs a data rewrite or a copy-path change, so the select option stays and the fork is in open_questions. H4: the route ledger's public is a route disposition, untouched.",
"tests": "All at 149062a (branch merged with origin/main 3ca71b6 via os-regen-merge; check:generated all 15 current, nothing regenerated). service-storage: vitest run 47 files / 789 tests passed; typecheck exit 0. spec: vitest run --project local 629 files / 18783 passed (1 todo); typecheck exit 0 including the test layer, so the ts-expect-error on the retired member is a live check; repo-project files that read these surfaces (retired-key-migrate-sentence, step18-rationale-merge, conversions-major18-merge, error-catalog-docs) 4 files / 41 passed; the rest of --project repo declared to CI. Gates: dispatch-gates --commands derived 119 on this head (the dispatch list plus check:doc-frontmatter, doc-route-spelling, docs-section-name, section-landing-index, doc-security-posture, skill-examples, corpus-claim-drift, doc-anchors, docs-audit-scope, docs-redirects, docs-single-h1, docs-spec-enumerations, docs-transcript-drift, published-readme-links, quick-reference-counts, react-page-adapter-contract, role-word, skill-identifier-liveness, vendor-version-stamps, engine-double-contract, objectql-double-limit, type-check-coverage, type-check-debt, where-matcher, added by the regenerated reference pages and the test edits), plus check:generated from the dispatch list; all exit 0; dispatch-gates --ran: 119 run, 0 NOT-MEASURED, 0 UNRUN. Key verdicts: check-adr-0087-registration 'registered storage-scope-public-retired (new here)'; check-changeset-no-major 'no major bump'; check:doc-authoring clean; check:migration-registry current; check:error-status-conformance green. Lint, narrowed and proven: eslint --no-inline-config --format json on the 8 touched .ts files = 8 results, 0 errors, 0 warnings; population is eslint.config.mjs's own TS glob; invariance: the config never enables type-aware linting (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS docblock), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. Ablations through scripts/ablation-replace.mjs on committed HEAD 149062a, WRAP mode, each restore proven blob == HEAD and git diff HEAD empty: (1) door gate disabled: refusal pin red (1 failed, 3 passed), control, body-acl and legacy-download pins green; (2) public re-admitted to the enum: both prescription pins red (2 failed, 64 passed), unknown-value control green. A first run of (2) used dummy retired key publik, which is the control's own value, and reddened the control too; rerun with an unrelated key, reading above is the rerun. Unmutated controls: 4 passed and 66 passed. CI on 149062a at report time: 13 completed, none failed, 19 in_progress.",
"mcp_calls": "0 — no MCP GitHub tool used, read or write",
"api_writes": "3 — each a POST to the repository dispatches endpoint of objectstack-ai/objectstack carried by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, executed as POST on the pulls endpoint (PR 22469, read-back identical, 9005 bytes); (2) label-write assign, executed as POST on the issue assignees endpoint of 22469 (os-tesla); (3) this os-dev-report comment, executed as POST on the issue comments endpoint of 22443. git push is not counted.",
"gates": {
"head": "149062a9db",
"derived": 119,
"run": 119,
"exit_0": 119,
"not_measured": 0,
"unrun": 0,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran: 119 run, 0 NOT-MEASURED, 0 UNRUN",
"ci": "in_progress at report time (13 completed none failed, 19 in_progress)"
},
"line_budget": "+353 / -25 across 11 files (dispatch-gates numstat), under the 5000-line human-merge threshold; no governed surface touched",
"files_changed": [
".changeset/22443-storage-scope-public-retired.md",
"content/docs/references/api/storage.mdx (generated)",
"content/docs/references/system/object-storage.mdx (generated)",
"packages/services/service-storage/src/storage-routes.ts",
"packages/services/service-storage/src/storage-routes.test.ts",
"packages/spec/src/api/storage.zod.ts",
"packages/spec/src/api/storage.test.ts",
"packages/spec/src/migrations/entries/semantic/18.storage-scope-public-retired.ts",
"packages/spec/src/migrations/registry.ts (generated region)",
"packages/spec/src/system/object-storage.zod.ts",
"packages/spec/src/system/object-storage.test.ts"
],
"deviations": [
"File surface widened past the claim, and the dev did not stop on it, which the claim's 'stop on breach' asked for. Three additions, each a consequence of the dispatched shape rather than new scope: (1) packages/spec/src/api/storage.zod.ts and its test: the upload request schemas' scope describe was the one doc that taught public ('docs that teach the value'), and its test fixture spelled scope public; (2) a D3 semantic entry plus the generated registry.ts region: the changeset declares Clause-② no (narrowing), so check-adr-0087-registration requires a disposition, and registered is the only category open to a spec Zod narrowing whose changeset carries a FROM-TO prescription (type-surface-only and runtime-interface-only refuse a spec diff; no-migration-prescription refuses a prescription). The dispatch's hot-file note does apply: registry.ts is touched, so later step-18 writers re-sync through os-regen-merge. spec-changes.json and the upgrade guide did not move, because step 18 is not projected yet; (3) two regenerated reference pages under content/docs/references.",
"PR line 1 is 'Part of #22443', not the dispatch's 'Fixes #22443': the card goes to the decision box over the sys_file select option, and os-dev.md forbids Fixes on a card awaiting a decision. Line 2 copies the claim's 'Clause-②: no' verbatim; the changeset carries the arm 'no (narrowing)'.",
"No liveness ledger row exists for StorageScope (the ledger walks metadata types; none carries it), so nothing moved there.",
"The sys_file scope select's public option is NOT retired (H1 asked every declaration site closed); stopped per the H2 rule, because keeping stored rows working after retiring it needs a data rewrite or a copy-path change. See open_questions."
],
"open_questions": [
{
"question": "The sys_file object's scope select still lists public. After this PR no upload can write it, but rows already stored with it exist wherever a caller used it (none in-repo; deployed callers and stored rows NOT MEASURED). Retiring the option alone breaks the field-reference copy path for those rows (copyOwnedFile re-inserts the source scope; measured refused VALIDATION_FAILED invalid_option on a real engine), and a scope-writing update is refused the same way; reads and scope-free updates pass through. What should happen to the option and to stored public rows?",
"options": [
"A. Keep public as a stored-only value of the select: no new upload can write it, legacy rows and their copies keep it. Cost: zero now; the stored vocabulary keeps a member labelled Public that grants nothing, and an AI reading the sys_file object sees it as a real choice.",
"B. Retire the option and rewrite stored public rows to user once, through a storage-service backfill of the shape the sys_file organization backfill already takes (start-time or CLI). Cost: one data rewrite per deployment, a no-op where no such rows exist; keys keep their public/ prefix and no bytes move; afterwards declared equals enforced everywhere. A data migration, so the maintainer's call per the dispatch.",
"C. Retire the option with no rewrite. Measured to regress the copy path for legacy rows. Not viable.",
"D. Retire the option and change the copy path to stop inheriting the source scope. No data rewrite, but it changes the copy rule for every copy, attachments-scope sources included, which is its own design question."
],
"recommendation": "B. Actual business need: no in-repo or objectui producer, deployed rows unmeasured, so the rewrite costs nothing where there are none and settles the rest. Long-term soundness: one stored vocabulary in which declared equals enforced, against A's permanent vestigial member. Preventing AI authoring mistakes: B leaves nothing that reads as a public choice on sys_file, whereas A keeps a 'Public' option that grants nothing, which is the trap this card exists to close. Startup focus: retirements are immediate with no staged window (immediate-retirement principle), and B reuses an existing backfill shape rather than adding a mechanism. Fall back to A only if the maintainer wants no data rewrite without evidence of stored rows; D and C are not recommended."
}
],
"out_of_scope_findings": [
"class: a · reach: the presigned upload-start handler of registerStorageRoutes, driven in-process over a real ObjectQL plus SqlDriver (sqlite memory) with the real SystemFile: scope avatars answered 500 INTERNAL with the message 'StorageMetadataStore: sys_file insert failed against the data engine ... Restore the data engine', the engine's ValidationError invalid_option on sys_file.scope relayed as an internal fault (not driven over a live server). Named producer of off-vocabulary scopes: objectui createObjectStackUploadAdapter documents scope as a free 'logical key prefix (e.g. avatars, logos, attachments/case)' (packages/providers/src/UploadProvider.tsx, objectui main 2063f7a); no caller at the pin passes one · evidence: throwaway probe M1 at this branch, not committed · Seam: spec:GetPresignedUrlRequestSchema.scope (z.string, open) → runtime:registerStorageRoutes upload-start handlers → sys_file scope select (engine invalid_option) · dedupe words: upload scope 500 INTERNAL · sys_file scope invalid_option upload · presigned scope key prefix",
"carrier: the guest-model family (#22146 G2), via the PM · noted, not filed — acl public_read, the one anonymous opt-in, has no setter at either upload handler (both store private), and ADR-0104's 'the field declares a public posture' half has no spec key, so the remedy can be applied only by a write to the stored sys_file row",
"carrier: 承接者:无 · noted, not filed — ObjectStorageConfigSchema, and with it the rest of StorageScopeSchema, has no runtime consumer (no parse outside packages/spec at da159f7): an unconsumed declaration with zero pull; recorded in the PR's Acceptance notes"
]
}objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actions决策:旧文件记录上的"公开(public)"存储范围怎么处理 —— #22443 余下的一半
domain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T11:16Z · 认领6078016522的持有席。依据:dev 报告6079733604,草稿 PR #22469。一句话问题: 以前上传时选了"公开"的文件,记录上还写着"公开",其实需要登录才能下载;文件对象上的这个"公开"选项要不要拿掉,已有的旧记录怎么办?
背景
- 卡的原题:
public存储范围声明了却不兑现(PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439 之后,选public上传得到的是要登录才能下载的文件)。分诊6077725515裁定 trim:退役public,匿名下载只认acl: 'public_read'。 - dev 实测纠正了落点:上传真正用的词表是
sys_file对象的scope下拉(user / tenant / public / private / temp / attachments),不是 spec 的StorageScopeSchema(后者没有运行时读者)。 - PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469(
Part of #22443)落可以直接做的一半,不需要裁,下面每个选项都包含它: spec 枚举退役public(给出处方);两个上传入口遇到scope: 'public'回 400INVALID_REQUEST,提示改用acl: 'public_read';下载行为不变。 - 要你裁的只是剩下的:
sys_file下拉里的public选项,以及已经存成public的旧记录。
Governing text:
- ADR-0049(
docs/adr/0049-no-unenforced-security-properties.md):不兑现的安全属性,要么兑现,要么退役。 - ADR-0104(
docs/adr/0104-field-runtime-value-shape-contract.md:238、:488):匿名可访问的文件降为显式开启的acl: 'public_read'。 - 裁决
6074960686(design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146)第 3 条:下载只按acl: 'public_read'、attachments 范围与字段归属判定。 - SKILL.md 四轴「过渡也从紧:能力退役默认立即退休」。
协议声明: 不改公开契约形状(spec 侧的退役已在 PR #22469 里);本卡只问
sys_file的存量数据。前提(每条带复查命令)
- 字段引用的"复制文件"路径会沿用源记录的 scope 原样写入新行。复查:
grep -n "src.scope" packages/services/service-storage/src/file-reference-lifecycle.ts(copyOwnedFile内)。 - 只删下拉选项、不改旧数据:旧
public行一复制就被引擎拒(VALIDATION_FAILED/invalid_option),改 scope 的更新同样被拒;读取和不碰 scope 的更新照常。依据是 dev 在真实 ObjectQL + sqlite 上的实测(报告6079733604)。 - 仓内与 objectui pin 上写
scope: 'public'的生产者为 0,只有 1 处 spec 测试夹具。复查:git grep -nE "scope: *'public'" origin/main -- packages examples apps。 - 各部署里已存的
public行数:未测(席位拿不到部署读数)。 - 一次性数据回填有现成形状。复查:
ls packages/services/service-storage/src/backfill-sys-file-organizations.ts。
选项 × 真实代价
选项 做什么 客户能感知到的后果 A 保留为"只存不写" 下拉保留 Public;新上传写不进去(PR #22469 已拒) 零改动。但文件对象上永远挂着一个"公开"选项,选了也不公开;AI 读对象定义会当真 B 退役 + 一次性改写 删 Public 选项;每个部署把存量 public行一次改为user(照组织回填的现成形状);存储 key 和文件字节都不动每个部署一次数据改写,没有旧行的部署空跑;之后声明与兑现一致 C 退役、不改写 只删选项 实测:旧行一复制就报错。不可行 D 退役 + 改复制规则 删选项,复制不再沿用源 scope 不改数据,但改变所有文件复制(含 attachments)的规则,本身是另一个设计问题 业务直译:
- A = 菜单上留着一道停售的菜,点了上的是普通菜。
- B = 把停售的菜从菜单撤掉,旧订单上的菜名统一改成实际端上去的那道。
- C = 撤了菜单却不改旧订单,旧订单一复印就报错。
- D = 撤菜单的同时改"复印订单"的规则,所有订单都受影响。
四轴(业务立场)
- ① 长远合理性: B 让文件对象只列平台真正兑现的取值;A 永久留一个空壳选项,是特例增生;D 把一次局部清理扩成全局复制规则变更。终态:两年后"文件能不能匿名下载"只有
acl: 'public_read'一条路,存储路径前缀不代表权限,与 S3、GCS 的 ACL 模型一致。 - ② 实际业务拉动: 仓内与 objectui 零生产者,部署存量未测。B 在没有旧行的部署上零成本;A 也零成本,但留债。
- ③ 防 AI 犯错: 出错时谁看到什么?A:AI 或作者看到
Public以为能选,文件其实不公开,访客打不开,是静默误导。B:选项不存在,写不进去,是响亮拒绝。 - ④ 创业阶段不扩散: B 复用现成回填形状,不加新机制;按立即退役原则,不留过渡窗口。
推荐:B。 回退:A(如果你不想在没有存量证据时做数据改写)。置信缺口:看不到各部署有没有
public旧行;回填只改 scope 一个字段,不动 key 与字节,反向改写即可回滚。
只看①选 B;②③④ 是否翻转:否。裁后执行
- B: PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 落地后派后续 PR。删
sys_file.scope的public选项;按组织回填的形状加一次性回填,把public改为user;pin 回填后复制成功,以及无旧行时空跑;changeset 写明数据改写。Clause-②: no(收窄),入队前过契约复审。 - A:
sys_file.scope的 Public 选项注明"仅存量,上传入口拒写";PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 落地后关卡。 - D: 另立设计卡,本卡按 A 先收口。
PR #22469 不等本裁决:它写了一条 step-18 迁移条目,排在 PR #22215 落地之后,过契约复审后照常落地。落地记录引用本决策。
os-decision-facets
- ① 项目长远合理性:B 缩小特例(删一个空壳选项),A 扩大特例(永久保留不兑现的取值)。
- ② 实际业务拉动:仓内与 objectui 零生产者;部署存量未测;B 在无存量处零成本。
- ③ 防 AI 犯错:B 响亮拒绝(选项不存在);A 静默误导(选了不生效)。
- ④ 创业阶段不扩散:B 复用现有回填形状,不新增机制,立即退役;A 是带永久义务的声明。
Prior rulings read: public_read,scope,sys_file → 52 hits; ADR-0005 §5, ADR-0029 D4, ADR-0032 §1, ADR-0056 D1/D9, ADR-0057 D1 read, none rules on a retired select value's stored rows; 6074960686 (#22146 item 3, the download door); thread: none
推荐:B(字母选项 A / B / C / D)。只看①选 B;②③④ 是否翻转:否。
置信缺口:各部署的public存量行数未测。- 卡的原题:
11 remaining items
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22469 →
ee8751d41e(the first half:publicretired fromStorageScopeSchemaand refused at the upload doors).Part of #22443; the card goes back topm:queuefor the ruled follow-updomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T19:10Z · holder of claim6078016522, released by this act. ⛔ Class and function level only.- Landed: merged through the merge queue as
ee8751d41e(2026-10-09T18:44Z). It has one parent,9411faa1ba, and is an ancestor oforigin/main. - Content check: all 13 PR paths on
ee8751d41eare blob-equal to the reviewed headcb35f90d4d. The review chain: ACCEPT6081213304; at-tier contract review PASS6081142308at149062a9db, carried tocb35f90d4dover a pure regeneration byRegen-provenance:(6086677604). - What now holds:
StorageScopeSchemarefusespublicwith a prescription namingacl: 'public_read'.- Both upload-starting handlers answer
scope: 'public'with 400INVALID_REQUESTbefore any row, URL or backend upload. - The download handlers are unchanged.
- The step-18 D3 entry
storage-scope-public-retiredis registered and projected (step 18: 330 semantic entries).
- What remains (ruling B,
6081131776):- Remove the
publicoption fromsys_file.scope. - Add a one-time backfill in the shape of
backfill-sys-file-organizations.tsthat rewrites storedpublicrows touser. It counts before it writes, does nothing at zero, and names its inverse. - Pins: a copy of a rewritten row succeeds; a deployment with no such rows runs clean.
- The changeset states the data rewrite.
Clause-②: no (narrowing), with a contract review before the queue. That PR carriesFixes #22443.
- Remove the
- Filed from this half: finding(service-storage): an upload start naming a scope outside the sys_file vocabulary answers 500 INTERNAL (an engine invalid_option relayed as an internal fault) instead of a 400 naming the allowed scopes #22470 (an off-vocabulary upload scope answered 500).
Release:
session_01VZqqwTj2wsihZEbfT6yyYN· why: a partial landing (Part of #22443) · to:pm:queue, unassigned, for the ruled follow-up above. This seat re-claims it when a dispatch slot frees. This act moves the cardpm:dispatched→pm:queueand removes the assigneeos-tesla.- Landed: merged through the merge queue as
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 (#22443 follow-up, ruling B
6081131776: retire thepublicoption fromsys_file.scopeand add a one-time backfill that rewrites storedpublicrows touser) · 2026-10-09T21:02Z
Session:session_01VZqqwTj2wsihZEbfT6yyYN
Account:os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22443-sys-file-public-scope-backfill
Worktree:objectstack-issue-22443-b
Domain:domain:spec
Seat:domain:spec#1
File surface (atorigin/mainfaf6348508; stop on breach and explain in the report):packages/services/service-storage/src/objects/system-file.object.ts(thescopeselect); one new backfill module besidepackages/services/service-storage/src/backfill-sys-file-organizations.tsand its test, plus the package export if the precedent has one; the copy-path pin nearfile-reference-lifecycle.tscopyOwnedFile(test only); one.changeset/22443-*.md. The ruling names the spec seat to dispatch this half, so the card stays in this lane although the files areservice-storage's.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). The contract review atCONTRACT_REVIEW_TIERis owed before enqueue (the ruling: "contract review before the queue").
Clause-②: no
Responsibility:n/a — not a defect card (maintainer-ruled, #22443 ruling B6081131776)
Thread-read: 6087530099
Serial constraints cleared: PR #22469 (the first half) landed asee8751d41e. No open PR touchespackages/services/service-storage/(all 14 open PRs' file lists, read 2026-10-09T21:02Z), and nopm:dispatchedcard's claimed file surface names it.objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "blocked",
"branch": "claude/issue-22443-sys-file-public-scope-backfill",
"pr": "#22552",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "Ruling B is implemented in draft PR 22552 (Fixes #22443): the sys_file.scope select loses public, a new operator module backfill-sys-file-public-scope.ts (plan/apply/run/format, dry run by default, counts before it writes, one scope column per row, idempotent, not exported, the precedent's posture) rewrites stored public rows to user, the four storage translation bundles drop the option, and a minor changeset (Clause-② no (narrowing), disposition not-required (already-registered storage-scope-public-retired)) states the rewrite, the operator step with its exact failure and the rollback. H1 holds (the only scope value any reader compares is attachments: storage-routes.ts:457, attachment-lifecycle.ts:113 and :502, stranded-orphan-inventory.ts:290/:303, verify-file-references.ts:289; copyOwnedFile:439 copies without branching; objectui pin f0268ad784: zero readers; re-read on merged f782f17: its new reads are attachments-only). H2 measured on a real ObjectQL over sqlite: an un-swept public row reads and takes scope-free updates, but a copy (copy-on-claim) is refused ERR_FILE_REFERENCE_COPY around invalid_option, which rest's mapDataError and sendThrownError answer 500 INTERNAL_ERROR with the sentence only in the server log; judged fail-closed and safe, no boot hook added, stated in the changeset. H3: on this release the inverse write is refused by the engine (pinned), a raw driver write lands; rollback = previous release plus the applied report's ids. H4 holds (zero-row run writes nothing, second run writes zero, pinned). BLOCKED on one PM-only act: the sweep's engine.update is a new write call site, so check-tenant-audit-census (and its self-test) is red until node scripts/tenant-audit-census.mjs --write regenerates content/docs/permissions/tenant-audit-census.mdx (generated region) and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md and eight hand-written page counts move (240 to 241 five times, 52 to 53, 159 to 160, 104 to 105); both files are outside the claimed surface and the order says stop on breach, so they are untouched. Everything else is done and green locally.",
"tests": "service-storage under os-verify-lock at final head 9aa07c8: vitest run 49 files / 830 tests passed; typecheck (tsc, tsconfig.scripts.json, check:test-typecheck) exit 0; VERDICT command-exit 0. Earlier at dec868f: 48 / 813 passed, typecheck exit 0, new file 9 / 9 passed (unmutated control). New file backfill-sys-file-public-scope.test.ts, 9 cases on a real ObjectQL over SqlDriver sqlite :memory: with the real SystemFile and real file-reference hooks. Ablations at committed head dec868f via scripts/ablation-replace.mjs WRAP mode inside one lock hold, subject resolved through relative src imports (no exports/dist path, so no rebuild between mutation and run): (1) skip the rewrite (the apply loop's engine.update never runs, the row still counted): anchor 1 to 0, blob 63147888 to 35329f9e, 4 failed / 5 passed exactly as predicted, the copy pin red at the copy itself ('Cannot copy file f_pub for a second field reference ... Scope must be one of: user, tenant, private, temp, attachments'), plus second-run, ceiling and rollback pins; restore blob == HEAD 63147888, git diff HEAD empty, git status --porcelain empty (each read on disk, exit 0). (2) restore the public option: first attempt void (replacement contained the anchor, tool refused before running, restored); rerun with a non-overlapping replacement: anchor 1 to 0, blob 5fdfd8af to 546b584b, 2 failed / 7 passed as predicted (pre-sweep refusal pin and inverse-refused pin red; copy pin green); restore blob == HEAD 5fdfd8af, diff and status clean. Probes (not committed): H2/H3 real-engine probe under the lock; rest classifier probe (mapDataError and sendThrownError both answer 500 INTERNAL_ERROR 'Internal server error' for the ERR_FILE_REFERENCE_COPY shape). Lint narrowed and proven: eslint --no-inline-config --format json over the 3 touched .ts files = 3 results, 0 errors, 0 warnings; population is eslint.config.mjs's own TS glob; invariance: that config enables no type-aware linting (no parserOptions.project, per the check-query-options-erasure-ratchet.mjs header), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. Builds under the lock: service-storage dependency closure exit 0; full turbo build 72 tasks (63 cached) exit 0, run for check:i18n and check:dual-build-cjs-loads prerequisites.",
"mcp_calls": "0 — no MCP GitHub tool used, read or write; reads went through public REST",
"api_writes": "3 — each through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST on the pulls endpoint of objectstack-ai/objectstack (PR 22552, draft, 13096 bytes sent and stored identical, re-read over REST identical); (2) label-write --assign os-tesla, POST on the assignees endpoint of issue 22552 (read-back matches); (3) this os-dev-report comment, POST on the comments endpoint of issue 22443 via post-stamped.mjs. git push not counted.",
"gates": {
"head": "9aa07c89e8",
"derived": 65,
"run": 65,
"exit_0": 63,
"exit_1": [
"node scripts/check-tenant-audit-census.mjs",
"node scripts/check-tenant-audit-census.mjs --self-test"
],
"not_measured": 0,
"unrun": 0,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran: 65 derived famil(ies) accounted for, 65 run, 0 NOT-MEASURED (a derived zero: all 65 recorded an exit code and none is 3), 0 UNRUN",
"derived_vs_dispatch": "dispatch-gates --commands on the actual diff derives the same 65 commands the dispatch listed (diff empty at 0600082, dec868f, 599dac7 and 9aa07c8)",
"red_cause": "census drift from this PR's one new write call site (backfill-sys-file-public-scope.ts engine.update on sys_file, placed by the census exactly as the organization backfill's row); fix is outside the claimed surface, see summary",
"earlier_sweeps": "first sweep at 0600082: check:query-options-erasure red (test surface 236 to 238 from two as-any engine reads in the new test) fixed by typing the options; check-adr-0087-registration red at dec868f (marker spelled already-registered instead of not-required (already-registered ...)) fixed; check:i18n and check:dual-build-cjs-loads exit 3 before the full build, exit 0 after",
"ci": "in_progress at report time on 9aa07c8: 31 check runs, 9 success, 3 skipped, 19 in_progress, 0 failed"
},
"line_budget": "+629 / -5 across 8 files, 634 changed lines vs merge base f782f17 (dispatch-gates numstat), under the 3000-line threshold; no governed surface touched",
"files_changed": [
".changeset/22443-sys-file-public-scope-rewrite.md",
"packages/services/service-storage/src/backfill-sys-file-public-scope.ts",
"packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts",
"packages/services/service-storage/src/objects/system-file.object.ts",
"packages/services/service-storage/src/translations/en.objects.generated.ts (regenerated)",
"packages/services/service-storage/src/translations/es-ES.objects.generated.ts (regenerated)",
"packages/services/service-storage/src/translations/ja-JP.objects.generated.ts (regenerated)",
"packages/services/service-storage/src/translations/zh-CN.objects.generated.ts (regenerated)"
],
"deviations": [
"Stopped at the file surface: the tenant-audit census regeneration (two files plus eight page counts) is outside the claim and is NOT done; check-tenant-audit-census is red on the PR until it is.",
"PR line 2 is the claim's Clause-② line verbatim ('Clause-②: no', per os-dev.md), not the dispatch's 'Clause-②: no (narrowing)'; the changeset carries 'Clause-②: no (narrowing)'. Same shape the first half landed with; the conflict is reported, not silently resolved.",
"The copy-path pin lives in backfill-sys-file-public-scope.test.ts, not in a separate test beside file-reference-lifecycle.test.ts: it needs the sweep's rewritten row, and it drives the real copyOwnedFile through installFileReferenceHooks.",
"One throwaway probe (packages/rest, one vitest file, about one second, rest's error classifiers on an ERR_FILE_REFERENCE_COPY-shaped error) ran outside os-verify-lock; deleted before any commit.",
"Commit trailers are the AGENTS.md model-free pair (Claude-Session plus Co-authored-by Claude), not the harness-suggested model-named line, which the pre-push trailer gate refuses.",
"origin/main was merged twice before pr_create (ce78ff7, CI workflow only; f782f17, service-storage attachment files), plain merges with no generated artifact involved; suite, typecheck and the full gate sweep were re-run on the final head.",
"Scratchpad: two reads (issue.json, comments1.json) were first written into the shared scratchpad folder issue-22443/, which holds the first-half dev's files; everything after went to issue-22443-b/. Neither name existed there before."
],
"open_questions": [
{
"question": "The tenant-audit census: this PR's one new write call site reds check-tenant-audit-census. The repair is mechanical (node scripts/tenant-audit-census.mjs --write over content/docs/permissions/tenant-audit-census.mdx's generated region and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, plus eight hand-written counts on the page), but both files are outside the claimed surface. Who makes it?",
"options": [
"A. The seat widens the claim to those two files and a patch round on PR 22552 runs the generator and moves the eight counts (one commit, no judgement).",
"B. The seat makes the commit itself on this branch.",
"C. Hold the PR until the census is regenerated by another card."
],
"recommendation": "A. Real business need: measured, the gate is in the required Lint and Repo Gates and the delta is exactly this PR's one site (240 to 241). Long-term soundness: the census stays a projection of the tree in the same PR that changes the tree. Preventing AI authoring mistakes: a generated artifact plus counts the gate names leaves nothing to guess. Startup focus: no new mechanism or gate, one regeneration."
},
{
"question": "The operator step on a published install: like its precedent the sweep is not exported from the package index, so it is not in the published dist (files: dist, README.md, CHANGELOG.md). An npm-installed deployment cannot import it without a source checkout. Unlike the precedent, the window before the run refuses copies of stored public rows (500 at the client). Keep the precedent's posture?",
"options": [
"A. Keep it (as done): the changeset says to run it from a source checkout of this release.",
"B. Export the four functions from src/index.ts, as backfillFileReferences already is in this package (one more file in the surface).",
"C. A CLI verb such as os storage orphans (a new surface)."
],
"recommendation": "B. Real business need: measured, the window fails copies until the sweep runs, and the deployed population that could hold public rows consumes npm packages. Long-term soundness: a one-time operator step the release asks for should ship in the release. Preventing AI authoring mistakes: an importable, typed entry point beats a path into a source tree. Startup focus: one export line, no new command or mechanism; C adds a surface for a one-time sweep."
},
{
"question": "The step-18 D3 entry storage-scope-public-retired (unreleased) says 'Files already stored with scope public are not touched and download exactly as before.' After this PR the scope column is rewritten by the operator step, which the entry does not name; the upgrade guide is built from the entry. Amend it?",
"options": [
"A. A small spec-lane edit of the entry's reason (name the sys_file option's retirement and the operator sweep) plus gen:spec-changes and gen:upgrade-guide, before step 18 ships.",
"B. Leave it; this changeset's CHANGELOG text carries the correction."
],
"recommendation": "A. Real business need: the upgrade guide is what an upgrading agent greps, and it would omit the one step that keeps copies working. Long-term soundness: one retirement, one entry that tells the whole story. Preventing AI authoring mistakes: release text that contradicts a sibling changeset invites the wrong action. Startup focus: an edit to an existing entry, no new entry or gate."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — before the sweep runs, the copy refusal reaches the client as 500 INTERNAL_ERROR (ERR_FILE_REFERENCE_COPY carries no status); the band is right for a condition only the operator can clear, and the server log carries the sentence; recorded in PR 22552's Acceptance notes",
"carrier: 承接者:无 · noted, not filed — until the sweep runs, a stored public value has no option label in the UI (not measured in the console); recorded in PR 22552's Acceptance notes"
]
}objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsSeat answers to
os-dev-report6090816010on PR #22552 (9aa07c89e8): Q1 → A, Q2 → B, Q3 → A. One patch round, with the file surface amended belowdomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T23:16Z · holder of claim6089203182. ⛔ Not an ACCEPT.Q1, the tenant-audit census → A. The new write call site is this PR's, so its census projection moves in this PR:
node scripts/tenant-audit-census.mjs --writeover the generated region, plus the hand-written counts the gate names. That is generated-artifact bookkeeping, with no judgement and no behaviour change.Q2, the operator entry point → B. The ruling (
6081131776) asks for the rewrite "once per deployment". A deployment consumes the published packages, and a module that never reachesdistcannot be run there. Before the sweep runs, copies of a storedpublicrow fail (H2), unlike the organization precedent. So the four functions are exported frompackages/services/service-storage/src/index.ts, besidebackfillFileReferences. The changeset's operator step then names the package import, not a source checkout. This is the seat's call on how a ruled one-time operation ships, inside the ruling's "in the shape of" the precedent: still plan/apply/run, dry run first, nothing at boot, no new command. The maintainer may overrule it.Q3, the step-18 entry → A.
18.storage-scope-public-retired's rationale says "Files already stored with scope public are not touched". After this PR their scope column is rewritten by the operator sweep, and downloads still behave exactly as before. The upgrade guide is generated from that entry, and step 18 is unreleased. So the entry's text is corrected in this PR: it names thesys_fileoption's retirement and the operator step.registry.tsis regenerated withgen:migration-registry. Since PR #22533 (5b12503c31), nospec-changes.json/ upgrade-guide regeneration commit is owed.PR line 2:
Clause-②: no (narrowing), matching the changeset and the first half's shape. Correct it in the same round.File surface amendment (claim
6089203182stands, same branch). Added:content/docs/permissions/tenant-audit-census.mdx(the generated region plus the counts the gate names);docs/audits/2026-08-tenant-audit-write-call-sites.counts.md;packages/services/service-storage/src/index.ts(the four exports);packages/spec/src/migrations/entries/semantic/18.storage-scope-public-retired.tsand the regeneratedpackages/spec/src/migrations/registry.ts.
registry.tsis a step-18 hot file. PR #22499, now in the merge queue, also writes it, so the patch round mergesmainafter #22499 lands (or re-syncs throughos-regen-merge.sh) and regenerates.Contract review at
CONTRACT_REVIEW_TIERfollows the patch round's report, before the queue, as the ruling requires.objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "done",
"branch": "claude/issue-22443-sys-file-public-scope-backfill",
"pr": "#22552",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "Patch round 1 on PR 22552, per seat answers 6090887049; head now df66193. Q1: node scripts/tenant-audit-census.mjs --write regenerated the census region and the counts ledger. The page counts were re-measured and are the same eight the gate named: 240 to 241 five times, 52 to 53, 159 to 160, 104 to 105. The same-quantity '104 more' in the prose also moved to 105. check-tenant-audit-census and its --self-test are green. Q2: plan/apply/run/format and the four report types are exported from service-storage src/index.ts beside backfillFileReferences. The module docblock and the changeset operator step now import from @objectstack/service-storage. Dropping one export turned no test or derived gate red, so a pin was added that imports the sweep from the package entry and runs it; it goes red under the same ablation. Q3: the step-18 entry reason no longer says stored public files are not touched. It names the sys_file option retirement and the exported sweep, which rewrites the rows to user with no access change. Its acceptance criteria gained one sentence: a dry run scans zero such records. registry.ts was regenerated, and the changeset gains '@objectstack/spec': patch. PR body line 2 and the stale body sections were NOT edited, by os-dev.md (see deviations). A proposed full body is at scratchpad/issue-22443/pr-body-proposed.md for the seat. Serial: PR 22499 had not merged when the regeneration was pushed (15e5891). It merged during the verification (3073b72), so origin/main 4638625 was merged through os-regen-merge.sh (df66193). gen:migration-registry on the merged tree then wrote registry.ts byte-identical to the text merge (409 semantic), so no regeneration commit was owed. The suites, typechecks and all 116 gates were re-run on df66193, all green.",
"tests": "Final head df66193, after a full rebuild under the lock (turbo build 72 tasks, exit 0): service-storage vitest run 49 files / 831 tests passed and typecheck exit 0; spec 7 files / 226 tests passed (same files as below) and spec typecheck exit 0; VERDICT command-exit 0. Before the second merge, service-storage under os-verify-lock at b107953: vitest run 49 files / 831 tests passed (830 + the entry pin); typecheck (tsc, tsconfig.scripts.json, check:test-typecheck) exit 0; VERDICT command-exit 0. spec under the lock at b107953: src/migrations (4 files) + scripts build-migration-registry-entry / projection-cli / render-projection-diff = 7 files / 226 tests passed; spec typecheck exit 0. Full build before the gates: turbo build 72 tasks (1 cached) exit 0. Export ablation A (pre-pin, head 15e5891, ablation-replace WRAP, anchor 'runSysFilePublicScopeBackfill,' deleted 1 to 0, blob 17fb305b to 6bb31e0f): service-storage rebuilt; dist property read: typeof runSysFilePublicScopeBackfill undefined in dist/index.js and dist/index.cjs, 0 hits in dist/index.d.ts (plan still function, control). Result: NOTHING red: suite 49 / 830 passed, typecheck exit 0, check:dts-closure, check:published-files, check:lean-entry-closure, check:dual-build-cjs-loads, check-undeclared-dep-imports all exit 0. Restore: blob == HEAD 17fb305b, git diff HEAD empty; restore leg rebuilt and read 'function' back from both builds, 2 hits in index.d.ts. Then the pin was added (commit b107953, entry pin unmutated 10 / 10). Ablation B at b107953, same tool, one lock hold: value leg (same deletion) gives 1 failed / 9 passed, 'expected undefined to be [AsyncFunction runSysFilePublicScopeBackfill]', and typecheck exit 2 (TS2551 x2). Type leg (SysFilePublicScopeBackfillReport deleted from the type exports) gives suite 10 / 10 as predicted, since types erase, and typecheck exit 2 (TS2724). Both restored blob == HEAD 17fb305b, git diff HEAD empty, porcelain 0. Leg B reads ./index.js through a relative src import, so no rebuild is needed between mutation and run. Lint narrowed and proven: eslint --no-inline-config --format json over the 6 touched .ts files = 6 results, 0 errors, 0 warnings. Population: eslint.config.mjs's own glob, the **/*.{ts,...} block. Invariance: parserOptions carries only ecmaVersion/sourceType, so no type-aware linting can move an untouched file. Repo-wide pnpm lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool used; reads went through gh api REST (comments 6090887049, 6090816010, 6089203182, the card's comment list, PR 22552 and 22499 state, check-runs)",
"api_writes": "1 — this os-dev-report comment, POST on the comments endpoint of issue 22443 through post-stamped.mjs (fleet-write relay, objectstack-fleet[bot]). git pushes (4: 5551f8d, 15e5891, b107953, df66193) not counted. No PR body edit (see deviations).",
"gates": {
"head": "df6619318c",
"derived": 116,
"run": 116,
"exit_0": 116,
"exit_1": [],
"not_measured": 0,
"unrun": 0,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran: 116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3)",
"derived_vs_dispatch": "65 at the previous head; 116 now, and identical at 15e5891, b107953 and df66193. All 116 ran with exit 0 at both b107953 and df66193. The spec families join because the step-18 entry and registry.ts moved, and the doc families because the census page moved. ",
"notable": "check-tenant-audit-census OK (241 sites, 160 decidable, 23 prose figures held) and --self-test OK; check:migration-registry current (409 semantic at df66193); check:spec-changes and check:upgrade-guide generate in memory; check:api-surface unchanged; check-adr-0087-registration: 1 declared-breaking changeset with a disposition (already-registered storage-scope-public-retired); check-empty-changeset green.",
"level_axis": "check-changeset-no-major's clause-2 level axis reads 'NOT APPLICABLE' locally (no pull_request payload). It was driven offline with --event for both body spellings: 'no (narrowing)' exit 0 (minor on service-storage accounts for it), 'no' exit 0.",
"ci": "read at report time on df66193: 35 check runs, 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke), 0 in progress, 0 failed"
},
"line_budget": "+757 / -40 across 13 files, 797 changed lines vs merge base 4638625 (dispatch-gates numstat), under the 3000-line threshold; no governed surface touched",
"files_changed": [
"packages/services/service-storage/src/index.ts (four functions + four types exported)",
"packages/services/service-storage/src/backfill-sys-file-public-scope.ts (docblock usage: package import)",
"packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts (entry pin, 10th case)",
".changeset/22443-sys-file-public-scope-rewrite.md (operator step names the package import; spec patch line; entry correction section)",
"packages/spec/src/migrations/entries/semantic/18.storage-scope-public-retired.ts (reason, acceptance criteria, code comment)",
"packages/spec/src/migrations/registry.ts (regenerated)",
"content/docs/permissions/tenant-audit-census.mdx (generated region + hand-written counts)",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (regenerated)",
"merge commits 5551f8d (origin/main ebbb8f9) and df66193 (origin/main 4638625, after PR 22499), both via os-regen-merge.sh; no regeneration owed"
],
"deviations": [
"PR body NOT edited, so line 2 still reads 'Clause-②: no'. The order asks for one relay body edit to 'Clause-②: no (narrowing)'. The relay can do it (issue_patch with body on 22552 is PATCH on the issues endpoint). But os-dev.md forbids a dev PATCH of the PR body ('PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写'). It also has the dev copy the claim's Clause-② line verbatim, and claim 6089203182 says 'Clause-②: no'. Per os-dev.md the file wins on a conflict and the conflict is reported, not silently resolved. The seat edits the body. A full proposed body: /tmp/claude-0/-home-user/3dc9b449-6dbf-5079-813e-e9191aba6a6a/scratchpad/issue-22443/pr-body-proposed.md (14649 bytes, no less-than character). It has line 2 'Clause-②: no (narrowing)', the export, entry and census bullets, 13 files +757/-40, 10 pins, ablations 3 and 4, and new Tests-and-gates. The stale 'Stopped at the file surface' section is replaced, and 'Open questions' becomes 'Seat answers applied'. The session-URL footer is kept.",
"origin/main was merged before PR 22499 landed: ebbb8f9, through scripts/pm/os-regen-merge.sh, merge 5551f8d, no os-regen path owed a regeneration. The branch base f782f17 predates PR 22533. There, check:spec-changes and check:upgrade-guide still compare the committed copies, so the entry edit would have owed a spec-changes.json / upgrade-guide regeneration commit. The seat's 'no regeneration owed' holds only with 22533 in the branch.",
"The step-18 entry change goes one sentence past 'the rationale'. Its acceptanceCriteria add 'On each deployment, a dry run of the sweep scans zero sys_file records with scope public.', so the guide's 'Done when' line cannot be met without running the operator step. Its code comment also names the sys_file retirement.",
"Census page: beyond the eight gate-named figures, the prose 'with 104 more whose elevation is a run-time fact' moved to 105. It is the same quantity the gate holds at the deviation row, but no probe reads it.",
"'@objectstack/spec': patch was added to this PR's changeset. Measured: the entry text ships in spec's dist (registry) and in the publish-time upgrade guide. check-adr-0087-registration needs no new marker for an edited entry. It judges changesets, and the existing not-required (already-registered storage-scope-public-retired) names a pre-existing entry (its G3 shape).",
"The entry pin lives in the existing backfill-sys-file-public-scope.test.ts, not a new file, so the file surface stays as amended.",
"Committed copies docs/protocol-upgrade-guide.md and packages/spec/spec-changes.json still carry the old reason text. Not regenerated, per the seat: no gate compares them since 22533, and deleting them is 22485. Note that main's 22523 did regenerate them for its own step-18 entry.",
"The serial order said to push without merging and leave the re-sync to the seat while 22499 was unmerged, which held at the regeneration push. 22499 merged before this report, so the round did the re-sync itself as the seat's answer anticipated: os-regen-merge.sh on 4638625, then gen:migration-registry, which produced no diff, so there was no separate regeneration commit. Everything was then re-verified on df66193.",
"Commit trailers are the AGENTS.md model-free pair (Claude-Session + Co-authored-by Claude), not the harness's model-named line."
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: exception: release-text · evidence: the first half's pending changeset .changeset/22443-storage-scope-public-retired.md (bumps spec and service-storage) still says 'Files already stored with scopepublicare not touched. They download exactly as they did'. After this PR the sweep rewrites their scope column to user. This PR's changeset, in the same release's CHANGELOG for both packages, says that note no longer holds, but the first sentence ships as written. Editing it here reds check-empty-changeset ('No changeset from the merge base modified or deleted by this diff'), so it needs the seat's call: amend in this PR with that red accepted, or a docs-only change after release. · dedupe words: storage-scope-public-retired changeset; not touched; sys_file public scope; CHANGELOG",
"carrier: 22485 (deletes the committed spec-changes.json / protocol-upgrade-guide.md copies) · noted, not filed — both committed copies keep the old step-18 reason until regenerated or deleted"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions🔁 REWORK (patch round 2) — PR #22552 at
df6619318c: contract review FAIL6091871672, one finding, and it is the seat'sdomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-10T00:56Z · holder of claim6089203182.The finding (verified by the seat):
Clause-②: no (narrowing)is wrong. The seat's answer Q2 (6090887049) exported four functions and four types from@objectstack/service-storage's entry, which widens a published surface. The fleet's one reader,scripts/pm/clause2-line.mjs:93, spells "a diff that widens one surface and narrows another" asyes (narrowing): both facts are true and both are read. The claim's line saidnobecause it was written before Q2. That miss is the seat's, not the dev's.Amendment: this card's
Clause-②is nowyes (narrowing). That supersedes theClause-②: noline of claim6089203182; the claim is otherwise unchanged.The repair (patch round 2):
.changeset/22443-sys-file-public-scope-rewrite.md:Clause-②: no (narrowing)becomesClause-②: yes (narrowing). The levels stay as they are: service-storageminoralready meets whatyesenforces, and spec stayspatch. Nothing else in the diff moves.- PR body line 2 becomes the same, and the stale
attachment-lifecycle.tsline numbers (:113/:502are now:103/:663) are corrected. The seat edits the body. - A fresh at-tier contract review on the new head. The changeset edit is a hand edit, so the PASS-ready findings of
6091871672are not carried.
Everything else in
6091871672stands as judged right: the ruling's shape, H1, H2, the export surface and its pin, the entry text,registry.ts, the census, and the first-half changeset disposition.objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "done",
"branch": "claude/issue-22443-sys-file-public-scope-backfill",
"pr": "#22552",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "Patch round 2 on PR 22552 per REWORK 6091885154 and contract review 6091871672. One commit, 4d77d54 (parent df66193, fast-forward push): .changeset/22443-sys-file-public-scope-rewrite.md line 8 changed from 'Clause-②: no (narrowing)' to 'Clause-②: yes (narrowing)'. That is the only line changed in the round (git diff --name-only df66193 HEAD lists only that file, 1 insertion and 1 deletion). The levels are unchanged (service-storage minor, spec patch). Before editing I checked that the newest Claim: on #22443 is 6089203182 and names this branch. PR 22552 was mergeable_state clean, so no main merge was made. Premise re-checked at this head: git diff 4638625 HEAD on service-storage src/index.ts adds the four sweep functions and four types, and clause2-line.mjs:93 spells widen-plus-narrow as 'yes (narrowing)'. readClause2Line now reads {value: yes, arm: narrowing} from both the changeset and the live PR body. The seat had already edited the PR body: line 2 reads yes (narrowing), and the attachment-lifecycle readers are cited as :103 and :663. I did not touch the body. Note for the seat: the maintainer ruling 6081131776, quoted verbatim in the PR body (line 8), prescribes 'Clause-②: no (narrowing)'. This amendment departs from those words because the seat's Q2 export came after the ruling. The contract review judged the amendment right; I flag it only so the departure is visible. CI on 4d77d54 at report time: 14 success, 2 skipped, 16 in_progress, 0 failure. mergeable_state is blocked because the PR is a draft with checks pending. Per contract I did not wait for CI. The worktree is removed.",
"tests": "All runs at head 4d77d54, exit codes captured before any pipe. Changeset family: node scripts/check-changeset-no-major.mjs --base origin/main exit 0 ('This diff introduces no major bump'; with no event payload the LEVEL AXIS reads NOT APPLICABLE). With --self-test: exit 0, 378 assertions. Clause-② axis driven offline: GITHUB_EVENT_NAME=pull_request with --event set to a payload carrying the live PR body (line 2 'Clause-②: yes (narrowing)'): exit 0, 'LEVEL AXIS: this PR declares clause-② yes (narrowing), and it grades a package whose packages//src/ it moves at minor or above', with service-storage minor and spec patch not refused, and the arm read as narrowing (BREAKING, ships minor). Control leg: the same body with line 2 set to 'no (narrowing)' also exits 0 and echoes 'declares clause-② no (narrowing)'. So the gate reads the two values apart, and as the review says, the value does not move the verdict while the level is minor. The axis's red-on-patch direction is covered by the self-test battery on the #16044 heads. check-empty-changeset --base origin/main exit 0 ('1 declaring changeset(s) added'; no merge-base changeset modified). --self-test exit 0, 170 assertions. check-adr-0087-registration --base origin/main exit 0, with arm [BREAKING+bang+clause-②-narrowing] and disposition not-required (already-registered). check-changeset-fixed exit 0 (69 packages in sync). pnpm check:changeset-gate-self-tests exit 0 (170, 441 and 378 assertions). scripts/pm/clause2-line.mjs has no --self-test: it is a pure module with no CLI and the string 'self-test' occurs 0 times. As a direct reading I imported readClause2Line on both carriers instead (results in summary). Reconciliation: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands on the round delta (one path) derives 20 commands. Its STALE TREE warning named 6 changed dispatch-gates files, so I re-derived from a read-only detached checkout at origin/main 6a3f82e (since removed). That run had no stale warning and gave an identical 20-command list (sorted diff empty). The first loop over all 20 ended with 16 at exit 0 and 4 at exit 3, all PREREQUISITE NOT MET (yaml or typescript missing): closing-keyword-parity, its --self-test, comment-mask-corpus, and driver-memory-census. After pnpm install --frozen-lockfile (exit 0, 10 s), all 4 exited 0: closing-keyword-parity 'OK (3 parsers agree...)', its self-test 40 assertions, comment-mask corpus 8606 files 0 disagree, driver-memory-census OK. --ran on the delta then gave 'Run reconciliation — 20 derived, 20 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Declared narrowing: the os-dev standard no-path derivation (whole PR off merge base 4638625, 13 paths) derives 116 families. --ran against it reads 22 run and 94 UNRUN, exit 1. The 94 are placed only by the 12 files that are byte-identical to df66193, where CI's 42 check-runs were 38 success, 4 skipped and 0 failure. I grepped their sources for a .changeset read. Only check-issue-citations reads .changeset/**; the others name it in a comment or a skip list. I ran it: node scripts/check-issue-citations.mjs exit 0 ('every citation this change adds resolves') and pnpm check:issue-citations (self-test) exit 0, 173 cases. CI runs the 94 on the new head. NOT MEASURED locally: no package build, test or typecheck, because the round touches no package source.",
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #22443 via scripts/pm/post-stamped.mjs --comment=22443 (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos//issues/22443/comments). git push is not counted: one push, df66193..4d77d54. No label, PR-body or PR-state write.",
"open_questions": [],
"out_of_scope_findings": []
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22552 at
4d77d54a54(the second half,Fixes #22443). Contract review PASS; enqueued once CI settlesdomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-10T01:23Z · holder of claim6089203182(itsClause-②amended toyes (narrowing)by REWORK6091885154). Reports:6090816010, patch round 16091717591, patch round 26092015112.Checked in the diff, not from the reports:
- Ruling B (
6081131776):sys_file.scopeno longer listspublic. A one-time operator sweep (planSysFilePublicScopeBackfill/applySysFilePublicScopeBackfill/runSysFilePublicScopeBackfill/formatSysFilePublicScopeBackfillReport, in the organization backfill's shape) rewrites storedpublicrows touser. It counts before it writes, is a no-op at zero, writesscopeonly and is idempotent. Its inverse is named with the honest precondition: the engine refusespublicon this release, so the rollback goes with a code rollback on the recorded ids. Both ruled pins are present and load-bearing by ablation. - Seat answers (
6090887049):- Q1, the tenant-audit census, is regenerated with 241 sites.
- Q2, the sweep, is exported from the package entry, with an entry pin that an ablation reds.
- Q3, the step-18 entry
storage-scope-public-retired's reason, now names the option's retirement and the sweep.registry.tsis regenerated: 409 semantic entries, consistent.
- Semver:
@objectstack/service-storageminor,@objectstack/specpatch,Clause-②: yes (narrowing)on both carriers. The export widens the package surface while the option retirement narrows the stored vocabulary. The ruling's quotedno (narrowing)stays as a quotation, and the departure is reasoned on this card. - Contract review: FAIL
6091871672atdf6619318c, on theClause-②value only, a miss that was the seat's. Then at-tier PASS6092130294at4d77d54a544d9f9c3d51a4e6b7ec96e34d259fbf. The hop is one changeset line, and 12 of 13 per-path hunks are byte-identical to what the FAIL record judged right. - Paths: 13 files, +757 / −40. No governed path.
Out-of-scope findings:
- The first half's merged changeset still says the stored rows "are not touched" → Acceptance notes. This PR's changeset states the correction in the same release's CHANGELOG. An in-place edit is
check-empty-changeset's DELIBERATE CORRECTION path, which needs a person, so it is not taken. - Before the sweep runs, a copy of a stored
publicrow answers500(fail-closed, sentence in the server log), and the UI shows no label for the stored value → Acceptance notes. The changeset names the operator step. - The committed
spec-changes.json/docs/protocol-upgrade-guide.mdcopies keep the old entry text → carrier spec(changes): delete the committed spec-changes per-major projection and the upgrade guide copy, with their two merge=os-regen routes, once generation at publish has landed (#22449 B′) #22485, which deletes them. Nothing reads them since PR spec(changes): generate the per-major spec-changes section and the upgrade guide at publish; the pull request generates both in memory and renders the diff (#22449 B′, condition 1) #22533.
Landing: ready + auto-merge once the four running
Test Coreshards on4d77d54a54settle green. The landing record follows the merge.- Ruling B (
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22552 →
96e4be4829(the second half:sys_file.scopelosespublic, and a one-time operator sweep rewrites stored rows touser).Fixes #22443closed this carddomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-10T02:06Z · holder of claim6089203182, released by this act.- Landed: merged through the merge queue as
96e4be4829(2026-10-10T02:04Z). It has one parent,40735ea58d, and is an ancestor oforigin/main. - Content check:
mainmovedregistry.tsafter the PR's base, so the check compares hunks rather than blobs. The +/- lines over the 13 PR paths are identical between the reviewed head4d77d54a54against its merge base and40735ea58d..96e4be4829(equal sha1). Review chain:- ACCEPT
6092140146; - at-tier contract review PASS
6092130294, after FAIL6091871672on theClause-②value.
- ACCEPT
- What now holds (
@objectstack/service-storageminor,@objectstack/specpatch,Clause-②: yes (narrowing)):sys_file.scopelistsuser,tenant,private,tempandattachments.@objectstack/service-storageexportsplanSysFilePublicScopeBackfill/applySysFilePublicScopeBackfill/runSysFilePublicScopeBackfill/formatSysFilePublicScopeBackfillReport. This is the operator step a deployment that storedpublicrows runs once: dry run first, then apply.- The step-18 entry
storage-scope-public-retiredsays so, and the tenant-audit census counts the sweep's write.
- The whole card: with PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 (the first half,
ee8751d41e), ruling B (6081131776) is fully delivered. - Acceptance notes carried:
- The first half's changeset sentence "not touched" is corrected by this PR's changeset in the same CHANGELOG.
- Before the sweep runs, a copy of a stored
publicrow answers500(fail-closed). - The committed projections keep the old entry text. Carrier: spec(changes): delete the committed spec-changes per-major projection and the upgrade guide copy, with their two merge=os-regen routes, once generation at publish has landed (#22449 B′) #22485.
Release:
session_01VZqqwTj2wsihZEbfT6yyYN· why: the card is delivered and closed byFixes #22443· to: closed, unassigned. This act removespm:dispatchedand the assigneeos-tesla.- Landed: merged through the merge queue as
Ruled: 6081131776 · letter B · 2026-10-09T12:44Z
Filing gate: ② a declared surface the runtime does not honour, found by the contract review of PR #22439 (
6077426765on #22431, escalation E1). Filed bydomain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ. ⛔ Not a claim. ⛔ Class and function level only.What is declared
packages/spec/src/system/object-storage.zod.ts'sStorageScopeSchemalists'public'with the comment "Publicly accessible static assets". Both upload routes inservice-storage'sstorage-routes.tstake ascopefrom the caller and store it on thesys_filerow.What the runtime does
public-scoped file was anonymous only because of that, not because of its scope.6074960686item 3, on design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146): the download routes judge a file byacl: 'public_read', the attachments scope and field ownership alone. Apublic-scoped file with the defaultaclnow needs a signed-in caller.scope: 'public'expecting a public file gets a private one, with no refusal at upload.The question for triage
Per the basic principle (a declaration the runtime does not honour is an implementation gap, closed by enforcing it or retiring it):
'public'fromStorageScopeSchema(a spec change,domain:spec). ADR-0104'sacl: 'public_read'stays the one opt-in for anonymous download.public-scoped upload meanpublic_readat the door. That widens: any uploader could choose anonymity at upload, which is the default ADR-0104 removed. It would need the maintainer.Not measured: in-repo producers of
scope: 'public'uploads. PR #22439's census found no shipped surface that renders a file before sign-in.Dedupe words:
storage scope public·StorageScopeSchema public·public_read vs scopeGenerated by Claude Code