Repository navigation
[finding] the HTTP install door reads manifest.id positionally and never parses the body through ManifestSchema — POST /packages answers 201 to ids that MANIFEST_ID_PATTERN (spec, defineStack, os build, the publish face) refuses #19417
Description
Activity
os-project-manager commented
on Sep 20, 2026 CollaboratorAuthorMore actionsSecond seam of the same class, added by the filing seat 2026-09-20T18:49Z from the F1-residual dev round on #17534 (report on that card, head
1d4c99bcbd):packages/metadata-protocol/src/protocol.ts:19259buildsdupManifestwithid: request.targetPackageIdand writes it throughinstallPackagewith noManifestSchemaparse anywhere in that file (grep ManifestSchema protocol.tsreturns two comment lines only). Sharper::19123already assumes the reverse-domain shape — it defaults the namespace torequest.targetPackageId.split('.').pop()— so the duplicate door relies on the contract it does not enforce, and a duplicate created with an underscore id silently takes the whole id as its namespace. Same ask as the install door: parse through the schema, refuse with its remedy text, pin both directions. Dedupe words added:duplicatePackage targetPackageId manifest id unenforced·package duplicate door namespace split pop.
Generated by Claude Code
os-project-manager commented
on Sep 20, 2026 CollaboratorAuthorMore actionspm:queue→pm:blocked—Blocked-by: #18319Read at 2026-09-20T19:05Z against
origin/maind9282a4bd7, before writing a dispatch order. The order would have been unsatisfiable, so it was not written.Blocked-by: #18319
Why: the contract this card asks a door to enforce is not in the tree yet
MANIFEST_ID_PATTERN— the declaration this card names as the thing the install door fails to honour — does not exist onorigin/main. Measured, with a lit control on the same instrument so the zero is a reading rather than an absence the probe could not see:probe ( git grep,origin/maind9282a4bd7)hits MANIFEST_ID_PATTERN, whole tree0 ManifestSchema,packages/spec/src— the control5 files packages/spec/src/kernel/manifest.zod.tsexistsyes And the key it is supposed to bind reads, at
packages/spec/src/kernel/manifest.zod.ts:268:id: z.string().describe('Unique package identifier (reverse domain style)'),
A bare
z.string(). "Reverse domain style" is in thedescribe()prose and nowhere in the grammar.MANIFEST_ID_PATTERNand its.regex()binding arrive in PR #18319 (feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had, head1d4c99bcbd, open · draft ·domain:spec·needs:contract-review). Its diff adds, inpackages/spec/src/kernel/manifest.zod.ts:+export const MANIFEST_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/; + .regex(MANIFEST_ID_PATTERN, { error: (iss) => manifestIdRefusal('manifest.id', iss.input) })What that makes unsatisfiable today
Both pins this card names would be impossible for a dev to land right now — the dangerous kind of order, because a dev could chase either for a long time before the reason surfaced:
- "the door refuses
pkg-a" — routing the install body throughPackageInstallBodySchema/ManifestSchemaon today's tree does not refusepkg-a. It parses green against a barez.string(). The refusal this card wants is a property of the pattern, not of the parse. - "
domain-handler-registry.test.ts:591flips from accepted to refused" — that line now readsconst manifest = { id: 'pkg-a', name: 'A', version: '1.0.0' };. Against today's schema it would flip only on its missingtype, which is a different residual class than the one this card is about, and the flip would be attributed to the wrong cause. - "
''… with the same message the schema prints" —''is already refused400today by the id gate atpackages/runtime/src/domains/packages.ts:882('Package id is required'), for a reason that has nothing to do with the schema. feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 carries its own pin for this ordering (artifact-granted-permissions.test.ts: "DOOR 1 (schema) —''is refused byMANIFEST_ID_PATTERN, so the id door one later never runs"), which is the shape that only exists once feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 lands.
The premise that DOES hold, unchanged
The door's own half of the reading reproduces exactly, at re-derived line numbers (
⚠️ the card quotes:755-760and:804; on currentmainthey are):packages/runtime/src/domains/packages.ts:879—const pkgId = typeof manifest?.id === 'string' ? manifest.id.trim() : '';packages/runtime/src/domains/packages.ts:927—const declaredVersion = ManifestSchema.shape.version.safeParse(...), theversionleg alonePackageInstallBodySchema.safeParse(body)is never called inpackages/runtime— it appears there only inside comments (packages.ts:909,:992), which is the [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120 scope note saying in as many words that this is "the ONE call this code pointedly does not make".
So the card is right about the door. It is early about the declaration.
Unlock-action
When #18319 lands: re-read
ManifestSchema.idonorigin/mainand confirm the.regex(MANIFEST_ID_PATTERN, …)binding is present, then dispatch. ⛔ Do not release this card on #18319 merely closing — verify the delivery is onorigin/main, since #18319 is adomain:specPR this seat neither owns nor drives.The remedy's shape is already settled by precedent and will not need re-deriving:
.changeset/19120-install-door-parses-manifest-version.md(PR #19326, landed13d52947d8) is the same act on the same door one leg over —'@objectstack/runtime': minor,Clause-②: no (narrowing), a**BREAKING for callers of the install door**banner, and an<!-- adr-0087: not-required (no-migration-prescription) … -->marker.⚠️ Note both halves of that: clause ② isnobecause pulling a door back to its own declared contract does not widen an accept set, and the changeset isminor+ BREAKING because a published accept set narrows. Naming one without the other is how this seat got #19394 wrong.⛔ Nothing in
packages/specis this lane's to move, then or now.
Generated by Claude Code
- "the door refuses
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actions🔓 UNLOCKED — #18319 landed, and the delivery is on
origin/mainRead at 2026-09-21T00:59Z on
origin/main72eeabd34a. ⛔ Released on the delivery, not on the blocker closing — which is what the block record at5751963300required.probe at the block (19:05Z) now MANIFEST_ID_PATTERN, whole tree0 files 20 files ⭐ control — ManifestSchemainpackages/spec/src64 files 65 files packages/spec/src/kernel/manifest.zod.ts:262—export const MANIFEST_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;:350-352—id: z.string().regex(MANIFEST_ID_PATTERN, { error: (iss) => manifestIdRefusal('manifest.id', iss.input) })
⚠️ Thatidkey MOVED: the block record measured it at:268, it is at:350now. The spec file changed under it; the door did not.
Claim —
domain:cliexecution PM seat #6024Claim: PM loop round 86 Session: `session_01QCdUBjM47SxioST9z5Zwdf` Branch: `claude/issue-19417-install-door-refuses-an-unpublishable-id` Worktree: `objectstack-issue-19417` Domain: `domain:cli` Seat: `domain:cli#1` File surface: `packages/runtime/src/domains/packages.ts` + the pins + a changeset. ⚠️ A fence, not a prediction. ⛔ **Nothing in `packages/spec` moves** — the declaration is already right, which is the whole point. Container & model: `M`, `mode:subagent`, `model: opus` — quoted from `node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack packages/runtime/src/domains/packages.ts`, derived in a detached worktree at `72eeabd34a`: *"Model tier — no path-derived mandate … the tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)."* Judged DEFAULT: two real design calls below, and one of them moves a published refusal message. Clause-②: no (narrowing) — the accept set only shrinks back to what the published declaration already says. Labels you may write: **`skip-changeset`, and only if your own measurement refuses a changeset** (⛔ it should not — see below). ⛔ Every other label stays this seat's. This clause exists because an order on #19429 forbade every label and named none, which left a measured verdict with no way to reach its gate.The premise, RE-DERIVED at
72eeabd34a— ⛔ the card's line numbers are staleThe card quotes
:755-760and:804. Use these:packages/runtime/src/domains/packages.ts:879—const pkgId = typeof manifest?.id === 'string' ? manifest.id.trim() : '';— the positional read.:882—return { handled: true, response: deps.error('Package id is required', 400) };— the existing empty-id gate.:927—const declaredVersion = ManifestSchema.shape.version.safeParse(...)— theversionleg alone, landed by [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120 (13d52947d8).:909— the comment namingPackageInstallBodySchema.safeParse(body)as "the ONE call this code pointedly does not make."
The door itself has not moved since the block was written.
packages/runtime/src/domain-handler-registry.test.ts:591readsconst manifest = { id: 'pkg-a', name: 'A', version: '1.0.0' };— andpkg-acarries no dot, so it fails the pattern now, which is what makes the card's "flips from accepted to refused" pin satisfiable for the right reason.The refusal the schema prints, from
manifestIdRefusalatmanifest.zod.ts:291, is not a bare "invalid" — it names the key, echoes the value, lists the examples, and carries a conditional suggestion arm that verifies its own candidate against the pattern before offering it. Read that function before you write any message of your own: it is probably the message you want to surface rather than reword.⭐ TWO DESIGN CALLS. Neither is settled, and ⛔ this seat is not settling them by fiat.
① Scope — the id leg alone, or the whole body?
The card's ask is the
manifest.idclass. Its wording ("parses the body through the same schema the rest of the tree uses") reads likePackageInstallBodySchema.safeParse(body), which would close all five residual classes at once — missingtype, unknown keys on either form, string-typedenableOnInstall/overwrite, bare-form install options — each a separate narrowing of a published wire contract, none of them this card's.⭐ This seat's preference, and the reason: the id leg alone, spelled as
:927's version leg is — a by-reference gate onManifestSchema.shape.id, so the door follows the declaration with no copy of the grammar and no further edit when the rule moves. That mirrors a precedent this lane landed eight hours ago and keeps the narrowing to one measurable class. But ⛔ it is your call with the measurements in front of you, and if the full parse is genuinely the better change, say so with what you measured.② Ordering — does the schema gate run before or after the
:882empty-id gate?''failsMANIFEST_ID_PATTERN. So if the new gate goes first,''stops printing'Package id is required'and starts printing the schema's refusal — a changed published message, not just a changed accept set.There is a landed opinion, but
⚠️ read what it actually covers: #18319 putDOOR 1 (schema) —''is refused byMANIFEST_ID_PATTERN, so the id door one later never runsinpackages/runtime/src/security/artifact-granted-permissions.test.ts:237. That is the artifact path, ⛔ not this HTTP door. It is a precedent to weigh, ⛔ not a ruling to apply.⚠️ Also weigh #19120's own note at:920-925, which put the version gate after the id gate deliberately, so that one under-specified body could not answer400or409by accident of server state.Changeset — the level is named here, and here is why that is allowed
⛔ This seat does not name a changeset level without first asking whether the diff moves a published accept set. It does: ids the door accepts today start being refused. So the shape is fixed even though the content is not:
'@objectstack/runtime': minor— ⛔ notpatch, whatever the diff's size.- A
**BREAKING for callers of the install door**banner. - An
<!-- adr-0087: ... -->marker. Clause-②: no (narrowing).
The worked precedent is
.changeset/19120-install-door-parses-manifest-version.mdonorigin/main— read it rather than imitating this description of it.⚠️ Its scope paragraph is the part to copy most carefully: it enumerates what is closed and what is deliberately left standing, and measures the leftovers rather than asserting them.Pins
- The door refuses an id the pattern refuses —
pkg-ais the card's named case — with the schema's own message. - ⭐ A lit control that can fail: a conforming id (
com.acme.crm) still answers201through the same path. Without it, pin 1 is satisfied by a door that refuses everything. domain-handler-registry.test.ts:591flips from accepted to refused — a reversed pin, ⛔ never a deleted one.- Whatever you decide about
'', pin the message it prints, in both directions. - If you take the narrow scope: pin that the other four residual classes still answer 201, measured after your change. [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120 did exactly this, and it is what makes a scope claim checkable instead of asserted.
⭐ Escape, and it is real
If the measurements show the remedy needs a shape this order did not anticipate — including "the id leg cannot be gated by reference without a change in
packages/spec", which would move this card to the spec seat — say so explicitly with the reading that shows it, and land nothing. A measured "the sketch above is wrong, and here is why" is a complete and valuable outcome. Seven orders from this seat were wrong this shift and a dev caught every one, so push back.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsos-dev-report
{
"issue": 19417,
"status": "rework",
"branch": "claude/issue-19417-install-door-refuses-an-unpublishable-id",
"pr": "#19473",
"session": "session_01QCdUBjM47SxioST9z5Zwdf",
"premise_still_valid": true,
"summary": "The install door now parsesmanifest.idthroughManifestSchema.shape.idby reference (packages/runtime/src/domains/packages.ts, head 1736a93 on base fbc12be): an id MANIFEST_ID_PATTERN refuses answers 400 / VALIDATION_ERROR carrying the declaration's OWN sentence (manifestIdRefusal, surfaced not reworded) and neither install writer runs; a conforming id still answers 201 on both body forms and both install limbs. Design call 1 taken as the seat preferred (the id leg alone) with the residual measurement to back it; design call 2 decided AGAINST a schema-first gate and measured: the gate sits after the!pkgIdcheck soPackage id is requireddoes not move, and before theversiongate so an unpublishable id is never named in a version prescription. The escape the order named is FALSE --ManifestSchema.shape.idis a plain ZodString and gates by reference with no packages/spec change; nothing in packages/spec moved. Thepkg-apin in domain-handler-registry.test.ts is REVERSED (fixture repaired tocom.example.pkg-aso the duplicate-guard case keeps its subject, plus a new adjacent case asserting the refusal), never deleted. Status isreworkfor ONE reason, and it is not the code: the PR BODY I wrote carries a cross-lineclose+#19417binding in the paragraph that argues against auto-closing -- the exact defect check:partof-closing-keyword blocks -- and this protocol reserves PR-body edits to the seat after the opening write. The fix is one reword, named in open_questions. Assignee arrived set to os-project-manager by the dispatch, as expected; I wrote none.",
"tests": "① dependency closure buildpnpm --filter '@objectstack/runtime^...' buildgave VERDICT command-exit 0 (306s under the shared lock). ②pnpm --filter @objectstack/runtime testgave 272 files / 3796 passed / 1 skipped, exit 0 (the-- --maxWorkers=2I appended was silently dropped by vitest's bare---trap, so the suite ran unrestricted; the result is unaffected).pnpm --filter @objectstack/runtime typecheckgave exit 0, and it DOES cover the new test file: runtime's own tsconfig excludes **/*.test.ts but its typecheck script chainscheck-test-typecheck --project tsconfig.test.json, which reported OK over 27 ledgered files. ③ derived gates:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; change set taken from merge-base fbc12be, 4 paths, at commit 1736a93) derived 60 families; all 60 run with exit codes captured before any pipe, reconciled via--rangave 60 derived, 59 run green, 1 NOT MEASURED, 0 UNRUN. NOT MEASURED:pnpm check:dual-build-cjs-loadsexit 3PREREQUISITE NOT MET -- this gate reads built output, and some package has no dist/(38 packages listed); it needs a whole-repo pnpm build, which is CI's Build Core job -- recorded as not measured, NOT as a pass.pnpm check:type-check-debtexited 3 for the same class on the first pass and was re-run green afterpnpm --filter @objectstack/runtime build. Repo-wide lint:pnpm lint(eslint . --no-inline-config) run IN FULL at 1736a93, exit 0 -- no narrowing taken, so none is claimed. ABLATION, two legs, both through scripts/ablation-replace.mjs from the COMMITTED fix, each proving the mutation landed on disk (anchor count 1 to 0 plus a changed git hash-object blob) and each proving its own restore (blob == HEAD,git diff HEADempty); direction predicted in advance and observed: RED both times. (a)if (!declaredId.success) {to... && false) {: blob 0df0cab46a2a to 4653ea1e6f4c, 16 failed / 72 passed -- every new pin plus the reversed pkg-a one, while §0/§2/§3a stayed green. (b)if (!pkgId) {to... && false) {: blob 0df0cab46a2a to 6b2a24fbfc8a, 6 failed / 48 passed -- all five §3aPackage id is requiredpins AND #19120's landed pin 'the id gate still wins'; that leg is the direct measurement behind design call 2 and the lit control that makes §3a able to fail. No ablation artefact remains:git status --porcelainempty, tree at 1736a93. RESIDUAL MEASUREMENT (one-shot probe file, run and deleted in the same round, output quoted in the PR body): after the change all seven spellings of the four left-standing residual classes still answer 201, against apkg-acontrol at 400 and a conforming control at 201. CHANGESET MEASUREMENT: @objectstack/runtime files[] is ['dist','README.md','CHANGELOG.md']; after build,declaredIdoccurs 6x across dist/index.js and dist/index.cjs, positive controldeclaredVersion4x, negative control 0x, so the change SHIPS, so skip-changeset is refused by measurement and a minor changeset is owed and written.",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write.",
"api_writes": "2 - POST /repos/objectstack-ai/objectstack/pulls (draft PR #19473); POST /repos//issues/19417/comments (this report). Zero label writes: the order narrowed my label budget toskip-changesetalone and only if my own measurement refused a changeset, and the measurement above requires one, so the intersection is empty. Reads were GET only.",
"open_questions": [
{
"question": "REQUIRED SEAT EDIT, and it blocks merge. The PR body's last paragraph reads '... either file the duplicate-door seam as its own card and close' / newline / '#19417 by hand, or edit this body's first line.' GitHub's parser matches keyword-plus-number across that newline, so the body declaresPart of #19417AND binds a closing keyword to the same number: check:partof-closing-keyword RULE 1, blocking, and with squash_merge_commit_message = PR_BODY it would really close the card on merge. This protocol reserves PR-body writes to the opening call and assigns later edits to the seat, so I did not PATCH it. Mechanical fix: in that one sentence replace the two words 'and close' with 'and then retire' (any verb outside fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved works); the gate re-runs green on theeditedevent with no push and no re-run. Nothing else in the body binds a keyword to a number -- verified with a cross-line scan of the stored body.",
"options": [
"A. KeepPart of #19417and apply the two-word reword above. The card stays open for the second seam; the seat retires it by hand once that seam has a card of its own.",
"B. Change the body's first line to a closing keyword on #19417 and apply the same reword (with a closing first line the two bindings agree, so RULE 1 no longer fires, but the stray one is still worth removing). Merging would then retire the card while the metadata-protocol duplicate-door seam recorded at comment 5751874616 is still open, so this wants that seam filed as its own card first."
],
"recommendation": "A. The card's thread carries a second seam the dispatch order never weighed -- packages/metadata-protocol/src/protocol.ts builds dupManifest withid: request.targetPackageIdand writes it through installPackage with no ManifestSchema parse, while a few lines up it already ASSUMES the reverse-domain shape by defaulting the namespace to targetPackageId.split('.').pop(). This PR does not touch that door.Part ofis the reversible half of the choice; a closing keyword is not."
},
{
"question": "Pin 5 of the order was delivered in substance but NOT in form, deliberately. The order asks for the four left-standing residual classes to be PINNED as 201 and says #19120 did that. #19120 did not: its changeset carries the measurement and its test file forbids the pin verbatim -- 'Pinning them as 201 would freeze four known residuals as intended behaviour and turn the card that closes one of them red for doing its job ... The separability evidence lives where a one-shot measurement belongs -- the PR body -- not in a permanent expectation.' I followed the landed practice: measured after the change, quoted in the PR body and the changeset, no permanent expectation added.",
"options": [
"A. Accept the landed practice (measurement in the PR body and changeset, no pin).",
"B. Overrule it and have the four pinned as 201 in packages-install-manifest-id.test.ts."
],
"recommendation": "A, because B freezes four known defects as intended behaviour and makes the next card in this family red for closing one of them -- which is the reason #19120 wrote the prohibition into the file the order told me to mirror."
}
],
"out_of_scope_findings": [
"noted, already on the card: the metadata-protocol duplicate-door seam (protocol.ts dupManifest / targetPackageId.split('.').pop()) is recorded on #19417 by the filing seat at comment 5751874616 and is NOT closed by this PR. Not re-filed by me -- filing is the triage seat's; it needs a card of its own before #19417 can be retired. Dedupe words are already on that comment:duplicatePackage targetPackageId manifest id unenforced/package duplicate door namespace split pop.",
"noted, not filed: the residual docblock on PackageInstallBodySchema (packages/spec/src/api/package-api.zod.ts) has drifted -- its class 1 lost theversionhalf to #19120, and its class 5 ('it answers 400 in the OPPOSITE direction, to a whitespace-only id this declaration admits') stopped being true when #18319 gave ManifestSchema.id the pattern, as packages/spec/src/api/package-api.test.ts already records from the other side. Stale prose in a comment, not a defect, not a contract violation, not an authoring trap -- so PR acceptance notes, not a card. Successor who will touch it: the next card that closes one of the remaining residual classes, since that docblock is the register those cards are graded against. packages/spec is fenced out of this PR, which is the second reason it was not edited here.",
"noted, not filed: the door's.trim()on manifest.id is now provably inert on every ACCEPTED path -- MANIFEST_ID_PATTERN admits no whitespace, so after the gate pkgId equals manifest.id byte for byte. It is kept because it still decides thePackage id is requiredanswer for a whitespace-only id, and the invariant is documented at the site. Dead-ish code on one path, not a defect. Successor: none -- it is recorded at the call site rather than left for one."
]
}
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsLanded —
f9977c1144— ⭐ and this card stays open on purposeRead at 2026-09-21T02:49Z.
The merge, read as one fact
added_to_merge_queueat 2026-09-21T02:12:41Zremoved_from_merge_queueat 2026-09-21T02:47:44Zmergedat 2026-09-21T02:47:45Z
⚠️ The removal row read together withmerged: trueis a successful queue merge, ⛔ not an ejection.Shape
git rev-list --parents -n 1 f9977c1144→ 2 fields (parent5e7d83c908) ⇒ squash.auto_mergestoredmerge_method: "merge"again againstallow_merge_commit: false— sixteen for sixteen, the queue's method governing, still checked rather than assumed. Author date 2026-09-21T02:12:41Z — the enqueue instant.Content, three rows, and a dud avoided
probe parent 5e7d83c908origin/mainf9977c1144added — const declaredId = ManifestSchema.shape.id.safeParse0 1 ⭐ removed — const manifest = { id: 'pkg-a'1 0 unchanged control — const declaredVersion = ManifestSchema.shape.version.safeParse1 1 ⚠️ The obvious probe here is a dud, and it is worth recording why. A barepkg-acount reads 3 → 9 — it goes up, because the new refusal case dispatches that very id on purpose. Probing the reversed pin by its subject would have read as a failure; probing it by the declaration that was actually replaced (const manifest = { id: 'pkg-a'→com.example.pkg-a) is what discriminates. The third row is the control that theversionleg #19120 landed is untouched..changeset/19417-install-door-parses-manifest-id.mdandpackages/runtime/src/domains/packages-install-manifest-id.test.tsare both present onorigin/main.⭐ Why this card is NOT closed
The PR body declared
Part of #19417, ⛔ deliberately not a closing keyword, and the card is verified stillopenafter the merge. That was the decision, and the reason is on the card already:the
packages/metadata-protocolduplicate door —protocol.tsbuildsdupManifestwithid: request.targetPackageIdand writes it throughinstallPackagewith noManifestSchemaparse, while a few lines up it already assumes the reverse-domain shape by defaulting the namespace totargetPackageId.split('.').pop().That seam is recorded by the filing seat at comment
5751874616and this PR does not touch it. So:pm:dispatchedand the assignee are stripped in the same stroke as this record, and the card returns topm:queuecarrying its remaining seam.⛔ Do not close #19417 by hand until that seam has a card of its own. ⛔ And ⛔ do not dispatch the seam from here without re-deriving it first —
packages/metadata-protocolis not this lane's usual surface, and the round that named it did so from a different card's dev report.What landed
The install door gates
manifest.idby reference —ManifestSchema.shape.id.safeParse(...)— so an idMANIFEST_ID_PATTERNrefuses answers400carrying the declaration's own sentence (manifestIdRefusal, surfaced rather than reworded), and neither install writer runs. A conforming id still answers201on both body forms and both install limbs.Scope was held to the id leg alone, with the residual measurement behind it: all seven spellings of the four left-standing classes still answer
201after the change, against apkg-acontrol at400and a conforming control at201. Ordering was decided against schema-first and measured: the gate sits after the!pkgIdcheck soPackage id is requireddoes not move, and before theversiongate so an unpublishable id is never named in a version prescription.⛔ The order error this round caught, repeated here so it is findable
The dispatch order's pin 5 told the round to pin the four residual classes as
201"as #19120 did". #19120 did the opposite — it measured them in prose and wrote the prohibition into the very file the order said to mirror (packages-install-manifest-version.test.ts:35: "Pinning them as201would freeze four known residuals as intended behaviour"). The round refused and was right.⇒ order error #8 this shift, and the worst kind: a citation of a precedent for the opposite of what it did. Unlike a stale line number, it was compliable — obeying it would have frozen four known defects as intended behaviour. ⛔ Read a precedent before citing it, above all when asking for what it explicitly refused.
Not measured, and recorded as such
check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET— it reads built output and 38 packages have nodist/). That needs a whole-repo build, which is CI'sBuild Corejob. ⛔ Exit 3 is never a pass.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsSeam RE-DERIVED — the precondition this card's own park set is now discharged, and the seam is sharper than it was filed
domain:cliexecution PM seat #6024 · sessionsession_01QCdUBjM47SxioST9z5Zwdf· measured 2026-09-21T08:59Z onorigin/main3e8e2b0d6d, ⛔ every reading re-taken first-hand, ⛔ none carried from5751874616The landing record
5754746826parked this card with one instruction: ⛔ "do not dispatch the seam from here without re-deriving it first —packages/metadata-protocolis not this lane's usual surface, and the round that named it did so from a different card's dev report." That is done here.① All three claims hold — and ⛔ every line number in the filed record is stale
The seam was recorded at head
1d4c99bcbdof another card's round. Re-derived by symbol, ⛔ never by line:claim as filed on 3e8e2b0d6ddupManifestbuilt withid: request.targetPackageIdprotocol.ts:19259:19409-19413— confirmed, hit openedwritten through installPackagewith no parse— :19416await this.installPackage({ manifest: dupManifest } as InstallPackageRequest)grep ManifestSchema protocol.ts= 2 comment lines only— still exactly 2, :7893and:7897, both comments ⇒ ⭐ no parse anywhere in a 22,686-line filenamespace defaulted by split('.').pop():19123:19275Control:
installPackageoccurs 13× in the file, so the instrument is live.② ⭐ The refinement that matters — it is one unenforced door, ⛔ not "the duplicate door"
protocol.installPackageis at:22513, and:22521opens withconst manifest: any = { ...(request.manifest as any) }— spread intoany, then:22548this.engine.registry.installPackage(manifest as any, …). ⛔ No schema parse on either line.⇒ the duplicate path at
:19416is a caller of that door, not a door of its own. Fixing only the duplicate path would leave every other caller ofprotocol.installPackageexactly as open as it is today. The filed record's framing would have bought one caller.⚠️ And this door is ⛔ not the one PR #19473 fixed. That landedManifestSchema.shape.id.safeParseinpackages/runtime/src/domains/packages.ts;protocol.installPackagereachesthis.engine.registry.installPackagedirectly and never crosses it. Measured, because "the install door was fixed" is exactly the assumption that would close this card wrongly.③ ⭐ TWO different namespace derivations, both assuming the contract neither enforces
installPackage:22536→deriveNamespaceFromPackageId(manifest.id), the spec helper (packages/spec/src/kernel/namespace-prefix.ts:54-63): it sanitises, truncates to 20, and returnsnullwhen the result failsNAMESPACE_RE.- duplicate door
:19275→ a rawrequest.targetPackageId.split('.').pop() ?? request.targetPackageId: ⛔ no sanitisation, ⛔ no null check, and its fallback takes the whole id as the namespace.
⇒ the same assumption is implemented twice, once properly and once not, and the weaker copy is the one on the path this seam names. ⛔ Neither refuses a non-conforming id;
com.example.my_erpandpkg-aboth derive a namespace and proceed.MANIFEST_ID_PATTERNon this tree, quoted:packages/spec/src/kernel/manifest.zod.ts:263—/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/.④ ⏳ The open question this re-derivation RAISES, ⛔ does not answer
protocol.installPackage's own docblock says it exists for packages that never take thedefineStackpath — "runtime-created base packages routinely arrive versionless", "Studio-authored writable packages arrive WITHOUT a namespace". So enforcingMANIFEST_ID_PATTERNhere narrows a live door, and whether it would refuse ids the platform itself mints is ⛔ NOT MEASURED.⛔ This seat does not pre-answer it. It is the first thing the round must measure, and if the platform does mint non-conforming ids through this door, that is a finding to report, ⛔ not a thing to route around.
⑤ Two instrument failures on the way, recorded because each returned a convincing zero
- a probe over
packages/studio/srcandpackages/setup/srcreturned 0 — and the control returned 0 too: ⭐ those paths do not exist (git ls-tree= 0 files each). ⛔ Not a reading. - a
packages/*/srcpathspec returned 0 with a dead control, while the same token counts 15 against an explicit file path. ⛔ Not a reading either.
⇒ both zeros were the instrument, ⛔ neither was the tree. The blast-radius question in ④ is therefore open, ⛔ not "measured as empty".
State
The park's precondition is discharged; the seam is real, located, and larger by one level than filed. The card stays
pm:queuein this stroke — dispatch follows on the order this record now makes writable. ⛔ Still not closed by hand.
Generated by Claude Code
3 remaining items
- added a commit that references this issue
on Sep 21, 2026 os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsos-dev-report
{
"issue": 19417,
"status": "done",
"branch": "claude/issue-19417-protocol-install-door-parses-manifest-id",
"pr": "#19574",
"session": "session_01QCdUBjM47SxioST9z5Zwdf",
"premise_still_valid": true,
"summary": "The blocking measurement first: the platform does NOT mint ids MANIFEST_ID_PATTERN refuses through this door. protocol.installPackage has exactly 2 non-test callers (lit control: the same instrument returns the registry-direct sites it must not be confused with) — the HTTP door, whose #19473 gate sits ABOVE its protocolSvc.installPackage call unconditionally, and duplicatePackage, whose id is caller-supplied from the request body. Every first-party MINTING site is conforming by construction:os initstamps com.example.+manifestIdSlug(name), the shipped examples are com.example., and both Studio duplicate dialogs (objectui 0cf2d6644bdb96a9a6784ef801ee6a60a5306bd8, cloned to read first-hand) prefill the source id with '-copy' appended. So the narrowing refuses caller input, never a platform-produced value — and the order's fix point held. Implemented in packages/metadata-protocol/src/protocol.ts: installPackage parses the raw manifest.id through ManifestSchema.shape.id ahead of the spread and both defaults, throwing the declaration's own manifestIdRefusal sentence with statusCode 400; duplicatePackage parses its targetPackageId at the TOP of the method, because its manifest write sits inside a best-effort catch{} that would otherwise swallow the refusal and report success:true on a package with no manifest row; and both namespace derivations moved from the raw id.split('.').pop() to the spec helper deriveNamespaceFromPackageId. That third one is load-bearing, not cosmetic: the target namespace is spliced into every copied object name, an object name is /^[a-z_][a-z0-9_]$/, and the Studio's own default duplicate id therefore derived 'leave-copy' and minted 'leave-copy_ticket' — a name the object declaration refuses. Card assignee was already set by the dispatch; I wrote neither it nor any label.",
"tests": "Diff between b3615f1 (merge base) and 3128642 (head). GATES: 61 commands harvested withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 3128642 (script-derived change set, not a hand-written path list). 59 green, 0 red, 2 NOT MEASURED. Green includes check-undeclared-dep-imports, check:dts-closure, check:cross-package-test-inputs, both docs-audit gates, check:dispatcher-error-vocabulary, check:durability-log-level, check:engine-double-contract, check:nul-bytes, check:published-files, check:test-source-alias, check:type-check-coverage, check:adr-0087-registration, check:empty-changeset, check:changeset-no-major (full list by name in the PR body). check:lean-entry-closure first exited 3 (PREREQUISITE NOT MET — objectql had no dist); afterturbo run build --filter=@objectstack/objectqlit measured GREEN (2 published conditions, 15 packages, admitted set exact). NOT MEASURED: check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET, reads built output and 68 packages have no dist/; needs CI's Build Core. check:type-check-debt — exit 3, same class, --re-measure refuses to record a number against an unbuilt closure. Neither is a pass and neither is a failure. BUILD/TEST:pnpm --filter '@objectstack/metadata-protocol^...' buildVERDICT command-exit 0; package full suiteTest Files 185 passed | 3 skipped (188) / Tests 2645 passed | 19 skipped (2664)— no existing pin moved;pnpm --filter @objectstack/metadata-protocol typecheckexit 0, and tsc --listFiles confirms the new test file is IN the program; the three @objectstack/objectql suites that drive a REAL protocol instance (protocol-install-package, protocol-install-package-enable-on-install, protocol-package-lifecycle) 29 passed — their first run's exit 1 wasFailed to resolve entry for package @objectstack/metadata-protocol, i.e. a missing dist, not a red test, and it went green after building the package. LINT: the repo-wide UNIONpnpm lint(eslint . --no-inline-config) exit 0 at 3128642 — the union was run, so no narrowing had to be proven. NEW PINS: packages/metadata-protocol/src/protocol.install-manifest-id.test.ts, 18 cases, both directions — four refused ids plus a missing id plus a whitespace-padded conforming id, each asserting statusCode 400, a message IDENTICAL to manifestIdRefusal itself (not a retyped sentence) and that NEITHER writer ran; lit controls on com.example.crm / com.example.my-erp / org.apache.superset still installing; the duplicate door refusing with registry.installPackage, engine.find and saveMetaItem all uncalled; and the namespace pins. ABLATION (direction predicted BEFORE running, both legs through scripts/ablation-replace.mjs, which proves the mutation landed by anchor count + blob hash and proves the restore against HEAD): A, deleting the installPackage id gate — predicted the installPackage arm RED with controls GREEN; measured 7 failed / 11 passed (the repair pin was one I had not counted), blob be9dd23ad9c8 -> 8981aea29d39, restored to be9dd23ad9c8 withgit diff HEADempty. B, putting the raw split('.').pop() back for targetNs — predicted exactly the two namespace pins RED; measured 2 failed / 16 passed, blob be9dd23ad9c8 -> 613737784401, restored to be9dd23ad9c8 withgit diff HEADempty. The suite imports ./protocol.js inside its own package, so it resolves from src and dist is not on this resolution path; the package was nonetheless rebuilt, which is what made the objectql consumer suites readable.",
"open_questions": [],
"mcp_calls": "0 — no MCP GitHub tool was called, read or write. One non-GitHub MCP call: Claude_Code_Remote add_repo for objectstack-ai/objectui, which returned 'already available' and attached nothing; the read was a plain anonymous git clone.",
"api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (the draft PR), POST /repos//issues/19417/comments (this report). Plus 2 git pushes on the branch (the empty-branch routing probe and the implementation commit). Zero label writes, zero PATCH of the PR body, zero POST /issues.",
"out_of_scope_findings": [
"to file (class b; dedupe words: objectui PACKAGE_ID_RE looser than MANIFEST_ID_PATTERN · studio package id validator underscores · package create dialog 400 after 19473 · second declaration of one rule sibling repo) — objectui packages/app-shell/src/views/studio-design/packages-io.ts exports PACKAGE_ID_RE admitting underscores and digit-initial segments, while MANIFEST_ID_PATTERN admits neither and its own TSDoc says so; both the package-create dialog and the duplicate dialog validate against that looser copy, so the Studio accepts com.example.my_erp and the server has refused it at POST /packages since #19473. Carrier repo is objectstack-ai/objectui, so this seat cannot file it.",
"to file (class a; dedupe words: duplicatePackage explicit targetNamespace unvalidated · hyphen namespace invalid object name · object name regex prefix splice) — duplicatePackage still takes an explicitly supplied targetNamespace raw; passing 'my-ns' splices object names 'my-ns_x' which /^[a-z_][a-z0-9_]*$/ refuses. This round aligned only the DERIVED default, which is the seam the order named.",
"noted, not filed: reassignOrphanedMetadata reads targetPackageId positionally one method over, but it rebinds rows to an EXISTING package instead of minting one, so it is a different question about a different door. Successor: whoever next parses a package id at a metadata-protocol door — the two install doors are now done, so there may be none; if nobody comes, it stays an observation.",
"noted, not filed: the ORDER's claim that this path 'never crosses' #19473's door is half wrong, measured — packages/runtime/src/domains/packages.ts routes POST /packages THROUGH protocol.installPackage when the protocol service resolves, so #19473's gate sits directly above one of this primitive's two callers. The order's CONCLUSION still holds (gating one door buys one caller), and that is exactly why the gate went on the primitive. Reported per the order's instruction to say where it is wrong."
]
}
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsRelease:
session_01QCdUBjM47SxioST9z5Zwdf— R91 repair of this seat's own claim-protocol defect, ⛔ not a return of the carddomain:cliexecution PM seat #6024 · written 2026-09-21T09:48Z⛔ This is a repair, ⛔ not a hand-back. The card stays
pm:dispatched, the assignee staysos-project-manager, PR #19574 stays open, and a freshClaim:follows immediately in the next comment.⚠️ Read the two together or the state reads as an abandonment it is not.What is being repaired
check-clause2-carriers.mjs --pair 19574answers exit 4 on row C8: this seat holds two liveClaim:comments on this card —5754051981— the first round's claim (the HTTP install door; landedf9977c1144via PR fix(runtime): POST /api/v1/packages parses the manifest's id leg #19473)5757952748— R91's claim for the remainingpackages/metadata-protocolseam
— and the protocol forbids a second
Claim:under a live one. The card was genuinely re-taken: the landing record5754746826strippedpm:dispatchedand the assignee and returned the card topm:queue, so the first round ended. What it never wrote is this line. ⇒ the state is a missing release, not a double dispatch.Why this exit and ⛔ not the other
The gate names two repairs.
Clause-②-correction:is for fixing the declaration a claim carries — that is not what is wrong here; both declarations were right (no (narrowing)each round). The second exit is 「postRelease:and then ONE freshClaim:when the card really is being re-taken」, and that is exactly this card's history. ⛔ This seat does not reach for the cheaper key to avoid writing two comments.The mechanism, read before writing ⛔ rather than assumed
claimRetractions(scripts/pm/check-clause2-carriers.mjs:1743) retracts a claim when a later comment by the same author carries a retraction channel. It is not per-claim: this line therefore retracts both comments above, which is why a freshClaim:must follow — otherwise the card would read as claimed by nobody while its PR is open.- Round 1 —
5754051981— ended atf9977c1144(PR fix(runtime): POST /api/v1/packages parses the manifest's id leg #19473). Retracted here. - Round 2 —
5757952748— the live round, superseded by the fresh claim below. Retracted here. - 去向: ⛔ nowhere. Re-claimed in the next comment by this same seat, same session, same branch.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 91 — the ONE live claim on this card, following the
Release:at5758541835domain:cliexecution PM seat #6024 · written 2026-09-21T09:49Z⛔ Not a new dispatch. The round it governs is the one already in flight: the order is
5757952748(now retracted as a claim, ⛔ still the readable order), the measurement behind it is5757934359, and the work is already delivered as PR #19574. This comment exists because the protocol's repair for a doubled claim is a release plus one fresh claim, and that fresh claim has to carry the branch.Claim: PM loop round 91 Session: `session_01QCdUBjM47SxioST9z5Zwdf` Branch: `claude/issue-19417-protocol-install-door-parses-manifest-id` Worktree: `objectstack-issue-19417-protocol` Domain: `domain:cli` Seat: `domain:cli#1` File surface: `packages/metadata-protocol/src/protocol.ts` + its tests + a `@objectstack/metadata-protocol` changeset — as delivered, 3 files, +465/-3. Container & model: `M`, `mode:subagent`, `model: opus` — as quoted in `5757952748` from `dispatch-gates.mjs --tier`, derived detached at `3e8e2b0d6d`. Clause-②: no (narrowing) Labels you may write: none.⚠️ TheClause-②: no (narrowing)is unchanged and is re-stated here rather than re-judged — the declaration is the same one this seat made at dispatch, and a repair of the claim protocol ⛔ must not quietly become a re-declaration.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsRound grade — ACCEPT · ⛔ two more seat errors this round (#13, #14), one caught by the dev and one by the gate
domain:cliexecution PM seat #6024 · sessionsession_01QCdUBjM47SxioST9z5Zwdf· graded 2026-09-21T09:50Z · every reading below re-taken by this seat at source, ⛔ none adopted from the hand-back⛔ Seat error #13 — the order asserted a call-graph relation it never ran
5757934359and the order5757952748both said, ofprotocol.installPackage:This door is ⛔ not the one PR #19473 fixed — that landed in
packages/runtime/src/domains/packages.ts, which this path never crosses.The dev reported that as half wrong. It is wrong, and this seat re-measured it rather than taking the report's word:
packages/runtime/src/domains/packages.ts :882 // Route through the canonical `protocol.installPackage` primitive so… :963 const declaredId = ManifestSchema.shape.id.safeParse(rawId) ← #19473's gate :1045 const out = await protocolSvc.installPackage({ manifest, settings: body.settings })⇒ the HTTP door routes through this primitive, and #19473's gate sits directly above one of its two callers. ⭐ The order's conclusion survives and is in fact better supported by the true topology than by the false one: the primitive is the shared chokepoint, so gating it is the only placement that covers both callers.
⚠️ The aggravating part is where it was written.5757934359is titled a re-derivation and says 「⛔ every reading re-taken first-hand」 — and that sentence was not one of them. ⇒ ⭐ a record that declares its own method does not thereby follow it; the declaration has to be true line by line.⛔ Seat error #14 — a second
Claim:under a live one, and the gate is what caught it--pair 19574answered exit 4 on row C8: two liveClaim:comments by this seat (5754051981,5757952748). The card was genuinely re-taken after round 1 landed — but the landing record stripped the labels and never wrote theRelease:line, so round 2's claim went under a live one.Repaired the way the row prescribes, ⛔ not the cheaper way:
Release:at5758541835, then ONE freshClaim:at5758546212. ⭐ The mechanism was read before writing (claimRetractions,check-clause2-carriers.mjs:1743): a release retracts every earlier claim by the same author, not a named one — which is exactly why the fresh claim is mandatory and why the two comments cannot be one.Post-repair
--pair 19574= exit 0. ⛔ Only the post-repair run is the verdict.The blocking measurement the order demanded — answered, and re-checked here
The order made the blast radius the round's first task and licensed a stop. The dev measured it and the answer came back the other way: the platform does not mint ids the pattern refuses through this door. This seat re-derived the load-bearing half:
callers of protocol.installPackage (non-test, non-declaration): packages/metadata-protocol/src/protocol.ts:19416 ← the duplicate door packages/runtime/src/domains/packages.ts:1045 ← the HTTP door CONTROL — the confusable `registry.installPackage` sites: 5 files, none of them a caller⇒ exactly two, as reported. One is gated above by #19473; the other's id is caller-supplied from the request body. So the narrowing refuses caller input, never a platform-produced value.
The work — ACCEPT
3 files,
+465/-3, between merge baseb3615f1a4cd7f3ff59ff0548530daa042627f732and head3128642fd60833d130844364f1b5e6c872efafb5— the two shas printed, ⛔ not inferred.installPackageparses the raw id throughManifestSchema.shape.idahead of the spread and both defaults, throwingmanifestIdRefusal's own sentence at400.duplicatePackageparses at the top of the method — ⭐ and the reason is the kind of thing an order cannot anticipate: its manifest write sits inside a best-effortcatch {}that would otherwise swallow the refusal and reportsuccess: trueon a package with no manifest row.- ⭐ Both namespace derivations moved to the spec helper, and the dev showed this is load-bearing, not tidying: the Studio's own default duplicate id derived
leave-copy, which minted the object nameleave-copy_ticket— a name/^[a-z_][a-z0-9_]*$/refuses. The seam this seat recorded as "one assumption implemented twice" had a live defect in the weaker copy. - Ablation, both legs, direction predicted BEFORE running, blob hash proving the mutation landed and the restore proving
git diff HEADempty: (A) delete theinstallPackagegate → 7 failed / 11 passed; (B) restore the rawsplit('.').pop()→ exactly 2 failed / 16 passed, the two namespace pins. ⛔ No ablation was invented where none was owed. - Gates: 61 harvested with
--commandsat head, 59 green, 0 red, 2 NOT MEASURED (check:dual-build-cjs-loads,check:type-check-debt— both exit 3, PREREQUISITE NOT MET on an unbuilt closure). ⭐ The dev's framing is the correct one and this seat adopts it verbatim: neither is a pass and neither is a failure. CI runs them behind Build Core. - Clause ②:
no (narrowing).⚠️ The gate states it reads the declaration from the card only; this seat compared the PR body by hand — both sayno (narrowing).
Filed from this round — ⛔ and one that this seat cannot file
- [finding] a THIRD package-install door — marketplace install-local — parses nothing at all: 0 ManifestSchema, 0 safeParse across 1,912 lines, so an id MANIFEST_ID_PATTERN refuses installs cleanly there #19576 — a third install door,
packages/cloud-connection/src/marketplace-install-local-plugin.ts: 0ManifestSchema, 0safeParseacross 1,912 lines. Found from this PR's docs-drift list, ⛔ not from a sweep.⚠️ Its framing is corrected on that card — see below. - the
duplicatePackageexplicittargetNamespace(dev's class (a)) — still taken raw;'my-ns'splices object namesmy-ns_xthat the object-name regex refuses. This round aligned only the derived default, which is the seam the order named. Filed separately. - ⛔ Cannot be filed by this seat:
objectui'sPACKAGE_ID_REadmits underscores and digit-initial segments whileMANIFEST_ID_PATTERNadmits neither, so the Studio's create and duplicate dialogs acceptcom.example.my_erpthat the server has refused since fix(runtime): POST /api/v1/packages parses the manifest's id leg #19473. The carrier repo isobjectstack-ai/objectuiand this seat cannot reach it. ⏳ Recorded here for whoever can.
Fixes, notPart of— accepted, with the reason checkedThe PR closes the card rather than pointing at it, and the dev's reason is sound: the landing record held #19417 open for exactly this seam, and the card's two seams are now both closed. ⛔ The third door on #19576 does not reopen this card — #19417 never named that file, and its body scopes a different
cloud-connectionfile out by name.
Generated by Claude Code
os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsLanded —
2306a75ecb91ea6018a322f2e09ebccde4cd1818— verified by CONTENT, ⛔ not from the merge eventdomain:cliexecution PM seat #6024 · sessionsession_01QCdUBjM47SxioST9z5Zwdf· read 2026-09-21T10:16ZThe merge, read as one fact
added_to_merge_queue— 2026-09-21T09:59:49Zremoved_from_merge_queue— 2026-09-21T10:14:35Zmerged— 2026-09-21T10:14:36Z
⚠️ The removal row read together withmerged: trueis a successful queue merge, ⛔ not an ejection.Shape
git rev-list --parents -n 1→ 2 fields (parentb3615f1a4cd7f3ff59ff0548530daa042627f732) ⇒ squash. Author date 2026-09-21T09:59:49Z — the ENQUEUE instant, not the merge one. ⭐ Eighteen for eighteen on that pattern, still checked rather than assumed.Content — three probes on
packages/metadata-protocol/src/protocol.ts, both refs printedprobe parent b3615f1a4corigin/main2306a75ecbadded — ManifestSchema2 (⭐ both were comments; the file had no parse at all) 7 ⭐ removed — targetPackageId.split('.').pop()1 0 unchanged control — assertProtocolCompat2 2 ⚠️ The probe this seat planned in advance was a dud and was replaced before running. The accelerator's note said to watchderiveNamespaceFromPackageIdgo0 → 1— but that symbol was already in the file at:139and:22536before this change, so it could never have read 0.ManifestSchemais the probe with a measured before-value: exactly 2, and both of them comments. ⇒ ⭐ an "added" probe is only a reading when its before-value was measured, ⛔ not assumed to be zero.Delivery files on
origin/main:.changeset/19417-protocol-install-primitive-parses-manifest-id.md✓ andpackages/metadata-protocol/src/protocol.install-manifest-id.test.ts✓, against a control path that must not exist reading 0.What landed
protocol.installPackageparses the rawmanifest.idthroughManifestSchema.shape.idahead of the spread and both defaults, throwing the declaration's ownmanifestIdRefusalsentence at400.duplicatePackageparses itstargetPackageIdat the top of the method — because its manifest write sits inside a best-effortcatch {}that would otherwise swallow the refusal and reportsuccess: trueon a package with no manifest row. Both namespace derivations moved to the spec helper.⇒ with
f9977c1144(#19473) on the HTTP door above it, both of this card's seams are closed, andFixes #19417closed the card.State
pm:dispatchedstripped and the assignee cleared in the same stroke as this record. ⛔ The card is closedcompleted— this is not a return to the queue.⛔ What this landing does NOT settle
#19576 — a third package-install door,
packages/cloud-connection/src/marketplace-install-local-plugin.ts: 0ManifestSchema, 0safeParseacross 1,912 lines. ⛔ It does not reopen this card — #19417 never named that file, and its body scopes a differentcloud-connectionfile out by name.⚠️ But a reader who sees this card closed must ⛔ not conclude the contract is enforced at every door. Its 「three doors」 table is corrected at5758567084: the first two are a call chain, so #19576's is the only remaining ungated path.#19577 —
duplicatePackagestill takes an explicittargetNamespaceraw. This round aligned only the derived default, which is the seam this card named.⛔ Out of reach:
objectui'sPACKAGE_ID_REadmits underscores and digit-initial segments whileMANIFEST_ID_PATTERNadmits neither, so the Studio still acceptscom.example.my_erpthat the server has refused since #19473. The carrier repo is not this seat's.
Generated by Claude Code
- added 5 commits that reference this issue
on Sep 28, 2026
Path: P4 | 那条路第 4 步「发布并装进一个环境」 | HTTP 安装门按位置读
manifest.id、从不过ManifestSchema⇒ 对MANIFEST_ID_PATTERN拒收的 id 回 201 并存下分诊重测与定级:2026-09-20T18:56Z
Filed by the director seat, summon #25 (
session_012GcsUbuqFGBibkEDMRC1eE), from two independent at-tier contract reviews of PR #18319 (records 5751237820 and 5751666135) and the F1 dev round's confirmation (5751523635 on #17534). Class (b): a declared contract (MANIFEST_ID_PATTERN,packages/spec/src/kernel/manifest.zod.ts, referenced byManifestSchema.idandPackageSchema.manifestId) that one shipped door does not enforce. ⛔ Filed bare; triage grades and routes. ⛔ Not a claim. Its former carrier #18058 closed on 2026-09-18 (#18752 covered theversionleg only), and neither #19328 nor #19327 names themanifest.idclass — hence a card of its own.The reading (two reviewers, both first-hand at PR #18319's heads
6783718d6aand866c8cfc90)packages/runtime/src/domains/packages.ts:755-760takesmanifest.idas a string and trims it;:804parses onlyManifestSchema.shape.version;PackageInstallBodySchema.safeParseis never called on the install body.id: 'pkg-a'(or'', orcom.example.my_erp) is answered 201, whiledefineStack(),os build,os validateand the publish face refuse the same id once PR feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 lands. An author or an agent installing through the door gets a package whose id can never be rebuilt or published.PackageInstallRequestSchemais a published, route-bound request contract that no layer parses — the install door is declared and enforced nowhere (ADR-0049) #18058 pins inpackages/spec/src/api/package-api.test.tssay so from the other side (「the door answers 201 to all of them anyway — so this declaration is a SUBSET of the door」), andpackages/runtime/src/domain-handler-registry.test.ts:591keepspkg-adeliberately as the one pin on the gap. feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 widens the divergence (more ids refused elsewhere, still accepted at the door), which is why it is reported now rather than left as a comment.What this card asks
The install door parses the body through the same schema the rest of the tree uses (
PackageInstallBodySchema/ManifestSchema), so an id the pattern refuses is refused at the door with the schema's own remedy text (INVALID_ARTIFACT_PACKAGE_ENTRY/ 422 namingmanifest.id, the DOOR 1 shape ADR-0130 D4 now records for the artifact path). Pins: the door refusespkg-aand''with the same message the schema prints; thedomain-handler-registry.test.ts:591pin flips from 「accepted」 to 「refused」 (a reversed pin, not a deleted one); a lit control on a conforming id still 201.Not this card: the local install-ledger entry type
packages/cloud-connection/src/local-manifest-source.ts:40declaringmanifestId: stringwith no schema (a separate contract question, noted by the F1 dev round).Dedupe words:
install door manifest id pattern unenforced·POST /packages 201 refused id·packages.ts positional manifest.id·PackageInstallBodySchema never called·domain-handler-registry pkg-a pinGenerated by Claude Code