Skip to content

feat(metadata-protocol,spec): importUnmanagedPackage, the unmanaged install mode's copy-once import (ADR-0131 D6, stage S1) - #22879

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-15213-s1-unmanaged-import-verb
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-15213-s1-unmanaged-import-verb

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Part of #15213. This is stage S1 only, the import verb behind the unmanaged install mode; the card stays open for S2 (the installModes manifest key with its install door, --install-mode and the installMode body key), S3 (the authoring docs page) and S4 (the objectui marketplace choice).
Clause-②: yes (widening)

The widening: two new published schemas in @objectstack/spec/api (ImportUnmanagedPackageRequestSchema, parsed by its producer, and ImportUnmanagedPackageResponseSchema), and two new error codes registered to @objectstack/metadata-protocol. Nothing existing is removed or narrowed. Changeset: .changeset/15213-unmanaged-import-verb.md (minor on both packages).

What it does

ObjectStackProtocolImplementation.importUnmanagedPackage({ manifest, targetPackageId?, targetName? }) (packages/metadata-protocol/src/protocol.ts, beside duplicatePackage) copies every metadata item of ONE package body into a NEW writable base in the environment ledger, through saveMetaItem, the save door a Studio edit takes. Every copied row is owned by the base, carries organization_id NULL, is served _provenance: 'org', and the next save edits it. Nothing is registered under the source id. This is the part of a managed install that the manifest service does, and nothing more.

The rulings it carries, as built:

  • The spelling is unmanaged (ruling 6108509158, letter B): the codes are UNMANAGED_INSTALL_* and the write face is 'package-unmanaged-import'.
  • Q2: the refusal reads the REQUESTED posture. resolveTenancyPosture(); an unrecognised value is read as walled (fail-closed, as the protocol's other posture gate does). group / isolated answer UNMANAGED_INSTALL_WALLED_POSTURE / 409, and details.posture names it.
  • Q3: a NEW base whose id differs from the source. targetPackageId defaults to the source id plus -copy (the Studio duplicate default). The source namespace is kept. The base is installed scope-less (writable) through installPackage, never best-effort. A target id naming an installed package is refused (RESOURCE_CONFLICT / 409): never into an existing base.
  • Q5, the door half: runtime-uncreatable types and code are refused before any write. The type verdict is the save door's own predicate (isRuntimeCreateAllowed, registry-only); OS_METADATA_WRITABLE is not consulted, and a test sets it and shows it does not open job. Code keys (functions, onEnable, plugins, devPlugins, runtimeModule, and a plugin artifact's main / runtime / packaging / integrity), extensions of another package's items (objectExtensions, picklistExtensions, navigationContributions, contributes) and the artifact envelope (packages, a nested manifest) are refused whole with UNMANAGED_INSTALL_UNCOPYABLE / 422, naming each key. The authoring half (defineStack / os validate) rides S2 with the manifest key.
  • Q7: every judgeable refusal before the base exists; any write failure deletes the base. Order: organization-scoped request, the published request declaration (a key it does not declare is a 400), posture, the body (uncopyable keys, item names against the save door's grammar, duplicate names, the target id), the ADR-0087 D1 handshake, an existing target, an owned namespace (a managed install of the same package included), conversion to today's protocol (INVALID_METADATA / 422, FLOW_CONVERSION_CONFLICT / 409), and collisions with environment rows or items a managed package ships (RESOURCE_CONFLICT / 409, details.collisions). A write that fails afterwards runs deletePackage(base), which removes its rows and drops its tables, and the thrown error keeps the failed item's own code and status, names the item, and says what the rollback did (details.rolledBack). A rollback that cannot finish is logged once at error with the consequence and the fix.

What is copied, and what is left to the door

The pure half lives in packages/metadata-protocol/src/package-unmanaged-install.ts. Copied: the items of every manifest collection (PLURAL_TO_SINGULAR, the one map the artifact loader and the authoring lint read) plus the assembled viewItems, written in registry loadOrder, a view container named by the object it binds (the artifact loader's naming). Every other top-level key is classified in UNMANAGED_IMPORT_KEY_DISPOSITIONS, and a test holds that table total against ManifestSchema (tombstoned keys excepted) and the stack definition in both directions, so a key either schema gains reddens a test instead of being refused with no reason on record. An unclassified key is refused, never skipped.

Two classes are deliberately not this verb's, because a managed install leaves them out of the manifest service too: seed data and translations are side effects install-local's step 5 applies in either mode (seeds are data, never metadata items), and deployment configuration (i18n, requires, datasourceMapping, …) is read by a stack's boot, by no install. S2's door owns their unmanaged fate; see the acceptance notes.

Measured before building (the dispatch's mechanism assumptions)

  • external-import is NOT this import's face: it serves @objectstack/service-datasource's "Import as Object" route, which relays 400 EXTERNAL_IMPORT_ERROR. One new face was needed, as the stage plan said, and both face switches state it (destructiveChangeRemedy gets its own 409 clause; specValidationFindings keeps the full-prose default, documented).
  • No request schema for duplicate exists anywhere in spec (zero hits for DuplicatePackageRequest). The request schema here is new, and ENFORCED rather than transcribed: the verb parses through it. The module header of package-lifecycle.zod.ts says so.
  • installModes / installMode: still zero hits in packages/ and examples/; nothing of S2 was needed, and no runtime file is edited.

Tests

packages/metadata-protocol/src/package-unmanaged-install.test.ts, 40 cases. The verb runs against a stub engine that keeps rows, so the REAL saveMetaItem, installPackage and deletePackage run; its write verbs route through the producer's dispatch predicates, and its find honours limit. Refusals are asserted as the envelope resolveThrownHttpError answers (code and status), never a bare throw. The stage-0 pins, each built:

  • every item copied with package_id = the base and organization_id NULL, nothing under the source id; the response parses through ImportUnmanagedPackageResponseSchema;
  • the registry is handed every copied item stamped _provenance: 'org' (a view's registry item; an object's registered schema, under the base id);
  • a walled requested posture (isolated, group, an unrecognised value) is refused 409 with ZERO writes and no package; the lit control on single imports;
  • a collision with an environment row, with a managed package's item, an owned namespace and an existing target are each refused before the base is created;
  • a mid-copy failure (a spec-invalid second item) is thrown as the save door's own INVALID_METADATA / 422 and leaves no package, no row, and the object's table dropped;
  • a follow-up save edits a copied view AND a copied object.

Ablations (one-shot, not kept as tests)

Each leg mutated through scripts/ablation-replace.mjs (the anchor must hit exactly once, the write is verified on disk by blob hash, the restore is proven against HEAD by blob hash and an empty git diff HEAD), inside a wrapper carrying an EXIT/INT/TERM restore trap. The suite imports src directly (no dist leg). Direction predicted before running: red for all four; observed red for all four, and only where predicted. Run at b5cfa28ee0; the two mutated files are byte-identical at this PR's head (protocol.ts blob 432f1d7841, package-unmanaged-install.ts blob 352e1b7afc).

Leg Mutation Red
A1 the posture refusal's throw removed the 3 walled-posture cases (37 of 40 still green)
A2 the rollback's deletePackage call replaced by an empty outcome the mid-copy failure case (39 of 40 green)
A3 the collision refusal disabled the environment-row and managed-item collision cases (38 of 40 green)
A4 the runtime-uncreatable refusal disabled the 5 per-type cases and the hatch case (34 of 40 green)

Gates (local, targeted; CI runs the full farm)

  • Derived on this diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at d15160d108: 122 commands, a strict superset of the 84 derived at dispatch (the additions come from the generated docs pages and the gate ledger this diff touches).
  • All 122 run at d15160d108, each exit code captured to disk before any pipe: 122 × exit 0. --ran reconciliation over the recorded codes: 122 derived, 122 run, 0 NOT-MEASURED, 0 UNRUN.
  • An earlier run of the union at b5cfa28ee0 found two real reds, both in this PR's test double, both fixed in d15160d108: check:objectql-double-limit (the stub's find ignored limit) and check:engine-double-contract (a new pinned double needs its ledger rows, written by the gate's own --write: 3 rows added, 0 lost). Three gates that run read a missing dist there (exit 3, not measured); after building the objectql and client-react closures all three measured green above.
  • @objectstack/metadata-protocol: typecheck (tsc --noEmit) green at d15160d108; the full suite at the post-merge tree 2a26dc7eec: 225 files passed, 3 skipped, 28,168 tests passed, with one red file, the lookupArtifactItem( call-site census, fixed in b5cfa28ee0 (the new call removed) and re-run green with this PR's suite and both face inventories (4 files, 100 tests).
  • @objectstack/spec: the full typecheck chain (tsc --noEmit, scripts, test-typecheck) green; every suite under src/api/ (51 files, 1,595 tests) green; check:generated reports all 14 generated artifacts current after the merge.
  • Not run locally, left to CI: the repo-wide pnpm lint, the full test farm, and the merge queue's rebuild. The derivation flags its tree as behind origin/main by at least 5 commits, two of which changed gate scripts (scripts/check-route-envelope.mjs, scripts/cross-package-test-inputs.mjs); CI reads the merged state.

Size: 1,566 changed lines (1,553 added, 13 deleted, generated included), 17 files.

Acceptance notes

Out of scope for S1, recorded for the carrier named; none is filed.

  • S2 owns seeds and translations for the unmanaged mode. The verb leaves data and translations to the door, as install-local's step 5 applies them for a managed install. One difference S2 has to decide on: a managed install rehydrates its translation bundles from its ledger entry at each boot; an unmanaged install writes no ledger entry, so bundles loaded once into the i18n service would not come back after a restart unless S2 carries them some other way. Carrier: S2.
  • installed_from has no carrier yet. The card body asks for an informational installed_from; the stage-0 report proposed the server-stated write source. The verb does not state a source on its saveMetaItem calls, because the save door reads any stated source as a stored-row rewrite and skips the authored-write rule for it. The answer names the source id and version, which the door can record. Carrier: S2.
  • Spec validity is judged at the write, then rolled back, not preflighted. The stage plan's preflight list does not include a per-item schema parse, and a second copy of the save door's gates in the verb would be a parallel judge. A spec-invalid item therefore creates the base and deletes it again (pinned). If the seat reads Q7's "every judgeable case" as including schema validity, the preflight can parse each item through getMetadataTypeSchema before the base. Carrier: the seat, at review.
  • Hooks with a function-name handler and no body are not judged by the verb: install-local's step 1d refuses them before any mode fork, using the runtime binder this package cannot import. Carrier: S2 (install-local runs 1d before any point where S2 can branch on the mode).
  • The managed-collision detail does not name the owning package. It says shipped by a managed package: naming it would add a lookupArtifactItem( call site, which protocol.lookup-artifact-item-call-sites.test.ts records with a disposition, in a file outside this stage's surface. The namespace refusal does name its owners. Carrier: none.
  • Unmeasured: an item carrying an authored protection lock is copied as written, so it may stay locked in the environment ledger; and an exported package whose viewItems carry an edited view-container expansion may be refused by the save door's container-expansion predicate on import (then rolled back). Carrier: S2's acceptance run on examples/app-crm.
  • Observation, no card: convertStoredItem's legacy-shape notice says "stored row … re-save it" when the import converts an artifact item; the wording describes a stored row, not an import. Carrier: none.

Generated by Claude Code

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec, touching 51 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

44 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d952358a278e081648bb8fca1385ec4c16b07047.

⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • 25 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d952358a278e081648bb8fca1385ec4c16b07047 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b86685a45fea3a7ca64b311ca84553bc70210e6b — the merge of head d15160d1087c0c96478872177e061f4d8998487e into base d952358a278e081648bb8fca1385ec4c16b07047, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b86685a45fea3a7ca64b311ca84553bc70210e6b && git checkout b86685a45fea3a7ca64b311ca84553bc70210e6b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d952358a278e081648bb8fca1385ec4c16b07047 d15160d1087c0c96478872177e061f4d8998487e && git checkout -B drift-repro d952358a278e081648bb8fca1385ec4c16b07047 && git merge --no-ff d15160d1087c0c96478872177e061f4d8998487e

node scripts/docs-audit/affected-docs.mjs --json d952358a278e081648bb8fca1385ec4c16b07047

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d952358a278e081648bb8fca1385ec4c16b07047 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d15160d1087c0c96478872177e061f4d8998487e
Local-runs: none

Inputs: card #15213 (body and all 17 comments, the S1 report 6112032490 and the seat's answer 6112058094 included), PR #22879 (body, 17-file list, the net diff origin/main...d15160d108, 1,553 added / 13 deleted), the check-runs on the head, and on origin/main only packages/spec/package.json exports, packages/spec/src/api/index.ts, packages/metadata-protocol/package.json with its entry, and ADR-0131 D6. Nothing built, run or re-run.

① Derived judgments

Published request schema — right. ImportUnmanagedPackageRequestSchema (@objectstack/spec/api, with ImportUnmanagedPackageRequest and its Parsed type) is a .strict() object of manifest (open record), targetPackageId? and targetName?, and the producer PARSES it (protocol.ts, step 2 of the verb): a key it does not declare answers 400, pinned by the test that sends installMode: 'unmanaged' and sees VALIDATION_ERROR / 400 with zero writes. This is the one departure from the claim's wording "transcribed as duplicatePackage's are", and it is the stronger shape: a declaration its producer parses cannot drift from the verb; the departure is written in the module header and the generated reference page. actor and organizationId are read off before the parse because the door states the actor and step 1 (refuseOrganizationScopedWrite) has already judged the scope — right. manifest declared as an open record is right for S1: the body is judged by planUnmanagedImport against ManifestSchema.shape.id, the collection map and the disposition table, and the flattened stack shape the door hands the manifest service is not ManifestSchema.

Published response schema — right. ImportUnmanagedPackageResponseSchema is transcribed from the verb's return (sourcePackageId, sourceVersion?, targetPackageId, namespace, copiedCount, copied[] in write order) and the first verb test pins parse(result) equal to result. No failed[] and no success field: right, because the verb is all-or-nothing and a failure is thrown, never answered.

Two error codes — right, statuses right. UNMANAGED_INSTALL_UNCOPYABLE (422) and UNMANAGED_INSTALL_WALLED_POSTURE (409) are added inside ERROR_CODE_LEDGER['@objectstack/metadata-protocol'] and stamped only in package-unmanaged-install.ts, the registering package. 422 for a body carrying what cannot become environment rows follows the ledger's own rule beside it (a body change is the remedy, as OS_PROTOCOL_INCOMPATIBLE's note says); 409 for the posture refusal is the status the ledger keeps for refusals from environment state (its FLOW_CONVERSION_CONFLICT note), and the deployment's declared posture is such state, not an actor's authorization. Both are pinned through resolveThrownHttpError code plus status. ApiErrorSchema.code widens by two, additive. Collisions, an existing target and an owned namespace reuse RESOURCE_CONFLICT 409 rather than minting codes — right.

The verb's refusal set and order — right, a superset of the plan, every addition pre-base. As built: (1) organization-scoped request; (2) the published declaration (400); (3) the REQUESTED posture through resolveTenancyPosture(), an unrecognised value read as walled, 409 with details.posture; (4) the body alone: runtime-uncreatable collections through the save door's own isRuntimeCreateAllowed (registry-only; the OS_METADATA_WRITABLE hatch is read one layer down in saveMetaItem, never here — pinned with OS_METADATA_WRITABLE=job still 422), code keys, extensions of other packages' items, the artifact envelope and any unclassified key (422, each key named), item-name grammar and duplicate names (INVALID_REQUEST 400), target id grammar and target equal to source (400); (5) the ADR-0087 D1 handshake (pinned 422 OS_PROTOCOL_INCOMPATIBLE); (6) a target naming an installed package, or an owned namespace, a managed install of the same package included (409); (7) conversion to today's protocol (INVALID_METADATA 422 / FLOW_CONVERSION_CONFLICT 409); (8) collisions with environment rows or managed items (409, details.collisions); then installPackage of a scope-less base (id defaults to the source id plus -copy, source namespace kept) and one saveMetaItem publish per item in registry loadOrder; a write failure runs deletePackage(base) and rethrows with the item's own code and status, details.rolledBack, and a one-time error log when the rollback leaves residue. Against the stage plan's S1 mechanism (6108149738): posture, uncreatable type, environment-row and registry collision, owned namespace, base-id grammar and equals-source, base then saveMetaItem, rollback — all present; the additions (scope, declaration, handshake, name grammar, conversion) all precede the base. Against the ACCEPT (6108175106): Q2 A — requested posture, and the fail-closed catch is the same reading the protocol's own orgWallEnforced() takes; Q3 A — new base, -copy default, source namespace, never into an existing base, pinned rows package_id = base and organization_id NULL with nothing under the source id; Q5 A, door half — right, the authoring half rides S2 with the key (stack.zod.ts is on S2's file list); Q6 A — the door's both-branch refusal is S2's; S1 supplies what it keys on (the base owns the source namespace) and refuses the reverse direction, pinned; Q7 A — see ③; Q8 — below; Q9 — S2's (no key in this diff). Order: posture before any store read is right (P2's "before any write", and no read of the store on a walled deployment); organization scope first is the protocol's uniform rule. One harmless note: the declaration parse precedes the posture check, so a malformed request on a walled deployment answers 400 rather than 409 — both pre-write.

Pins read in the test file, each built against the REAL saveMetaItem, installPackage and deletePackage over a row-keeping stub engine: rows owned by the base with organization_id NULL and nothing under the source id; registry handed _provenance: 'org' for a view item and an object schema under the base id; walled isolated / group / unrecognised refused 409 with writes empty and no package, lit control on single; environment-row, managed-item, owned-namespace and existing-target collisions refused before the base; handshake refused before the base; unknown request key 400; mid-copy spec-invalid item thrown as INVALID_METADATA 422 with no package, no row and the object's table dropped; follow-up saveMetaItem edits a copied view and a copied object; the key-disposition table held total against ManifestSchema (tombstones excepted) and ObjectStackDefinitionSchema in both directions. One limit of the measurement: the verb reaches getPackage, getNamespaceOwners and (through isArtifactBacked) getArtifactItem by optional chaining, and the stub supplies all three; on a registry lacking one, that pre-base refusal is skipped and the next guard answers (the registry's own namespace gate at installPackage, per the stage-0 census C7, or the save door) — the outcome "nothing under the source id, no half copy" holds either way, the code and message may differ. Measured on the real registry by S2's P1/P3 acceptance on examples/app-crm (③).

The ruled spelling unmanaged — right throughout. Verb importUnmanagedPackage, module package-unmanaged-install.ts, face 'package-unmanaged-import', codes UNMANAGED_INSTALL_*, changeset, docblocks. Swept every added line of the diff: no template and no copy as a mode value; the only copy is the -copy target-id suffix, which is the ACCEPT's Q3 default and names the base, not the mode; installMode appears only as the unknown key a test proves refused and in prose naming S2's door. ADR-0131 on origin/main now spells D6 unmanaged (#22845), so record and code agree.

The new MetadataWriteFace member — right, with one wording drift. 'package-unmanaged-import' is server-stated (the verb sets it on every saveMetaItem; no body reaches the field). destructiveChangeRemedy gains its own clause: the import accepts no force, and since every name was preflighted, reaching the 409 means a row appeared during the run — reconcile it, install again; what became of the base is left to the verb's own sentence. Right. specValidationFindings keeps it on the full-prose default by design, named in that switch's default comment and in the type's docblock: right under that switch's "declare a structured channel, only then trim" polarity, and the conservative direction while no HTTP door fixes the envelope. The drift: the rolled-back refusal does forward cause.issues when present, while the docblock says the verb's thrown error carries "nothing structured beside" the message — nothing is withheld (full prose plus issues, duplication at worst), so it is a sentence to correct when S2's door settles the envelope, not a defect. The type is module-private, so the member is not a published surface. The face inventory (protocol.destructive-409-face-inventory.test.ts) enumerates every saveMetaItem caller whose catch puts the message on a response; the verb is one and is stated — Test Core is its verdict (see the check-run state below).

Q8 — a published verb with no door is the ruled verb-first order, not a declared-but-unenforced capability. Three grounds. Everything this diff declares is enforced by the thing that declares it: the request schema is parsed by the verb, the response is the verb's return, the codes are thrown by the verb, and the verb performs the import end to end. No author-facing declaration ships — no installModes key, no --install-mode, no installMode body key, no route — so there is no window in which an author can declare a mode no installer honours, which is exactly the harm Q8 A was chosen to avoid; the changeset, the ledger note, the module header and the generated reference page each say no route serves the verb yet. What remains is an in-process entry point on ObjectStackProtocolImplementation, the standing duplicatePackage had before its route. Prime Directive 10's test is declared ≠ enforced, and nothing here is declared without its enforcement.

Generated shards and the gate ledger — right. api-surface, authorable-surface, declaration-map, export-origins, json-schema.manifest, the three reference pages, the references index counts and the strictness-ledger count each carry exactly the two schemas and two codes, read hunk by hunk. scripts/engine-double-contract.pinned.json gains the three rows (delete, findOne, update) the gate itself writes for a new pinned engine double; not a governed surface (Governed Surface Queue Guard green).

② Semver level

What the diff publishes. @objectstack/spec (17.7.0, released; ./api is an export of package.json, and src/api/index.ts re-exports package-lifecycle.zod and error-code-ledger.zod): two new schemas, four new types and two Parsed types; two new ERROR_CODE_LEDGER members, which widen the ErrorCode and ApiErrorSchema.code enums. @objectstack/metadata-protocol (17.7.0, released; its entry exports ObjectStackProtocolImplementation): one new public method, importUnmanagedPackage. Nothing is removed or narrowed — the 13 deleted lines are a comment rewrite, the union's reformat, one import line and regenerated counts. The pure module's exports are internal to the package (not re-exported from the entry) and MetadataWriteFace is module-private.

The changeset. .changeset/15213-unmanaged-import-verb.md declares '@objectstack/metadata-protocol': minor and '@objectstack/spec': minor, body line Clause-②: yes (widening). Additive widening on two released packages takes at least minor; minor is right on both. No migration text and no ADR-0087 marker are owed because nothing breaks. No skip-changeset. Check Changeset green on both runs.

The Clause-②: line. PR body line 2 reads Clause-②: yes (widening) (set by the seat in 6112058094; read on the live body); the changeset reads Clause-②: yes (widening); the S1 claim (6110005583) reads Clause-②: yes and names the widening in its next line. The three agree: yes with the (widening) arm is a well-formed declaration of the same fact, and it matches what the diff publishes. The claim's own dispatch-gates --tier flagged Clause-② SUSPECT on packages/spec/src/**, which is why this record exists.

③ Boundary flags

From the S1 os-dev-report (6112032490); the seat's answer is 6112058094.

open_questions.

  • Q7, is per-item schema validity in the pre-base set? Seat: A, as built. Concur. The ACCEPT fixed the outcome (no half-copied app), the plan's mechanism named the preflight set and then "on any write failure, deletePackage(base) and report", and the S1 pin "a mid-copy failure leaves no base" presupposes write-time failures; a second parse in the verb would be a parallel judge of validity beside the save door, the drift Prime Directive 12 forbids. Pinned: INVALID_METADATA 422 thrown, base removed, table dropped, rolledBack: true. Answered.

deviations.

  1. scripts/engine-double-contract.pinned.json +3 rows — the gate's own --write output for the new pinned double; a new engine double cannot land without them, so the addition is entailed by the test the claim named; mechanical, not a governed surface. Accepted (not addressed in the seat's comment; answered here).
  2. Generated shards beyond the two spec files — check:generated --fix output, each hunk carrying only the two schemas and two codes; AGENTS.md requires them committed with a packages/spec change. Accepted.
  3. The lookupArtifactItem( call removed rather than recorded in a census test outside the surface — the only consequence is that the managed-collision detail says "shipped by a managed package" without naming it; the refusal still fires before the base (pinned), and the namespace refusal does name its owners. Accepted; carrier none, as the dev says.
  4. PR body line 2 copied the claim's Clause-②: yes — fixed by the seat; the live body reads Clause-②: yes (widening). Closed.
  5. One merge of origin/main (55382dc02a), tree since behind by five or more commits, two of them gate scripts — CI's check-runs on this head are the verdict (mergeable: true), and the queue rebuilds at landing. Accepted.
  6. A build outran the foreground cap and was stopped — process note, nothing of it in the diff. Accepted.
  7. Worktree removed after the PR — hygiene. Accepted.

out_of_scope_findings — the seat's carry list, each judged.

  1. Translation bundles are not reloaded at boot for an unmanaged install (no ledger entry) → S2. Right carrier: the ledger-entry question is the door's.
  2. installed_from has no carrier; the verb states no saveMetaItem source because the save door reads a stated source as a stored-row rewrite; the answer names source id and version → S2. Right carrier, and the card's "informational installed_from" (D6: provenance recorded for information only) stays owed on this card: S2's ACCEPT must say where the door records it.
  3. Unmeasured: an authored protection lock copied as written; a viewItems container expansion the save door may refuse and roll back → S2's examples/app-crm acceptance run. Right.
  4. The managed-collision detail does not name the owner → note, no carrier. Accepted (deviation 3).
  5. convertStoredItem's legacy-shape notice says "stored row … re-save it" on an import → note. Accepted as wording; S2's door is where such a notice would reach an installer, so S2 rewords it if it does.
  6. From the PR body's acceptance notes, not in the report's list: hooks with a function-name handler and no body are not judged by the verb → S2 (install-local step 1d runs before any point where the door can branch on the mode). Right by construction for the door; for an in-process caller there is no door, which is every protocol verb's standing.

This review's own carries, none a defect.

  • To S2's acceptance run: the three registry reads the verb reaches by optional chaining (getPackage, getNamespaceOwners, getArtifactItem) are measured here only against the stub; P1/P3 on the real registry measures them.
  • To S2, when the door fixes the envelope: the MetadataWriteFace docblock sentence "nothing structured beside it" versus the forwarded cause.issues (① above) — settle whether the face declares the structured channel, then correct the sentence.
  • To S2: the answer does not name the keys the verb left to the door (data, translations); if the verb stays callable outside the door, naming them in the answer tells a caller what was not applied.

Check-runs on d15160d108 at this record's final read (2026-10-11T18:24Z; 42 runs over 35 names, the newest run per name governs): every run complete, 0 non-green, 0 pending. Green: Build Core, Build Docs, Check Changeset, Check Documentation Links, Dogfood Regression Gate (1/3), (2/3), (3/3) and its summary, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates (the farm that carries check:error-code-provenance, check:api-surface, check:generated, check:error-status-conformance and check:engine-double-contract), 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, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core (1/6) through (6/6) and its summary (the shards that carry the @objectstack/metadata-protocol suite, the 40-test file and the face inventory included), The card this PR closes must claim this branch, Type Check (source gates, consumer gates, debt ledger, workspace, TypeScript Type Check), filter. Skipped: Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in). At an earlier read (18:18Z) Test Core (2/6) to (6/6) were still running; they completed green between 18:20Z and 18:23Z, so nothing was pending when this verdict was written.

Implemented-by: claude/issue-15213-s1-unmanaged-import-verb
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-11T18:27Z from an isolated at-tier reviewer whose transcript shows read-only tool use (its writes are its own scratch record).

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants