Repository navigation
Description
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
Filing gate: ④ child of epic #28236 (P1, cut after ADR-0146's rewrite merged in #28295); D3's auto-create is epic ruling D-3, and Q4 「全部同意」 fixes the explicit <base namespace>_custom. ⛔ Not a claim.
Blocked-by: #28404
Part of #28236 (P1, child N-13) · governs: ADR-0146 D3, Supersede row ADR-0070 D3/D5 (paired case) · lane: domain:server (packages/metadata-protocol, packages/runtime; spec half rides N-08) · size: M.
This card is the .custom ensure door, the only creation path for <base>.custom (D3: "no second door builds the pair"). It is consumed by N-21 (#28149, the clone door's default target; comment 6542769510 asks for its Blocked-by: to point here), by N-16 (the migration) and by the P2 Studio writes. Once this card is filed, #28149's Blocked-by: moves to it. Why it waits on N-08: N-08 carries the pair-id constant and the namespace rule, and this door reads them, ⛔ never a local copy.
Business reason. A customer's first change to an installed app has to land somewhere real: a package that exports, deploys and is never mixed up with another app's customizations. One door creates it the same way every time, so Studio, the clone and the migration never build three different packages.
Measured (75ecaf4ad8).
- No path creates or ensures a
.custompackage (git grepoverpackages/**/src: no hit). metadata-protocol/src/protocol-package-install.ts:248–250derives a missing namespace withderiveNamespaceFromPackageId, which givescustomfor everyx.custom.spec/src/kernel/namespace-prefix.ts:18NAMESPACE_RE = /^[a-z][a-z0-9_]{1,19}$/:<ns>_customis valid only for a base namespace of ≤ 13 characters.runtime/src/domains/packages.ts:1379–1382(install) and:2443–2447(duplicate) gate withrequireManageMetadata+refuseWalledCatalogPackageWrite.metadata-protocol/src/package-writability.ts#isWritablePackageis the one read-only predicate.
Wanted.
- One protocol primitive plus one HTTP route over it (route ledger, spec request/response schemas, SDK, OpenAPI). Given a base id, it answers the pair and whether it was created or already there.
- It creates through
installPackage, with:- id from N-08's constant;
namespace=<base namespace>_custom, set explicitly so the derivation never runs;dependencies: { <base>: '^<major of the base manifest.version>' };- a writable package.
- Idempotent: an existing valid pair is returned unchanged, never rewritten. Two concurrent calls leave one pair.
- Worded refusals that leave nothing behind:
- the base is not installed, is writable (customize it directly), or is itself a pair;
- the base declares no namespace;
- the composed namespace fails
NAMESPACE_REor is owned by another package; - an existing
<base>.customlacks the dependency or the namespace (named, never adopted).
- Authority as the install door:
manage_metadataundersingle; undergroup/isolatedonly the platform operator, and a tenant administrator gets403. POST /packagesand/duplicaterefuse a pair-shaped id whose manifest breaks item 2, naming this door. An exported pair that carries both still installs (ADR-0141 D2 judges its range).
Tests. Ensure creates crm_custom (not custom) with ^<major>; a re-ensure is a no-op; each refusal leaves no package; walled tenant admin gets 403 and the operator 201; concurrency. namespace-prefix.test.ts and protocol.duplicate-package-target-namespace.test.ts stay green.
Not here. Constants and the namespace rule (N-08); uninstall protection (N-14); the clone door (N-21); migration (N-16); Studio's folded switcher (N-19).
Dedupe. Searched custom package, .custom package ensure, namespace custom derived: only #28149 (consumer), #28390, #28236. No card creates the pair.
Links back: #28236, #28149, ADR-0146 D3, ADR-0070 D3, ADR-0141 D1/D2, ADR-0048 §3.2.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 31, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked · p1 ·
pm:queue· 2026-10-31T23:54Z⛔ Not a claim.
Blocked-by: #28404(N-08), and spec: the override declaration, the.custompairing constants, the<base_ns>_customnamespace rule, the__creservation, the overridable-type list and the base-field display whitelist #28404 closed as completed at 2026-10-31T23:30Z. The card's "Why it waits" names only N-08.pm:blockedis removed, and the card is inpm:queueat p1, as epic [Epic] Package-level override: every customization of an installed package lives in its paired <base>.custom package, one declared override shared by code and Studio (supersedes #28130) #28236's lane work. Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149 (N-21) already points itsBlocked-by:here.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
Claim: PM loop round 2
Session:
session_01QUs3BtyH9zgxKzYrJiH18JAccount:
os-zhuangBranch:
claude/project-thread-7yvsri(the card thread's platform-assigned push branch, the measured platform exception to the protocol branch shape)Worktree:
objectstack-issue-28407Domain:
domain:serverSeat:
domain:server#4File surface:
packages/metadata-protocol/src/**,packages/runtime/src/domains/packages.ts, the two newpackages/specrequest/response schemas with regenerated route ledger / SDK / OpenAPI, tests, a new.changeset/28407-*.md(stop on breach; explain in the report). ⛔ No existing spec shape changes.Container & model:
M,mode:thread,model: default judgment tier (dispatch-gates --tier: no path-derived mandate),effort: xhighClause-②: yes (widening: one new protocol primitive, one new HTTP route and its two spec schemas; review at CONTRACT_REVIEW_TIER before the queue)
Metadata-protocol: to be derived from the diff (a new pair-creating door; the dev names the line)
Responsibility:
metadata-protocol's package install path | none: no path creates or ensures a .custom package today | Studio, the clone door N-21 (#28149) and the migration N-16, which all consume this doorThread-read: 6550829258
Premise: holds per triage 6550829258 (N-08 #28404 closed 2026-10-31T23:30Z); the dev re-measures the install path and N-08's constant on current main
Scope: the card's Wanted 1–6 and its tests, under epic ruling D-3 and Q4 「全部同意」 (explicit
<base namespace>_custom). Not here: N-08, N-14, N-16, N-19, N-21.Serial constraints cleared: #28414 (seat 4, branch
claude/project-thread-a0v6t5) also editspackages/runtime/src/domains/packages.ts; both sides work on merged code and the later PR rebases. No other in-flight claim names these files.Taken at 2026-11-01T02:11Z: p1, child N-13 of #28236, unblocked by triage at 2026-10-31T23:54Z; second of the three unblocked N-cards (the clone door #28149 waits on it).
objectstack-fleet commentedon Nov 1, 2026
domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
Claim amendment to
6552182806· 2026-11-01T02:20Z · one line correctedClause-②: yes (narrowing)
POST /packagesand the duplicate target will refuse an id ending in.customwhose manifest lacks the explicit<base ns>_customnamespace or the dependency on its base, where today it installs with the derived namespacecustom. The new door (POST /packages/:id/ensure-customizationand its two spec schemas) still widens.yes (narrowing)is the spellingclause2-line.mjsgives a diff that widens one surface and narrows another.packages/specManifestSchemais untouched, soMetadata-protocol: noneholds.CONTRACT_REVIEW_TIER; one review at that tier, naming the final head, is owed before the queue.objectstack-fleet commentedon Nov 1, 2026
domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
Claim amendment to
6552182806· 2026-11-01T02:54Z · one line correctedMetadata-protocol: added
packages/spec/authorable-surface/api.jsonwhen the spec build regenerates it.check-changeset-no-majorrefusesMetadata-protocol: nonewhile an authorable-surface baseline moves, and passes withaddedand@objectstack/specatminor(metadata-protocol,runtime,clientpatch).check-adr-0087-registrationalso passes, confirming theruntime-interface-onlyclaim onObjectStackProtocolImplementation.Clause-②: yes (narrowing)stands as amended in 6552182806.objectstack-fleet commentedon Nov 1, 2026
domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
os-dev-report
{ "issue": 28407, "status": "done", "branch": "claude/project-thread-7yvsri", "pr": "https://github.com/objectstack-ai/objectstack/pull/28512", "session": "session_01QUs3BtJ6E3jM8dgVzLbpGi", "premise_still_valid": true, "summary": "POST /api/v1/packages/:id/ensure-customization (protocol ensureCustomizationPackage, SDK client.packages.ensureCustomization, spec EnsureCustomizationPackageRequestSchema strict + EnsureCustomizationPackageResponseSchema six fields, ledger row) answers a read-only base's pair BASE.custom and creates it through installPackage with explicit namespace BASE_NS_custom, dependency ^MAJOR, version 0.1.0, type module; 201 created / 200 present; idempotent; single-flight per registry and pair id. Refusals leave nothing: 400 malformed or pair-of-pair, 404 not installed, 409 writable / no namespace / namespace too long / non-SemVer / existing pair lacking namespace or dependency (never adopted), 422 NAMESPACE_CONFLICT (also over a registry co-owner). Authority as the install door (manage_metadata; walled: operator only). POST /packages, installPackage and duplicatePackage refuse a pair-shaped id lacking the pair namespace or the base dependency, naming the door; an exported pair declaring both installs. Declaration: Clause-2 yes (narrowing), Metadata-protocol added, spec minor, others patch (check-changeset-no-major refuses none: the 7 new authorable-surface keys; seat agreed).", "tests": "runtime packages-ensure-customization.test.ts 35/35 (real protocol + SchemaRegistry, recording sys_packages publish); door-family sweeps (walled 14 doors, capability gate, closed query, handle-typing 501) + route-ledger conformance + install-dependency-missing: 7 files 243/243; spec package-lifecycle.test.ts 25/25; client packages-lifecycle + return-type-precision + url conformance + rest ledger coverage 18/18; named tests namespace-prefix.test.ts 10/10 and protocol.duplicate-package-target-namespace.test.ts 13/13; test-layer typecheck client/runtime/spec exit 0. Ablations 19/19 red via scripts/ablation-replace.mjs WRAP, each 'ok mutation landed' and 'ok restored: blob == HEAD, git diff HEAD empty' (A1 walled, A2 body, A3 query, A4 201, A5 POST gate (fallback arm only; composed arm held by the primitive), A6 capability, B1 namespace, B2 range, B3 idempotent, B4 writable, B5 owner, B6 single-flight (persist-failure race only; plain two-call test green both ways), B7 primitive predicate, B8 duplicate predicate, B9 pair-of-pair sentence, B10 base pair-namespace, B11 existing lacks, B12 SemVer, B13 not installed). B legs rebuilt metadata-protocol dist (OS_SKIP_DTS tsup); ablation-dist-preflight marker reading exit 1 on every B leg because esbuild folds void \"MARKER\" to void 0; mutation in dist shown by every B leg red with runtime source untouched and emitted guard 'if (!base && (void 0, false))' for B13; dist rebuilt clean after, door test 35/35, git status empty.", "gates": "dispatch-gates --commands derived 108; each run with stdin closed, recorded 'COMMAND :: exit CODE'; --ran: 108 derived, 108 run, 0 NOT-MEASURED, all exit 0. First-run red: check:route-ledger-census (113 vs 114) fixed by --fix. After the predicate type fix (found by test-layer typecheck of runtime: TS2322 in domains/packages.ts) re-run exit 0: check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt, check:type-check-coverage, check:nul-bytes, check:dispatcher-error-vocabulary, check:undeclared-dep-imports, check:route-ledger-census. pnpm build (full, under lock) VERDICT command-exit 0; pnpm lint exit 0; check-changeset-no-major with a pull_request payload (added) exit 0; check-adr-0087-registration exit 0 (runtime-interface-only packages/metadata-protocol/src/protocol.ts#ObjectStackProtocolImplementation verified). PR check-runs: not yet concluded at report time.", "dogfood_battery": "33590a408e75cead7e14e9d4ddcd7cdf38fcea97 · os-verify-lock: VERDICT command-exit 0 · held the lock 1229s (20m29s) · waited 0s · Test Files 363 passed | 3 skipped (366) · Tests 2926 passed | 28 skipped (2954) · no failures", "mcp_calls": "0 GitHub MCP calls (one claude-code-remote subscribe_pr_activity on #28512; reads were REST GETs)", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create, run 41271648138, body read back identical), POST /repos/objectstack-ai/objectstack/dispatches (this os-dev-report via post-stamped); plus git push", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — files outside the literal file surface, declared in the PR: runtime dispatcher-plugin.ts (mount), route-ledger.ts (row, census), client index.ts + 2 client tests, 4 door-family sweep tests, content/docs/api/client-sdk.mdx (packages row 17 to 18)", "carrier: none · noted, not filed — N-08 reads any id ending in .custom as a pair, so com.acme.custom is now judged as the pair of com.acme at the install doors", "carrier: none · noted, not filed — prerelease base: ^MAJOR is not admitted by ADR-0141 D2, so the ensure door answers 422 DEPENDENCY_MISSING (read, not tested)", "carrier: none · noted, not filed — cross-process concurrency falls to the sys_packages key (not measured); check:type-check-debt re-measures ledger entries only, so the runtime TS2322 was caught by check-test-typecheck, not by a derived gate" ] }objectstack-fleet commentedon Nov 1, 2026
domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
Landed · #28407 · 2026-11-01T06:08Z
0ecc2c494fc2d280829078566c8425cfe0231640, ACCEPT 6554051128 at head108a8625bcompletedby the PR'sFixes2026-11-01T06:07:53ZState change:
pm:dispatchedremoved. The card's Wanted items are on main:POST /api/v1/packages/:id/ensure-customizationandclient.packages.ensureCustomizationanswer a read-only base'sBASE.custompair. When the pair is missing, the door creates it with namespaceBASE_NS_customand a dependency on the base at^MAJOR.POST /packages,installPackageand the duplicate door refuse a pair-shaped id that lacks the pair namespace or the base dependency.The landing took two extra heads:
c85bce56mergedmainafter feat(metadata-protocol): one effective read serves the .custom declared override; the env-wide list and export agree; a withdrawn public form stays withdrawn #28507.108a8625fixed two feat(metadata-protocol): one effective read serves the .custom declared override; the env-wide list and export agree; a withdrawn public form stays withdrawn #28507 fixtures that the narrowing refuses.Each extra head got its own delta contract review (6553662707, 6553886687). Two observations from the review are recorded in the ACCEPT for the epic #28236 and not filed:
PATCH /packages/:idcan delete a pair's dependency, and artifact and marketplace installs bypass the predicate.