From 7a440c612ae19b8e9c0d8ab5c70b7e8ccae8703c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 18:33:50 +0000 Subject: [PATCH 1/4] refactor(metadata-protocol,rest,runtime): one declared shape for the `deletePackage` seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `protocol.deletePackage` had three independent statements of its own contract and none of them agreed: 1. the producer's own inline structural type on the method, 2. `PackageRoutesOptions.protocol.deletePackage` in `@objectstack/rest`, which named neither `organizationId` nor `keepData` and omitted `deleted` from the response, 3. the dispatcher twin in `@objectstack/runtime`, which typed the seam not at all and reached it through `(protocol as any)` — while routinely sending exactly the two keys (2) could not express. `organizationId` is the key that decides an uninstall's blast radius (the protocol refuses a call naming neither it nor `allTenants` — `TENANT_SCOPE_REQUIRED`, 400), so the member the REST seam had no word for is the one that matters most. `DeletePackageRequest` / `DeletePackageResponse` are now declared once at the producer and exported from `@objectstack/metadata-protocol`; both consumers import them and the `as any` is gone. Types only — identical members, identical call, no accept-set or behaviour change. No `packages/spec` declaration: minting protocol surface for a verb with zero external consumers is a spec-seat decision nobody has asked for. The member stays OPTIONAL and the runtime's `typeof … === 'function'` capability probe stays: the `protocol` service slot is deliberately uncontracted, the spec's `PackageProtocol` does not declare this verb, and registrants that carry no `deletePackage` are real in-tree. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM --- packages/metadata-protocol/src/index.ts | 5 ++ packages/metadata-protocol/src/protocol.ts | 76 ++++++++++++++++---- packages/rest/src/package-routes.ts | 82 +++++++++++++++++++--- packages/runtime/src/domains/packages.ts | 38 +++++++--- 4 files changed, 169 insertions(+), 32 deletions(-) diff --git a/packages/metadata-protocol/src/index.ts b/packages/metadata-protocol/src/index.ts index 571ec939af..cb1f59e9f6 100644 --- a/packages/metadata-protocol/src/index.ts +++ b/packages/metadata-protocol/src/index.ts @@ -104,6 +104,11 @@ export type { SeedTenancyCollision, } from './migrations/seed-tenancy-backfill.js'; export type { UninstallCleanup, UninstallCleanupOutcome } from './protocol.js'; +// [#9960] The ONE declared shape of the `deletePackage` seam, exported so the +// two consumers that speak it (`@objectstack/rest`'s direct-mount package +// registrar and the `@objectstack/runtime` dispatcher twin) type the seam +// against the producer's contract instead of restating it locally. +export type { DeletePackageRequest, DeletePackageResponse } from './protocol.js'; export type { MetadataMutationEvent, MetadataMutationProjector, MutationProjectionOutcome } from './protocol.js'; export type { MetadataAuthoringGate, MetadataAuthoringGateContext } from './protocol.js'; diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index dfd1c27a33..87706b013b 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -3224,6 +3224,67 @@ export interface UninstallCleanupOutcome { error?: string; } +/** + * [#9960] The declared REQUEST shape of + * {@link ObjectStackProtocolImplementation.deletePackage} — the ONE statement + * of this verb's contract, imported by every seam that speaks it. + * + * WHY IT IS NAMED. `deletePackage` has no `packages/spec` declaration (it is + * absent from `PackageProtocol`), and until this type existed its shape was + * stated THREE times, differently, by the three modules that share the seam: + * an inline structural type on the method below; a narrower restatement on + * `PackageRoutesOptions.protocol.deletePackage` in `@objectstack/rest`, which + * named neither `organizationId` nor `keepData`; and no type at all in the + * dispatcher twin (`packages/runtime/src/domains/packages.ts`), which reached + * the verb through `(protocol as any)` and routinely sent exactly the two keys + * the REST restatement could not express. Naming the shape once, HERE at the + * producer, is what makes the other two seams compile-checked against the + * contract instead of each against its own copy. + * + * ⛔ Deliberately NOT declared in `packages/spec`: minting protocol surface for + * a verb with zero external consumers is declare-and-maintain the platform has + * not asked for. Should an external consumer ever appear, the spec declaration + * is its own card for the spec seat, not a rider on a typing convergence. + * + * `organizationId` is the load-bearing member. The tenant-scope gate in + * `deletePackage` refuses a call naming neither it nor `allTenants` + * (`TENANT_SCOPE_REQUIRED`, 400 — #7780), so it is precisely the key whose + * presence decides an uninstall's blast radius, and it was the key the REST + * seam's own type had no word for. + */ +export interface DeletePackageRequest { + packageId: string; + /** + * Scope the uninstall to ONE organization's rows (#7705). Omitted together + * with `allTenants` ⇒ refused, never inferred as "every tenant" (#7780). + */ + organizationId?: string; + /** DECLARE a cross-tenant uninstall. Never deduced from an absent org (#7780). */ + allTenants?: boolean; + actor?: string; + /** Remove the metadata but PRESERVE each object's physical table. */ + keepData?: boolean; +} + +/** + * [#9960] The declared RESPONSE shape of + * {@link ObjectStackProtocolImplementation.deletePackage}, stated once for the + * same reason as {@link DeletePackageRequest}. + * + * `deleted` is part of it. The REST seam's restatement omitted that member + * entirely, so a caller reading the option's type was told this verb reports + * only a COUNT of what it removed — while the producer has always returned the + * per-item list beside it. + */ +export interface DeletePackageResponse { + success: boolean; + deletedCount: number; + failedCount: number; + deleted: Array<{ type: string; name: string; state: string }>; + failed: Array<{ type: string; name: string; error: string; code?: string }>; + cleanups: UninstallCleanupOutcome[]; +} + /** * Post-persistence metadata-mutation notification (#2588). Emitted by * `saveMetaItem` / `publishMetaItem` / `deleteMetaItem` AFTER the write @@ -15376,20 +15437,7 @@ export class ObjectStackProtocolImplementation implements * so each object's table is torn down once. Per-item failures are collected * without aborting the rest. */ - async deletePackage(request: { - packageId: string; - organizationId?: string; - allTenants?: boolean; - actor?: string; - keepData?: boolean; - }): Promise<{ - success: boolean; - deletedCount: number; - failedCount: number; - deleted: Array<{ type: string; name: string; state: string }>; - failed: Array<{ type: string; name: string; error: string; code?: string }>; - cleanups: UninstallCleanupOutcome[]; - }> { + async deletePackage(request: DeletePackageRequest): Promise { // [#7780] A cross-tenant uninstall must be DECLARED, never inferred from // an absent parameter. Maintainer ruling (2026-08-12): // 跨租户卸载必须显式声明,缺省缺参永远不等于「全部租户」. diff --git a/packages/rest/src/package-routes.ts b/packages/rest/src/package-routes.ts index 2b963fc1a8..2589ee7435 100644 --- a/packages/rest/src/package-routes.ts +++ b/packages/rest/src/package-routes.ts @@ -29,6 +29,18 @@ import { readSingleQueryValue, repeatedQueryParamMessage } from './query-multipl // while this file keeps compiling green. Same discipline as the sibling // meta-read doors in `rest-server.ts` (#9805 / #9741). import type { GetMetaItemsRequest, GetMetaItemsResponse } from '@objectstack/spec/api'; +// [#9960] The declared uninstall shapes for the `protocol.deletePackage` seam +// below, imported from the PRODUCER for the same reason the meta-read shapes +// above come from the spec: so this module's idea of the request/response is +// the one statement of that contract rather than a local restatement the +// producer can drift away from while this file keeps compiling green. There is +// no spec shape to import for this verb — `deletePackage` is deliberately +// undeclared in `packages/spec` (zero external consumers, #9960) — so the +// producer's own exported type IS the contract. Type-only: no runtime import of +// `@objectstack/metadata-protocol` exists here, and this package still does not +// depend on it at run time (see `query-multiplicity.ts` and `rest-server.ts`, +// which duck-type the same seam for exactly that reason). +import type { DeletePackageRequest, DeletePackageResponse } from '@objectstack/metadata-protocol'; /** * [#7033 / #7023] The authorization gate for the REST package transport. @@ -250,16 +262,25 @@ export interface PackageRoutesOptions { * tolerates — a behaviour question, deliberately not answered here. */ getMetaItems?(req: GetMetaItemsRequest): Promise; - // [#7780] `allTenants` is the explicit carrier for cross-tenant uninstall - // semantics; the protocol refuses a call that names neither it nor an - // `organizationId` (`TENANT_SCOPE_REQUIRED`, 400). - deletePackage?(req: { packageId: string; actor?: string; allTenants?: boolean }): Promise<{ - success: boolean; - deletedCount: number; - failedCount: number; - failed: Array<{ type: string; name: string; error: string; code?: string }>; - cleanups: Array<{ name: string; success: boolean; removed: number; error?: string }>; - }>; + /** + * [#7780] `allTenants` is the explicit carrier for cross-tenant uninstall + * semantics; the protocol refuses a call that names neither it nor an + * `organizationId` (`TENANT_SCOPE_REQUIRED`, 400). + * + * [#9960] Request/response are the PRODUCER's declared shapes. The local + * restatement they replace named neither `organizationId` nor `keepData` + * and omitted `deleted` from the response — so the one key that decides an + * uninstall's blast radius had no word for it here, while the dispatcher + * twin sent that key on every org-scoped call. The member stays OPTIONAL + * and the call site below keeps its `typeof … === 'function'` + * feature-detection: the `protocol` service slot is deliberately + * uncontracted (`ServiceSlotContracts`), the spec's own `PackageProtocol` + * does not declare this verb at all, and registrants that carry no + * `deletePackage` are real — so requiring the member here would change what + * this seam tolerates, which is a behaviour question this card does not + * answer. + */ + deletePackage?(req: DeletePackageRequest): Promise; }; /** * [#7033 / #7023] Resolve the caller's execution context for a package route @@ -329,6 +350,47 @@ export type _PinGetMetaItemsStaysOptional = Pinned< undefined extends NonNullable['getMetaItems'] ? true : false >; +/** + * [#9960] The same three pins for the `protocol.deletePackage` seam, and for + * the same reason — with one difference worth stating: `getMetaItems` above is + * pinned to the SPEC's declared shapes, while this verb has no spec + * declaration, so the producer (`@objectstack/metadata-protocol`) is the + * contract these pin against. That is the adjudicated shape of #9960, not an + * oversight: declaring a protocol verb for a surface with zero external + * consumers is a spec-seat decision nobody has asked for. + * + * WHAT THEY CATCH: that this option's request/response are still the producer's + * types rather than a hand-rolled restatement of them. Exact equality, not + * mutual assignability — the shape this card removed (`{ packageId; actor?; + * allTenants? }`) is assignable to `DeletePackageRequest` in one direction, so + * an assignability check would have passed on the very divergence that made + * `organizationId` and `keepData` unsayable here. + * + * They live in compiled source, not a `*.test.ts`, for the reason spelled out + * above the `getMetaItems` pins: this package's `tsconfig.json` excludes its + * test files, so a type-level assertion written there is compiled by nothing. + */ +type DeclaredDeletePackage = NonNullable['deletePackage']>; + +/** The REQUEST type is exactly the producer's `DeletePackageRequest`. */ +export type _PinDeletePackageRequestIsProducerDeclared = Pinned< + ExactlyEqual[0], DeletePackageRequest> +>; + +/** The RESPONSE type is exactly the producer's `DeletePackageResponse`. */ +export type _PinDeletePackageResponseIsProducerDeclared = Pinned< + ExactlyEqual>, DeletePackageResponse> +>; + +/** + * The member stays OPTIONAL — see the option's own note. This pin fails if a + * later edit quietly makes it required, which would turn a protocol registrant + * without the verb from a supported shape into a type error. + */ +export type _PinDeletePackageStaysOptional = Pinned< + undefined extends NonNullable['deletePackage'] ? true : false +>; + /** * Register package management API routes * diff --git a/packages/runtime/src/domains/packages.ts b/packages/runtime/src/domains/packages.ts index 413a253f77..6838063292 100644 --- a/packages/runtime/src/domains/packages.ts +++ b/packages/runtime/src/domains/packages.ts @@ -38,6 +38,12 @@ import { OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES } from '@objectstack/metada // this defect existed precisely because the lifecycle routes had no copy of it, // and a second copy would be the next place it drifts. import { isWritablePackage } from '@objectstack/metadata-protocol'; +// [#9960] The uninstall seam's DECLARED shapes, from the same producer and for +// the same reason as the predicate above: this door reached `deletePackage` +// through `(protocol as any)` and routinely sent two keys — `organizationId` +// and `keepData` — that the sibling REST door's own option type could not even +// express. One statement of the contract, imported by both doors. +import type { DeletePackageRequest, DeletePackageResponse } from '@objectstack/metadata-protocol'; // [#8443] ADR-0112's disclosure rule (#8086 / #8136 / #8333), and the DECLARED // 422 that keeps the one quotable population quotable. Both imported from the // producer for the reason the line above is: this door's seed-apply fallback is @@ -890,13 +896,29 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin // Persisted removal (AI/runtime packages live in sys_metadata, not // just the in-memory registry — the registry uninstall alone would // leave the rows and tables behind). - let persisted: unknown = undefined; - const protocol = await deps.resolveService(_context, 'protocol'); - if (protocol && typeof (protocol as any).deletePackage === 'function') { + let persisted: DeletePackageResponse | undefined = undefined; + // [#9960] `protocol` is an UNCONTRACTED service slot — `ServiceSlotContracts` + // leaves it unmapped on purpose ("no written contract … rather than being + // given a shape nothing checks") — so `resolveService` hands this door an + // `any`. That `any` is what let the call below send keys no declared shape + // named: `organizationId` (the key that decides an uninstall's blast radius) + // and `keepData` are exactly the two the sibling REST door's option type + // could not express, and nothing compared the two doors' requests. Narrowed + // HERE to the producer's declared verb, so what this door sends is checked + // against the contract the implementation states. + // + // The `typeof … === 'function'` probe STAYS and the member stays optional: + // the verb is absent from the spec's `PackageProtocol` (every member of + // which is optional anyway), the slot takes whatever a host registers under + // the name, and registrants carrying no `deletePackage` are real in-tree. + // A capability question, asked as a capability probe — not a cast. + const protocol: { deletePackage?(request: DeletePackageRequest): Promise } | undefined = + await deps.resolveService(_context, 'protocol'); + if (protocol && typeof protocol.deletePackage === 'function') { try { const organizationId = await deps.resolveActiveOrganizationId(_context); const keepData = query?.keepData === 'true' || query?.keepData === '1'; - persisted = await (protocol as any).deletePackage({ + persisted = await protocol.deletePackage({ packageId: id, ...(organizationId ? { organizationId } : {}), ...(keepData ? { keepData: true } : {}), @@ -906,8 +928,8 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin } } - const deletedCount = (persisted as any)?.deletedCount ?? 0; - const failedCount = (persisted as any)?.failedCount ?? 0; + const deletedCount = persisted?.deletedCount ?? 0; + const failedCount = persisted?.failedCount ?? 0; // [#7557] A failed persistence used to ride inside a 200: this // handler stated `success: true` unconditionally and forwarded the @@ -941,8 +963,8 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin { code: 'PACKAGE_DELETE_PARTIAL', registryRemoved, - failed: (persisted as any)?.failed, - cleanups: (persisted as any)?.cleanups, + failed: persisted?.failed, + cleanups: persisted?.cleanups, }, ), }; From 13cf2fa0e2d8c50f437b5bb9e9ba429f9f4724b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 18:42:46 +0000 Subject: [PATCH 2/4] chore(changeset): record the deletePackage seam convergence (#9960) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM --- .changeset/deletepackage-shared-seam-type.md | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .changeset/deletepackage-shared-seam-type.md diff --git a/.changeset/deletepackage-shared-seam-type.md b/.changeset/deletepackage-shared-seam-type.md new file mode 100644 index 0000000000..c722f59f47 --- /dev/null +++ b/.changeset/deletepackage-shared-seam-type.md @@ -0,0 +1,44 @@ +--- +"@objectstack/metadata-protocol": patch +"@objectstack/rest": patch +"@objectstack/runtime": patch +--- + +refactor(metadata-protocol,rest,runtime): one declared shape for the `protocol.deletePackage` seam, imported by both doors (#9960) + +`deletePackage` had **three** independent statements of its own contract, and +they did not agree: + +| site | what it said | +|---|---| +| `packages/metadata-protocol/src/protocol.ts` (the producer) | an inline structural type on the method — `packageId`, `organizationId?`, `allTenants?`, `actor?`, `keepData?` | +| `packages/rest/src/package-routes.ts` (direct-mount option) | `{ packageId; actor?; allTenants? }` — named **neither** `organizationId` **nor** `keepData`, and its response omitted `deleted` | +| `packages/runtime/src/domains/packages.ts` (dispatcher twin) | nothing at all — it reached the verb through `(protocol as any)` | + +The twin routinely sent exactly the two keys the REST option's type could not +express, and the only reason that was not a compile error was the cast. + +`organizationId` is the member that makes this load-bearing rather than +cosmetic: the protocol refuses a call naming neither it nor `allTenants` +(`TENANT_SCOPE_REQUIRED`, 400), so it is precisely the key whose presence +decides an uninstall's blast radius — and it was the key one of the two doors +had no word for. + +**What changes:** `DeletePackageRequest` and `DeletePackageResponse` are +declared once at the producer and exported from `@objectstack/metadata-protocol` +(the only user-visible half of this change — two additive type exports); both +consumers import them, and the `as any` seam is gone. `@objectstack/rest` also +gains three compile-time pins over its option, in compiled source rather than a +test file, so a later hand-rolled restatement fails `tsc` instead of drifting +green. + +**What does not change:** nothing about what the verb accepts or returns. The +members are identical to the ones the implementation already had, the live call +sites send the same keys, and the emitted JavaScript of both consumers is +unchanged. The member stays optional at both seams and the runtime's +`typeof … === 'function'` capability probe stays — the `protocol` service slot +is deliberately uncontracted, the spec's `PackageProtocol` does not declare this +verb, and registrants carrying no `deletePackage` are real. + +No `packages/spec` declaration: minting protocol surface for a verb with zero +external consumers is a spec-seat decision nobody has asked for. From c32b8150e8bc8a0ead6456c8eec248d2eb7bde4a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 19:18:49 +0000 Subject: [PATCH 3/4] test(rest): the `deletePackage` double speaks the declared response (#9960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bounded in-place fix, same defect class as the card: `package-routes-query-multiplicity.test.ts` built a protocol double whose uninstall response omitted `deleted`. That compiled only while the option's type omitted it too — with the option now carrying the producer's `DeletePackageResponse`, the double stops type-checking, and `@objectstack/rest`'s TEST_DEBT ledger entry drifted 155 → 156. Fixed at the author's end (`deleted: []`), never by raising the ratchet: the ledger is shrink-only and raising it is maintainer-only. Re-measured back to 155, and `check:type-check-debt` reports "none above its recorded number". The cases in this file count protocol CALLS, not deleted rows, so an empty `deleted` changes nothing they assert. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM --- packages/rest/src/package-routes-query-multiplicity.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/rest/src/package-routes-query-multiplicity.test.ts b/packages/rest/src/package-routes-query-multiplicity.test.ts index 12107a8b86..954e909372 100644 --- a/packages/rest/src/package-routes-query-multiplicity.test.ts +++ b/packages/rest/src/package-routes-query-multiplicity.test.ts @@ -67,7 +67,10 @@ function harness(options: { protocol?: boolean } = {}) { protocol: { deletePackage: async () => { spy.protocolCalls += 1; - return { success: true, deletedCount: 3, failedCount: 0, failed: [], cleanups: [] }; + // [#9960] `deleted` is part of the verb's declared response — + // the option's own type says so now, so a double that omits it no + // longer compiles. Empty here: these cases count CALLS, not rows. + return { success: true, deletedCount: 3, failedCount: 0, deleted: [], failed: [], cleanups: [] }; }, }, } From ad2a23d3e3933501d27cb3e3981ed153e5a67a94 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 04:39:02 +0000 Subject: [PATCH 4/4] fix(rest): resolve the metadata-protocol type import to source, not to dist (#9960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:type-source-resolution` went red on the branch: ✗ @objectstack/rest: NEW dist-resolved type import(s) since this entry was measured: @objectstack/metadata-protocol. Correct, and caused by this card: `src/package-routes.ts` type-imports the declared `deletePackage` shapes, and with no `paths` rule tsc resolved that specifier through the dependency's `exports` map — `dist/index.d.ts`, a build artifact. This package's `typecheck` was therefore a verdict about the last `pnpm build` rather than about the producer's source in the checkout, which is the failure mode whose symptom is a typecheck that PASSES. Fixed where the gate prescribes — the package's own `tsconfig.json`: * ONE `paths` rule, bare key, targeting `../metadata-protocol/src/index.ts`. No `/*` sibling: that package's `exports` map has only `"."`, so there is no subpath to redirect, and a rule pointing at files that are not on disk makes tsc fall back to node resolution silently. * `rootDir` widened from `./src` to `..`, as a consequence rather than a preference: the producer's source is now in the program, `rootDir` is enforced even under `--noEmit` (measured: 20 x TS6059, and deleting the key makes tsc infer one and report the identical 20), and `..` is the directory that genuinely contains every file in the program. Nothing that ships reads it — the package builds with tsup. ⛔ The `KNOWN_DIST_RESOLVED_TYPE_IMPORTS` registry was NOT widened. It is shrink-only and maintainer territory; `@objectstack/rest`'s entry keeps its eight other dist-resolved deps unchanged, and the gate audits that set for equality in both directions. Verified red-to-green, in that order: `node scripts/check-type-source-resolution.mjs` exit 1 with the exact CI message, then exit 0 ("76 packages scanned; 51 registered"). `pnpm --filter @objectstack/rest typecheck` is exit 0 with ZERO errors now that it reads the producer's source — no error was hiding behind the stale artifact, and no ledger was touched to get there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM --- packages/rest/tsconfig.json | 49 +++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/rest/tsconfig.json b/packages/rest/tsconfig.json index 1294309a21..143187843a 100644 --- a/packages/rest/tsconfig.json +++ b/packages/rest/tsconfig.json @@ -2,8 +2,53 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src", - "types": ["node"] + // [#9960] Widened from `./src` as a CONSEQUENCE of the `paths` rule below, + // not as a preference. Redirecting the producer's specifier to its source + // puts `packages/metadata-protocol/src/**` into this program, and `rootDir` + // is enforced over every program file even under `--noEmit`: measured here + // at 20 x `TS6059: File '.../packages/metadata-protocol/src/...' is not + // under 'rootDir'`, and with `rootDir` DELETED tsc infers one from this + // package's own inputs and reports the identical 20. `..` is the directory + // that genuinely contains every file in the program, which is what the + // diagnostic asks for. Emit is unaffected in everything that ships: this + // package builds with tsup (`tsup --config ../../tsup.config.ts`), which + // takes entry and out dir from that config, and `typecheck` passes + // `--noEmit`. The one script that emits through tsc is `dev` (`tsc -w`). + "rootDir": "..", + "types": ["node"], + // [#9960] `@objectstack/metadata-protocol` is type-imported by + // `src/package-routes.ts` (the declared `deletePackage` request/response + // shapes). Without this rule tsc resolves that specifier through the + // dependency's `exports` map — i.e. `dist/index.d.ts`, A BUILD ARTIFACT — + // so this package's `typecheck` would render a verdict about the last + // `pnpm build` rather than about the producer's source in the checkout. + // `check:type-source-resolution` refuses exactly that, and its header + // states why the dangerous case is a typecheck that PASSES. + // + // ONE rule, not two, and that is the predicate rather than a shortcut: + // `@objectstack/metadata-protocol` publishes a single entry point (its + // `exports` map has only `"."`), so there is no namespace subpath for a + // `@objectstack/metadata-protocol/*` rule to redirect. The gate judges + // rules INDIVIDUALLY against the specifiers actually imported and does not + // demand a block for imports that do not exist — a second rule here would + // point at files that are not on disk, which is worse than absent: a + // `paths` target that does not exist makes tsc fall back to node + // resolution, i.e. to `dist`, silently. + // + // ⛔ Never spell the key `@objectstack/metadata-protocol*` (star NOT + // preceded by a slash). That matches the bare name AND every subpath and + // folds them all onto one target; because the index re-exports most of the + // surface it does not crash, it type-checks against the wrong module and + // stays green. See `packages/qa/downstream-contract/tsconfig.json`, the + // repo's other `paths` block, for the measured version of that trap. + // + // No `lib` / `types` mirroring was needed, unlike that block: both packages + // extend the same root config and both already declare `types: ["node"]`, + // so the producer's source compiles under this package's environment with + // zero errors (measured: `tsc --noEmit` exit 0 over the merged program). + "paths": { + "@objectstack/metadata-protocol": ["../metadata-protocol/src/index.ts"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]