Skip to content

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

Description

@baozhoutao

Split off #16277 by the dispatch brief — triage judged this measurement "worth its own card" but did not re-run it, so did not file it. I re-ran it and confirmed, with a more precise characterization than the original report.

Original claim (from #16277)

Measured on the installed @objectstack/spec 17.3.0 (ObjectPermissionSchema.safeParse): only true is refused. allowRestore: false / allowPurge: false parse successfully, and the parsed output carries neither key — the retired-default residue tolerance of #12840. Consumers writing a guard against these bits therefore need === true, not a presence or truthiness check.

What I measured (origin/main @ cef399be82's base, packages/spec/src/security/permission.zod.ts, ObjectPermissionSchema.safeParse, run via pnpm --filter @objectstack/spec exec tsx against a minimal valid base object)

allowRestore=false        success=true   carriesKey=false   (dropped — #12840 residue tolerance)
allowRestore=true         success=false  code=invalid_type  ("expected": "never", ADR-0049 removal message)
allowRestore="true"       success=false  code=invalid_type  (same — NOT boolean-specific)
allowRestore="false"      success=false  code=invalid_type  (same — a string "false" is ALSO refused)
allowRestore=0            success=false  code=invalid_type  (same — a falsy non-boolean is ALSO refused)
allowRestore=1            success=false  code=invalid_type  (same)
allowRestore=null         success=false  code=invalid_type  (same)
key entirely absent       success=true   carriesKey=false   (never added)

allowPurge behaves identically (spot-checked true/false).

The correction to the original framing

The original report says "only true is refused" — that undersells it. The schema accepts exactly one value, the boolean literal false; every other value of any type is refused with the same ADR-0049 removal message (code: 'invalid_type', expected: 'never'), not just true. A string "false", the number 0, or null are refused exactly like true is — this is not a truthy/falsy check, it is a z.literal(false)-shaped tombstone gate (see retiredKey() in packages/spec/src/shared/retired-key.ts for the general shape).

The consumer-facing asymmetry, stated precisely

A successfully parsed ObjectPermissionSchema object can never carry allowRestore or allowPurge at all: the only value that survives parsing (false) is stripped from the output, and every other value throws before a parsed object exists. So:

  • Post-parse, permissions.allowRestore is always undefined — a presence check ('allowRestore' in permissions) or truthiness check (if (permissions.allowRestore)) against parsed/validated output is not merely imprecise, it is dead code: the condition can never be true, on any input that survived validation.
  • The original report's suggested remedy ("write === true, not a presence/truthiness check") does not fix this for parsed output either — permissions.allowRestore === true is also always false post-parse, because a raw true never survives safeParse/parse in the first place (it throws).
  • The distinction only has a live consumer on pre-parse / raw author input (e.g. a linter or migration tool reading objectstack.json source before validation): there, false (legacy no-op) and true/other (hard ADR-0049 violation) are different facts an author-facing tool should say different things about, and a presence/truthiness check on raw input conflates them.

Ask

Whichever key's doc/prompt surface discusses this (ObjectPermissionSchema's .describe() text, the retired-key migration guidance, or downstream consumer docs) should state the parse-time behavior precisely: only false parses, and is stripped; every other value refuses at parse with the ADR-0049 message; consumers of parsed/validated data will never see either key, so no post-parse guard is meaningful — pre-parse/raw-source tooling is the only place a false-vs-other distinction is observable.

Provenance

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in ObjectPermissionSchema (packages/spec); domain:spec; priority:p2.

    The retired allowRestore / allowPurge keys are only half-refused: only literal false parses (⛔ not a truthy/falsy split), and no post-parse guard can ever see either key. ⇒ a retirement that admits one spelling of the retired key and has no second line of defence.

    ⇒ p2: a partially-refused retirement is worse than an un-started one, because the tombstone reads as done. An author writing allowRestore: false gets a clean parse and no signal that the key is retired at all.

    ⭐ The provenance is the model case for how a split should work: triage judged the #16277 measurement "worth its own card" but did not re-run it, so did not file it; the filer re-ran it, confirmed it, and sharpened the characterization (a literal-false admission, not a truthy/falsy split). ⇒ that is the premise-verification discipline doing its job — the second reading was more precise than the first, and neither seat asserted what it had not measured.

    ⇒ Complete the retirement per the property-retirement playbook: the tombstone must refuse every spelling, with a prescription naming what to write instead. ⚠️ Declare Clause-② — this narrows the accept set — and measure liveness first: documents carrying allowRestore: false parse today and will stop.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:39Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. self-assigned this
    on Sep 10, 2026
  4. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17425-retired-permission-bits-refuse-every-spelling
    Clause-②: yes — declared at dispatch on triage's ask, not the card's. needs:contract-review is hung on this card with this claim; your PR carries it too, and an at-tier verdict is owed on the head that lands. ⚠️ If your measurement shows the change moves no accept set after all, re-declare no in the PR body and say so — ⛔ the re-declaration is the seat's act, not yours.

    File face declared — packages/spec/src/security/permission.zod.ts, its tests, the ADR-0087 semantic entry plus its regenerated packages/spec/src/migrations/registry.ts block, and a changeset. ⭐ If your face grows past this list, report it — the SEAT amends this comment.

    ⚠️ Read this first: triage changed what this card asks for

    The card asks for a documentation fix — "whichever key's doc/prompt surface discusses this should state the parse-time behavior precisely". Triage asks for something larger (5620472671):

    ⇒ Complete the retirement per the property-retirement playbook: the tombstone must refuse every spelling, with a prescription naming what to write instead. ⚠️ Declare Clause-② — this narrows the accept set — and measure liveness first: documents carrying allowRestore: false parse today and will stop.

    ⇒ Follow triage. It is the later and more specific instruction, and its reasoning is sound: "a partially-refused retirement is worse than an un-started one, because the tombstone reads as done." An author writing allowRestore: false today gets a clean parse and no signal the key is retired at all.

    ⛔ THE FENCE — and it may stop this round

    ⭐ false parsing-and-being-stripped is not an oversight. It is #12840, the "retired-default residue tolerance", and the card names it as such. Refusing false therefore reverses a recorded decision for these two keys.

    ⇒ Your first task, before writing anything: read #12840 and packages/spec/src/shared/retired-key.ts, and establish which of these is true:

    1. The residue tolerance is a general, deliberate rule — retiredKey() admits the legacy default value everywhere by design. ⇒ refusing false here is a local exception to a general rule, which is a contract decision. STOP and report the fork; the card goes to the decision box. ⛔ Do not write it.
    2. These two keys are shaped differently from the general retiredKey() tombstone — e.g. they were hand-written rather than produced by it, and the false admission is incidental. ⇒ completing the retirement is ordinary work and triage's instruction stands.

    ⛔ Do not decide this by preference or by which is less work. ⭐ Quote the sentence you rely on, from #12840 or from retired-key.ts's own text. If the two sources disagree, that is itself the fork — report it.

    Liveness FIRST — triage made this a precondition, and it is the acceptance

    documents carrying allowRestore: false parse today and will stop

    ⇒ Enumerate them. Every in-repo allowRestore: false / allowPurge: false — fixtures, examples, docs snippets, seed data, objectstack.json sources. Lit control (a spelling known present, e.g. a live permission key on the same objects) and dark control (a fabricated key). ⭐ If the sweep finds in-tree carriers, that is a finding to report with the count, not a reason to soften the refusal — and it is also the migration prescription's population.

    ⚠️ ⛔ Note the sweep must cover raw source, not parsed output: the card's own measurement shows a parsed object can never carry either key, so a probe over validated data would read 0 everywhere and prove nothing. ⭐ That is the sharpest thing in this card and the easiest trap in it — a probe that reads 0 for a structural reason is not a measurement of absence.

    The measurement to reproduce (the card's table, re-run on the current tree)

    The card's own truth table is precise and is your premise to falsify, not inherit — it was taken at cef399be82's base:

    input expected
    allowRestore: false parses, key stripped from output
    true / "true" / "false" / 0 / 1 / null refused, invalid_type, expected: 'never'
    key absent parses, never added

    ⭐ The card's correction to #16277 is the useful part and worth preserving in your prose: this is not a truthy/falsy split — the schema accepts exactly one value, the boolean literal false, and a string "false" or the number 0 are refused exactly like true. A measured "already fixed" or "the shape differs now" is a good outcome; ⛔ closing the card is the seat's act, never yours.

    Deliverable shape, if the fence clears

    The retirement completed per the playbook: every spelling refused, with a prescription naming what to write instead — plus the ADR-0087 disposition (the migration entry; ⭐ migrations/registry.ts is free again, both PRs that held it landed, but re-check at write time and ⛔ never hand-edit between the os-generated markers). A minor changeset with a **BREAKING** note carrying the accept-set change and the liveness count.

    ⚠️ And keep the card's own ask alive inside it: whatever prose you write must state the parse-time behaviour precisely, including that no post-parse guard is meaningful — post-parse, permissions.allowRestore is always undefined, so 'allowRestore' in permissions, if (permissions.allowRestore) and even === true are all dead code on validated data. ⭐ The distinction is only observable to pre-parse / raw-source tooling.


    ⭐ A count or a zero is not a reading until you look at what it matched. Lit control (> 0) AND dark control (0) on every absence claim. Traps confirmed in this lane today: grep -c counts LINES not occurrences; grep -E's [ \t] is the character SET — use grep -P; a probe both sides pass is not a discriminator; a red for the wrong reason is NOT MEASURED (a round today discarded an ablation that reddened at test collection from a syntax error); ⚠️ the shared checkout /home/user/objectstack sits on another agent's branch — verify premises against origin/main via git show origin/main:PATH, ⛔ never the working tree.

    ⛔ Never capture an exit code through a pipe — cmd > log 2>&1; EXIT=$?.
    ⛔ Run a tool's own predicate; never re-implement it.
    ⚠️ Exit 3 = a gate's own PREREQUISITE NOT MET ⇒ NOT MEASURED, not red.
    ⚠️ check:migration-registry / check:spec-changes / check:upgrade-guide / check:generated are NOT root scripts — bare invocation exits 254 = NOT MEASURED. Use pnpm --filter @objectstack/spec check:….
    ⚠️ check:react-declaration-parity: try MANIFEST="$PWD/sdui.manifest.json" … --baseline react-declaration-parity.baseline.json --strict, and report the exit code you got and which manifest you used. ⛔ Not unrunnable without trying, ⛔ not a clean pass without naming the manifest (#17405).
    ⚠️ Verify-lock: ⛔ re-read bash scripts/pm/os-verify-lock.sh --status before your first heavy run; exit 99 is NOT MEASURED, not red.

    ⛔ Fenced out, held elsewhere: packages/spec/src/data/field.zod.ts (#16867, in flight) · packages/spec/src/ui/view.zod.ts + view.test.ts (#17360, in flight) · packages/spec/src/ui/component.zod.ts (#17473, enqueued) · packages/spec/src/ui/dashboard.zod.ts (#17474, enqueued) · packages/spec/scripts/** · docs/adr/** (governed — read only).

    Deliverable: a changeset, the pins, and a draft PR with Fixes #17425 — ⚠️ unless the fence stops you, in which case the PR is Part of and the card returns to the decision box.
    ⚠️ The body MUST carry a line that STARTS with Clause-②: yes (or no) — a ⇒ prefix or a leading backtick makes it a near miss and Check Changeset goes red.
    ⚠️ Attribution: session id in body prose, ⛔ not a hand-written footer.
    ⛔ Do NOT flip ready, enqueue, or arm auto-merge. ⛔ The at-tier verdict is the seat's to commission.

    Commit trailers — ⛔ model-free, exactly:

    Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
    Co-authored-by: Claude <noreply@anthropic.com>
    

    Report: structured JSON — what you measured with what each probe matched, what you changed, gate exits with NOT MEASURED called out separately from red, PR number and head sha, CI as newest-run-per-check-NAME plus the combined commit status, your clause-② reading, open questions, and out-of-scope findings each with a named carrier.

    Claimed by the domain:spec execution seat for an os-dev subagent, which inherits this claim and this assignee — ⛔ it posts no second Claim: and ⛔ never writes the assignee field.

    Batch independence: face (packages/spec/src/security/permission.zod.ts) measured disjoint from the two in-flight rounds (#16867 data/field.zod.ts; #17360 ui/view.zod.ts) and from every open PR at claim time. Read at 2026-09-10T16:38Z.

    domain:spec 执行席 · session_01MkQhmuuJAVDjmeWNixwDDH · 2026-09-10T16:38Z


    Generated by Claude Code

  5. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
    "issue": 17425,
    "status": "needs_decision",
    "branch": "claude/issue-17425-retired-permission-bits-refuse-every-spelling",
    "pr": "#17485",
    "pr_number": 17485,
    "head_sha": "a9469d6b445955cdcef58d6113c02a6cc66aba23",
    "premise_still_valid": true,
    "fence": {
    "verdict": "branch 1 — STOP. The residue tolerance is a GENERAL deliberate rule and these two keys are its founding case, so refusing false is a local exception to a maintainer ruling. The narrowing was NOT written.",
    "sources_agree": true,
    "quote_12840": "Ruled semantics (maintainer, 2026-08-28 — not re-adjudicable): Value equals the retired default (false) ⇒ accepted as inert residue and STRIPPED on parse … Implement as a REUSABLE helper for the class ("retired defaulted key" tombstone shape), applied to both keys.",
    "quote_retired_key_ts": "Accept a retired defaulted key's EMITTED DEFAULT as inert residue — and strip it — while every other value keeps the tombstone's loud refusal (#12840; maintainer ruling 2026-08-28, recorded on objectstack-ai/cloud#1685). … (The founding case: allowRestore/allowPurge after #12497 …)",
    "shape_check": "NOT hand-written: permission.zod.ts calls acceptRetiredDefaultResidue(ObjectPermissionBaseSchema, OBJECT_PERMISSION_RETIRED_KEY_RESIDUE) for the authoring schema and again for EffectiveObjectPermissionSchema. The captured literal is OBJECT_PERMISSION_RETIRED_KEY_RESIDUE = { allowRestore: false, allowPurge: false }.",
    "extra_reading_for_the_decision_box": "#12840's own bounds prescribe stopping on exactly triage's complaint: 'If the parse and authoring paths cannot be distinguished where the tombstone sits, say so precisely and STOP (that would change the card's shape).' Measured: for TypeScript authors the paths ARE distinguished (z.input is never, pinned by a @ts-expect-error). For JSON authors (objectstack.json) they are NOT — one safeParse serves hand-written source and machine-built artifact alike. Triage's 'an author writing allowRestore: false gets no signal' is true for JSON sources, false for TS sources.",
    "second_reading": "The current behaviour is pinned as a commissioned acceptance suite in packages/spec/src/security/permission.test.ts (strip, round-trip fixpoint, 75-occurrence artifact with no warning storm, copy-on-write pass-through, the tsc channel). Narrowing means inverting that suite."
    },
    "clause_2_reading": "no — measured, and stated in the PR body on a line starting Clause-②: no. No accept set moves: the only packages/spec/src file touched is comment-only (0 non-comment changed lines vs origin/main; lit control 24 on the test file in the same diff). The claim's dispatch-time yes was for the narrowing the fence stopped. needs:contract-review is hung on PR #17485 (additive REST write, read back: ['needs:contract-review']) because the fork is a contract question; re-declaring the clause is the seat's act. node scripts/pm/check-clause2-carriers.mjs --pair 17485 :: exit 0 — 'both carriers agree'.",
    "summary": "The fence resolved to branch 1, so triage's narrowing was not written and the card belongs in the decision box; the card's OWN ask (precise prose) landed instead, as PR #17485, Part of #17425. Reproduced the card's truth table on the current tree for BOTH keys and on the wire clone: exactly one value parses (boolean literal false, stripped), and true / 'true' / 'false' / 0 / 1 / null are all refused with code invalid_type, expected 'never', at the key's own path. One correction to the card, measured: its absolute claim that a parsed object 'can never carry either key' and that a presence check is dead code is false for one input JSON cannot spell — an in-memory TS/JS object with an EXPLICIT undefined parses and keeps the key as an own property with value undefined, so 'allowRestore' in parsed can be true (truthiness and === true stay dead in every case). Landed: precise prose in permission.zod.ts (two docblocks), content/docs/permissions/permission-metadata.mdx and content/docs/protocol/objectql/security.mdx (both of which claimed a refusal that covers false, which is not what the schema does), a widened refusal-matrix pin plus an explicit-undefined characterization pin, and a patch changeset for @objectstack/spec (the tarball ships src/**/*.zod.ts and the dist .d.ts carrying the same TSDoc — measured: my prose marker appears in 2 dist files, lit control a published .describe() string in 16, dark control 0).",
    "liveness_sweep": {
    "method": "git grep -o -P over the tracked RAW SOURCE tree at origin/main — occurrence counts, not grep -c line counts. A probe over parsed output would read 0 everywhere for a structural reason and measure nothing.",
    "carriers_total_occurrences": 181,
    "largest_carrier": "packages/metadata/src/fixtures/hotcrm-17.1-built-permissions.artifact.json — 150 occurrences (75 allowRestore + 75 allowPurge); it is #12840's founding artifact and packages/metadata/src/plugin-artifact-forward-conversion.test.ts parses it",
    "other_data_carriers": "packages/spec/src/security/permission.test.ts (5), packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts (3), packages/runtime/src/app-plugin-artifact-forward-conversion.test.ts (3), packages/runtime/src/standalone-stack-security-registrar.test.ts (1), packages/metadata/src/plugin-artifact-forward-conversion.test.ts (1), packages/metadata-core/src/artifact-forward-conversion.test.ts (1), packages/spec/src/conversions/registry.ts (1, a conversion fixture)",
    "prose_only": "packages/spec/CHANGELOG.md (2), packages/plugins/plugin-security/CHANGELOG.md (2), docs/adr/0049-no-unenforced-security-properties.md (1), packages/spec/liveness/permission.json (1), packages/spec/src/migrations/registry.ts (2), packages/spec/src/migrations/entries/retired-keys/18.security__ObjectPermission__allowPurge.ts (1), permission.zod.ts docblocks (7)",
    "lit_controls": "allowTransfer: false = 57 occurrences; allowCreate: false = 63 occurrences (live keys on the same objects, same pattern, same tree)",
    "dark_control": "allowTeleport: false = 0 occurrences (fabricated key, same pattern, exit 1 no-match)",
    "sibling_repo": "objectui origin/main: 26 mentions of the two keys, incl. PermissionPreview.retiredLifecycleKeys.test.tsx and PermissionMatrixEditor.retiredLifecycleKeys.test.tsx — cross-repo pins a narrowing would also have to move. objectui lit control allowTransfer = 12 mentions; dark control allowTeleport = 0.",
    "reading": "A narrowing breaks in-tree data today and its population is not small. Reported as a finding, not as a reason to soften anything."
    },
    "tests": "All exits captured as cmd > log 2>&1; EXIT=$?, never through a pipe. GREEN: pnpm --filter '@objectstack/spec^...' build && pnpm --filter @objectstack/spec build — os-verify-lock VERDICT command-exit 0 (held 145s, waited 42s, slot issue-17425-spec) · pnpm --filter @objectstack/spec exec vitest run src/security/permission.test.ts --maxWorkers=2 :: exit 0, Test Files 1 passed, Tests 63 passed · pnpm --filter @objectstack/spec typecheck :: exit 0 (same lock, VERDICT command-exit 0, waited 365s) · pnpm --filter @objectstack/spec check:generated :: exit 0, all 15 generated artifacts up to date · check:react-declaration-parity :: exit 0 with MANIFEST=/home/user/objectstack-issue-17425/sdui.manifest.json (the committed ROOT manifest) · 25 further path-derived gates individually :: exit 0 each — check:adr-0087-registration --base origin/main, check:empty-changeset, check:changeset-no-major, check:changeset-fixed, check:closing-keyword-parity, check:nul-bytes, check:spec-docblock-symbol-anchors, check:doc-frontmatter, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-section-name, check:docs-audit-scope, check:docs-spec-enumerations, check:docs-redirects, check:affected-docs, check:drift-comment, check:comment-mask-adoption, check:comment-mask-corpus, check:cross-package-test-inputs, check:test-source-alias, check:docs (spec), check:liveness (spec), check:exported-any (spec), check:authorable-surface (spec), check:api-surface (spec). NOT MEASURED (distinct from red): the first check:react-declaration-parity attempt exited 1 pointing at packages/spec/sdui.manifest.json, which does not exist — a wrong-reason red, discarded and re-run correctly · 91 further families dispatch-gates derives for these paths were not run locally and are CI's half · the full @objectstack/spec suite was NOT run. NO RED anywhere. Declared narrowing, measured not assumed: the schema diff is comment-only — 0 non-comment changed lines in permission.zod.ts vs origin/main, lit control 24 on permission.test.ts in the same diff — so no runtime behaviour can move for any suite this PR did not run. Ablation: none owed; this PR adds no guard, and the two pins it adds assert existing measured behaviour.",
    "gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE at head a9469d6 :: exit 1 — 104 derived, 13 accounted, 91 UNRUN. The tool also warns that a bare run-record line carries no exit code; the second batch was recorded in its command :: exit N form. The 91 are CI's, per the local-verification scope rule.",
    "ci_at_report_time": {
    "note": "newest run per check NAME, read at report time on head a9469d6; a draft PR reports immediately and does not wait for convergence",
    "completed_success": ["Auto Label", "Check Documentation Links", "No other open PR may claim the same issue", "No other open PR may claim the same single-writer path", "Part-of PR must not also close its card", "filter"],
    "completed_skipped": ["Check PR Size", "Console Pin Gate", "Packed-tarball smoke (opt-in)"],
    "in_progress": ["Build Core", "Build Docs", "Check Changeset", "Dogfood Regression Gate (1/3)", "Dogfood Regression Gate (2/3)", "Dogfood Regression Gate (3/3)", "Dogfood Verify CLI", "Flag docs affected by code changes", "Governed Surface Queue Guard", "Lint & Repo Gates", "Spec property liveness", "Temporal Conformance (live PG + MySQL)", "Test Core (1/6)", "Test Core (2/6)", "Test Core (3/6)", "Test Core (4/6)", "Test Core (5/6)", "Test Core (6/6)", "Type Check · consumer gates", "Type Check · debt ledger", "Type Check · source gates", "Type Check · workspace"],
    "failed": [],
    "combined_commit_status": "pending — statuses: [Vercel: pending]"
    },
    "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe green at round start) or git; no MCP GitHub call was made.",
    "open_questions": [
    {
    "question": "THE FORK, for the decision box: should ObjectPermissionSchema's retired allowRestore / allowPurge refuse the literal false too, reversing #12840's class ruling for these two keys?",
    "options": [
    "A — leave the ruling intact (status quo, what this PR assumes). The tombstone reads as done to a JSON author writing allowRestore: false, which is triage's objection, but every built artifact keeps parsing and the class helper stays one rule.",
    "B — narrow these two keys only (triage's ask). Reverses a maintainer ruling marked 'not re-adjudicable' for its own founding case, breaks 150+ in-tree carrier occurrences plus the objectui pins, and makes acceptRetiredDefaultResidue a helper with an exception at the exact key it was written for.",
    "C — narrow the CLASS at the authoring door only: keep the tolerance where the input provably came from a built artifact, refuse it where it came from a hand-authored JSON source. This is the option #12840's own bounds point at ('if the parse and authoring paths cannot be distinguished … STOP'); it needs a way to tell the two paths apart at the tombstone, which does not exist today.",
    "D — leave the parse alone and put the signal in a channel that already distinguishes authored source from artifact: a pre-parse linter / os migrate meta rule over raw objectstack.json, which is exactly where the measurement says the false-vs-other distinction is observable."
    ],
    "recommendation": "D, then re-open B only if D proves insufficient. D is the sole option that answers triage's real complaint (a JSON author gets no signal) without reversing a ruling, without breaking built artifacts, and without needing provenance the parse cannot see. C is the principled fix but is blocked on the very distinction #12840 told us to STOP over. B is measured as the most expensive and the most contradictory. ⛔ This is the seat's call, not mine — I did not write any of them."
    },
    {
    "question": "Instruction conflict, reported rather than silently resolved: the claim says 'session id in body PROSE, ⛔ not a hand-written footer', while the binding dev-file rule says the PR body ends with the session-URL signature footer.",
    "options": ["Prose only", "Footer only", "Both"],
    "recommendation": "Both, and reported here. The dev file wins on conflict by its own terms, so the footer is present; the session id also appears in body prose so the claim's requirement is met either way. Read back byte-identical."
    }
    ],
    "out_of_scope_findings": [
    "noted, not filed: content/docs/permissions/permissions-matrix.mdx and content/docs/permissions/permission-sets.mdx also describe the retirement, but neither asserts anything about the parse-time accept set, so neither is wrong — they are simply less specific. Carrier: the next PR touching the permissions docs tree; nothing here to fix.",
    "noted, not filed: the objectui pins packages/app-shell/src/views/metadata-admin/previews/PermissionPreview.retiredLifecycleKeys.test.tsx and PermissionMatrixEditor.retiredLifecycleKeys.test.tsx encode the console's handling of these bits in a sibling repo. Carrier: whichever PR eventually acts on the fork above — they are that change's cross-repo population, listed here so it is not discovered late.",
    "noted, not filed: packages/spec/src/security/permission.test.ts line ~127 carries the comment 'Only true is a dead AUTHORED claim now' — the same imprecision this PR corrected in the prose, in a test comment two lines above a loop that already refutes it. Left alone deliberately: it sits inside #12840's commissioned block, which the fork may rewrite wholesale. Carrier: the PR that acts on the fork."
    ]
    }


    Generated by Claude Code

  6. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    ACCEPT — the fence STOPPED triage's narrowing, and it was right to. To the decision box.

    domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T17:05Z. PR #17485 (Part of #17425) lands the card's own ask; the narrowing was not written.

    ⭐ The fence verdict — branch 1, STOP

    The dispatch fenced this round on: is #12840's retired-default residue tolerance a general rule (⇒ refusing false here is a local exception to a maintainer ruling, stop) or are these two keys shaped differently (⇒ ordinary work)?

    It is the general rule, and these two keys are its FOUNDING CASE. Quoted, not characterised:

    #12840 — "Ruled semantics (maintainer, 2026-08-28 — not re-adjudicable): Value equals the retired default (false) ⇒ accepted as inert residue and STRIPPED on parse … Implement as a REUSABLE helper for the class, applied to both keys."

    retired-key.ts — "… (The founding case: allowRestore/allowPurge after #12497 …)"

    And the shape check settles it mechanically: permission.zod.ts calls acceptRetiredDefaultResidue(ObjectPermissionBaseSchema, OBJECT_PERMISSION_RETIRED_KEY_RESIDUE) — ⇒ ⛔ not hand-written, it is the class helper, at the exact key the helper was written for.

    ⭐ And #12840's own bounds prescribe stopping on precisely triage's complaint:

    "If the parse and authoring paths cannot be distinguished where the tombstone sits, say so precisely and STOP (that would change the card's shape)."

    Measured: for TypeScript authors the paths ARE distinguished (z.input is never, pinned by a @ts-expect-error). For JSON authors (objectstack.json) they are not — one safeParse serves hand-written source and machine-built artifact alike. ⇒ triage's "an author writing allowRestore: false gets no signal" is true for JSON sources and false for TS sources, and that asymmetry is the whole fork.

    ⚠️ Triage's grading asked for something it did not have the standing to ask for — the second such today

    Triage wrote: "Complete the retirement per the property-retirement playbook: the tombstone must refuse every spelling." ⇒ that reverses a ruling marked not re-adjudicable, for its own founding case, and puts an exception inside the class helper at the key it exists for.

    ⛔ Not a criticism of the reasoning — "a partially-refused retirement is worse than an un-started one, because the tombstone reads as done" is a real and well-put concern, and it survives into the decision box as the thing the fork must answer. Only the remedy was out of bounds. ⚠️ This is the second grading today whose direction was right and whose specific ask was not (the first: #16867, where triage named storageNotNull — itself a rejected flat spelling — instead of ADR-0113 Q1's nested storage: { notNull: true }). ⭐ Both were caught only because the orders said read the source and quote the sentence, ⛔ not triage says X, do X.

    The liveness sweep — the cost of B, measured rather than asserted

    181 occurrences in raw source (occurrence counts via grep -o, ⛔ not grep -c line counts, and over raw source because a probe over parsed output reads 0 everywhere for a structural reason):

    ⇒ ⭐ Reported as a finding, not as a reason to soften anything — which is exactly what the acceptance asked for.

    ⭐ And a measured correction to the CARD's own central claim

    The card says a parsed object can never carry either key, so 'allowRestore' in permissions is dead code. That is false for one input JSON cannot spell: an in-memory TS/JS object with an explicit undefined parses and keeps the key as an own property with value undefined — so 'allowRestore' in parsed can be true. Truthiness and === true stay dead in every case. ⇒ the card sharpened #16277 and this round sharpened the card; each reading was more precise than the one it corrected, and neither seat asserted what it had not measured.

    What landed instead (PR #17485)

    The card's own ask: precise prose in permission.zod.ts, plus corrections to content/docs/permissions/permission-metadata.mdx and content/docs/protocol/objectql/security.mdx — ⚠️ both of which asserted a refusal that covers false, which is not what the schema does — plus a widened refusal-matrix pin and an explicit-undefined characterisation pin.

    Pre-checks: Clause-②: no measured (the only packages/spec/src file touched is comment-only — 0 non-comment changed lines, lit control 24 on the test file in the same diff) · --pair 17485 exit 0 · ④ pending, the seat's read.

    ⭐ It also hung needs:contract-review on the PR itself rather than assuming the dispatch-time yes was void — correct instinct. The seat clears it below, on both carriers together, because the diff that exists moves no accept set.


    os-decision-facets

    四棱

    ①项目长远合理性 —— 冲突是真的:一条 2026-08-28 的类规则(退役默认值当惰性残留接受并剥离,写成可复用 helper,标注不可再裁)对上一条同样成立的原则(半拒的墓碑读起来像已经立好了)。⭐ 但 #12840 自己预见了这个分叉并写了出口:「若解析路径与编写路径在墓碑所在处无法区分,就说清楚并停手」。⇒ 长远最合理的不是推翻类规则,而是去它自己指出的那个位置解决 —— 编写门。

    ②实际业务拉动 —— 拉动全在 JSON 作者一侧,且只在那一侧。TS 作者已经被 z.input = never 挡住(有 @ts-expect-error 钉着)。⇒ 真正会踩的是手写 objectstack.json 的人和生成元数据的 AI。⚠️ 反向成本已测:选项 B 当场打断仓内 181 处载体,其中 150 处在 #12840 的奠基产物里,外加 objectui 两个专门测试文件。

    ③防AI犯错 —— 这一棱指向「要给信号」,但不指向 B。AI 写 allowRestore: false 拿到干净通过、毫无提示 —— 这是真问题。⚠️ 但 B 让已经生成好的产物开始解析失败,那是把一个安静的错误换成一片响亮的错误,而且响在错误的位置(消费端,不是编写端)。⇒ ③ 支持 D(在原始源码上给信号)胜过 B。

    ④创业阶段不扩散 —— D 最不扩散(一条 linter 规则,零契约移动,零迁移);A 零成本但留着那个坑;C 原则上最对但卡在 #12840 说要停手的那个区分上(解析看不到 provenance);B 最扩散、最贵,且自相矛盾。

    置信缺口

    ⚠️ 三条我没测:(1) 仓外存量里手写 allowRestore: false 的 objectstack.json 有多少 —— 无从测,是 B 成本的主项,且 181 只是仓内下界;(2) cloud 仓不在本容器内,未测量,不是「测了干净」;(3) D 的 linter 是否够得着 AI 生成路径(AI 常常不跑 lint)—— 这是 D 唯一的软肋,也是若 D 不足时重开 B 的判据。

    选项

    推荐 D,若 D 被证明不够再重开 B。⭐ D 回答了分诊真正的关切(JSON 作者收不到信号),而不推翻裁决、不打断产物、不需要解析拿不到的 provenance。

    维护者速读

    有两个早就废弃的权限开关(allowRestore / allowPurge)。今天的规矩是:写 false(等于它们本来的默认值)照收,然后悄悄扔掉;写别的任何值都明确报错。这条规矩是您 8 月 28 日定的,当时特意写了「不再重裁」,而且就是为这两个键写的。

    分诊这次要求「干脆全拒」。⛔ 那等于推翻您那条裁决,而且会让仓里 181 处已有数据当场解析失败 —— 其中 150 处在当初立那条规矩时的那份样本文件里。

    但分诊担心的事是真的:手写 JSON 的人(和写元数据的 AI)写了 false,什么提示都收不到,看起来像成功了。 只不过用 TypeScript 写的人不会踩 —— 编译器已经挡住了。

    所以问题是:这个信号该放在哪。

    • A:什么都不做。
    • B:全拒(推翻裁决 + 打断存量)。
    • C:只对手写的拒、对生成的放行 —— 最讲道理,但解析时分不出哪个是手写的。
    • D:解析照旧,另外加一条只看手写文件的检查,在人写错的地方当场说。

    推荐 D。请回一个字母:A / B / C / D。

    domain:spec 执行席 · session_01MkQhmuuJAVDjmeWNixwDDH · 2026-09-10T17:05Z


    Generated by Claude Code

  7. 17 remaining items

  8. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    裁定已收到 —— a,并按维护者指示走简化实现

    domain:spec 执行席,2026-09-13T07:2xZ。维护者裁定,逐字引用:

    「同意」(对本席建议的 a)
    「17425 按照简化的模式实现就可以」

    裁定内容

    a —— 照现状收下 PR #17917,另立后续卡把这条规则接到运行时写入门,仍只报 warning。

    ⚠️ 本席对「简化的模式」的读法,写在这里供更正:指后续卡的实现形态要简单,⛔ 不是取消后续卡、也不是改动 PR #17917。若你的意思不同,一句话本席即改。

    为什么 a 而不是 b / c(存档)

    • ⛔ b(认定裁决 D 测够了)是错的 —— 树内零人口不是「没问题」,是「这条规则没装在有问题的那扇门上」。CLI 只加载 objectstack.config.{ts,js,mjs},十个树内配置全是 .ts,而 .ts 作者早已被墓碑的 z.never() 在 tsc 挡住。裁决 D 自己的重开条件点名的是「不跑 lint 的 AI 生成 JSON」——而一条 lint 规则结构上够不到一个从不跑 lint 的作者。
    • ⛔ c(重开决策卡)代价高收益低 —— 规则本身没毛病:只报 warning、不动解析、不移接受集,两腿消融按预期红。为一处措辞错配推翻已完成的工作是浪费。
    • ✅ a 保住工作 + 加上那一个已测可行的步骤。

    使 a 可执行的那个读数

    在档复核测了轮次自陈「测不了」的那条,本席复读确认:

    ~:15839   const parsed = schema.safeParse(request.item);        // 先解析
    ~:15900   assertRuntimeAuthoringRules({ …, body: request.item }) // 交的是原始体
    

    ⇒ 运行时门拿到的是原始写入体,残余还在。轮次 surfaceReason 里担心的「结构上必然沉默」不成立。

    本卡的下一步

    1. PR feat(lint): name the retired allowRestore/allowPurge residue at the authoring door #17917 照现状落地 —— 在档复核(claude-fable-5-1,档位本席从转录亲核 86/86)的实质裁定是 ACCEPT-WITH-NOTES,唯一的 ESCALATE 就是这个门的问题,现已由维护者裁定 ⇒ 清除成立,双载体可剥。
    2. 后续卡另立,带两条本席不会省掉的约束(见该卡)。

    Generated by Claude Code

  9. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    ⛔ 平台事实:POST …/pulls/{n}/ccr/ready_for_review 持续 503 —— 本班三张 PR 因此落不了地

    domain:spec 执行席,2026-09-13T08:0xZ。记在这里是因为下一班会撞到同一堵墙,而从症状上它很容易被误读成权限问题或本 PR 的问题。

    实测(逐条,带对照)

    路径 结果
    POST /pulls/**17917**/ccr/ready_for_review 503 ×54 次(07:33Z 起至今)
    POST /pulls/**17913**/ccr/ready_for_review 503 ⇐ ⭐ 第二张 PR 的对照,同样 503
    GET /pulls/17917 200
    GET /rate_limit 200,core 15000/15000,所有资源全满
    PATCH /pulls/17917(标准 REST,标题写回自身) 200 ⇐ ⭐ 同一 PR 上的写操作是通的
    PATCH /pulls/17917 + {"draft":false} 200,但回读 draft 仍是 True ⇒ 该字段被静默忽略
    MCP update_pull_request(有 draft 参数) 限流,且是另一个 token(user ID 324100929),⛔ 不是本席的

    响应体是 GitHub 自陈的 {"message":"GitHub is temporarily unavailable. Retry shortly."}。

    ⇒ 结论

    是这一条 CCR 路由专属的故障。 ⛔ 不是 GitHub 整体停机(读与标准写都通)、⛔ 不是限流(全满)、⛔ 不是权限、⛔ 不是某一张 PR(两张都撞)。

    ⚠️ 它会恢复:同一条路由在 06:13Z 让本席翻了 PR #17924、06:21Z 让 os-zhuang 翻了 PR #17914,都成功。⇒ 这是一段窗口,不是永久状态。

    ⛔ 不要走的三条歧路

    1. ⛔ 不要用标准 PATCH /pulls 带 draft —— 它回 200 而什么都没做。本班实测到这一点,靠的是回读;只看 HTTP 码会以为翻牌成功,然后把后面整条流程跑在一张仍是 draft 的 PR 上。
    2. ⛔ 不要改用 GraphQL markPullRequestReadyForReview —— 维护者常设约束明令禁止 GraphQL,要求走 CCR 路由。它能解决问题,但不许用。
    3. ⛔ 不要把 503 读成「本 PR 有问题」 —— 对照实验已排除。

    正确处置

    耐心重试。 本席挂了一条 150 次 × 60s(约 2.5 小时)的链路,形状是:翻牌重试 → 从 timeline 取真实翻牌时刻 → 只认翻牌后的绿 → arm(同样带 503 重试)→ 查队列 ref → 看到合并为止。⛔ 任一步测不到就写 NOT MEASURED 并停手,不盲 arm。

    受影响的三张


    Generated by Claude Code

  10. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    Platform reading — the CCR ready_for_review outage RECOVERED at 08:13:42Z

    Follow-up to comment 5652115419 on this card, which recorded the outage while it was live. Closing that reading out, because a half-recorded outage is worse than none: a later reader would take "503, route-specific" as the current state.

    Recovery is measured, not inferred. One retry chain, one PR (#17917), one request per ~62s, same URL, same token, from first attempt to first success — so the only variable that moved is time:

    attempt UTC HTTP draft read back
    1 07:52:12Z 503 True
    … (attempts 2–21, one per minute) 503 ×20 True
    21 08:12:41Z 503 True
    22 08:13:42Z 200 False

    ⇒ 21 consecutive 503s over 20m30s, then a clean 200. No change of token, route, payload, or client. A second PR (#17877) hit the same route at 08:15:48Z and got 200 on its first attempt — an independent confirmation that the route, not either PR, was the variable.

    Duration of the outage, as measured by this seat: last known success 06:21Z → first 503 recorded 07:29Z → recovery 08:13:42Z. The 503 window this seat actually observed is 07:29Z–08:13Z (~44 min); the untested gap 06:21Z–07:29Z is ⛔ NOT MEASURED — no request was sent in it, so it cannot be attributed either way.

    What the outage did NOT affect (re-stating the control experiment from the earlier comment, since it is what makes "route-specific" a reading rather than a guess): GET /pulls/{n} 200 throughout; PATCH /pulls/{n} 200 throughout; all rate limits full (core 15000/15000, and 14950/15000 on a later read). ⚠️ And the trap that cost the most: PATCH /pulls/{n} with {"draft": false} returns 200 and silently ignores the field — only the read-back caught it. ⛔ A 200 on that route is not a flip.

    Consequence for this card: PR #17917 flipped ready at 08:13:42Z, settled green post-flip at 08:14:37Z (three settle polls, first two pending only on Governed Surface Queue Guard), and armed auto-merge at 08:14:38Z ({"enabled":true,"merge_method":"squash"}, HTTP 200). It is now waiting behind five PRs already on the queue ref. ⛔ The arm is not the landing — this seat verifies by content on origin/main, not by the arm's 200.

    For the next seat that hits this: the working response to a 503 here is a patient retry at ~60s, ⛔ not a fallback to PATCH {"draft": false} (silently inert, see above) and ⛔ not GraphQL (banned). Budget ~45 minutes before treating it as anything other than transient.


    Generated by Claude Code

  11. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    ✅ Landed and verified BY CONTENT on origin/main

    domain:spec execution seat, session session_01MkQhmuuJAVDjmeWNixwDDH, reading taken 2026-09-13T09:00Z after git fetch origin main. ⛔ A merged: true field is not a verification — the readings below are git grep / git ls-tree against origin/main 38472cef9210ba8eebb2e5696310bdcf0fb2ee11, each with a lit control.

    #17425 → PR #17917 (merged 08:59:31Z)

    packages/lint/src/validate-retired-permission-residue.ts        present (git ls-tree)
    packages/lint/src/validate-retired-permission-residue.test.ts   present
    authoring-rules.ts:140    import { validateRetiredPermissionResidue } from './validate-retired-permission-residue.js'
    authoring-rules.ts:1262   name: 'validateRetiredPermissionResidue'
    authoring-rules.ts:1266   source: 'packages/lint/src/validate-retired-permission-residue.ts'
    authoring-rules.ts:1276   validateRetiredPermissionResidue(stack).map(…)
    index.ts:844              export { validateRetiredPermissionResidue } …
    index.ts:845              export type { RetiredPermissionResidueFinding } …
    

    ⇒ the rule is not merely present, it is wired: imported, registered in the authoring-rules table with its source path, invoked, and exported. A file that landed without its registration would have passed a file-existence check and done nothing.

    #17054 → PR #17877 (merged 08:59:54Z)

    view.zod.ts:1395   allDayField: z.string().optional().describe('Field carrying the all-day flag …')
    authorable-surface/ui.json   1 matching row
    CONTROL: colorField in view.zod.ts → 4 hits (instrument lit)
    

    ⇒ the fifth key the object-calendar prescription already named is now declared on CalendarConfigSchema, optional, with the .describe() the review checked against objectui's renderer.

    ⚠️ Consequence that is now live, not hypothetical

    The cross-repo notice on objectstack-ai/objectui#8831 (comment 5652138403) is now load-bearing: four assertions on objectui origin/main go red the moment objectui installs a @objectstack/spec carrying this key —

    • packages/types/src/__tests__/calendar-doc-key-set-8830.test.ts:175, and :178-186 (assertions at :181, :184, :185)
    • packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts:416-421 and :422-426

    plus two stale docblocks (8830:24-28, 8466:413-415) that go quietly wrong rather than red. ⛔ None of this is visible to objectstack CI: those files post-date .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694.

    The changeset is unconsumed, so the trigger is the next release plus objectui's bump — there is still a window.

    Housekeeping

    pm:dispatched removed from both cards in the same pass (a closed card keeping a pm:* state label is a half-state). domain:spec and priority:* left in place — ⛔ routing and grading are triage's, and neither is a state.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions