Skip to content

cross validateRetiredPermissionResidue to the runtime publish door — the CLI door cannot reach the AI/Studio JSON authors ruling D named #17944

Description

@os-bill

把 validateRetiredPermissionResidue 从 CLI_ONLY 接到运行时写入门(runtime-publish),让它够得到裁决 D 真正点名的那批作者:AI / Studio / REST /meta / MCP —— 他们从不跑 lint,但每一次写入都过那扇门。

裁决

维护者于 2026-09-13T07:2xZ 裁定 a(记录在 #17425 评论 5651940657),并指示:

「17425 按照简化的模式实现就可以」

⇒ 本卡走简化实现。 ⛔ 不要为它造框架。

为什么这张卡存在

裁决 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 席复读确认:

packages/metadata-protocol/src/protocol.ts
  ~:15839   const parsed = schema.safeParse(request.item);          // 先解析
  ~:15900   assertRuntimeAuthoringRules({ …, body: request.item })  // 交的是【原始体】

⇒ 门拿到的是原始写入体而非 parsed.data,残余还在。规则现有 surfaceReason 里写的「没测过,接过去可能是幻觉检查」已被这条读数推翻。

⚠️ 两条约束,⛔ 简化实现也不能省

① 音量:逐条报会变成它自己预警过的那场风暴

复核实测:把这条规则跑在那个奠基性的构建产物夹具上 → 150 条 findings。

而规则引用的契约文本(acceptRetiredDefaultResidue 的 docblock)已经逐字预警过同一件事:

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.

⇒ 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

Activity

  1. added theissue type on Sep 13, 2026
  2. os-bill commented on Sep 18, 2026

    @os-bill
    CollaboratorAuthor

    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 · labels pm:queue,domain:spec,priority:p3 · assignees 为空。
    • ⚠️ issue_read get 的 comments 字段读 1,而评论列表实读 0 条 —— 按 platform-readings.md「comments 计数会多读 … 线程空否读那两条,⛔ 不读计数」,本卡线程是空的 ⇒ Thread-read: none,且 ⛔ 无他席认领。

    派发时给施工席的围栏

    1. ⭐ 本卡有维护者裁决,而且裁决里有一句限定语,它比卡面任何别的话都重要:裁定 a(记录在 ObjectPermissionSchema's retired allowRestore/allowPurge: only literal false parses (not a truthy/falsy split), and no post-parse guard can ever see either key #17425 评论 5651940657),并明写「17425 按照简化的模式实现就可以」。⇒ 走简化实现,⛔ 不要为它造框架。 先把那条裁决第一手读出来再动手,⛔ 不要只读本卡对它的转述。

    2. 卡面自己点了一个必须先解决的前提:裁决 D 把信号定在「原始 objectstack.json 源」,而仓里没有这种文件 —— CLI 的 loadConfig 只接受 objectstack.config.{ts,js,mjs}。⇒ 第一腿是重取这个不存在:把搜索半径写出来,并给一条会命中的同主题控制(例如真实存在的那几种配置文件名确实被 loadConfig 接受),否则「没有这种文件」只是一个空结果,不是读数。

    3. ⚠️ 计数不是读数:凡是「X 处 / Y 个文件」的数,打印路径或行,⛔ 不要 grep -c。本席今天在三个不同的判据上各栽过一次,最近一次是一条单行正则够不到多行声明,却返回了一个「看起来像通过」的绿。

    4. Clause-② = no:把一个既有校验接到另一扇门上,不放宽接受集、不扩大公开面(它只会让更多输入被拒绝,而收窄不触条款②)。⚠️ 若你的读数显示这条接线会让某些今天被拒的输入变成被接受,那一刻起就是 yes,停下来报回。

    5. ⛔ 不碰 PD-14 受管面:docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md。裁决若把你推到那些面上,停下报回 —— 那里的落地是保留的,需要 GOVERNED_APPROVERS。

    6. 卡面的读数是立卡席的,不是本席的;⛔ 不要把它当既成事实。行号会腐烂,逐条重取。


    Generated by Claude Code

  3. self-assigned this
    on Sep 18, 2026
  4. os-bill commented on Sep 18, 2026

    @os-bill
    CollaboratorAuthor

    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

  5. os-bill commented on Sep 18, 2026

    @os-bill
    CollaboratorAuthor

    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,⛔ 不关卡、⛔ 不代裁

    ⚠️ 一条我自己的派发缺陷,记下来

    施工席指出:我的派发词没有带四轴升级框架,而常设条款禁止它自造轴 ⇒ 它只能把选项摆出来而不能逐轴分析。这是我的模板漏项,⛔ 不是它的缺陷 —— 一张有可能升级成决策的卡,派发时就该把框架给它。下一轮起补上。


    Generated by Claude Code

  6. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Sep 18, 2026
  7. removed their assignment
    on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions