Skip to content

Active metadata save stores an object without its package namespace prefix that publish-drafts then refuses (NAMESPACE_PREFIX) #24547

Closed

Description

@objectstack-fleet

Filing gate: ① product defect — reproduced through a public HTTP door, landing file named (the two runtime save doors disagree on one rule).
Path: automation.rollup-summary-filter (checklist item priority P1; found while driving its step 7, outside its clauses — no clause failed)
QA-source: #24545 · automation.rollup-summary-filter · step 7 (incidental, outside clauses)
Type: Bug — violates ADR-0129 D2 ("every object it defines MUST be named namespace_shortName").
Reader: the packages/metadata-protocol owner picked at triage — first step is the direct/active saveMetaItem path in packages/metadata-protocol/src/protocol.ts; if the fix is instead an ADR exemption for the direct-save door, it is a maintainer ruling (precedent class: #23388).
Duplicate check (open AND closed): "metadata PUT save object unprefixed name namespace prefix not enforced direct save active" → 16 hits (package-binding / D9.9 ownership families, none match) · "NAMESPACE_PREFIX publish-drafts refused but saveMetaItem accepts" → 40 hits (none match) · "namespace prefix enforcement studio publish only, active object save bypasses prefix rule ADR-0129" → 29 hits (none exact; #23388 is the same class for a different rule).
reach: PUT /api/v1/meta/object/v1_report?package=com.v1.rollup (package namespace rollup) → 200 state:active, the object is live; a draft edit of the same object → POST /api/v1/packages/com.v1.rollup/publish-drafts → outcome refused, code NAMESPACE_PREFIX. Measured on stock showcase at 5b14b7f.

What happens

For a package with a declared namespace, the direct (active) object save door accepts an object name that lacks the namespace_ prefix and makes it live. The draft-publish door for the same package refuses the very same name with NAMESPACE_PREFIX. So an object can be created that can never again be edited through drafts, and the two runtime doors give opposite verdicts on one name in one package. os validate (exit 1) and os build (exit 2) both refuse the shape — the gap is runtime-only.

Reproduction (stock showcase, framework 5b14b7f)

  1. Boot showcase, sign in admin@objectos.ai / admin123.
  2. POST /api/v1/packages body {"id":"com.v1.rollup","name":"V1 Rollup","version":"0.1.0","type":"app"} → 201, manifest.namespace = rollup.
  3. PUT /api/v1/meta/object/v1_report?package=com.v1.rollup body {"name":"v1_report","label":"V1 Report","sharingModel":"private","fields":{"title":{"type":"text","label":"Title"}}}
    • Expected (ADR-0129 D2): refusal NAMESPACE_PREFIX, "Rename it to rollup_v1_report".
    • Actual: 200 state:"active"; GET /api/v1/meta/object/v1_report serves it and POST /api/v1/data/v1_report {"title":"x"} creates a record.
  4. Same PUT with &mode=draft and a changed label → 200 state:"draft".
  5. POST /api/v1/packages/com.v1.rollup/publish-drafts body {} → 200 with outcome:"refused", failed[0].code:"NAMESPACE_PREFIX": "Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup')."

Contract

  • docs/adr/0129-object-name-is-the-canonical-id.md:45-55 (D2): every object a package defines MUST be namespace_shortName; enforcement named at compile time (validateNamespacePrefix()) and at Studio publish.
  • packages/spec/src/kernel/namespace-prefix.ts:3-14 states the same MUST.
  • packages/metadata-protocol/src/protocol.ts:26329-26340 (publishPackageDrafts) refuses the unprefixed draft; protocol.ts:27873-27874 itself notes "a direct-publish save, which no prefix gate reads".
  • The direct-save door already runs other author-time gates on the same body (it refused a missing sharingModel with 422 INVALID_METADATA), so the prefix rule is the one that is skipped.

Mechanism and fix landing

  • packages/metadata-protocol/src/protocol.ts — the active/direct saveMetaItem path for type object under a package with manifest.namespace does not call the prefix validator that publishPackageDrafts uses (~26366).
  • Fix (one of): apply the same validateObjectNamespacePrefix check on the active save (refuse with NAMESPACE_PREFIX), or record a ruling exempting the direct-save door and amend the ADR-0129 D2 chokepoint list. Either way the two doors should agree.
  • Severity hint: medium — reachable by any metadata author with save rights; leaves a live object that drafts can no longer publish.

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p1 + target:v18 (protocol-first, narrowing) · bug · domain:engine · area:studio · pm:queue · put to the maintainer for pulling forward · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T14:05Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Fresh read on origin/main ba4afcc239. NAMESPACE_PREFIX is a preflight code of the draft-publish path (packages/metadata-protocol/src/protocol.ts:6439, :26375). The card measures the direct/active saveMetaItem path accepting what that preflight refuses. The two runtime save doors disagree on one ADR-0129 D2 rule.

    • Grade. Making the active save door refuse an unprefixed object narrows what a metadata door accepts. That is the protocol-first class, so p1 + target:v18.

    • Put to the maintainer for pulling forward (6261447528):

      • kind: narrowing;
      • consequence: 18.0 ships to fresh installs only, so closing it now leaves no stored unprefixed objects to convert. After the cut, it waits for 19 as a refusal, or ships in 18.x as a warning first;
      • workload: S–M, one preflight on the active path plus pins on both doors. No PR;
      • lean: in 18.0 if claimed today, otherwise 18.1 as a warning.

      Until the maintainer answers, nothing on this card moves on that question.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruled: pull forward into 18.0 — the active save door refuses an unprefixed object · recorded by domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-19T22:41Z


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · 2026-10-19T22:42Z
    Session: session_01AHeUBbe6S3JRawcpbLejXD (the dispatching seat; the dev runs as this session's subagent, identified by the branch)
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24547-active-save-namespace-prefix
    Worktree: objectstack-issue-24547
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966)
    Ruling-ref: 6278955141
    File surface: packages/metadata-protocol/src/protocol.ts, the active (publish-mode) saveMetaItem path for type object under a package whose manifest declares a namespace: it runs the same validateObjectNamespacePrefix check publishPackageDrafts runs, and refuses with the same NAMESPACE_PREFIX code and authored sentence. Pins on both doors in packages/metadata-protocol. One .changeset/24547-*.md (@objectstack/metadata-protocol minor, BREAKING). ⛔ No new error code, no ADR edit, no change to packages/spec's validateObjectNamespacePrefix. Any other door that makes an object live without the publish preflight is reported, not widened into. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate)
    Clause-②: no (narrowing) (the active save door newly refuses an object whose name lacks its package's namespace prefix, which today it makes live; a narrowing owes one CONTRACT_REVIEW_TIER review of record before the queue)
    Responsibility: packages/metadata-protocol's active saveMetaItem path (no prefix gate) | publishPackageDrafts' preflight already refuses the same name with NAMESPACE_PREFIX, the check to reuse | any metadata author with save rights on a namespaced package; measured through PUT /api/v1/meta/object/v1_report?package=com.v1.rollup on the stock showcase at 5b14b7f
    Thread-read: 6278955141
    Serial constraints cleared: PR #24666 (#24535, another lane) edits protocol.ts in the getMetaItem region (hunks from old line 11210 to 11567), not saveMetaItem (about :22834 on origin/main bfc1539fe0) nor the publish preflight (about :26488); whichever lands second merges main. No lane claim touches either region. Same-day churn: several merges touched protocol.ts today.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24547,
     "status": "done",
     "branch": "claude/issue-24547-active-save-namespace-prefix",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24702",
     "session": "session_01AHeUBbe6S3JRawcpbLejXD — the dispatching seat's id (this run is its subagent; no separate harness id)",
     "premise_still_valid": true,
     "summary": "The direct (publish-mode) saveMetaItem now refuses an object whose name lacks the namespace prefix of the package it writes into. It answers 422 NAMESPACE_PREFIX with validateObjectNamespacePrefix's verbatim prescription, through one private judge (objectNamespacePrefixViolation) that publishPackageDrafts' pre-flight now also calls, so the two doors cannot disagree. Draft saves, packages with no namespace, package-less saves and the server-stated `source` rewrites are unchanged. The mechanism assumptions held: the code is already in ERROR_CODE_LEDGER for metadata-protocol, the HTTP envelope is the same flat {error, code} the door gives INVALID_METADATA, and no new code was minted. One extra door was found that still makes an unprefixed object live: per-item publish (publishMetaItem). It is reported and not widened into, per the dispatch.",
     "tests": "HEAD 3b94654c58 (merge of origin/main 486745ec75). (1) Repro first, unfixed code: new suite protocol.active-save-namespace-prefix.test.ts had 3 failed / 3 passed. The refusal cases saw 'saved'; the controls were green. (2) Fixed: 6/6 passed. (3) Full metadata-protocol suite on the final head: 'Test Files 250 passed | 3 skipped (253) / Tests 20787 passed | 19 skipped'. (4) objectql: the whole suite was run pre-merge on the fixed dist: 449 files, '14 failed | 8438 passed'. All 14 were in 3 duplicatePackage D4 fixture files, each failing on the new refusal (\"Object 'ticket' is missing the package namespace prefix…\" x8, \"Object 'dupsrc_ext' …\" x6). After the fixture staging change, those 3 files ran 15/15 passed, before and after the merge. (5) typecheck: metadata-protocol and objectql (incl. check:test-typecheck, ledger unchanged) exit 0 on the final head. (6) Live HTTP, fresh showcase on this branch:\n- PUT /api/v1/meta/object/v1_report?package=com.v1.rollup answered 422 {\"error\":\"Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup').\",\"code\":\"NAMESPACE_PREFIX\"}, then GET answered 404.\n- The prefixed name and the no-package save both answered 200 active.\n- A draft save answered 200 draft.\n- publish-drafts answered 200 outcome refused, failed[0].code NAMESPACE_PREFIX, with the same sentence.\n- The sibling missing-sharingModel refusal answered 422 {error, code: INVALID_METADATA, issues}.\n(7) Ablation, scripts/ablation-replace.mjs on the committed fix:\n- the anchor 'if (prefixViolation) {' became 'if (false && prefixViolation) {'; anchor 1 → 0, blob 72a5cff9503e → 88f5360fcedf;\n- cases (a), resolved-package and doors-agree went red; the 3 controls stayed green;\n- restore proven: blob == HEAD, git diff HEAD empty;\n- direction red as expected; no dist involved, since the suite imports ./protocol.js from source.\n(8) Dogfood, once on the final head, files that write objects under a package: package-first-authoring, flow-shipped-name-published-door and install-local-unmanaged-mode passed. wall-environment-metadata-refusal had 2 failures, unrelated (a view list-view-object-outside-closure refusal brought by origin/main; probe output in findings).",
     "gates": "Derived union (dispatch-gates --commands --repo objectstack-ai/objectstack) on 3b94654c58: 78 commands, each run with its exit captured before any pipe; all 78 exit 0. --ran reconciliation with recorded exit codes: '78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero'.\n- check:dts-closure and check:dual-build-cjs-loads first read a packages/spec/dist mid-rebuild by my own concurrent dogfood-closure build and exited 1 and 2. Re-run after the build settled: exit 0, '177/177 declared declaration file(s) present' and '723 emitted CommonJS file(s) parse'.\n- check:engine-double-contract was red on the new pinned double until its --write remedy (3 rows added, 0 lost), then exit 0.\n- check:objectql-double-limit was red on the new find double (limit-blind) until the double applied the bound, then exit 0.\n- check:adr-0087-registration accepted not-required (no-migration-prescription) with exit 0.\n- check:engine-split-ratio --days 90 exit 0 on a shallow clone: its floor predates the window, so it answered.\nCI, PR #24702 at 3b94654c58, snapshot at open:\n- in_progress: Build Core, Test Core (1-6/6), Lint (all lanes), Type Check (all lanes), Governed Surface Queue Guard, Check Changeset;\n- success: Check PR Size, Check Documentation Links, the three claim/single-writer guards, filter;\n- Dogfood Regression Gate (matrix) skipped on PR, as expected.\nNot run locally: test:repo (diff touches no repo-project test); the consumer packages' own suites beyond metadata-protocol and objectql (CI).",
     "mcp_calls": "0 — no MCP GitHub tool was called",
     "api_writes": "2 relay strokes (fleet-write, as objectstack-fleet[bot]), 3 REST requests:\n- stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24702/assignees. Read-back: body byte-identical, assignee os-warren.\n- stroke 2: POST /repos/objectstack-ai/objectstack/issues/24547/comments (this report).\nTransport: one POST /repos/objectstack-ai/objectstack/dispatches per stroke. git push is not counted.",
     "open_questions": [
      {
       "question": "The per-item publish door (publishMetaItem, protocol.ts:25034 via promoteDraftForPublish :25351; POST /api/v1/meta/:type/:name/publish?package=) still makes an unprefixed object live under a namespaced package. Measured over HTTP on this branch: 200, then GET 200, then a data insert 201. So ADR-0129 D2 does not yet hold at every runtime door. Should it be closed before the 18.0 cut?",
       "options": [
        "A: amend this card's claim surface and close it in this PR or a sibling one before Version Packages: one call of objectNamespacePrefixViolation in the object branch of promoteDraftForPublish (publish only), one pin, and one changeset line moved from 'Not judged' to TO. Size XS; same ruling spirit ('D2 holds at the runtime save doors').",
        "B: file a separate card. After the 18.0 cut it waits for 19 as a refusal, or ships in 18.x as a warning, per the triage standing rule."
       ],
       "recommendation": "A. It is the same verdict, the same code and the same one-line judge. Leaving it open keeps the exact AI-authoring trap the ruling closed, through Studio's save-then-publish loop, and closing it later costs a deprecation window that 18.0's fresh-install-only stance avoids today."
      }
     ],
     "out_of_scope_findings": [
      "class: b · reach: POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup after a draft PUT → 200, GET /api/v1/meta/object/v1_report 200, POST /api/v1/data/v1_report 201 (fresh showcase, branch head 3b94654c58) · evidence: ADR-0129 D2 'every object it defines MUST be named namespace_shortName'; Seam: spec:validateObjectNamespacePrefix (packages/spec/src/kernel/namespace-prefix.ts) → runtime:ObjectStackProtocolImplementation.publishMetaItem / promoteDraftForPublish (packages/metadata-protocol/src/protocol.ts:25034, :25351); a protocol-level probe agrees (success: true, row active under com.v1.rollup) · fits as a sub-issue of #24547's family (see open_questions) · dedupe words: publishMetaItem namespace prefix; per-item publish unprefixed object; meta publish NAMESPACE_PREFIX bypass; promoteDraftForPublish prefix",
      "class: a · reach: packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts 'thirteen write doors' (isolated and group) fails on head 3b94654c58, whose non-diff content is origin/main 486745ec75: POST /api/v1/packages/com.em.iso.op/publish-drafts answers outcome refused, failed[0] view crm_lead.em_iso_pv INVALID_METADATA [list-view-object-outside-closure], so commits.length is 0 at :242 · evidence: probe via ablation-replace logging the publish response; this diff touches no view path; the Dogfood Regression Gate does not run on PRs, so it lands on main's push battery (main-red convention) · dedupe words: wall-environment-metadata-refusal dogfood list-view-object-outside-closure; thirteen write doors commits length 0; crm_lead view package closure dogfood",
      "carrier: none · noted, not filed: ADR-0129 D2 and the namespace-prefix.ts header name MetadataManager.publishPackage() (packages/metadata/src/metadata-manager.ts:1879) as the runtime chokepoint, but it runs no object-name prefix check; the real runtime doors are publishPackageDrafts and now saveMetaItem (stale citation in governed or spec text, outside this surface)",
      "carrier: none · noted, not filed: importUnmanagedPackage meets this refusal mid-copy (rolled back) rather than at plan time for a namespace-less source whose objects lack its id-derived prefix; inference, no fixture has that shape"
     ],
     "deviations": [
      "File surface beyond the claim, declared in the PR body: packages/objectql/src/protocol-duplicate-package-{unprefixed-object,names-in-step,permission-field-keys}.test.ts. Their fixtures staged an unprefixed object under a namespaced package through the direct save, which now refuses it, so Test Core would go red. The fix stages those rows with the namespace hidden for that one save; no assertion changed. Also scripts/engine-double-contract.pinned.json, the gate's own --write remedy for the new pinned double.",
      "One single-file vitest run, the new suite after the find-double fix (6/6), ran outside os-verify-lock while my own showcase build held it. Every other build and test went through the lock.",
      "check:dts-closure and check:dual-build-cjs-loads were first run concurrently with my own build and read a half-built spec dist (exit 1 and 2). They were re-run after it settled (exit 0), and the --ran record carries the re-run codes.",
      "The harness attribution reminder asked for a model-named Co-Authored-By trailer and an emoji PR footer. AGENTS.md's model-free trailer pair and session-URL footer were used instead.",
      "Dogfood was run on the four files that write objects or packages under a package, not the full dogfood battery, which is main push only."
     ],
     "files_changed": [
      ".changeset/24547-active-save-namespace-prefix.md",
      "packages/metadata-protocol/src/protocol.ts",
      "packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts",
      "packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts",
      "packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts",
      "packages/objectql/src/protocol-duplicate-package-permission-field-keys.test.ts",
      "scripts/engine-double-contract.pinned.json"
     ],
     "line_budget": "n/a — no skills/** or line-ratcheted ledger touched; diff 508 changed lines (+486/-22) vs the 3000 human-merge threshold"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Surface amendment · claim 6278973624 · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-20T00:41Z

    The dev's report on PR #24702 measured a third door that makes an unprefixed object live under a namespaced package: the per-item publish (publishMetaItem, through promoteDraftForPublish, POST /api/v1/meta/object/:name/publish?package=). Over HTTP on the branch it answered 200, then GET 200, then a data insert 201.

    • Answer to its open question: A, in this PR. The ruling 6278955141 is that ADR-0129 D2's MUST holds at the runtime doors that make an object live, as a refusal, in 18.0. When it was recorded, two doors were known.
      • This is the same rule, the same code and the same judge on a third door, with no semantic difference.
      • So it reuses the parent ruling (meta-criterion ①: a new branch of an existing refusal set goes straight to the queue) and does not go back to the decision box.
      • Leaving it open would keep the authoring trap the ruling closed, reachable through Studio's save-then-publish loop.
    • Added to the file surface: in packages/metadata-protocol/src/protocol.ts, the object branch of the per-item publish (publish only) asks objectNamespacePrefixViolation and refuses with the same NAMESPACE_PREFIX / 422 and the same sentence. One pin goes in the PR's suite, and the changeset line "Not judged by this change" moves into FROM/TO.
      • The ⛔ clauses stand: no new code, no ADR edit, no packages/spec change.
    • Next: the same dev patches PR fix(metadata-protocol)!: the direct metadata save refuses an object without its package namespace prefix #24702. The seat's review and the CONTRACT_REVIEW_TIER review of record then cover the final head, before the Version Packages PR.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24547,
     "status": "done",
     "round": "patch round 1 (surface amendment 6280587888)",
     "branch": "claude/issue-24547-active-save-namespace-prefix",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24702",
     "head": "afb7f17447",
     "session": "session_01AHeUBbe6S3JRawcpbLejXD — the dispatching seat's id (this run is its subagent; no separate harness id)",
     "premise_still_valid": true,
     "summary": "The per-item publish (publishMetaItem, through promoteDraftForPublish) now asks objectNamespacePrefixViolation for an object draft on promoteInto, the package the active row is written under. On a violation it refuses with the same Error shape as saveMetaItem (message = the judge's sentence, code NAMESPACE_PREFIX, status 422), before repo.promoteDraft writes, so the draft stays a draft. The exemptions are the same: no package, no namespace, sys_*, and sealed managed content, mirroring the D9.9 refusal it sits beside. The judge's docblock, the saveMetaItem comment and the pre-flight comment now name all three doors. The changeset moves the per-item publish into one FROM/TO door list. origin/main moved on protocol.ts, so it was merged first through os-regen-merge.sh: merge commit 52b8ccf806, no conflict, no generated artifact to regenerate for this surface.",
     "tests": "HEAD afb7f17447.\n(1) New suite protocol.active-save-namespace-prefix.test.ts: 8/8 passed.\n- New case 7: a draft save, then publishMetaItem, is refused with code NAMESPACE_PREFIX, status 422, the first sentence and the rename; rows stay [draft only].\n- New case 8, control: a prefixed draft publishes to active.\n- Case 6 now spans three doors: the publish-drafts failed[0].error and the per-item refusal's message both equal the direct save's message byte for byte, and nothing goes live.\n(2) Ablation of the new branch, scripts/ablation-replace.mjs on the committed fix:\n- direction declared before running: red on exactly cases 6 and 7, the other six green;\n- the anchor 'const prefixViolation = this.objectNamespacePrefixViolation(request.name, promoteInto);' became 'const prefixViolation = false && …'; anchor 1 → 0, blob aecdeed0e1a2 → b1797c55d012;\n- result: '2 failed | 6 passed', exactly cases 6 and 7;\n- restore: blob == HEAD aecdeed0e1a2, git diff HEAD empty.\n(3) Full metadata-protocol suite (after its dist rebuild): 'Test Files 251 passed | 3 skipped (254) / Tests 20798 passed | 19 skipped'.\n(4) Full objectql suite on the fixed metadata-protocol dist: 'Test Files 449 passed (449) / Tests 8452 passed (8452)'. This covers the three duplicate fixture files and every objectql test that saves or publishes objects under packages.\n(5) typecheck: metadata-protocol and objectql (incl. check:test-typecheck, ledger unchanged) exit 0.\n(6) Live HTTP, fresh showcase at afb7f17447:\n- draft PUT /api/v1/meta/object/v1_report?package=com.v1.rollup&mode=draft answered 200 draft;\n- POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup answered 422 {\"error\":\"Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup').\",\"code\":\"NAMESPACE_PREFIX\"};\n- then GET /api/v1/meta/object/v1_report answered 404 RESOURCE_NOT_FOUND and POST /api/v1/data/v1_report answered 404 OBJECT_NOT_FOUND;\n- the direct PUT answered 422 with the same body;\n- publish-drafts answered outcome refused, failed[0] NAMESPACE_PREFIX with the same sentence, so the draft was still a draft;\n- control: the prefixed draft rollup_v1_report, then per-item publish, answered 200, then GET 200.",
     "gates": "Derived union (dispatch-gates --commands --repo objectstack-ai/objectstack) on afb7f17447: 78 commands, each run with its exit captured before any pipe; all 78 exit 0. --ran reconciliation with recorded exit codes: '78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero'.\n- check:dual-build-cjs-loads and check:lean-entry-closure first answered exit 3 (PREREQUISITE NOT MET: the fresh worktree had no full build). Re-run after the dogfood-closure build: exit 0 ('723 emitted CommonJS file(s) parse'; 'Admitted set held exactly (15 packages)'). check:dts-closure re-checked after that build: '177/177'.\n- check:adr-0087-registration accepted the updated changeset: not-required (no-migration-prescription), exit 0. check:engine-double-contract exit 0 (the merged pinned ledger is consistent). check:objectql-double-limit exit 0.\nCI on PR #24702 at afb7f17447, read before this report:\n- success: Build Core, TypeScript Type Check (all lanes), Lint & Repo Gates (all lanes), Check Changeset, Governed Surface Queue Guard, Test Core 1, 2, 3, 5 and 6 of 6, the claim/single-writer guards;\n- in_progress: Test Core (4/6).\nNot run locally: test:repo (diff touches no repo-project test); dogfood (not re-run this round).",
     "mcp_calls": "0 — no MCP GitHub tool was called",
     "api_writes": "1 relay stroke (fleet-write, as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/issues/24547/comments (this report). Transport: one POST /repos/objectstack-ai/objectstack/dispatches. The PR body was NOT edited: see pr_body_edits. git push is not counted.",
     "pr_body_edits": [
      {
       "old_str": "Ruling-ref: 6278955141 (the maintainer: 「同意」 — pull forward into 18.0 as a refusal).",
       "new_str": "Ruling-ref: 6278955141 (the maintainer: 「同意」 — pull forward into 18.0 as a refusal). Surface amendment: 6280587888 (the per-item publish door: same rule, same code, same judge)."
      },
      {
       "old_str": "The direct (publish-mode) `saveMetaItem` stored the very same name `active` under the very same package. Now both doors ask one judge and give one answer.",
       "new_str": "The direct (publish-mode) `saveMetaItem` and the per-item publish (`publishMetaItem`) made the very same name `active` under the very same package. Now all three doors ask one judge and give one answer."
      },
      {
       "old_str": "  - A comment in `duplicatePackage` that called the direct save",
       "new_str": "  - `promoteDraftForPublish`, the per-item publish's promotion, asks the same judge for an `object` draft on `promoteInto`, the package the active row is written under. On a violation it throws the same `NAMESPACE_PREFIX` / 422 with the same sentence, before `repo.promoteDraft` writes, so nothing is promoted and the draft stays a draft. The exemptions are the same: no package, no namespace, `sys_*`, and sealed managed content (as for the D9.9 refusal beside it). `publishPackageDrafts` also promotes through here, but its pre-flight refuses the same name first.\n  - A comment in `duplicatePackage` that called the direct save"
      },
      {
       "old_str": "    - Not asked of draft saves: the publish pre-flight judges a draft, batch-atomically.",
       "new_str": "    - Not asked of draft saves: the door that promotes the draft judges it, the publish pre-flight batch-atomically and the per-item publish before its promotion writes."
      },
      {
       "old_str": "has FROM / TO / Fix lines, the",
       "new_str": "has FROM / TO / Fix lines covering both doors (the direct save and the per-item publish), the"
      },
      {
       "old_str": "## Measured on the wire (fresh showcase on this branch, after the fix)",
       "new_str": "## Measured on the wire (fresh showcase on this branch, after the fix: the table at `3b94654c58`; the per-item rows, and a re-read of the direct PUT and publish-drafts with the same answers, at `afb7f17447`)"
      },
      {
       "old_str": "| `POST /api/v1/packages/com.v1.rollup/publish-drafts` | 200 `outcome: refused`, `failed[0].code: NAMESPACE_PREFIX`, the same sentence (unchanged) |",
       "new_str": "| `POST /api/v1/packages/com.v1.rollup/publish-drafts` | 200 `outcome: refused`, `failed[0].code: NAMESPACE_PREFIX`, the same sentence (unchanged) |\n| `POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup` (per-item publish of that draft) | **422** `{\"error\":\"Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup').\",\"code\":\"NAMESPACE_PREFIX\"}`. Then `GET /api/v1/meta/object/v1_report` 404 and `POST /api/v1/data/v1_report` 404 `OBJECT_NOT_FOUND`, and a following publish-drafts still lists the draft in `failed[]` |\n| prefixed draft `rollup_v1_report`, then `POST …/meta/object/rollup_v1_report/publish?package=com.v1.rollup` | 200, then `GET` 200 |"
      },
      {
       "old_str": "## Pins (`packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts`, 6 cases)",
       "new_str": "## Pins (`packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts`, 8 cases)"
      },
      {
       "old_str": "6. A draft save is unchanged. `publishPackageDrafts` refuses it with `NAMESPACE_PREFIX`, and `failed[0].error` equals the active door's message byte for byte.",
       "new_str": "6. A draft save is unchanged. `publishPackageDrafts` refuses it with `NAMESPACE_PREFIX`, and `publishMetaItem` refuses it too. `failed[0].error` and the per-item refusal's message both equal the active door's message byte for byte, and the draft is still the only row.\n7. The per-item publish of an unprefixed object draft under a namespaced package is refused. The pin asserts `code: NAMESPACE_PREFIX` and `status: 422`, the first sentence and the rename. Nothing is promoted and the draft stays a draft.\n8. Control: the per-item publish promotes a prefixed object draft under the same package."
      },
      {
       "old_str": "No dist build was needed: the suite imports `./protocol.js` from source.",
       "new_str": "No dist build was needed: the suite imports `./protocol.js` from source.\n\nSecond ablation, the per-item publish branch, on the committed fix `afb7f17447`.\n- Declared direction, before running: red on exactly cases 6 and 7, green on the other six.\n- `const prefixViolation = this.objectNamespacePrefixViolation(request.name, promoteInto);` was changed to `const prefixViolation = false && this.objectNamespacePrefixViolation(request.name, promoteInto);`. The anchor went 1 → 0 and the blob aecdeed0e1a2 → b1797c55d012.\n- Result: '2 failed | 6 passed', exactly cases 6 and 7.\n- Restore was proven: blob == HEAD and `git diff HEAD` empty."
      },
      {
       "old_str": "## Verification on the final head `3b94654c58` (merge of `origin/main` `486745ec75`)",
       "new_str": "## Verification on the final head `afb7f17447` (the per-item publish fix, on top of a merge of `origin/main` `d1173825b7` through `os-regen-merge.sh`)"
      },
      {
       "old_str": "- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 250 files passed (3 skipped), 20787 tests passed.",
       "new_str": "- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 251 files passed (3 skipped), 20798 tests passed."
      },
      {
       "old_str": "- objectql, the three adjusted files: 15/15 passed. The whole objectql suite was also run before the merge: 449 files, 8452 tests. Its only 14 failures were these three files, each failing on the new refusal.",
       "new_str": "- objectql, the whole suite on the fixed dist: 449 files and 8452 tests, all passed. In round 0, before the fixture change, its only 14 failures were the three adjusted files, each failing on the new refusal."
      },
      {
       "old_str": "`check:dts-closure` and `check:dual-build-cjs-loads` were re-run after a concurrent build settled. Their first reads (exit 1 and 2) caught `packages/spec/dist` mid-rebuild.",
       "new_str": "`check:dual-build-cjs-loads` and `check:lean-entry-closure` first answered exit 3 (PREREQUISITE NOT MET: the fresh worktree had no full build yet). Re-run after the build: exit 0."
      },
      {
       "old_str": "- Dogfood, run once on this head,",
       "new_str": "- Dogfood, run once on `3b94654c58` (not re-run in this round),"
      },
      {
       "old_str": "## Other doors that make an object live (enumerated, not widened into)",
       "new_str": "## Other doors that make an object live (enumerated; line numbers at `3b94654c58`)"
      },
      {
       "old_str": "- `publishMetaItem` (`protocol.ts:25034`, through `promoteDraftForPublish` `:25351`; `POST /api/v1/meta/:type/:name/publish`) **still makes an unprefixed object live**. Measured on this branch over HTTP: a draft PUT of `v1_report?package=com.v1.rollup` then `POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup` answered 200. `GET` then answered 200 and `POST /api/v1/data/v1_report` answered 201. The changeset says so under \"Not judged by this change\". The same one-line judge call would close it. That is the seat's call, so it is reported here and not done.",
       "new_str": "- `publishMetaItem` (through `promoteDraftForPublish`; `POST /api/v1/meta/:type/:name/publish`) is now judged too, per the seat's surface amendment 6280587888 on the card. At `3b94654c58` it still made the unprefixed object live: 200, then `GET` 200 and a data insert 201. At `afb7f17447` it answers 422 `NAMESPACE_PREFIX` (table above)."
      },
      {
       "old_str": "- The per-item publish door gap above.",
       "new_str": "- The per-item publish branch sits after the D9.9 refusal on `promoteInto` and before `repo.promoteDraft`, with the same sealed-managed exemption as that refusal. Case 7 and the second ablation pin it."
      }
     ],
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: #24700 (seat) · noted, not touched: the wall-environment-metadata-refusal dogfood red is main's, attributed to #24630, as the coordinator recorded",
      "carrier: none · noted, not filed: ADR-0129 D2 and the namespace-prefix.ts header name MetadataManager.publishPackage() (packages/metadata/src/metadata-manager.ts) as the runtime chokepoint, but it runs no object-name prefix check; the runtime doors that do are now publishPackageDrafts, saveMetaItem and publishMetaItem (stale citation in governed or spec text, outside this surface)",
      "carrier: none · noted, not filed: importUnmanagedPackage meets the saveMetaItem refusal mid-copy (rolled back) rather than at plan time for a namespace-less source whose objects lack its id-derived prefix; inference, no fixture has that shape"
     ],
     "deviations": [
      "From round 0, still standing: the file surface beyond the claim. That is packages/objectql/src/protocol-duplicate-package-{unprefixed-object,names-in-step,permission-field-keys}.test.ts (fixture staging only) and scripts/engine-double-contract.pinned.json (the --write remedy).",
      "In this round every build and test went through os-verify-lock. The check:* gates and the dev server ran outside it, as the lock's scope allows."
     ],
     "files_changed": [
      ".changeset/24547-active-save-namespace-prefix.md",
      "packages/metadata-protocol/src/protocol.ts",
      "packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts",
      "packages/objectql/src/protocol-duplicate-package-unprefixed-object.test.ts",
      "packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts",
      "packages/objectql/src/protocol-duplicate-package-permission-field-keys.test.ts",
      "scripts/engine-double-contract.pinned.json"
     ],
     "line_budget": "n/a — no skills/** or line-ratcheted ledger touched; diff vs merge base d1173825b: 600 changed lines (+578/-22), under the 3000 human-merge threshold"
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-20T02:38Z

    reading result
    PR #24702 merged through the queue 2026-10-20T02:37Z, merge 0b4be0b0ba
    origin/main carries 0b4be0b0ba (git merge-base --is-ancestor); git grep -c objectNamespacePrefixViolation in packages/metadata-protocol/src/protocol.ts = 7
    Version Packages PR #24524 still open and unmerged at this reading, so the refusal lands before the 18.0 cut, as ruling 6278955141 requires

    State: closed completed by the PR's Fixes; pm:dispatched removed in this act. All three runtime doors that make an object live under a namespaced package (direct publish-mode save, per-item publish, package draft publish) now refuse a name without the package's namespace prefix with NAMESPACE_PREFIX / 422 and one sentence. The @objectstack/metadata-protocol minor (BREAKING) rides #24524.


    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

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:enginepriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions