Repository navigation
cross validateRetiredPermissionResidue to the runtime publish door — the CLI door cannot reach the AI/Studio JSON authors ruling D named #17944
Description
Activity
Claim: PM loop round 45
Session:session_01JbZnqu8bt6YqfJsr9vaFb3
Branch:claude/issue-17944-retired-permission-residue-runtime-door
Worktree:objectstack-issue-17944
Domain:domain:spec
Seat:domain:spec#2
File surface:packages/metadata-protocol/src/**(stop on breach; explain in the report)
Container & model:M,mode:subagent
Clause-②: no
Thread-read: none
Serial constraints cleared:packages/metadata-protocol/src/protocol.ts—— 逐 PR 拉files重建的占用表(32 个开着的 PR / 371 行文件行;亮控:同表点出 #19107 持有 2 个文件)读作 FREE,无在飞写者。
认领前的完整读(取数时刻 2026-09-18T19:59Z)
- 本卡
state=open· labelspm:queue,domain:spec,priority:p3· assignees 为空。 ⚠️ issue_read get的comments字段读 1,而评论列表实读 0 条 —— 按platform-readings.md「comments计数会多读 … 线程空否读那两条,⛔ 不读计数」,本卡线程是空的 ⇒Thread-read: none,且 ⛔ 无他席认领。
派发时给施工席的围栏
-
⭐ 本卡有维护者裁决,而且裁决里有一句限定语,它比卡面任何别的话都重要:裁定 a(记录在
ObjectPermissionSchema's retiredallowRestore/allowPurge: only literalfalseparses (not a truthy/falsy split), and no post-parse guard can ever see either key #17425 评论5651940657),并明写「17425 按照简化的模式实现就可以」。⇒ 走简化实现,⛔ 不要为它造框架。 先把那条裁决第一手读出来再动手,⛔ 不要只读本卡对它的转述。 -
卡面自己点了一个必须先解决的前提:裁决 D 把信号定在「原始
objectstack.json源」,而仓里没有这种文件 —— CLI 的loadConfig只接受objectstack.config.{ts,js,mjs}。⇒ 第一腿是重取这个不存在:把搜索半径写出来,并给一条会命中的同主题控制(例如真实存在的那几种配置文件名确实被loadConfig接受),否则「没有这种文件」只是一个空结果,不是读数。 -
⚠️ 计数不是读数:凡是「X 处 / Y 个文件」的数,打印路径或行,⛔ 不要grep -c。本席今天在三个不同的判据上各栽过一次,最近一次是一条单行正则够不到多行声明,却返回了一个「看起来像通过」的绿。 -
Clause-②=no:把一个既有校验接到另一扇门上,不放宽接受集、不扩大公开面(它只会让更多输入被拒绝,而收窄不触条款②)。⚠️ 若你的读数显示这条接线会让某些今天被拒的输入变成被接受,那一刻起就是yes,停下来报回。 -
⛔ 不碰 PD-14 受管面:
docs/adr/**·.claude/**·skills/**·AGENTS.md·CLAUDE.md。裁决若把你推到那些面上,停下报回 —— 那里的落地是保留的,需要GOVERNED_APPROVERS。 -
卡面的读数是立卡席的,不是本席的;⛔ 不要把它当既成事实。行号会腐烂,逐条重取。
Generated by Claude Code
- 本卡
os-dev-report
{ "issue": 17944, "status": "needs_decision", "branch": "claude/issue-17944-retired-permission-residue-runtime-door", "pr": null, "session": "session_01JbZnqu8bt6YqfJsr9vaFb3", "premise_still_valid": false, "summary": "This card's ask is ALREADY ON origin/main and no code change was needed, so no PR was opened. PR #18722 (merged 2026-09-17T17:50:56Z, closing the sibling card #17936) moved the registry entry off CLI_ONLY: authoring-rules.ts:1287 now reads `surfaces: CLI_AND_RUNTIME` and :1288 `runtimeTypes: ['permission']`, and :364 defines `CLI_AND_RUNTIME = ['cli', 'runtime-publish']` -- literally the surface this card names. The 'two doors, deliberately split in half (authoring vs publish)' reading that #17936's claim comment 5717487096 asserted, and that PR #18722's own body repeated verbatim ('Not in this PR: the PUBLISH-door half of the same file (#17944 ...) is not addressed here and remains open'), is FALSE: protocol.ts has ONE gate, the private `assertRuntimeAuthoringRules`, reached from two call sites (saveMetaItem ~:15917 and the draft-to-active promotion ~:17231), and that method's own docblock states it -- 'one gate, two doors, one receipt vocabulary'. Rule selection there is by metadata TYPE, not by door, so one registry entry serves both. Measured, not argued: a residue permission DRAFT save returns success with no advisories key at all (D1: drafts are not gated, so nothing can be carried over), and publishing that same untouched draft returns success plus the `permission-retired-lifecycle-residue` advisory -- the signal is BORN at the publish door. ONE card constraint did not land, and it is real rather than theoretical: constraint 1 (aggregate to a single finding per write) was not implemented; measured at the publish door, one publish of a 75-object permission set returns 150 residue advisories in a single response. That is the open question below.", "tests": "PREREQUISITE LEG (card fence 2), radius stated and control that HITS. Instrument: `git ls-files` over the worktree at BASE=07c6f822ed = 8957 tracked files, matched by basename regex. TARGET `(^|/)objectstack\\.json$` -> grep exit=1, ZERO matches (exit captured from grep itself, output redirected to a file first -- not read through a pipe). CONTROL, same instrument, same radius, the three spellings resolveConfigPath actually accepts (packages/cli/src/utils/config.ts:186-189 `objectstack.config.ts|js|mjs`) -> grep exit=0, 10 paths printed, ALL `.ts`: examples/app-crm, examples/app-multi-package, examples/app-showcase, examples/app-todo, packages/create-objectstack/src/templates/blank, packages/drivers/driver-memory, packages/plugins/plugin-auth, packages/plugins/plugin-hono-server, packages/plugins/plugin-security, packages/services/service-i18n. Second control on the same claim: metadata-plugin.zod.ts:1086 `permission` declares filePatterns ['**/*.permission.ts', '**/*.permission.yml'] (no .json) while :724 `object` declares '**/*.object.json' -- so the .json spelling IS representable and permission simply does not carry it. The only `objectstack.json` in the tree is the BUILD ARTIFACT `dist/objectstack.json` (untracked, gitignored), never an authored source. Card premise on the absence: CONFIRMED. || LANDED PINS, green on main: `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/runtime-gate.permission-residue.test.ts src/validate-retired-permission-residue.test.ts` -> VERDICT command-exit 0, 'Test Files 2 passed (2) / Tests 27 passed (27)'. `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.runtime-authoring-gate.test.ts` -> VERDICT command-exit 0, 'Test Files 1 passed (1) / Tests 35 passed (35)'. Both through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17944; dependency closures built first under the same lock (`--filter '@objectstack/lint^...' build` VERDICT 0, `--filter '@objectstack/metadata-protocol^...' build` VERDICT 0). || MY OWN RED/GREEN/DARK AT THE PUBLISH DOOR (one-shot probes appended to protocol.runtime-authoring-gate.test.ts, run, then reverted; injection proved on disk by grep -c on both anchors, revert proved by blob identity 21bea06d2b02339a215a83758231c2ec5eb446a1 == HEAD blob and an empty `git diff HEAD`; every probe carried `trap 'git checkout HEAD -- $F' EXIT INT TERM` with an absolute REPO_ROOT path). RED: publish of a residue draft -> PROBE_B_RED success=true rules=['permission-retired-lifecycle-residue'] path=permissions.pub_red.objects.leave_request.allowRestore hint_len=520. GREEN (must still be accepted): publish of a clean draft -> PROBE_B_GREEN success=true advisories=null; and a direct clean active save -> PROBE_GREEN success=true hasAdvisories=false. DARK/CONTROL that the probe reaches a real door at all, two independent legs: (i) differential -- PROBE_C_DRAFT success=true hasAdvisories=false advisories=null on the residue draft save, then PROBE_C_PUBLISH on that same untouched draft success=true rules=[residue, residue] paths=[permissions.diff_probe.objects.obj_0.allowRestore, permissions.diff_probe.objects.obj_0.allowPurge]; since the draft leg is a true zero the publish leg cannot be a carry-over, so the gate ran AT the promotion; (ii) refusal control -- a non-residue value on the retired key (allowRestore: true) is refused, PROBE_DARK refused=true code=INVALID_METADATA status=422 with the tombstone prescription in the message, so the probe is driving the real write path and not a stub. A first control attempt FAILED and is reported rather than quietly retried: an unrecognized-key body meant to be refused at publish was refused earlier, by saveMetaItem at protocol.ts:15874 on the DRAFT save, so it never reached the publish door -- that probe measured nothing and was replaced by the differential above. || VOLUME (constraint 1), measured at both call sites, counts paired with printed first/last paths: direct active save PROBE_VOLUME objects=1 -> 2 residue advisories; 5 -> 10; 25 -> 50; 75 -> 150. Publish door PROBE_C_VOLUME objects=1 -> 2; 25 -> 50; 75 -> 150 (advisories_total equals residue count in every row, so no other rule is inflating it). || NOT MEASURED: no gate family and no changeset are owed -- the final tree is byte-identical to origin/main (`git status --porcelain` empty, `git diff HEAD` empty), so there is no diff for dispatch-gates.mjs to derive a family from and nothing published moves.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub access was REST proxy curl with GITHUB_TOKEN.", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/17944/comments (this report). No POST /pulls (nothing to open), no POST .../labels (no PR to label), no POST /issues. Outside REST: one `git push -u origin claude/issue-17944-retired-permission-residue-runtime-door` of the EMPTY claim branch (the mandated write-routing probe, 200 OK); it still sits at exactly origin/main with zero commits and can be deleted by the seat.", "open_questions": [ { "question": "Constraint 1 of this card (aggregate the residue signal to ONE finding per write) did not land with #17936/PR #18722, and I measured it live: one publish of a 75-object permission set returns 150 `permission-retired-lifecycle-residue` advisories in a single response. The contract text the rule's own module docblock quotes verbatim warns against exactly this shape -- 'the strip is deliberately SILENT — real artifacts carry the residue once per permission entry, and a per-occurrence notice would be a 75-line storm that teaches operators to skim'. Changing it now means rewriting the wire shape of `advisories[]` that Studio and MCP render, and rewriting pins landed one day ago that assert per-occurrence explicitly (protocol.runtime-authoring-gate.test.ts asserts two entries for the two-key case; the lint-layer test asserts the per-occurrence differential). That is a public-envelope decision, not a dev judgement call, and it sits outside my declared file surface (`packages/metadata-protocol/src/**`) because the aggregation belongs in `packages/lint/src/authoring-rules.ts` or `runtime-gate.ts`. NOTE ON PROCESS: my dispatch did NOT carry the four-axis escalation framework, and the standing clause forbids me to author axes of my own, so the options below are stated without a per-axis analysis -- send me the framework if you want one.", "options": [ "A — Close #17944 as delivered by PR #18722 and file constraint 1 as its own card against the LANDED behaviour. Cost: the storm ships meanwhile, bounded by how wide a real tenant's permission set is (in-tree population is zero, so nobody is hit today). Benefit: the just-landed, reviewed, pinned contract is not rewritten on a duplicate card, and the volume change gets the UX decision the gate's own test text already says it needs ('a UX/volume decision with its own card, not a drive-by').", "B — Close #17944 as delivered and rule NOW that per-occurrence is correct at this door, recording why the 75-line-storm sentence does not govern here (it is about the parse-time strip, one layer down). Cost: one prose correction so the tree does not keep a warning its own shipped behaviour contradicts. Benefit: the question stops reopening.", "C — Keep #17944 open and re-scope it to constraint 1 alone (aggregate to one), dispatched against the landed code with a file surface that includes `packages/lint/src/**`. Cost: rewrites pins one day old and needs a seat that can touch the lint package; the accept set is untouched either way so Clause-2 stays `no`. Benefit: the card's own stated minimum bar is met under its own number." ], "recommendation": "A. The card as written is delivered and should not stay open on a falsified premise; the volume question is a separate, measured, contract-quoting finding that deserves its own card rather than a silent rewrite of a one-day-old landed decision. B is the cheap variant if you would rather rule the storm acceptable than carry a card -- but B still owes the prose correction, because the tree currently ships a per-occurrence notice while quoting a contract that calls a per-occurrence notice a storm. I recommend against C under this number: re-scoping a card whose title and body describe work that is already done makes the history unreadable." } ], "out_of_scope_findings": [ "to file (class b, declared-contract violation, contract quoted, repro named) — the runtime authoring/publish door emits `permission-retired-lifecycle-residue` once PER OCCURRENCE, while the contract text the rule's own module docblock quotes verbatim from `acceptRetiredDefaultResidue` says 'a per-occurrence notice would be a 75-line storm that teaches operators to skim'. Repro: publish one permission set with 75 object entries each carrying `allowRestore: false` and `allowPurge: false` -> 150 advisories in a single response (measured this run at both the saveMetaItem door and the draft-to-active promotion door; 1 object -> 2, 25 -> 50, 75 -> 150). Dedupe words: permission-retired-lifecycle-residue, advisories volume, per-occurrence storm, aggregate one finding, runtime authoring gate.", "noted, not filed: PR #18722's merged body states 'Not in this PR: the PUBLISH-door half of the same file (#17944, still pm:queue and unclaimed) is not addressed here and remains open', and #17936's claim comment 5717487096 states the two cards are 'two doors deliberately cut in half, not twins'. Both are measurably false — one registry entry serves both call sites of the single `assertRuntimeAuthoringRules` gate, which its own docblock calls 'one gate, two doors, one receipt vocabulary'. Not filed because the carrier is a merged PR body nobody will edit again and a closed card's comment. 承接者:无 — no open PR and no person is routed back to either text; the correction that matters is this card's disposition, which is the open question above.", "noted, not filed: `authoring-rule-wiring.test.ts` requires a substantive `surfaceReason` only when `surfaces` OMITS 'runtime-publish', so a stale 'why the gate does not run this' sentence can outlive a crossing and contradict it silently. Already noted by the #17936 seat and already pinned FOR THIS ENTRY by the new lint test; the class-wide gap is unchanged. 承接者:the next seat that crosses any other rule onto the runtime surface — no open PR touches the wiring test today." ] }
Generated by Claude Code
PM 处置 — 卡 #17944:本卡要的东西已经在 main 上,零实现
取数时刻 2026-09-18T20:20Z。这一轮没有开 PR,因为没有代码要改。下面三条是我第一手复核过的,⛔ 不是复述施工席的叙述 —— 判读的是
origin/main=07c6f822edd9c4c48a7ce34767de420c6096b9c3。1. 本卡点名的那条接线,已经在了
本卡要的是把
validateRetiredPermissionResidue从CLI_ONLY接到运行时写入门runtime-publish。实读packages/lint/src/authoring-rules.ts::364 const CLI_AND_RUNTIME: readonly AuthoringSurface[] = ['cli', 'runtime-publish']; …permission 那条登记项… surfaces: CLI_AND_RUNTIME, runtimeTypes: ['permission'],⇒
runtime-publish字面就在里面。据施工席,是 PR #18722(2026-09-17T17:50:56Z 合入,关的是姊妹卡 #17936)带上去的。2. 「两扇门被刻意切成两半」这个前提,是假的
packages/metadata-protocol/src/protocol.ts里只有一个闸门 —— 私有方法assertRuntimeAuthoringRules(:4871),被两个调用点够到::15917(saveMetaItem)与:17231(草稿转正)。规则选取按元数据类型、⛔ 不按门 ⇒ 一条登记项同时服务两个门。⚠️ 也就是说:#17936 的认领评论5717487096说两张卡是「两扇刻意切开的门,不是双胞胎」,以及 PR #18722 正文里那句「Not in this PR: the PUBLISH-door half of the same file (#17944 …) is not addressed here and remains open」—— 两句都可测地为假。⛔ 本席不去改它们:一个是已合 PR 的正文、一个是已关卡的评论,没人会再编辑,而真正要紧的更正就是本卡的处置本身。3. 派发词第一条(前提腿)也成立,但读法要说清
git ls-files覆盖 8957 个跟踪文件:按(^|/)objectstack\.json$匹配 —— 一条都没打印出来;同一把仪器按loadConfig真正接受的三种拼写匹配 —— 打印出 10 条,全是.ts(四个 examples、create-objectstack模板、driver-memory、三个 plugins、service-i18n)。⇒ 「仓里没有objectstack.json这种授权源」是读数,不是空结果。⚠️ 一处我自己的仪器要更正:我那条命令行里打印的exit=0取的是管道最后一段(sed)的退出码,⛔ 不是grep的。零命中的依据是什么都没打印,⛔ 不是那个 0。记下来免得下一个人照抄那条命令。处置:转
pm:retriage,⛔ 不关卡、⛔ 不代裁- 本卡不是本席立的,而「已被别的 PR 交付、该如何收尾」是路由判断 ⇒ 交分诊,⛔ 本席不关另一个席位的卡。这与本班早前 [finding] tracing
condition's permissive record branch makes the expression branch's refusal unreachable — a typo'd CEL envelope is silently reinterpreted as an opaque filter instead of rejected #18731 的处置同法。 - 施工席给了 A/B/C 并推荐 A。⛔ 本席不选:其中 B 是「现在就裁定逐条通知在这扇门上是对的,并改掉那句散文」—— 那是公开信封与契约文本的裁决,是维护者的地板,⛔ 不是执行席的。
- 施工席量到的那条真实残项(一次发布 75 个对象 ⇒ 150 条
permission-retired-lifecycle-residueadvisory,而该规则模块自己逐字引用的契约句把逐条通知称作「75-line storm that teaches operators to skim」)已按判据另立卡 [finding] the runtime gate emits permission-retired-lifecycle-residue once PER OCCURRENCE — 75 objects yield 150 advisories in one response, the exact 'storm' its own quoted contract warns against #19113,⛔ 不重挂回本卡、⛔ 不并进任何 PR。那张卡上写明:volume 那几个数是施工席的,本席没有重跑。 - 施工席按规定推的那条空分支
claude/issue-17944-retired-permission-residue-runtime-door(零提交,正停在 main 上)我随后删掉。
⚠️ 一条我自己的派发缺陷,记下来施工席指出:我的派发词没有带四轴升级框架,而常设条款禁止它自造轴 ⇒ 它只能把选项摆出来而不能逐轴分析。这是我的模板漏项,⛔ 不是它的缺陷 —— 一张有可能升级成决策的卡,派发时就该把框架给它。下一轮起补上。
Generated by Claude Code
- 本卡不是本席立的,而「已被别的 PR 交付、该如何收尾」是路由判断 ⇒ 交分诊,⛔ 本席不关另一个席位的卡。这与本班早前 [finding] tracing
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removed
on Sep 18, 2026 - added a commit that references this issue
on Sep 28, 2026
把
validateRetiredPermissionResidue从CLI_ONLY接到运行时写入门(runtime-publish),让它够得到裁决 D 真正点名的那批作者:AI / Studio / REST/meta/ MCP —— 他们从不跑 lint,但每一次写入都过那扇门。裁决
维护者于 2026-09-13T07:2xZ 裁定 a(记录在 #17425 评论
5651940657),并指示:⇒ 本卡走简化实现。 ⛔ 不要为它造框架。
为什么这张卡存在
裁决 D 把信号定在「原始
objectstack.json源」。仓里没有这种文件:CLI 的loadConfig只接受objectstack.config.{ts,js,mjs},十个树内配置全是.ts,而.ts作者早已被墓碑的z.never()在tsc挡住;permission类型声明的filePatterns是*.permission.ts/*.permission.yml,没有.json。⇒ 规则如今新增的覆盖只是
.js/.mjs配置与 TS 逃逸口。树内这类作者为零。 而裁决 D 自己的重开条件点名的是「不跑 lint 的 AI 生成 JSON」——一条 lint 规则结构上够不到那种作者。可行性已实测,⛔ 不是推断
PR #17924 的在档复核测了,
domain:spec席复读确认:⇒ 门拿到的是原始写入体而非
parsed.data,残余还在。规则现有surfaceReason里写的「没测过,接过去可能是幻觉检查」已被这条读数推翻。① 音量:逐条报会变成它自己预警过的那场风暴
复核实测:把这条规则跑在那个奠基性的构建产物夹具上 → 150 条 findings。
而规则引用的契约文本(
acceptRetiredDefaultResidue的 docblock)已经逐字预警过同一件事:⇒ CLI 门一次跑一个项目,150 条还能忍;每一次元数据写入都按条报,就是它自己写的那场风暴。
简化实现的最低线:聚合成一条(「本次写入含 N 条退役残余,形如 …」),⛔ 不是 N 条。这是测量出来的约束,不是偏好。
②
surfaceReason那段话现在是错的,必须重写它现在写着运行时门那条「没测」。测了,而且结论相反。 ⇒ 接门时必须重写那段散文,⛔ 不能只改 surface 列表 —— 否则树里留下一句被自己的改动证伪的话。
建议的第一个动作:先量树外人口
allowRestore: false/allowPurge: false。⇒ 这个读数不需要任何裁决就能做,且两个方向都能帮决策。建议作为本卡的第一步。
出处
由 PR #17924 的在档复核(
claude-fable-5-1,档位由 spec 席从转录核验 86/86)提出为 ESCALATE;维护者裁 a;domain:spec执行席据此立卡。⛔ 裸卡、未定级 —— 无
domain:*、无priority:*,二者皆分诊席独有产出;仅预填 type。Generated by Claude Code