Proposal
Add `defineCapability()` as an additive sibling to `defineContract()`, for the "operations + a conformance obligation, no entity, no lifecycle" category of problem — the shape three independent repos have each hand-rolled a plain TypeScript interface for instead: edge-auth's `ResourceQuotaProvider`, aegis-daemon's `ITaskExecutorDO`, llm-providers' `LLMProvider`.
`defineContract()`'s ontology (schema/states/authority over a persisted, owned entity) is a confirmed category error for these — forcing `LLMProvider` into it would mean inventing a phantom entity with no lifecycle, the same mistake class as keeping a service's between-calls state as if it were a data state.
Evidence: a rule-of-three experiment, not a proposal on spec
Rather than propose this shape speculatively, we hand-mocked `defineCapability()` in Stackbilt-dev/contracts-lab (`capability/define-capability.ts` — explicitly NOT built into this package, per the additive/no-speculative-abstraction discipline) and tested it against all three real interfaces:
- `capability/llm-provider.capability.ts` — translated from the real `LLMProvider` (6 operations, 4 capability flags)
- `capability/resource-quota.capability.ts` — translated from the real `ResourceQuotaProvider` (3 operations; edge-auth already had real Zod schemas at the param/result boundary, just never wrapped in an operations+flags shape)
- `capability/task-executor.capability.ts` — translated from the real `ITaskExecutorDO` (2 operations)
Result: the shape fit all three cleanly, no phantom fields needed anywhere. A conformance checker (`checkConformance()`) run against realistic sample payloads for each caught a deliberately-planted real violation precisely (a missing required field in a `ConsumeQuotaResult` sample), and a test generator (`generateConformanceTests()`) emitted real, runnable Vitest per capability — the concrete payoff a hand-rolled TS interface can never give. Full writeup: `CAPABILITY-FINDINGS.md` in contracts-lab.
Two caveats to carry into any real design (from the findings doc)
- `flags` isn't universal. Only `LLMProvider` used capability flags meaningfully (streaming/tools/batching/vision genuinely vary by provider). `ResourceQuotaProvider` and `ITaskExecutorDO` both had zero flags — every conformer implements every operation unconditionally. `flags` should stay optional on the primitive, not a required part of what makes something a capability.
- Streaming/async-iterator outputs are a real blind spot. `LLMProvider.streamResponse`'s real return is `Promise<ReadableStream>` — a stream, not a value with a meaningful static schema to validate against. Not solved in the experiment; recorded honestly as `z.unknown()` rather than faked. Any real `defineCapability()` design needs an answer here (or an explicit "streaming operations opt out of output conformance checking" carve-out).
Also worth knowing: capability and entity contracts aren't alternatives
The `ITaskExecutorDO` translation exists alongside an entity contract for the same underlying system (`TaskExecutionContract`, modeling the persisted run's phase machine). Neither forced the other out — capability answers "what can you call," entity answers "what's the persisted thing's lifecycle." A real system can have both, describing different questions about the same boundary.
Not proposing to build this here
Filing per the additive-only, no-speculative-abstraction policy — this is an evidence package for a design discussion, not a PR. The hand-mocked prototype and all three translations are real and available in contracts-lab if useful reference material for whoever picks this up.
Proposal
Add `defineCapability()` as an additive sibling to `defineContract()`, for the "operations + a conformance obligation, no entity, no lifecycle" category of problem — the shape three independent repos have each hand-rolled a plain TypeScript interface for instead: edge-auth's `ResourceQuotaProvider`, aegis-daemon's `ITaskExecutorDO`, llm-providers' `LLMProvider`.
`defineContract()`'s ontology (schema/states/authority over a persisted, owned entity) is a confirmed category error for these — forcing `LLMProvider` into it would mean inventing a phantom entity with no lifecycle, the same mistake class as keeping a service's between-calls state as if it were a data state.
Evidence: a rule-of-three experiment, not a proposal on spec
Rather than propose this shape speculatively, we hand-mocked `defineCapability()` in Stackbilt-dev/contracts-lab (`capability/define-capability.ts` — explicitly NOT built into this package, per the additive/no-speculative-abstraction discipline) and tested it against all three real interfaces:
Result: the shape fit all three cleanly, no phantom fields needed anywhere. A conformance checker (`checkConformance()`) run against realistic sample payloads for each caught a deliberately-planted real violation precisely (a missing required field in a `ConsumeQuotaResult` sample), and a test generator (`generateConformanceTests()`) emitted real, runnable Vitest per capability — the concrete payoff a hand-rolled TS interface can never give. Full writeup: `CAPABILITY-FINDINGS.md` in contracts-lab.
Two caveats to carry into any real design (from the findings doc)
Also worth knowing: capability and entity contracts aren't alternatives
The `ITaskExecutorDO` translation exists alongside an entity contract for the same underlying system (`TaskExecutionContract`, modeling the persisted run's phase machine). Neither forced the other out — capability answers "what can you call," entity answers "what's the persisted thing's lifecycle." A real system can have both, describing different questions about the same boundary.
Not proposing to build this here
Filing per the additive-only, no-speculative-abstraction policy — this is an evidence package for a design discussion, not a PR. The hand-mocked prototype and all three translations are real and available in contracts-lab if useful reference material for whoever picks this up.