Skip to content

feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import - #29521

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-pr0nu5
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-pr0nu5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29373

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — an export response key the runtime writes; no metadata author writes it, so no skill or authoring doc teaches it

维护者速读

  • 改了什么:包导出清单(GET /packages/:id/export 的契约)新增可选键 withheldMaps,逐条列出导出时被整张扣下的连接器请求头 / 环境变量表的位置(只列位置,不列任何头名或变量名);非托管导入的键分类表把它记为 door。导出一直声明的 label 在该表里也从未分类,同 PR 一并记为 door。
  • 为什么改:导出读的是脱敏后的条目,连接器的四张凭据表在导出里直接缺席,导入方分不清"被扣下"还是"本来就没有"。这是 [Decision] connector credential follow-ups after #28596: header-value advisory, legacy auth keys at rest, whole-map withholding #28640 裁决 ③A 的 spec 半边,server 半边 packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342 等它落地后再写导出和两道安装门。
  • 风险与回滚:纯 spec 声明加一行分类,无运行时改动,导出暂不写该键。实测:改动前,带 label 或 withheldMaps 的导出被非托管导入整包拒收;改动后两者都被接受、新基座清单不带它们。回滚即 revert 本 PR。
  • 先看:packages/spec/src/api/package-lifecycle.zod.ts(新键与 describe)、packages/spec/src/kernel/unmanaged-install.ts(两行 door 及注释)、packages/spec/src/kernel/unmanaged-install.test.ts(全量性测试改为同时读导出清单的固定键)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts src/kernel/unmanaged-install.test.ts,预期 2 个文件全部通过,含「refuses a list of the withheld header names」与「classifies every fixed key of the package export」两条新用例。
  • 需要你做的:无,席位落地。

New names (proposed; the seat's call)

  • PackageExportManifestSchema.withheldMaps: a new fixed key on a published contract.
  • Its entry's sub-keys: type (the literal connector), name, path (closed set: providerConfig.headers, providerConfig.defaultHeaders, providerConfig.transport.headers, providerConfig.transport.env).
  • No new export: the entry is declared inline, so api-surface does not move.
  • Alternatives considered: withheld (shorter, but reads as a boolean), incompleteMaps (names the import's reading rather than the export's act), redactedMaps (collides with the redactor's internal redactedKeys, which is deliberately never put on the wire).

What changes

The path vocabulary, measured

The card spells three of the four maps without their root (defaultHeaders, transport.headers, transport.env). On main the connector redactor's CONNECTOR_CREDENTIAL_MAP_PATHS (packages/spec/src/integration/connector-credential-redaction.ts) holds all four under providerConfig, and redactConnectorItem reports them in redactedKeys as providerConfig.headers, providerConfig.defaultHeaders, providerConfig.transport.headers, providerConfig.transport.env. The closed enum uses that spelling, so the export can copy the redactor's own report. A closed set of four is right for this card: it is exactly the set the redactor withholds whole as maps. The redactor's other positions (undeclared auth keys, authentication keys, credential URL parts) are single values, not maps, and webhook headers is out of scope by the seat ruling. A test holds the enum equal to the redactor's list and to its redactedKeys on a real redaction, so the two cannot drift.

label, measured

The card noted (not measured) that the export's top-level label is not classified. Measured on this branch's base (04e0c2c0): PackageExportManifestSchema declares label; UNMANAGED_INSTALL_KEY_DISPOSITIONS has no row for it; the import planner's default arm refuses an unclassified key. A probe calling planUnmanagedImport with { id, name, version, label: 'Example CRM' }, run against a spec build with the two rows removed, answered UNMANAGED_INSTALL_UNCOPYABLE "label — not a key a package body declares, so the import cannot place it" (and the same for withheldMaps); against this PR's build both are accepted and the base manifest is { name, version, id, namespace }. So it is a real gap, classified here and declared as a deviation. It is door, not identity: the base is installed through the closed PackageInstallManifestSchema, which has no label, so carrying it would refuse the base. Reach: the export writes label only from pkg.manifest.label ?? pkg.label, and no schema in this tree declares either (ManifestSchema and PackageInstallManifestSchema are closed without label), so no declared producer here emits it today; the gap is between two spec contracts.

Tests

New:

  • package-lifecycle.test.ts: an export carrying withheldMaps parses and is preserved; the key is optional; the path enum equals CONNECTOR_CREDENTIAL_MAP_PATHS and the redactor's redactedKeys; five refusals (a position outside the four, a position without its providerConfig root, a non-connector type, an entry carrying a list of header names, an entry with no name).
  • unmanaged-install.test.ts: "classifies every fixed key of the package export", and a pin that withheldMaps and label are door.

Forced edits (named):

  • unmanaged-install.test.ts "classifies nothing neither schema declares": renamed "classifies nothing no schema declares", and its declared set now includes the export's fixed keys. Without this, the new withheldMaps row would red it as a stale row.
  • unmanaged-install.test.ts and package-lifecycle.test.ts describe titles: "both schemas" to "the three schemas"; "four fixed keys" to "five fixed keys"; "requires exactly the four fixed keys" to "requires only the three identity keys" (the old title was already loose: label was optional).

Reverse check (each table row deleted with ablation-replace.mjs, then unmanaged-install.test.ts): deleting withheldMaps: 'door' reds "classifies every fixed key of the package export" and the door pin; deleting label: 'door' reds the same two. Tree restored to HEAD each time (git diff HEAD empty).

Acceptance notes

Verification

Readings on head bb1718cc (base 04e0c2c0), all under os-verify-lock.sh (VERDICT command-exit 0 each). Head 27e2a7a5 adds one commit that edits only the changeset's level and declaration line (no package source, no generated artifact), so these readings carry to it; the changeset gate was re-run on 27e2a7a5.

Spec and consumer suites
  • turbo run build --filter='./packages/**': 78/78 tasks, 0 cached.
  • pnpm --filter @objectstack/spec test: Test Files 792 passed (792), Tests 23529 passed, 1 todo.
  • pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/spec check:generated: 2 of 14 stale before --fix (check:docs, check:strictness-ledger), both regenerated and committed; api-surface not stale (no new export).
  • @objectstack/metadata-protocol package-unmanaged-install.test.ts + protocol.duplicate-package-install-refusal.test.ts: 2 files, 68 tests passed.
  • pnpm --filter @objectstack/client test: 54 files, 693 tests passed; typecheck exit 0 (PackageExportManifest is the client's packages.export return type).
  • @objectstack/runtime packages-export-override-declaration, packages-export-per-package-layer, packages-seed-apply-read-decorations, packages-vetted-org-source: 4 files, 114 tests passed.
  • @objectstack/cloud-connection marketplace-install-local-install-modes.test.ts: 23 tests passed.
  • Planner probe (temporary test file, deleted): before (rows removed, spec rebuilt, dist preflight --absent passed) label and withheldMaps each refused UNMANAGED_INSTALL_UNCOPYABLE; after, both accepted.
Derived gates (dispatch-gates --commands)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 families at bb1718cc; each ran with stdin closed, all 103 exit 0. Reconciliation: "103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN" (a DERIVED zero). Changed lines 161 (+144 / -17) against the 3000 threshold.

…fied door for the unmanaged import

PackageExportManifestSchema gains the fixed key withheldMaps: each map the
export withheld whole ({ type: 'connector', name, path }, path one of the
connector redactor's four map positions). UNMANAGED_INSTALL_KEY_DISPOSITIONS
classifies it door, and the export's label (declared by the export contract,
never classified) door too, so an unmanaged import accepts an export as
written. The table's totality test now reads the export's fixed keys as the
third schema a package body is read with.

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
…ness counts for withheldMaps; add the changeset

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via /packages/:id/export (route, a path literal in a comment on a changed line))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 0bfb225e4d6b359d8481ec3f5351221c634ed35c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 36d64618552ec31dbf211f89ebcec16532ae0932 — the merge of head d558859f50d9e6f3bc0f904bb639334a3572402e into base 0bfb225e4d6b359d8481ec3f5351221c634ed35c, 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 36d64618552ec31dbf211f89ebcec16532ae0932 && git checkout 36d64618552ec31dbf211f89ebcec16532ae0932
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0bfb225e4d6b359d8481ec3f5351221c634ed35c d558859f50d9e6f3bc0f904bb639334a3572402e && git checkout -B drift-repro 0bfb225e4d6b359d8481ec3f5351221c634ed35c && git merge --no-ff d558859f50d9e6f3bc0f904bb639334a3572402e

node scripts/docs-audit/affected-docs.mjs --json 0bfb225e4d6b359d8481ec3f5351221c634ed35c

⚠️ 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 0bfb225e4d6b359d8481ec3f5351221c634ed35c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ack/spec minor, as the level gate reads the moved authorable-surface shard

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 27e2a7a5 · read 2026-11-02T17:38Z

维护者速读

席位意见:可落地,等契约复审。

席位核对 — ACCEPT · domain:spec#1 · head 27e2a7a5

  1. Shape: draft, base main, Fixes #29373, Clause-②: yes (widening) / Metadata-protocol: added in the body and the changeset, spec minor. This follows the claim as amended on spec: the package export manifest declares withheldMaps (each connector header/env map withheld whole), classified door for the unmanaged import (#28640 ruling ③A, spec half of #29342) #29373: the level gate refuses none, because authorable-surface/api.json moves. 8 files, about 161 lines. Not governed. No packages/metadata-protocol/src/protocol.ts change, so the [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467 E2 freeze does not apply.
  2. Ruling: [Decision] connector credential follow-ups after #28596: header-value advisory, legacy auth keys at rest, whole-map withholding #28640 ③A (6566912252) and packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342's seat ruling A. The maps stay withheld whole; there is no per-entry hiding and no name list.
  3. Surface: inside the claim:
    • the key on the export manifest;
    • its strict entry shape, with path as a closed enum pinned equal to CONNECTOR_CREDENTIAL_MAP_PATHS and spelled the way redactedKeys reports it;
    • two door rows (withheldMaps, and label, a measured gap: a planner probe shows both refused UNMANAGED_INSTALL_UNCOPYABLE before the change and accepted after);
    • the totality test, which now also reads the export's fixed keys.
  4. Names: the seat's call; accepted as built.
  5. Readings (dev):
  6. Landing: one CONTRACT_REVIEW_TIER review by the seat (claim line follows). After a PASS and green CI, the seat readies and queues, then tells server seat 3 that packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342 is unblocked.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 27e2a7a · 2026-11-02T17:38Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27e2a7a53f91d227c865a77e4d5b0c4841baf22f
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts, packages/spec/src/kernel/unmanaged-install.ts + packages/spec/src/api/package-lifecycle.test.ts, packages/spec/src/kernel/unmanaged-install.test.ts, .changeset/29373-export-withheld-maps.md, packages/spec/src/integration/connector-credential-redaction.ts (main), packages/runtime/src/domains/packages.ts assemblePackageManifest (main), packages/metadata-protocol/src/package-unmanaged-install.ts planUnmanagedImport (main), packages/spec/src/stack.zod.ts (the authoring-side reader of the table), packages/spec/src/api/package-api.zod.ts PackageInstallManifestSchema (main), packages/cloud-connection/src/marketplace-install-local-plugin.ts declaredPackageManifest (main), draft PR #29100 head 1e673873a5ee

Inputs read: card #29373 body and every comment (triage 6580300184, claim 6582245473, dev report 6583817463, claim amendment 6583855373); #28640 director record 6566912252 (ruling ③A); #29342 dev report 6578135265 and seat ruling 6578196369 (option A); PR #29521 body, 8 files, net diff origin/main...27e2a7a5; the head's check-runs. The seat's conclusions were not adopted; each point below was re-read in the tree.

① Derived judgments

  • Ruling ③A is honoured by the shape. withheldMaps is a list of { type, name, path } positions, path a closed enum, the entry .strict() so a keys: [...] name list is refused (pinned: "refuses a list of the withheld header names"). No per-entry hiding, no name list. The credential heuristic does not become an export contract: the enum is the redactor's map list (CONNECTOR_CREDENTIAL_MAP_PATHS, module note item 3), not the credential-literal predicate or the URL/query heuristics (items 5 and 6), which stay the redactor's.
  • The closed path enum is exactly the redactor's four maps, spelled as it reports them. Read on main: CONNECTOR_CREDENTIAL_MAP_PATHS = ['providerConfig','headers'], ['providerConfig','defaultHeaders'], ['providerConfig','transport','headers'], ['providerConfig','transport','env'], and redactConnectorItem reports path.join('.'). The test pins the enum to both the constant and a real redaction's redactedKeys, so they cannot drift. The card's three root-less spellings were wrong; the PR's correction is right.
  • The providerConfig. root resolves at an import. The redactor's paths are item-relative (readAt(item, path) from the connector body), and an import holds exactly that body (enumerateItems in the planner, manifest[plural] in the export), so providerConfig.headers is the key an import re-enters into; a root-less headers would not be.
  • type: z.literal('connector') is right for this card. The seat ruling keeps webhook headers (.meta({ credential: 'map' }), automation/webhook.zod.ts:203) out of scope. A later slot is a literal-to-enum widening on an optional key: Clause-②: yes (widening), no break. Noted, not required: the describe's sentence "a connector's header and environment-variable maps" will need rewording when that slot arrives.
  • door for withheldMaps opens nothing unintended. The only two readers of the table are read: planUnmanagedImport (case 'door': break — nothing reaches identity, so nothing reaches baseManifest), and defineStack's authoring judge (stack.zod.ts:2541), which acts only on UNMANAGED_INSTALL_REFUSING_DISPOSITIONS; door is not one, so no authoring refusal moves. The base is registered through the closed PackageInstallManifestSchema (ManifestSchema is strictObject), which declares neither key, so carrying either onto the base would refuse the base — door is the only disposition that lets an export through without breaking the base.
  • label was really refused before. On main the table has no label row and the planner's default: arm refuses an unclassified key with UNMANAGED_INSTALL_UNCOPYABLE ("not a key a package body declares"), while assemblePackageManifest writes manifest.label when pkg.manifest.label ?? pkg.label is set. The gap is real by reading, independent of the dev's probe. door is defensible (see ④ for the reservation).
  • The totality test is sound. It holds the table total against ManifestSchema (live, non-collection), ObjectStackDefinitionSchema (non-collection) and now PackageExportManifestSchema.shape (the five fixed keys; the catch-all carries the collections, judged by the registry), and the reverse test ("classifies nothing no schema declares") now admits the export's keys so label and withheldMaps are declared rows, not stale ones. lazySchema proxies .shape through, so exportShape is the real declaration. A sixth fixed key added to the export later reds the spec test on the PR that adds it, as intended.
  • Consumers. PackageExportManifest is the client's packages.export return type (packages/client/src/index.ts:3106) and the runtime route ledger's responseSchema (route-ledger.ts:547); an optional key widens both without a break. api-surface does not move (inline declaration). The managed install path for an exported body reads the manifest loosely (declaredPackageManifest(body) in marketplace-install-local-plugin.ts:608, no whole-body strict parse found), and the strict POST /packages door takes authoring-stage manifests (objects are glob strings), not exports — so no further spec classification is needed for the server half to read withheldMaps at the managed door.

② Semver level

  • Clause-②: yes (widening) is right: the unmanaged door's accept set grows by two keys and the export contract gains one optional key; nothing is refused that was accepted before.
  • Metadata-protocol: added with '@objectstack/spec': minor is consistent across the PR body and .changeset/29373-export-withheld-maps.md, and matches the precedent pair on main for new API response keys (28737-record-page-timeline-count.md, 28722-customizations-contributions.md: both yes (widening) + added + spec minor). The gate's own text ties the kind to the moved authorable-surface/api.json shard, which this PR moves by one row.
  • Check-run reading: the one completed Check Changeset on this head is red (job 125437733906, done 2026-11-02T17:34:23Z) because it read the PR body as it stood then — Metadata-protocol: none beside the moved shard; the body was amended to added at 2026-11-02T17:38:27Z, the 17:37 rerun was cancelled, and a third run (41493231828) was in progress at review time. The tree's changeset already carries added + minor, so the red is the body's history, not the tree's — but the record notes that no green Check Changeset existed on this head at review time. Landing waits on every check green regardless.

③ Boundary flags

  • No governed surface in the file list; the review is owed by the published schema face alone (reviewFace hits: the two spec source files).
  • Spec only: no runtime change; assemblePackageManifest does not write the key and no door reads it — the server half is packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342, pm:blocked on this card.
  • feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100 compatibility: that draft composes packages: z.array(PackageExportManifestSchema) by identity in a different hunk (@@ -191,6 +191,104 @@, below this PR's edit); the constant's identity is unchanged, so the bundle inherits withheldMaps per entry. Git reading on this clone: git merge-tree --write-tree origin/main 27e2a7a5 is clean (origin/main at 399df5bba8 at review time; the PR was opened on base 04e0c2c0).
  • Generated artifacts in the diff (content/docs/references/api/package-lifecycle.mdx, authorable-surface/api.json, the strictness-ledger count 487 to 488) read as the regeneration of the one new .strict() entry and one new row; not re-run here (read-only), CI's TypeScript Type Check lanes are the authority and were in progress at review time.
  • Test Core, Build Core, Type Check and the Lint lanes were in_progress at review time; only Spec property liveness, Governed Surface Queue Guard, PR-body guards, Lint · PM dispatch-gates and Lint · scoped · tooling self-tests had concluded success. The record does not claim green.

④ Design judgment

  • MUST-FIX — the describe promises a reading of absence that no runtime serves, with no PLANNED marker. The withheldMaps describe ends "Absent when the export withheld nothing." and says "an import reads this list to tell an incomplete map from one the package never had". On this tree the export never writes the key and no import reads it, so today every export that withheld a map has the key absent — the describe's stated meaning of absence is false against the only producer that exists, and it ships into content/docs/references/api/package-lifecycle.mdx as the contract. The file's own header says every schema here is a "DESCRIBE-ONLY TRANSCRIPTION of the return type its producer already declares inline" with the named exceptions listed; withheldMaps is declared ahead of its producer and is not named among them. The house's own convention for a declaration ahead of its producer, in this same file and by this same seat, is the [PLANNED — not yet served] prefix on the describe with a test pinning the marker and the mounting PR dropping it (feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100 PackageExportBundleSchema; ui/app.zod.ts:422, view.zod.ts:1217, component.zod.ts:1098 for the rendered twin). The changeset's "The export does not write it yet; the runtime half follows" lives only in the changeset, which the reference docs and the client types do not carry. Remedy (small, text-only, same PR): open the describe with [PLANNED — not yet served], state that until the export writes it an absent key says nothing about what was withheld (so an import must not read absence as "nothing withheld" from an exporter predating the key), add that pin to package-lifecycle.test.ts, and name the exception in the module header; packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342 drops the marker when it writes the list. Prime Directive chore: version packages #10's corollary (declared ≠ enforced) is the rule this reds against.
  • Reservation, not a must-fix — label: 'door' is a silent drop dressed as a door act. The planner's door arm does nothing with label; the base takes targetName ?? name, and nothing in ImportUnmanagedPackageResponseSchema says the display label was set aside (its precedent for "a loss, stated" is strippedLocks). The PR widened the door definition text to "or, for an export's display label, set aside by it" to fit. It closes a real refusal with no new authorable surface, which is the right call for this card, and a display label is low-stakes; but the server half (or the dev's option B card: label on the install manifest, classified identity) should make the set-aside loud. Recorded so packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342 reads it.
  • The redactedKeys equality test is a good drift pin, but it only proves the four map positions on a connector carrying all four; it does not (and need not) prove the export's future list matches a given export — that is packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342's test to write.
  • Names (withheldMaps, type / name / path) are the seat's call per the claim amendment; the ruling's own spelling, accepted as built.

Implemented-by: claude/project-thread-pr0nu5
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: FAIL

…ame the exception in the module header

No export writes withheldMaps yet and no install door reads it, so its
describe now opens [PLANNED — not yet served] (ADR-0049), says what absence
means from an exporter that predates the key, and a test pins the marker.
The module header names the two members declared ahead of their producer.

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
…ed withheldMaps describe

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3980d79c259181cd4da2c5dfd2e0b643803f868e
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts, packages/spec/src/kernel/unmanaged-install.ts + packages/spec/src/api/package-lifecycle.test.ts, packages/spec/src/kernel/unmanaged-install.test.ts, .changeset/29373-export-withheld-maps.md, content/docs/references/api/package-lifecycle.mdx, packages/spec/scripts/liveness/check-liveness.mts (main), packages/spec/src/integration/connector-credential-redaction.ts (main), packages/runtime/src/domains/packages.ts assemblePackageManifest (main), packages/metadata-protocol/src/package-unmanaged-install.ts planUnmanagedImport (main), packages/spec/src/stack.zod.ts (main), packages/spec/src/api/package-api.zod.ts PackageInstallManifestSchema (main), packages/cloud-connection/src/marketplace-install-local-plugin.ts declaredPackageManifest (main), draft PR #29100 head 1e673873a5ee

Delta review on the FAIL record for head 27e2a7a5 (the earlier record on this card). Read: git diff 27e2a7a5..3980d79c (two commits: cbce5d3b the fix, 3980d79c the regenerated reference; 5 files, +47/−11), the two hits files whole at 3980d79c, the check-runs on 3980d79c, the PR body as it stood at 2026-11-02T18:06:54Z. Every ① / ② / ③ judgment of the earlier record that the delta does not touch is re-stated here so this record stands alone.

① Derived judgments

  • The must-fix is cured in the tree. The withheldMaps describe now opens [PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list yet and no install door reads it yet., says once served an import reads this list…, and closes with Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. That is the house marker in the house spelling: the nav arm of GetPackageCustomizationsResponseSchema.contributions on main (package-lifecycle.zod.ts:459) opens its describe with the identical prefix and the identical ADR-0049 citation, so the two declared-ahead members of this file now read alike.
  • The schema docblock carries a DECLARED AHEAD OF ITS PRODUCER (ADR-0049) paragraph naming the server half (packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342) as the change that makes assemblePackageManifest write the list and both doors name each incomplete map, drops the marker, and points at the pin test. It also says the door row is live today — which is the correct split: the row keeps the unmanaged import from refusing an export that carries the key, and is enforced on main's planner (case 'door': break); the key's semantics are what wait on packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342.
  • The module header names the exception. The header that says every schema here is a describe-only transcription now lists the two declared-ahead members (the nav arm, The served app navigation names the package that contributed each folded entry (#27356 row 20) #27928; withheldMaps, [Decision] connector credential follow-ups after #28596: header-value advisory, legacy auth keys at rest, whole-map withholding #28640 ③A served by packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342). Verified the first member exists on main with that marker (:367, :459); the header does not invent a precedent.
  • Tense corrected where it was ahead of the runtime. unmanaged-install.ts row comment: "declared ahead of its producer: the server half (packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342) has the door read it… This row is live today". Changeset: "It is declared ahead of its producer: no export writes it and no install door reads it yet, and its describe opens [PLANNED — not yet served]… Until then, an export's lack of the key says nothing about what was withheld." The label row and its comment are unchanged from 27e2a7a5.
  • The pin test holds the marker. package-lifecycle.test.ts reads PackageExportManifestSchema.shape.withheldMaps.description (the .describe() sits on the .optional() wrapper, so that is the right node) and asserts /^\[PLANNED — not yet served\] /, toContain('ADR-0049'), and not.toMatch(/#\d{3,}/) — so the runtime string carries the ADR and no tracker number, the rule AGENTS.md states for authored-facing strings; the tracker numbers live only in docblocks and comments, as they already did on main (#12038, #18576 in this same header). packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342's serving change must flip this pin when it drops the marker, which is the pattern the nav arm set.
  • Liveness claim holds by reading. check-liveness.mts is registry-rooted: its source of truth is BUILTIN_METADATA_TYPE_SCHEMAS via listMetadataTypeSchemaTypes / getMetadataTypeSchema (header lines 8–11), the set of registered metadata kinds, and the ledgers are packages/spec/liveness/<type>.json per governed type. PackageExportManifestSchema is an @objectstack/spec/api response schema, not a registered metadata type, so no ledger row is owed and none could be written; packages/spec/liveness/api.json names no PackageExportManifest row today. The Spec property liveness check-run on 3980d79c concluded success at 2026-11-02T18:06:52Z. The [PLANNED — not yet served] marker is the API-schema convention, distinct from the liveness ledger's [EXPERIMENTAL — not enforced] describe marker; the nav precedent uses the same split.
  • The regenerated reference matches its source. content/docs/references/api/package-lifecycle.mdx gains the header paragraph and the rewritten withheldMaps row with the marker, byte-for-sense equal to the docblock and describe at 3980d79c; authorable-surface/api.json and the strictness count are unchanged by the delta (no key moved). Not regenerated here (read-only); CI's check:docs lane in TypeScript Type Check is the authority and was in progress at review time.
  • Unchanged from the earlier record and still true at 3980d79c (the delta touches no schema shape, no table row and no test of theirs): ruling ③A honoured (positions only, .strict() entries refuse a name list, closed path enum equals CONNECTOR_CREDENTIAL_MAP_PATHS spelled as redactedKeys reports them, pinned to both); the providerConfig. root is the item-relative path an import resolves; type: z.literal('connector') is right for this card and a webhook slot later is a literal-to-enum widening; door for withheldMaps opens nothing (planner break, defineStack judges only refusing dispositions, the base is registered through closed PackageInstallManifestSchema); label was really refused on main (no row, default: arm) and door is the one disposition that admits an export without refusing the base; the totality test is sound against three schemas; PackageExportManifest consumers (client packages.export, route ledger responseSchema) widen without a break; the managed install path reads an exported body loosely (declaredPackageManifest) and the strict POST /packages door takes authoring-stage manifests, so no further spec classification is needed for the server half.

② Semver level

  • Clause-②: yes (widening) and Metadata-protocol: added with '@objectstack/spec': minor, unchanged by the delta and consistent across the PR body and the changeset; matches main's precedent pair for API response keys (28737-record-page-timeline-count.md, 28722-customizations-contributions.md). The marker changes no level: a declared-ahead optional key is still a key the authorable-surface/api.json shard records.
  • Check Changeset on 3980d79c concluded success at 2026-11-02T18:07:43Z — the red on 27e2a7a5 (a body that still read none) is behind this head.

③ Boundary flags

④ Design judgment

Implemented-by: claude/project-thread-pr0nu5
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 18:14
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 18:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Hold re-enqueueing until the maintainer approves the new key name. ⛔ Not a claim.

  1. Name approval. PackageExportManifestSchema.withheldMaps, with its sub-keys type, name and path, is a new key on a published contract, so it is a protocol key. Protocol keys are a maintainer decision (human floor). Ruling 6566912252 (③A) binds that the export marks withheld maps. It does not delegate the spelling: "the marker's spelling is the implementer's" is packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342's own filing text, not the ruling's. Triage graded spec: the package export manifest declares withheldMaps (each connector header/env map withheld whole), classified door for the unmanaged import (#28640 ruling ③A, spec half of #29342) #29373 with this condition (「⛔ The new name withheldMaps waits for the maintainer's approval before merge」), and the name has been put to the maintainer. Re-enqueue once their approval is recorded on spec: the package export manifest declares withheldMaps (each connector header/env map withheld whole), classified door for the unmanaged import (#28640 ruling ③A, spec half of #29342) #29373.
  2. The 18:43Z queue red was the strictness-ledger collision again (anchor Queue-flake anchor: scripts/strictness-ledger-doc.test.ts #29524). This PR carries docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md. Before re-entering, merge origin/main, regenerate the ledger with its generator, and land serially with the other ledger-touching PRs (landing-operations.md §D).

Generated by Claude Code

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41503306729 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1298 object site(s) across…' to be 'untriaged: 1297 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 32 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

…eep both sides' tests in package-lifecycle.test.ts

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
…ness counts on the merged tree

Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Merge-only head d558859f · 2026-11-02T21:50Z

The merge queue removed the PR 2026-11-02T19:07Z. In the merge group, the strictness api.md shard read 488 against 489 measured. Main and #29100 also write that shard, which is #29524's class. The seat held this PR until #29100 merged (f1d795b1a5, 2026-11-02T20:32Z).

  • Merge. The dev merged origin/main 81cd9eb0 with os-regen-merge.sh (078a91b). There was one non-generated conflict, in package-lifecycle.test.ts: feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100's PackageExportBundleSchema tests met this PR's withheldMaps tests. Both sides were kept. The seat read the merged file: both describe blocks are present, and the branch hunks are unchanged. The schema file auto-merged, and the module header keeps both sections.
  • Regeneration (d558859). The api strictness shard is now 490, which is main's 489 + this PR's one .strict(). authorable-surface/api.json is main + 1 row (api/PackageExportManifest:withheldMaps).
  • The delta against main is unchanged: the same 8 files, +182/-18.
  • Re-checked on d558859f, all exit 0:
    • full spec test: 801/801 files, 23709 tests
    • spec test:repo: 89/89 files
    • spec typecheck, check:strictness-ledger, check:generated

There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER delta PASS on 3980d79c forward and re-queues.

@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 21:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 45b4922 Nov 2, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-pr0nu5 branch November 2, 2026 22:36
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/m tests tooling

Projects

None yet

1 participant