Repository navigation
fix(metadata-protocol): a sealed-item refusal of a create names a free name, not the edit remedy (#22591) - #22623
Conversation
…e name, not the edit remedy The locked-base sentence now follows what the caller did: a save that takes a name a managed package or a built-in holds (a create, or a rename into it) is told to choose a name no package or built-in holds; an edit keeps the remedy it had, byte for byte. The verdict, code and status do not move. - packaged-base-regime.ts: one create sentence for every type (heldNameSentence), chosen by the new 'create' operation. - protocol.ts: packagedBaseRefusal accepts operation 'create'; the /meta save door reads its first-write pin (parentVersion null, If-None-Match: *) as a create, for the flow rule and the package door alike. - plugin-security: Setup's position write-through asks as a create. Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG Co-authored-by: Claude <noreply@anthropic.com>
…and a create on every reachable type The enumeration pin iterates every type the refusal reaches (the registry's types with no overlay channel, and a plugin-registered type) for both acts and asserts the remedy kind: an edit names the edit remedy, a create names a name no package or built-in holds. Controls: the verdict, code and status do not move. The /meta door's first-write pin and Setup's position create and rename are pinned at their doors. Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fdf8fbff1516569e6c4b10fe5e8206930043230c && git checkout fdf8fbff1516569e6c4b10fe5e8206930043230c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e22315238f9426708e10cfff1d6a96e56cca33a1 caeb7b58f1a0f6a207d0036b73788c19be28595f && git checkout -B drift-repro e22315238f9426708e10cfff1d6a96e56cca33a1 && git merge --no-ff caeb7b58f1a0f6a207d0036b73788c19be28595f
node scripts/docs-audit/affected-docs.mjs --json e22315238f9426708e10cfff1d6a96e56cca33a1
|
…or for both packages packagedBaseRefusal on the exported ObjectStackProtocolImplementation now accepts operation 'create', and tenantAuthoredWriteRefusal takes an optional operation. That widens a public input surface, so the declaration reads Clause-②: yes and both packages take minor. No verdict, code or status moves. Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22591 (body; triage 6093105279; claim 6093560675 as amended; os-dev-reports 6094519413 and 6094616222), PR #22623 (body, its six-file list, the net diff against ① Derived judgmentsPublic surface of
Accept set:
Tests against the triage pin (6093105279): Docs: no page under ② Semver levelClause-②: yes
③ Boundary flagsEvery deviation and open question in 6094519413 and 6094616222, and the amended claim's clauses:
Check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22591
Clause-②: yes
The sealed-item refusal (
403 NOT_OVERRIDABLE) now names the remedy that fits what the caller did. This closes out the family that #20819 (flow) and #20910 (action, permission) worked through one type at a time.GET /api/v1/meta/TYPElists the names in use.Which writes are refused does not change, and neither do the code or the status.
Measured at the real doors (showcase,
single)The probe was a local file, never committed and deleted after the run. It booted the real showcase composition through
bootShowcase, signed in as the seeded admin, and used the HTTP doors. Before wasorigin/maind748ae80af, with the dogfood dependency closure built from it. After was this branch at6499e49c0f, withmetadata-protocolandplugin-securityrebuilt from that head.POST /data/sys_positionwithmanager(packagecom.example.showcase)NOT_OVERRIDABLE, "… Edit the source artifact and redeploy."NOT_OVERRIDABLE, "… so its name is taken. To author your own position, choose a name no package or built-in holds (GET /api/v1/meta/position lists the names in use)."guest(built-in,com.objectstack.plugin-security)PATCHrenaming an admin position intoexecPUT /meta/position/managerwithIf-None-Match: *(the first-write precondition)PUT /meta/flow/showcase_task_completedwithIf-None-Match: *PUT /meta/position/managerwith no precondition, and with?mode=draftPUT /meta/flow/showcase_task_completedwith no preconditionPOST /automationontoshowcase_task_completedp22591_lead(control)GET /api/v1/meta/positionlistedmanager,exec,everyone,guestand the other held names, so the remedy's pointer answers.One create-aware sentence, not a
positionrowThis was the claimant's choice to make by measurement. The create remedy does not depend on the type: on every type, a create under a held name has one remedy, which is another name. So
heldNameSentenceinpackaged-base-regime.tsis one sentence for every type. Only its opener and its citation come from the type's row, as in the edit sentences. An origin-gateddatasourceis "code-defined", not shipped by a managed package. A Regime C type cites ADR-0126, whose §2 makes a new machine name mandatory for an item authored beside a packaged one. A per-typepositionrow would have fixed one of the 24 reachable types and left the rest for a fourth card. A type added to the registry later gets the create remedy without a row.Where the act comes from
The act is the caller's fact and is never inferred. A door that cannot tell keeps the edit remedy.
managedItemSealedSentence(type, name, operation)takes'save' | 'create' | 'delete'. A'create'readsheldNameSentence.ObjectStackProtocolImplementation.packagedBaseRefusaltakesoperation: 'create', with the same predicate, code and status as'save'.refusePackagedBaseOverridepasses the act through to the sentence and to nothing else.plugin-security,position-write-through.ts): the S10 verdict is asked only for a create or a rename into a name, so it now asks withoperation: 'create'. This is the producer of the card's measured symptom, in another package. The dispatch allowed fixing it at the producer./metasave door (saveMetaItem): its one fact about the name is the first-write pin. A presentnullparentVersion(If-None-Match: *) asserts that the caller holds no row there, which the spec calls the first-write pin, so it is read as a create. It reaches both locked-base doors:tenantAuthoredWriteRefusal(flows, which gains an optionaloperation) and the package door. Any other save is an edit.SysMetadataRepository.assertAllowed(the third caller the dispatch named) is unchanged. Its saves are server replays of the item's own stored history (draft promotion, restore, revert), which are edits, and it has no create fact to pass.Tests
packages/metadata-protocol/src/protocol.sealed-remedy-kind.test.ts(new):PACKAGED_BASE_REGIME), the source/redeploy remedy, or the type's listing route. The wording is not asserted.NOT_OVERRIDABLE/ 403 on every row. A free name isnullfor both acts. A removal names no new name. Every create sentence for an 88-character name arrives whole under the REST door's 500-character bound, with no hatch, clone or switch./metadoor. Forpositionandflow: a first-write save names the create remedy, and an unpinned orIf-Matchsave keeps the edit remedy, with the same code and status. The door's message equals the verdict's (one emitter).packages/plugins/plugin-security/src/position-write-through.test.ts:everyone) names the create remedy, while the door's edit refusal of the same name keeps the edit remedy. A rename intoguestnames the create remedy.Ablation, run at
6499e49c0fwith the fix committed first. Throughscripts/ablation-replace.mjs, the lineif (operation === 'create') return heldNameSentence(singular, name);was replaced with a comment, which collapses a create back to the edit remedy. On disk the anchor count went 1 → 0 and the marker count 0 → 1. Both suites then ran.metadata-protocolhad 5 failed and 5 passed: every create row flipped to"create": "edit", while the reachability, verdict and removal controls stayed green.plugin-securityhad 3 failed and 41 passed. The restore put the blob back equal toHEAD(a0fda00025d8) withgit diff HEADempty. Neither suite readsdist/: the test imports./protocol.js, and plugin-security's vitest config aliases@objectstack/metadata-protocolto itssrc. So the ablation needed no rebuild.Runs at
3c3e4ef362(this branch after mergingorigin/main18d999031b; the merge brought no change to either touched file):pnpm --filter @objectstack/metadata-protocol exec vitest run:Test Files 224 passed | 3 skipped (227),Tests 28314 passed | 19 skipped (28333).pnpm --filter @objectstack/plugin-security exec vitest run:Test Files 194 passed (194),Tests 4096 passed | 45 skipped (4141).typecheckpasses for both packages, plugin-security'scheck:test-typecheckincluded.tsc --listFilescounts each touched test file once in its package's program.dispatch-gates --commandsderives for this diff all exit 0, reconciled with--ran: 69 run, 0 NOT MEASURED. One note:check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3) because 8 unrelated packages had nodist/. After building them it exited 0.--no-inline-configand--format json: 5 files, 0 errors, 0 warnings. Each file is inside the config (--print-configresolves). The config enables no type-aware linting (noparserOptions.project), so this diff cannot move the verdict on any untouched file. The repository-widepnpm lintis CI's.Acceptance notes
/metaPUT is an upsert, so an unpinned save onto a held name is an edit by the door's contract. Studio's create mode sends no precondition (objectuiResourceEditPagecreate path). Its comment says the door cannot express "expect no row", which has not been true since the door tookIf-None-Match: *. The server half is here. The client half is to sendIf-None-Match: *on create. It is reported for the seat to file in objectui, with this PR as its blocker.POST /automationonto a packaged flow's name is unchanged. That door's own contract makes a create onto an existing name an overwrite, which is an edit, so its clone-and-switch remedy matches what it does.POST /automation/:name/clonerefuses any target name the engine already holds with its own name-taken refusal first (read inautomation.ts, not probed).allowRuntimeCreate: false) are refused at/metaby their own emitter (codeOnlyOverrideError) before the package door. It says "declare it in source". That is right for a create too, because such a type has no runtime create.yes.packagedBaseRefusalacceptsoperation: 'create', andtenantAuthoredWriteRefusaltakes an optional operation. That widens a public input surface, so both packages takeminor. No verdict, code or status moves. The seat answered the question A and amended its claim (6093560675). Patch round 1 (6094616222, headcaeb7b58f1) changed the changeset only.protocol.tsorsys-metadata-repository.tsis touched. The seat's feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 S5 read-path work does not overlap.Generated by Claude Code