-
Notifications
You must be signed in to change notification settings - Fork 9
feat(api): re-arranging Cell types #1983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 issues found across 27 files
Prompt for AI agents (all 3 issues)
Understand the root cause of the following 3 issues and fix them.
<file name="packages/api/index.ts">
<violation number="1" location="packages/api/index.ts:34">
`BrandedCell` dropped the underlying value type, so helpers like `UnwrapCell` now infer `unknown`, breaking key() and wrapping typings.</violation>
<violation number="2" location="packages/api/index.ts:62">
`push` now widens the non-array branch to `any[]`, so non-array cells can call `.push(...)` without a compile-time error, risking runtime failures.</violation>
</file>
<file name="packages/runner/src/link-utils.ts">
<violation number="1" location="packages/runner/src/link-utils.ts:250">
This branch has the same issue: `isAnyCell` can match streams, but streams do not have `entityId`, so `toURI` receives `undefined` and throws when a stream is supplied as the base. Pull the id from `getAsNormalizedFullLink()` (which streams implement) instead.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
| // If no id provided, use base cell's document | ||
| if (!id && base) { | ||
| id = isCell(base) ? toURI(base.entityId) : base.id; | ||
| id = isAnyCell(base) ? toURI(base.entityId) : base.id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch has the same issue: isAnyCell can match streams, but streams do not have entityId, so toURI receives undefined and throws when a stream is supplied as the base. Pull the id from getAsNormalizedFullLink() (which streams implement) instead.
Prompt for AI agents
Address the following comment on packages/runner/src/link-utils.ts at line 250:
<comment>This branch has the same issue: `isAnyCell` can match streams, but streams do not have `entityId`, so `toURI` receives `undefined` and throws when a stream is supplied as the base. Pull the id from `getAsNormalizedFullLink()` (which streams implement) instead.</comment>
<file context>
@@ -247,7 +247,7 @@ export function parseLink(
// If no id provided, use base cell's document
if (!id && base) {
- id = isCell(base) ? toURI(base.entityId) : base.id;
+ id = isAnyCell(base) ? toURI(base.entityId) : base.id;
}
</file context>
| id = isAnyCell(base) ? toURI(base.entityId) : base.id; | |
| id = isAnyCell(base) ? base.getAsNormalizedFullLink().id : base.id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No issues found across 17 files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No issues found across 7 files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No issues found across 1 file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed changes from recent commits (found 1 issue).
1 issue found across 1 file
Prompt for AI agents (all 1 issues)
Understand the root cause of the following 1 issues and fix them.
<file name="packages/api/index.ts">
<violation number="1" location="packages/api/index.ts:151">
The new KeyResultType no longer unwraps nested branded cells, so key("nested") on Cell<{ nested: Cell<number> }> now infers Cell<Cell<number>>, breaking the documented guarantee that nested cells don’t accumulate wrappers.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
d59b811 to
98ad0ea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't yet completely grok all the new types, but the composable feature approach to cell types LGTM
packages/api/perf/README.md
Outdated
|
|
||
| ## Prerequisites | ||
|
|
||
| The commands below assume you are inside the repo root (`labs-secondary`) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
secondary?
packages/api/perf/README.md
Outdated
|
|
||
| The commands below assume you are inside the repo root (`labs-secondary`) and | ||
| that the vendored TypeScript binary at | ||
| `node_modules/.deno/typescript@5.8.3/node_modules/typescript/bin/tsc` is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably in your local deno cache, there are no node_modules in our workspace
packages/api/perf/README.md
Outdated
| like so: | ||
|
|
||
| ```bash | ||
| node -e 'const trace=require("./packages/api/perf/traces/ikeyable-cell/trace.json");\ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use node in our codebase
| */ | ||
| export interface IDerivable<T> { | ||
| map<S>( | ||
| this: IsThisObject, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice specialization work here
packages/api/index.ts
Outdated
| | BrandedCell<Array<unknown>> | ||
| | BrandedCell<Array<any>> | ||
| | BrandedCell<Record<string, unknown | any>> | ||
| | BrandedCell<unknown> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could "inherit" from IsThisArray:
type IsThisObject =
| BrandedCell<JSONObject>
| BrandedCell<Record<string, unknown | any>>
| IsThisArrayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea, done
packages/api/index.ts
Outdated
| | BrandedCell<JSONArray | JSONObject> | ||
| | BrandedCell<Array<unknown>> | ||
| | BrandedCell<Array<any>> | ||
| | BrandedCell<Record<string, unknown | any>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the result of unknown | any?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just unknown seems to work, so removed the any
| * Used for type-level operations like unwrapping nested cells without | ||
| * creating circular dependencies. | ||
| */ | ||
| export type BrandedCell<T, Brand extends string = string> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the meaning of the brand value (second generic)? I see "opaque" and "comparable" etc., could these be constrained to some subset? Or are all strings allowed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes it so that Cell<T> & Opaque<T> isn't allowed. We could restrict it further, but in the interest of keeping types simple I didn't see the upside, this is very internal type.
783412f to
8bdf98c
Compare
| * Overloads just help make fields non-optional that can be guaranteed to exist | ||
| * in various combinations. | ||
| */ | ||
| export function parseLink( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseLink was updated to accept AnyCell, but when the base is actually a stream we still try to read entityId and blow up.
I made a separate branch https://github.com/commontoolsinc/labs/tree/dx1/unify-types-stream-base-fix where I added a targeted regression test (stream-base-parse-link.test.ts) that reproduces the failure, then taught parseLink to normalize the base via getAsNormalizedFullLink() before dereferencing id/space. With that change the test passes, without it the test fails.
Unfortunately that branch has typechecking errors and I can't tell whether they're coming from my change or not... leaving this comment so you can help me figure it out Berni
Implements brand-based type system for unified cell types as specified in docs/specs/recipe-construction/rollout-plan.md task 2. ## Changes ### packages/api/index.ts - Add CELL_BRAND symbol and CellBrand type with capability flags - Create AnyCell<T, Brand> base type for all cell variants - Factor out capability interfaces: - Readable, Writable, Streamable (I/O operations) - Keyable, Resolvable, Equatable (structural operations) - Derivable (transformations) - Define cell types using brands: - OpaqueCell (opaque references with .key() and .map()) - Cell (full read/write/stream interface) - Stream (send-only) - ComparableCell, ReadonlyCell, WriteonlyCell - Add OpaqueRefMethods<T> interface for runtime augmentation - Update OpaqueRef<T> to extend OpaqueCell + OpaqueRefMethods - Add CellLike<T> and OpaqueLike<T> utility types ### packages/runner/src/builder/types.ts - Update module augmentation for OpaqueRefMethods - Add runtime methods: .export(), .setDefault(), .setName(), etc. - Remove duplicate method definitions (inherited from base) ### packages/runner/src/cell.ts - Add [CELL_BRAND] property to StreamCell and RegularCell - Import AnyCell and CELL_BRAND from @commontools/api - Update push() signature to accept AnyCell<any> ## Results - Reduced type errors from 51 to 10 (80% reduction) - Cell and Stream remain interfaces for module augmentation - Backward compatible with existing code - Foundation for future cell system refactoring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Use Omit to exclude methods from OpaqueCell that are redefined in OpaqueRefMethods, ensuring the OpaqueRefMethods versions take precedence. This fixes the issue where .key() on OpaqueRef was returning OpaqueCell instead of OpaqueRef, causing test failures when chaining methods like .key().export(). ## Changes - Add .key() to OpaqueRefMethods with OpaqueRef return type - Use Omit<OpaqueCell<T>, keyof OpaqueRefMethods<any>> in OpaqueRef type - Ensures OpaqueRef.key() returns OpaqueRef<T[K]>, not OpaqueCell<T[K]> ## Results - Reduced errors from 8 to 2 (75% reduction) - OpaqueRef.key() now correctly preserves OpaqueRef type - Total error reduction: 51 → 2 (96% improvement!) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Refactors isOpaqueRefType() to clarify detection strategy: 1. Primary: Check CELL_BRAND property (most reliable when accessible) 2. Fallback: Name-based detection for edge cases The fallback is necessary because in some TypeScript type resolution scenarios (alias resolutions, interface references), the CELL_BRAND property isn't directly exposed by TypeScript's type system APIs. Improvements: - Extract fallback logic into isCellTypeByName() helper - Consolidate name checks into isCellTypeName() and containsCellTypeName() - Add clear documentation explaining why fallback is needed - Eliminate code duplication All tests pass (103 steps). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Through systematic testing, discovered that only ONE fallback check is needed: checking the type reference target symbol name. Removed unnecessary checks: - type.getSymbol() - not needed - type.aliasSymbol - not needed - resolvesToCommonToolsSymbol() - not needed - containsCellTypeName() with qualified names - not needed The minimal fallback that works: - Check typeRef.target.symbol.getName() matches known cell type names This reduces code from ~100 lines to ~20 lines while maintaining all functionality. The fallback is only needed when CELL_BRAND isn't accessible during certain TypeScript type resolution stages. All tests pass (103 steps). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added detailed comments explaining why the fallback is necessary and
when each detection method works.
Key findings from diagnostic investigation:
- CELL_BRAND check: 0 successes across all tests
- Fallback check: 1,244 uses across all tests
The issue: TypeScript's type.getProperty() doesn't expose properties from
generic interface declarations. When we receive TypeReference types
(e.g., OpaqueCell<number>), we're looking at a reference to the generic
interface OpaqueCell<T>, not a fully instantiated type. The CELL_BRAND
property exists in the source, but isn't accessible via the type system API.
The fallback works by checking typeRef.target.symbol.getName() which gives
us the interface name ("OpaqueCell", "Cell", "Stream") without needing to
access the interface's properties.
This is a fundamental limitation of how TypeScript's Compiler API exposes
type information for generic interfaces.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Switch the opaque-ref transformer to resolve branded cell types by walking apparent/reference/base types and reading the CELL_BRAND unique symbol. This makes detection resilient to renames and ensures we respect the authoritative metadata exposed by the public API. Drop the legacy symbol-name fallback that was compensating for the missing brand lookup and rely fully on the brand instead. The new helper handles unions, intersections, and class/interface hierarchies so existing call sites continue to work. Align the test harness with the real OpaqueRef definition by modeling the brand instead of a stub field. This keeps the analyzer fixtures representative of production behavior while staying intentionally minimal.
…s tsc to be available
8bdf98c to
5ab8f2b
Compare
- add explicit type to note.tsx
Unify Type System: IKeyable Interface and Cell Type Architecture
Note: This carries over some duplication between OpaqueRef and Cells as separate types. And there is duplicative use of symbols to brand types. This will be fixed in a future PR.
Overview
This PR fundamentally restructures the type system to use a Higher-Kinded Type (HKT) pattern for the
IKeyableinterface, replacing the previous scattered type definitions with a unified, composable architecture. This change improves type safety, eliminates circular dependencies, and provides a foundation for better variance control across the codebase.Major Changes
1. Introduced HKT-based
IKeyable<T, Wrap>Interface (packages/api/index.ts).key()method signatures with a single, reusableIKeyable<T, Wrap>interfaceCell<T>,OpaqueCell<T>,ReadonlyCell<T>)out Tmodifier and variance guards (unknown extends K)BrandedCelltypes to prevent accumulation (e.g.,Cell<Cell<T>>becomesCell<T>)Key type:
2. Unified Cell Architecture with Capability Interfaces
Restructured the entire cell type system into composable capability interfaces:
IAnyCell<T>- Base marker interfaceIReadable<T>- Read operations (.get())IWritable<T>- Write operations (.set(),.update(),.push())IStreamable<T>- Event sending (.send())IEquatable- Equality checks (.equals())IKeyable<T, Wrap>- Property access (.key())IResolvable<T, C>- Cell resolution (.resolveAsCell())IDerivable<T>- Derivation operations (.map(),.mapWithPattern())IOpaquable<T>- Legacy opaque operations (deprecated methods)3. Brand-Based Cell Variants
Introduced a brand system using
BrandedCell<T, Brand>to distinguish cell types at compile-time:Cell<T>- Full cell with read/write/stream capabilities (brand: "cell")OpaqueCell<T>- Opaque reference for keying and derivation only (brand: "opaque")Stream<T>- Stream-only for event sending (brand: "stream")ReadonlyCell<T>- Read-only variant (brand: "readonly")WriteonlyCell<T>- Write-only variant (brand: "writeonly")ComparableCell<T>- Equality checks only (brand: "comparable")Each variant composes only the capability interfaces it needs, preventing misuse.
4. Replaced
Cellify<T>withAnyCellWrapping<T>BrandedCellunwrapping in nested structuresIDandID_FIELDmetadata symbols.set(),.update(),.push(), and.send()methods5. Unified
CellLikeandOpaqueSemanticsCellLike<T>- Simplified to justAnyCell<T>(top level must be a cell)Opaque<T>- AcceptsTOR any cell wrappingT, recursively at any levelCellLikerequires cells,Opaqueaccepts plain or wrapped values6. Module Augmentation Refactor (
packages/runner/src/cell.ts)IAnyCell<T>,IWritable<T>,IStreamable<T>with runtime-specific methods@commontools/api) and runtime implementation7. Type Utilities
UnwrapCell<T>- Recursively unwrapsBrandedCelltypes while preservinganyandunknownKeyResultType<T, K, Wrap>- Implements the.key()return type logic with all variance guardsBenefits
UnwrapCellandBrandedCellbreak previous circular type issues.key()now correctly infers nested property types across all cell variants.key()behavior viaIKeyableMigration Notes
Cellify<T>‚ÜíAnyCellWrapping<T>(semantically the same for write operations)OpaqueRefMethods<T>‚ÜíIOpaqueCell<T>(interface for opaque cell capabilities)BrandedCell<T, Brand>for compile-time differentiationIOpaquableare marked as deprecatedTesting
UnwrapCelland nested cell handling.key()works correctly across all cell variantsSummary by cubic
Unifies the Cell type system using brands and capability interfaces, and adds an HKT-based key() that returns correctly wrapped types. This improves type safety, simplifies APIs, and fixes inference across runner, CLI, and UI.
Refactors
Migration
Written for commit 16e2226. Summary will update automatically on new commits.