Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions drive/calibration/grep-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ rg 'tables:\s*\{\s*[a-z][A-Za-z_]+\s*:' packages/ -g '*.test.ts' -g '*.test-d.ts
## Contract-cast hygiene

```bash
# Descriptor contractSpace.contractJson → Contract casts outside framework-components (forbidden):
# ControlStack owns the one sanctioned narrowing (extensionContracts assembly in
# control-stack.ts); consumers property-pick composed contracts from the stack
# instead of re-deriving them from extension descriptors. -U catches multi-line casts.
# Descriptor contractSpace.contractJson → Contract casts — forbidden anywhere:
# ControlExtensionDescriptor declares contractSpace: ContractSpace (typed contractJson),
# so every consumer — including ControlStack's extensionContracts assembly — property-picks;
# no narrowing cast is sanctioned anymore. -U catches multi-line casts.
# Form 1: reach-through (blindCast<...>(x.contractSpace.contractJson)):
rg -U 'blindCast<[^(]*\([^)]*contractSpace!?\??\.contractJson' packages/
# Form 2: picked-variable (const contractJson = ...; blindCast<...>(contractJson)):
rg -U 'blindCast<[^(]*\(\s*contractJson\s*\)' packages/
# (Casts of other contractJson values — e.g. a query-builder accepting user-supplied
# contract JSON at its API boundary — are separate boundaries, not this anti-pattern.)
rg -U 'blindCast<[^(]*\([^)]*contractSpace!?\??\.contractJson' packages/ -g '!**/1-core/framework-components/**'
```

## Cross-cutting anti-patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ControlFamilyInstance,
ControlTargetInstance,
} from './control-instances';
import type { ContractSpace } from './control-spaces';
import type { ControlStack } from './control-stack';
import type { EmissionSpi } from './emission-types';

Expand Down Expand Up @@ -85,5 +86,6 @@ export interface ControlExtensionDescriptor<
TTargetId
> = ControlExtensionInstance<TFamilyId, TTargetId>,
> extends ExtensionDescriptor<TFamilyId, TTargetId> {
readonly contractSpace?: ContractSpace;
create(): TExtensionInstance;
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,27 +433,15 @@ interface DependencyDeclaringDescriptor {
};
}

interface ContractSpaceCarryingDescriptor {
readonly id: string;
readonly contractSpace?: {
readonly contractJson?: unknown;
};
}

function assembleExtensionContracts(
extensions: ReadonlyArray<ContractSpaceCarryingDescriptor>,
extensions: ReadonlyArray<
Pick<ControlExtensionDescriptor<string, string>, 'id' | 'contractSpace'>
>,
): ReadonlyMap<string, Contract> {
const result = new Map<string, Contract>();
for (const ext of extensions) {
const contractJson = ext.contractSpace?.contractJson;
if (contractJson === undefined) continue;
result.set(
ext.id,
blindCast<
Contract,
'contractSpace.contractJson is the emitted, validated contract for this extension space'
>(contractJson),
);
if (ext.contractSpace === undefined) continue;
result.set(ext.id, ext.contractSpace.contractJson);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Contract, StorageBase } from '@prisma-next/contract/types';
import { expectTypeOf, test } from 'vitest';
import type { ControlExtensionDescriptor } from '../src/control/control-descriptors';
import type { ContractSpace } from '../src/control/control-spaces';

// Mirrors how the sql/mongo families narrow contractSpace to their storage shape.
interface DemoStorage extends StorageBase {
readonly demoOnly: true;
}

interface NarrowedExtensionDescriptor extends ControlExtensionDescriptor<'sql', 'postgres'> {
readonly contractSpace?: ContractSpace<Contract<DemoStorage>>;
}

test('core extension descriptor declares an optional framework-level contract space', () => {
expectTypeOf<ControlExtensionDescriptor<'sql', 'postgres'>['contractSpace']>().toEqualTypeOf<
ContractSpace | undefined
>();
});

test('family descriptors narrow contractSpace and stay assignable to the core descriptor', () => {
expectTypeOf<NarrowedExtensionDescriptor>().toExtend<
ControlExtensionDescriptor<'sql', 'postgres'>
>();
expectTypeOf<NarrowedExtensionDescriptor['contractSpace']>().toEqualTypeOf<
ContractSpace<Contract<DemoStorage>> | undefined
>();
});

test('typed contract access needs no casts', () => {
const readComposedContract = (
descriptor: ControlExtensionDescriptor<string, string>,
): Contract | undefined => descriptor.contractSpace?.contractJson;

expectTypeOf(readComposedContract).returns.toEqualTypeOf<Contract | undefined>();
});
23 changes: 23 additions & 0 deletions projects/lsp-interpreter-diagnostics/plans/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Derived from the Project DoD (AC numbers = DoD order) and cross-cutting requirem
| TC-16 | Playground manual QA: unresolvable relation appears live positioned on the span, disappears on fix, no restart; config break/fix cycle on the config URI | AC1, AC9 (manual halves) |
| TC-17 | `pnpm lint:deps` green with the new psl-parser → config edge; cast-ratchet count unchanged | AC6 |

| TC-18 | Core `ControlExtensionDescriptor` declares `contractSpace?: ContractSpace`; both family overrides compile as covariant narrowings; `assembleExtensionContracts` + load-order reads use typed access; tightened grep gate: zero `contractJson` casts repo-wide | AC12 (scope addition) |

_TC-15 (end-to-end parity test, former AC2) dropped by operator decision (2026-07-09);
the spec's DoD is amended accordingly. Build/editor parity is held by construction
(one shared inner interpretation function per provider, pinned by TC-4/TC-6), span
Expand Down Expand Up @@ -145,6 +147,27 @@ stack-shaped objects surface via typecheck and gain the new property.
- [ ] Gate: existing emit/e2e tests, `pnpm fixtures:check`, `pnpm lint:deps`,
cast-ratchet ≤ baseline

### Implement M2b: contractSpace declared on the core extension descriptor

**Status:** ► In progress — slice 03 delivered (`2c3c1de79`, SATISFIED 4/4 SDoD), stacked on slice 02; PR opens after #948 merges (scope addition, operator-authorized 2026-07-10)

_Outcomes_
Core `ControlExtensionDescriptor` carries `contractSpace?: ContractSpace`; sql + mongo
overrides remain as covariant narrowings; the `assembleExtensionContracts` `blindCast`
and the structural descriptor views in `control-stack.ts` are deleted — typed access
end-to-end; the grep gate tightens from "outside framework-components" to "nowhere".
`extensionContracts` stays the consumer surface; only its construction changes.

**Shipping strategy:** type-level addition; optional member, so every existing
descriptor remains valid; families' narrowed overrides are already assignable.
Behavior identical by construction (same values, typed instead of cast).

**Tasks:**

- [ ] Declare the member in core; delete the cast + structural views; verify the
`MigrationPackage` fit and whether the load-order dependency view can go typed;
tighten the grep-library gate (satisfies: TC-18)

### Implement M3: Providers implement the capability (sql + mongo)

**Status:** ☐ Not started
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Brief: S3-D1 — lift contractSpace declaration to core, delete the bridges

## Task

Declare `readonly contractSpace?: ContractSpace;` on `ControlExtensionDescriptor`
(`packages/1-framework/1-core/framework-components/src/control/control-descriptors.ts:80`,
importing `ContractSpace` from `./control-spaces`). Then delete the bridges the
missing declaration forced in `control-stack.ts`: remove
`ContractSpaceCarryingDescriptor`; make `assembleExtensionContracts` read
`descriptor.contractSpace?.contractJson` typed and **delete its `blindCast`**.
Investigate the load-order view (`DependencyDeclaringDescriptor`): if `Contract`'s
type exposes `extensionPacks` usably, type that read too and delete the view; if not,
keep it and state why in your report (no code comment needed). Type tests first: pin
that (a) sql + mongo family descriptors remain assignable to
`ControlExtensionDescriptor` with their narrowed `contractSpace`, (b) typed access to
`contractSpace.contractJson` yields `Contract` without casts, (c) family overrides
still compile (their descriptor-self-consistency suites are the proof).

Tighten the grep-library gate (`drive/calibration/grep-library.md` § Contract-cast
hygiene): scope becomes "zero `contractSpace.contractJson` casts **anywhere**" —
drop the framework-components exclusion; keep the positive-control note (against the
pre-change tree, e.g. `git show` of the parent commit). Verify both directions.

## Scope

**In:** `packages/1-framework/1-core/framework-components/` (control-descriptors,
control-stack, tests); `drive/calibration/grep-library.md`; family packages ONLY if
typecheck demands (expected: zero changes — their overrides are already narrowings).
**Out:** `toExtensionInputs` and all CLI code (its `readonly unknown[]` boundary is a
separate concern); extension pack descriptor construction sites; `projects/**`;
behavior of any kind.

## Completed when

- [ ] Core declaration in place; `ContractSpaceCarryingDescriptor` gone;
`assembleExtensionContracts` cast-free; zero `contractJson` casts repo-wide.
- [ ] Load-order view either typed (view deleted) or kept-with-reason in the report.
- [ ] Type tests pin the covariant narrowing + typed access; family suites green
unchanged.
- [ ] Tightened gate documented + passing, with positive control.

## Standing instruction

Stay focused on the goal; control scope. If a variance snag surfaces in the family
overrides (e.g. an invariant generic), **halt and surface — do not loosen family
typing or widen core's member type.**

## References

- Slice spec: `projects/lsp-interpreter-diagnostics/slices/03-contractspace-core-descriptor/spec.md`
- Project spec § Place in the larger world (scope-addition bullet).

## Operational metadata

- **Time-box:** 45 min. Halt conditions: family override variance snag; descriptor
migrations fail `MigrationPackage` typing (the dirPath wrinkle materializing);
the declaration forces changes outside framework-components beyond type-test files.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Slice 03 — contractSpace declared on the core extension descriptor

**Project:** [`../../spec.md`](../../spec.md) · **Project plan:** [`../../plans/plan.md`](../../plans/plan.md) § M2b · **Linear:** TML-2984
**Stacked on:** slice 02 (`tml-2984-slice-02-extension-contracts`, PR #948) — consumes `extensionContracts` machinery it makes cast-free.

## Design (operator-authorized scope addition, 2026-07-10)

`ContractSpace<TContract extends Contract = Contract>` already lives in core
(`framework-components/src/control/control-spaces.ts:77` — "contract-space identity
is a framework concept, not a SQL-specific one"). Both families declare the identical
optional member, pinning only the storage generic
(`SqlControlExtensionDescriptor` → `ContractSpace<Contract<SqlStorage>>`,
`MongoControlExtensionDescriptor` → `ContractSpace<MongoContract<MongoStorageShape>>`).
The missing piece is the declaration site:

1. **Core `ControlExtensionDescriptor`** (`framework-components/src/control/control-descriptors.ts:80`)
gains `readonly contractSpace?: ContractSpace;` (default generic). Optional member
→ every existing descriptor stays valid; family overrides are covariant readonly
narrowings and must keep compiling unchanged.
2. **`control-stack.ts` sheds its structural bridges**: `ContractSpaceCarryingDescriptor`
is deleted and `assembleExtensionContracts` reads `descriptor.contractSpace.contractJson`
typed — **the `blindCast` goes**. The load-order view
(`DependencyDeclaringDescriptor`, reads `contractJson.extensionPacks`) goes typed
too **iff** the `Contract` type exposes `extensionPacks` with a usable shape;
otherwise it stays and the slice records why (in-code comment is not needed — the
report suffices).
3. **Grep gate tightens** in `drive/calibration/grep-library.md`: from "no
`contractSpace.contractJson` casts outside framework-components" to "none anywhere".
4. **Out of scope:** `toExtensionInputs` (its cast is a `readonly unknown[]` API
boundary, a separate concern); descriptor construction sites in extension packs;
any behavioral change.

## Coherence rationale

One reviewable PR: "declare in core what both families already agree on, and delete
the bridges the gap forced." Type-level only; bit-identical behavior by construction
(same values flow, typed instead of cast).

## Slice Definition of Done (beyond CI / reviewer / project-DoD)

- [x] SDoD1 — Core declaration in place (`control-descriptors.ts:89`); sql + mongo
overrides compile with **zero edits** (298/298 + 170/170); test-d pins the
covariant narrowing structurally (core cannot import families — the
`extends`-declaration itself is the compile-time proof, `toExtend` as
belt-and-braces). ✓ `2c3c1de79`
- [x] SDoD2 — Zero `contractJson` casts repo-wide; view deleted; gate tightened to
**two regex forms** (reach-through + picked-variable — the exclusion-drop alone
would have been blind to the form the control-stack cast used);
reviewer-executed both directions with historical positive controls. ✓
- [x] SDoD3 — Load-order view **kept-with-reason**, reviewer-accepted:
`buildExtensionLoadOrder` is public API (exported via `exports/control.ts`,
consumed by `family-sql/control-instance.ts`); its structural parameter is a
contract, not a cast bridge (casts nothing); typing it costs fixture
fabrication + API narrowing for zero cast savings. ✓
- [x] SDoD4 — `MigrationPackage` fit proven by zero-edit family compiles under
override-compatibility checking; the dirPath wrinkle did not materialize. ✓

**Slice-close ritual (2026-07-10):** single dispatch SATISFIED; 4/4 SDoD PASS; manual
QA: **N/A — no user-observable change** (type-level declaration lift, bit-identical
behavior). Follow-up candidate recorded in `learnings.md`: `CrossSpaceFkView`
(`family-sql/control-instance.ts:382`) could take the same typed-declaration
treatment. Stacked on slice 02; PR mechanics per orchestrator (base = slice-02 branch
until #948 merges).

## Edge cases (pre-investigated)

- The families' overrides re-declare the member; TypeScript requires override
compatibility, not identity — `ContractSpace<Contract<SqlStorage>>` must be
assignable to `ContractSpace<Contract>` (readonly members, covariance holds if
`Contract<SqlStorage>` extends `Contract`'s default). If a variance snag surfaces
(e.g. invariant generic in `MigrationPackage`), halt — do not loosen family typing.
- The CLI's `DescriptorMigrationPackage` mirror ("minus `dirPath`") hints descriptor
migrations may be in-memory; but both families already type their member as
`ContractSpace<…>` today, so their descriptors already satisfy `MigrationPackage` —
expected non-issue, verify via typecheck.

## Dispatch plan

Single dispatch.

### S3-D1 — lift the declaration, delete the bridges

- **Outcome:** the § Design list, complete; SDoD1–4.
- **Builds on:** slice 02 (`assembleExtensionContracts`).
- **Hands to:** M3+ (cleaner base; no API change for them).
- **Focus:** `packages/1-framework/1-core/framework-components/` (descriptor + stack +
tests); `drive/calibration/grep-library.md`; family packages only if typecheck
demands (expected: no changes).
- **Gate:** `pnpm --filter @prisma-next/framework-components test` + typecheck + lint,
family descriptor-self-consistency suites
(`pnpm --filter @prisma-next/family-sql test`, `pnpm --filter @prisma-next/family-mongo test`),
`pnpm typecheck`, `pnpm test:packages`, `pnpm lint:deps`, tightened grep gate.
23 changes: 23 additions & 0 deletions projects/lsp-interpreter-diagnostics/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ graph TD
module needed; the CLI's inline blindCasts are deleted and the one unavoidable
`contractJson → Contract` cast lives in framework-components only (settled by
operator, 2026-07-09).
- **Contract-space declaration lift** _(scope addition, operator-authorized 2026-07-10)_:
`ContractSpace<TContract>` is already a framework-level type
(`framework-components/control/control-spaces.ts:77`, "contract-space identity is a
framework concept"), yet core's `ControlExtensionDescriptor` never declared the
member — both families declare identical `contractSpace?: ContractSpace<…>` overrides,
and every consumer bridges the gap with structural casts. The core descriptor gains
`contractSpace?: ContractSpace`; family overrides stay as covariant narrowings; the
`assembleExtensionContracts` blindCast and `control-stack.ts` structural views are
deleted (typed access). Verify in-slice: descriptors' shipped migrations satisfy
`MigrationPackage`; whether the load-order dependency view can go typed.
- **Language server** (`packages/1-framework/3-tooling/language-server/`): consumes the
guard + capability; `config-resolution.ts` grows the context construction (property
picks off the control stack), `pipeline.ts` grows the interpret stage,
Expand Down Expand Up @@ -265,6 +275,10 @@ durable and reusable. Commit: author an ADR (or a pattern doc under
test).
- [ ] ADR / pattern doc for the capability-intersection pattern authored and linked
from `docs/architecture docs/`.
- [ ] Core `ControlExtensionDescriptor` declares `contractSpace?: ContractSpace`;
family overrides compile as narrowings; zero `contractJson` casts remain
anywhere in the repo (grep gate tightened accordingly). _(Scope addition,
operator-authorized 2026-07-10.)_

## Open Questions

Expand All @@ -288,6 +302,15 @@ function in `@prisma-next/config`, helper in config-loader, inline-in-both (adds
Also settled: tracked as Linear issue TML-2984 (not a Linear Project);
done when merged to `main`, no release cut._

_Settled by operator (2026-07-10, mid-flight): scope addition — lift the
`contractSpace` member declaration to core `ControlExtensionDescriptor`. Triggered by
the operator's design challenge ("contract spaces should be framework-level"); code
review confirmed `ContractSpace` already lives in core and only the declaration site
was family-level — the orchestrator's earlier "hoisting family shape into core"
framing during OF1 was overstated and is corrected. Runs as its own slice stacked on
slice 02; `extensionContracts` (M2) remains the consumer surface — the lift makes its
construction cast-free._

_Settled by operator (2026-07-09, plan refinement): the end-to-end parity-test DoD
item ("LSP diagnostic set equals `contract emit` diagnostic set, demonstrated by a
parity test") is dropped. Build/editor parity remains a cross-cutting requirement
Expand Down
Loading
Loading