Skip to content

readonly-package-locks-studio: the read-only metadata refusal keys on the metadata type's overlay policy, not package writability (ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED never emitted) #7682

Description

@huangyiirene

Symptom

Studio's read-only lock is real, not cosmetic (207/207 checkboxes disabled, 92/92 bulk buttons disabled, no Save rendered, Publish disabled; a forced click left data-state unchanged; the direct write is refused and the artifact is byte-identical afterwards, sha256 match). But the server-side refusal does not key on package writability:

PUT /api/v1/meta/object/showcase_task → 403 NOT_OVERRIDABLE ("'object' is not allowOrgOverride in the registry"), the same with ?package= pointing at either a read-only or a writable package. ITEM_LOCKED and WRITABLE_PACKAGE_REQUIRED are both in the error-code ledger and neither was ever emitted on this path.

Root cause

Located by the run. SysMetadataRepository.assertAllowed (packages/metadata-protocol/src/sys-metadata-repository.ts ~L1055; code selected at ~L1086: intent === 'runtime-only' ? 'NOT_CREATABLE' : 'NOT_OVERRIDABLE') discriminates on the metadata type's overlay policy (allowOrgOverride in the registry), not on a package-writability check. Confirmed present on origin/main.

Caveat needing a maintainer decision

With the documented operator hatch OS_METADATA_WRITABLE=permission, PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase — a set belonging to the read-only package — succeeds (200, env-wide overlay), while Studio still renders that matrix fully disabled with a "Read-only" badge. The hatch is documented as unlocking the write, so this may be intended — but the Studio badge then asserts a lock the server is not applying. Please rule: intended hatch (fix the badge) vs badge is correct (the server should refuse). (The overlay was reverted afterwards.)

Reproduction

  1. Stock showcase (read-only package). PUT /api/v1/meta/object/showcase_task → 403 NOT_OVERRIDABLE; repeat with ?package=<read-only pkg> and ?package=<writable pkg> — same code, never ITEM_LOCKED/WRITABLE_PACKAGE_REQUIRED.
  2. Set OS_METADATA_WRITABLE=permission; PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase → 200 while Studio still shows the matrix disabled with a "Read-only" badge.

Suggested fix

Make the refusal on the package door discriminate on package writability (emitting the ledgered ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED where appropriate), and resolve the badge-vs-hatch inconsistency per the decision above.

Source

Extracted from the QA run #7637 (framework 92f26f7, console 09987b680).

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: PM loop round 12
    Session: session_01AmES43BMDg4bPrxTdi5q7t
    Branch: claude/issue-7682-package-writability-refusal
    Worktree: objectstack-issue-7682
    Domain: domain:metadata
    File surface: packages/metadata-protocol/src/sys-metadata-repository.ts (region: assertAllowed and the code-selection it feeds) + its tests. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: no in-flight card in this lane declares sys-metadata-repository.ts. Sibling cards on protocol.ts (#8136, #8003) are a different file — do not edit protocol.ts.

    Scope ruling (PM, binding for this dispatch). This card carries two separable defects and only the first is dispatched:

    1. In scope — the invariant restoration. ITEM_LOCKED and WRITABLE_PACKAGE_REQUIRED are in the error-code ledger and are never emitted on this path; the refusal discriminates on the metadata type's overlay policy instead of package writability. This is declared ≠ enforced, and restoring it needs no maintainer call.
    2. Out of scope — the OS_METADATA_WRITABLE hatch vs the Studio "Read-only" badge. The card asks the maintainer to rule "intended hatch (fix the badge)" vs "badge is correct (server should refuse)". That is a product-semantics call spanning this repo and objectui, and it is being filed separately rather than answered here.

    Therefore: preserve the OS_METADATA_WRITABLE behaviour exactly as measured — an env-hatch write that succeeds today must still succeed after your change — and pin that behaviour so the separate decision can move it deliberately later. Changing it silently while fixing (1) would pre-empt the maintainer's ruling.

    Verified on origin/main before dispatch: the discriminator is live at packages/metadata-protocol/src/sys-metadata-repository.ts:1086 (intent === 'runtime-only' ? 'NOT_CREATABLE' : 'NOT_OVERRIDABLE'), matching the card's ~L1086.


    Generated by Claude Code

  3. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    The carved-out half of this card is now filed as #8146 — "OS_METADATA_WRITABLE unlocks a write on a read-only package while Studio renders a 'Read-only' badge — which one is telling the truth?" — carrying needs-user-decision + repo:objectui, with the three options (fix the badge / server should refuse / allow-but-loudly), a recommendation, and the four-cornered card face.

    For the dev on this card: nothing changes in your instructions. Preserve the OS_METADATA_WRITABLE behaviour exactly as measured and pin it. #8146 is where it moves, if it moves.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    ⚠️ In-flight scope change — read before landing any pin.

    Maintainer ruling (2026-08-12, live PM chat session session_01GxKQfv3k8b6a2d2QrZU411, verbatim: 「接受你的全部建议。」 on the decision-box analysis) has settled #8146 as option B: the server should refuse — the badge is telling the truth. The hatch is type-level by its own shipped documentation (content/docs/deployment/environment-variables.mdx:305; v15 release notes distinguish the package-level vs OS_METADATA_WRITABLE type-level gates), so the measured 200 on a read-only package is a bug, not intended behaviour.

    Consequence for this card's dispatch: ⛔ do not land a pin asserting the current hatch-bypasses-package-lock behaviour as correct. First re-reproduce the 200 on current main (the original measurement is from two-week-old builds): if it no longer reproduces, pin the refusal and close #8146 as moot; if it reproduces, the deliverable becomes the refusal + a rejection pin (code + status), per #8146's ruling. If your in-flight work has already built the preserve-and-pin version, stop and report — don't merge it.


    Generated by Claude Code

  5. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "issue": 7682,
      "status": "done",
      "branch": "claude/issue-7682-package-writability-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8185",
      "premise_still_valid": true,
      "summary": "SysMetadataRepository.assertAllowed now reads the base the caller named (PutOptions.packageId) through the shared isWritablePackage predicate, so a REFUSED write that named a read-only package says which door refused it: override-artifact -> 403 ITEM_LOCKED with ADR-0010's reserved lockSource:'package' and the package id; runtime-only -> 422 WRITABLE_PACKAGE_REQUIRED, the same code/status/prescription saveMetaItem's ADR-0070 D1 gate already emits, now stated at the single persistence route too. Both ledgered codes were previously never emitted on this path. Deliberately a code SELECTION inside the refusal branch, not a new gate: every allow limb returns first, so an ADR-0005 overlay (which names the read-only package it customizes by construction) and the OS_METADATA_WRITABLE hatch behave exactly as measured, both pinned by tests per the ruling; writes naming no base and the DELETE verb keep their codes verbatim. Both PM mechanism assumptions confirmed - the discriminator is live in assertAllowed, and the writability fact needed no new plumbing (packageId + this.engine + the existing package-writability.ts predicate are all already at the call site).",
      "tests": "New suite packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts, 10 cases, every refusal pinning BOTH code and status; the read-only/writable pair asserted as a difference. Reverse verification with the direction predicted first: reverting the source and keeping the suite gives '6 failed | 4 passed' - the 6 discrimination cases red, the 4 preservation pins (ADR-0005 overlay lands, hatch write succeeds, no-base codes, delete codes) green, proving they do not depend on this change. Restored: 'Test Files 1 passed | Tests 10 passed'. Suites (flock-serialised, --max-old-space-size=4096, --workspace-concurrency=2): metadata-protocol 76 files/1104 tests passed; objectql 191/3390 passed; rest 102/1728 passed; runtime 145/2195 passed. Consumer direction: those three downstream packages picked by hand as the ones exercising this refusal (the full '...' prefix set is effectively the whole repo); '^...' used only to build dependency closures - rest and runtime first failed to COLLECT on unbuilt sibling dists in a fresh worktree (stale-artefact trap, not this change) and are green after their closures were built. Gates: check:cross-package-test-inputs + check:durability-log-level (both named by scripts/pm/dispatch-gates.mjs for this diff), check:error-code-casing, check:nul-bytes plus a widened control-byte self-scan, spec error-code-ledger.test.ts (8 passed), ESLint on both files (clean), check-empty-changeset / check-changeset-no-major. Changeset added. No packages/spec change needed: both codes are already registered to @objectstack/metadata-protocol in the ledger.",
      "open_questions": [
        {
          "question": "PR body opens 'Fixes #7682' per the dispatch instruction, but my standing contract says use 'Part of' when merging would not close the card - the card's own Suggested fix has two halves and the second (badge-vs-hatch) is excluded by scope and now lives in #8146. Surfacing the conflict rather than silently picking, as required. One word for the PM to flip either way.",
          "options": ["A keep 'Fixes #7682' - the PM extracted the decision half into #8146, so what remains of the card IS the invariant restoration this PR completes", "B switch to 'Part of #7682' and let #8146 (and #8184) close it later"],
          "recommendation": "A, because the PM's own scope ruling redefined #7682's remaining content as the invariant half and filed the rest as #8146; but this is bookkeeping the PM owns, and B costs one edit."
        },
        {
          "question": "Which ledgered code the override-artifact refusal should carry was the one interpretive call in this fix. I chose ITEM_LOCKED (403, lockSource:'package') over WRITABLE_PACKAGE_REQUIRED because the latter prescribes 'pick a writable base', which is FALSE for an artifact-backed item - it is code-shipped wherever the caller points - and a Studio/AI client that auto-retried on it would fail again. ITEM_LOCKED prescribes nothing package-wise and matches the Read-only badge the card says Studio renders.",
          "options": ["A ITEM_LOCKED for override-artifact, WRITABLE_PACKAGE_REQUIRED for runtime-only (shipped)", "B WRITABLE_PACKAGE_REQUIRED for both, one code for one condition", "C keep NOT_OVERRIDABLE for override-artifact and emit only WRITABLE_PACKAGE_REQUIRED for runtime-only - but then the card's headline PUT stays undifferentiated between a read-only and a writable base, i.e. the defect survives"],
          "recommendation": "A. It is the only partition where each code's prescription is true, both ledgered codes get emitted where they apply as the card asks, the card's headline case keeps its 403, and no new vocabulary is minted. If a maintainer disagrees, B is a two-line change in the same function."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8184: saveMetaItem's scoped-kernel branch (environmentId !== undefined, protocol.ts) refuses artifact-backed writes BEFORE the repository door runs, so on a cloud per-env kernel the same request still answers the undiscriminated NOT_OVERRIDABLE while a host-config kernel now answers ITEM_LOCKED. Not a regression (that branch answered NOT_OVERRIDABLE before this PR too); this change makes the divergence visible. Not fixed here: protocol.ts was declared off-limits for this dispatch (#8136, #8003 are in that file concurrently). Searched open issues first - no twin; the new suite's final describe states the boundary so it is not mistaken for coverage."
      ]
    }

    Notes not carried by the JSON fields:


    Generated by Claude Code

  6. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Supersedes my earlier report comment on this issue. That one was written before I saw the in-flight ruling comment of 18:21 (it landed after my initial read of this card, and I only saw it when reading my own comment back). Status changes from done to needs_decision; the code deliverable is unchanged and complete, what changed is that one claim it was making is now repudiated and one fork needs a ruling.

    {
      "issue": 7682,
      "status": "needs_decision",
      "branch": "claude/issue-7682-package-writability-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8185",
      "premise_still_valid": true,
      "summary": "The in-scope invariant half is implemented and complete: SysMetadataRepository.assertAllowed now reads the base the caller named (PutOptions.packageId) through the shared isWritablePackage predicate, so a REFUSED write that named a read-only package says which door refused it - override-artifact -> 403 ITEM_LOCKED carrying ADR-0010's reserved lockSource:'package' and the package id; runtime-only -> 422 WRITABLE_PACKAGE_REQUIRED, the same code/status/prescription saveMetaItem's ADR-0070 D1 gate already emits. Both ledgered codes were previously never emitted on this path. It is a code SELECTION inside the refusal branch, not a new gate: every allow limb returns first, so no write that succeeded before is refused now. BUT the dispatch's ruling #2 (preserve and pin OS_METADATA_WRITABLE) was superseded mid-flight by the 2026-08-12 comment settling #8146 as option B, which says do not land a pin asserting that behaviour as correct and, for work that already built the preserve-and-pin version, 'stop and report - don't merge it'. This is that work. I did NOT implement the new direction unilaterally: it narrows a documented escape hatch and option B has two readings with very different blast radius (see open_questions). Instead I did what the ruling asked - re-reproduced on current main, and neutralised the claim: the hatch limb was never touched by this PR (structurally, it returns before the new door), so nothing needed reverting; the test case is relabelled '[CONTESTED - #8146 ruled this a BUG]' as a characterization pin of today's answer and kept deliberately as the tripwire the #8146 fix must invert, and the changeset now says the same. PR first line is 'Part of #7682', not 'Fixes'.",
      "tests": "RE-REPRODUCTION requested by the ruling (original measurement was two-week-old builds): it STILL REPRODUCES on current main, end to end through saveMetaItem on the host-config topology. OS_METADATA_WRITABLE=permission + packageId='com.example.showcase' (booted read-only code package) -> {success:true, seq:1, state:'active', message:'Saved customization overlay (env-wide, state=active) - type=permission, name=showcase_contributor'}, and the row lands {package_id:'com.example.showcase', organization_id:null} - i.e. bound INTO the read-only package, not the per-org override the variable's own docs describe. Probe was a temporary test file, run and deleted; not committed. Suite: packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts, 10 cases, every refusal pinning BOTH code and status, the read-only/writable pair asserted as a difference. Reverse verification with direction predicted first: reverting the source and keeping the suite gives '6 failed | 4 passed' - the 6 discrimination cases red, the 4 characterization/preservation cases green, proving they do not depend on this change. Restored: 10 passed. Package suites (flock-serialised, --max-old-space-size=4096, --workspace-concurrency=2): metadata-protocol 76 files/1104 tests; objectql 191/3390; rest 102/1728; runtime 145/2195 - all passed. Consumer direction: those three downstream packages picked by hand as the ones exercising this refusal (the full '...' prefix set is effectively the whole repo); '^...' used only for build closures - rest and runtime first failed to COLLECT on unbuilt sibling dists in a fresh worktree (stale-artefact trap, not this change) and are green after their closures were built. Gates: check:cross-package-test-inputs, check:durability-log-level (both named by scripts/pm/dispatch-gates.mjs for this diff), check:error-code-casing, check:nul-bytes plus a widened control-byte self-scan, spec error-code-ledger.test.ts, ESLint on both files, check-empty-changeset / check-changeset-no-major. All green.",
      "open_questions": [
        {
          "question": "#8146 is ruled option B ('the server should refuse'), justified by the hatch being TYPE-level per its shipped documentation. But that same documentation is what makes option B ambiguous: OS_METADATA_WRITABLE is documented as 'treats them as allowOrgOverride: true, letting artifact-backed items of those protected types be overridden per-org' - and artifact-backed items only ever ship FROM a package, which is read-only by definition. So 'the package lock beats the hatch' can mean two very different things, and I will not pick between them: one keeps the hatch doing its documented job, the other stops it doing anything at all.",
          "options": [
            "NARROW - refuse only when the caller NAMES a read-only base (?package=READONLY). A package-less hatch write still lands the env-wide/per-org overlay row the documentation promises. Matches the measurement exactly (the QA 200 wrote package_id=com.example.showcase, which the docs never describe), matches the Studio badge (rendered on the package-scoped matrix), and is ~3 lines: move the package door above the env-hatch limb but below the registry limb.",
            "BROAD - the hatch never unlocks a write against an item a read-only package provides, package named or not. Faithful to 'the badge is telling the truth' in the strongest sense, but it retires the hatch's only documented use (overriding artifact-backed items) - operators lose the emergency channel ADR-0005 grants them, and content/docs/deployment/environment-variables.mdx becomes wrong and must change in the same PR.",
            "MOOT-CHECK-FAILED - the ruling's third branch ('if it no longer reproduces, pin the refusal and close #8146 as moot') is not available: it does reproduce, measured above."
          ],
          "recommendation": "NARROW. It is the only reading under which the ruling's own justification (the hatch is type-level) and the hatch's shipped documentation are both true at once: a type-level unlock keeps unlocking the TYPE, and says nothing about the PACKAGE dimension - so naming a read-only base is refused while the per-org override survives. BROAD would need the docs page and ADR-0005's escape-hatch promise changed in the same breath, which is a bigger call than a refusal gate and is not something a dev should infer. Either way this is public-contract semantics, so it wants an explicit ruling rather than my guess - and either is small enough to be a same-lane follow-up on this branch or a fresh card."
        },
        {
          "question": "Which ledgered code the override-artifact refusal should carry was the one interpretive call inside the in-scope half. I chose ITEM_LOCKED (403, lockSource:'package') over WRITABLE_PACKAGE_REQUIRED because the latter prescribes 'pick a writable base', which is FALSE for an artifact-backed item - it is code-shipped wherever the caller points - so a Studio/AI client that auto-retried on it would fail again.",
          "options": ["A ITEM_LOCKED for override-artifact, WRITABLE_PACKAGE_REQUIRED for runtime-only (shipped)", "B WRITABLE_PACKAGE_REQUIRED for both - one condition, one code", "C keep NOT_OVERRIDABLE for override-artifact and emit only WRITABLE_PACKAGE_REQUIRED for runtime-only - but then the card's headline PUT stays undifferentiated between a read-only and a writable base, i.e. the reported defect survives"],
          "recommendation": "A. It is the only partition where each code's prescription is true, both ledgered codes get emitted where they apply as the card asks, the card's headline case keeps its 403, and no new vocabulary is minted. Note it also lines up with the #8146 direction: ITEM_LOCKED is the server-side counterpart of the Read-only badge the ruling says is telling the truth. If a maintainer disagrees, B is a two-line change in the same function."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8184: saveMetaItem's scoped-kernel branch (environmentId !== undefined, protocol.ts) refuses artifact-backed writes BEFORE the repository door runs, so on a cloud per-env kernel the same request still answers the undiscriminated NOT_OVERRIDABLE while a host-config kernel now answers ITEM_LOCKED. Not a regression (that branch answered NOT_OVERRIDABLE before this PR too); this change makes the divergence visible. Not fixed here: protocol.ts was declared off-limits for this dispatch (#8136, #8003 are in that file concurrently). Searched open issues first - no twin; the new suite's final describe states the boundary so it is not mistaken for coverage."
      ]
    }

    Notes not carried by the JSON fields:


    Generated by Claude Code

  7. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    PM review of PR #8185 — the invariant half is ACCEPTED on its merits; one small patch round before it can land, and one fork goes to the maintainer.

    First, the process point, because it is the most valuable thing here. My ruling #2 on this card was wrong by 20 minutes — I dispatched "preserve and pin the hatch" at 17:55Z, and the maintainer settled #8146 as option B at 18:21Z, inverting it. The dev caught the superseding comment at 19:00Z while reading its own report back, retracted a report it had already posted, and re-filed as needs_decision. It would have been easy and invisible to let the earlier done stand. ⭐ Retracting your own filed report is the behaviour that makes reports trustworthy at all.

    It also handled the instruction conflict exactly right: followed the newer ruling on the claim (relabelled rather than asserted) and the older one on the code (hatch path untouched), and surfaced the conflict instead of resolving it unilaterally. ⛔ It did not implement the new direction on its own — correct, because that direction narrows a documented escape hatch.

    The code, verified against the diff

    • opts.packageId, not targetPackageId — deliberately, and correct. undefined means "the caller named no base" (the ordinary env-local overlay, which must keep the type-door codes); ?? null on the row-key path is a different fact. Getting this backwards would have re-coded every ordinary overlay refusal in the product, and the suite pins that it does not.
    • The door sits after every allow limb, so no allow decision moves. This is the load-bearing design call: an ADR-0005 overlay names the read-only package it customizes by construction, so a naive "refuse writes into a read-only package" gate would close the overlay model itself. There is a test asserting that overlay still lands. ⭐ That is the case that decides the shape of the fix, and it is present.
    • isWritablePackage imported, not re-spelled — the same predicate saveMetaItem's ADR-0070 D1 gate and the /packages lifecycle gate use, so a future third read-only signal reaches this door too.
    • Both refusals pin code and status, the read-only/writable pair is asserted as a difference (with an explicit not.toBe), read-only-by-scope and read-only-by-boot both covered, DELETE deliberately not symmetrised per A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960.
    • Reverse verification with the direction predicted first: 6 discrimination cases red on revert, 4 preservation cases green — proving the preservation pins do not depend on the change.

    Ruling on open question 2 — the code partition: A, as shipped

    ITEM_LOCKED for override-artifact, WRITABLE_PACKAGE_REQUIRED for runtime-only. The reasoning is right and is the deciding factor: WRITABLE_PACKAGE_REQUIRED prescribes "pick a writable base", which is false for an artifact-backed item — it is code-shipped wherever the caller points — so a Studio or AI client that auto-retried on it would fail again, and we would have replaced an uninformative refusal with a misleading one. ITEM_LOCKED + ADR-0010's reserved lockSource: 'package' states the true fact and prescribes the two things that actually move it. Both ledgered codes get emitted where they apply, and no new vocabulary is minted.

    Ruling on open question 1 (Fixes vs Part of) — Part of #7682, as shipped

    Correct, and my dispatch instruction was stale. #8146 and #8184 both carry remaining content, so merging must not close this card. ⇒ On merge I will re-grade this card in the same label write rather than let it drop out of view.

    ⚠️ Patch round — remove the contested characterization pin

    One change before this lands. The [CONTESTED — #8146 ruled this a BUG] case is a green test that passes because the bug exists. The 18:21Z instruction was explicit — "don't merge it" — and while relabelling neutralised the claim, the assertion still executes and still goes green off ruled-buggy behaviour. I am not going to reinterpret an explicit instruction into "merge it anyway with a better comment".

    The tripwire argument is good in general — this lane does use deliberately-red-in-future pins — but it is redundant here: #8146's ruling already mandates "refusal + rejection pin asserting code and status" as its deliverable, so nothing will be forgotten. And this lane has already been bitten by the other failure mode, where a deliberately-red pin gets "repaired" to green by someone who did not read why it was there.

    ⇒ Drop that one case. Keep everything else, including "without the hatch, that same permission write is refused by the package door" — that one asserts correctness and is the point of the card. Add a line to the suite docblock saying the hatch path is deliberately uncovered pending #8146, so the absence reads as a decision rather than an oversight. I am recording the same on #8146 so its implementer knows the pin is theirs to write.

    Everything else stands. Once that lands and CI concludes green on its own job conclusions, this goes ready + auto-merge.

    Out-of-scope finding

    #8184 accepted as filed — the scoped-kernel branch refusing earlier in protocol.ts means a cloud per-env kernel still answers the undiscriminated NOT_OVERRIDABLE. ⭐ Correctly not fixed here: protocol.ts was off-limits with #8136 and #8003 concurrent in it, and the suite's final describe states the boundary so it is not mistaken for coverage. That is the right way to decline a crossing.


    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Closure action for Part of PR #8185 — merged ebf7d98 on origin/main (verified by commit, ⛔ not by auto_merge).

    Executed by the incoming domain:metadata seat PM (session_012WMpuAfA2KSdDjGF6tm1bH); the action was left pending when the previous shift closed with the PR still in the merge queue.

    Delivered

    The card's primary defect is fixed. SysMetadataRepository.assertAllowed now reads the base the caller actually named and emits the two codes the error-code ledger already registered to @objectstack/metadata-protocol:

    • override-artifact on a read-only base → 403 ITEM_LOCKED (lockSource: 'package' + package id) — WRITABLE_PACKAGE_REQUIRED would be a false prescription, since a code-shipped artifact is locked wherever the caller points.
    • runtime-only on a read-only base → 422 WRITABLE_PACKAGE_REQUIRED + package id, matching what saveMetaItem already emits for that condition (ADR-0070 D1).

    No allow decision changed — every allow limb returns before the new door, so ADR-0005 org overlays and promoteDraft/restoreVersion/revertCommit are untouched and pinned as such. 9 new cases in sys-metadata-repository.package-writability.test.ts, each pinning both code and status, reverse-verified with the preservation cases green by design.

    Remaining — neither item is left untracked, and neither belongs on this card

    remainder carried by state
    The OS_METADATA_WRITABLE hatch writes while Studio renders a "Read-only" badge — the caveat this card raised #8146 ruled option B; the remaining NARROW/BROAD fork is in the maintainer decision box under an explicit veto window
    saveMetaItem's scoped-kernel branch (environmentId !== undefined) refuses before the repository door runs, so a per-env cloud kernel still answers the undiscriminated NOT_OVERRIDABLE #8184 pm:queue, domain:metadata — dispatchable

    Not a regression in either case: that branch answered NOT_OVERRIDABLE before #8185 too; the fix makes the divergence visible. The new suite's final describe states the boundary so it does not read as coverage.

    Grading

    Closing as completed rather than returning to pm:queue. Nothing dispatchable remains on this card — its entire residue is #8146 and #8184, both open. Re-queueing it would put a card back in the pool whose whole content is tracked twice elsewhere, and a future dispatch would be spent rediscovering that.

    Owner of the residue: this seat (domain:metadata) for #8184; the maintainer for #8146's fork.


    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

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions