Linked Contracts With Mixed Phase Properties
Design Log 79: Linked Contracts with Mixed Phase Properties
Written for AI agents. See Log Methodology Note below for details.
Background
Jay contracts support linked sub-contracts via the link property, allowing contracts to reference reusable component contracts. For example:
# category-page.jay-contract
- tag: products
type: sub-contract
repeated: true
trackBy: _id
link: ./product-card # Links to product-card.jay-contract
Jay also supports rendering phases (Design Log #50):
slow: Build-time renderingfast: Request-time SSRfast+interactive: Client-side mutable
Design Log #50 establishes that slow arrays can have fast/interactive child properties:
"Array's child properties can have later phases than the array itself"
Problem Statement
When a slow array links to a contract that has fast/interactive properties, those properties are not properly handled:
Example
# category-page.jay-contract
- tag: products
type: sub-contract
repeated: true
trackBy: _id
link: ./product-card # No explicit phase = defaults to 'slow'
# product-card.jay-contract
- tag: name
type: data
dataType: string
# phase: slow (default)
- tag: isAddingToCart
type: variant
dataType: boolean
phase: fast+interactive # Fast/interactive property!
Current Behavior (Broken)
Type Generation:
CategoryPageFastViewStatedoes NOT includeproducts// Generated - WRONG export type CategoryPageFastViewState = Pick<..., 'loadedProducts' | 'hasMore' | ...>; // Missing: products array with isAddingToCartSlow Render: Array is unrolled correctly, but expressions like
{isAddingToCart ? is-adding}reference undefined values at runtimeRuntime Error:
vs1.isAddingToCartis undefined because the fast ViewState doesn't include the products array
Expected Behavior (Per Design Log #50)
"Array appears in phase where it's set AND all later phases"
So products should appear in:
SlowViewState: with slow properties (name,thumbnail.url, etc.)FastViewState: with slow + fast properties (includingisAddingToCart)InteractiveViewState: with all properties
Root Cause Analysis
Two components don't follow linked contracts:
1. Phase Type Generator (phase-type-generator.ts)
// extractPropertyPathsAndArrays only checks tag.tags
if (tag.type.includes(ContractTagType.subContract) && tag.tags) {
// Processes inline nested tags
// Does NOT handle tag.link!
}
2. Slow Render Transform (slow-render-transform.ts)
We just fixed this in the previous session - buildPhaseMap now follows tag.link via the import resolver. But the type generation still has the same gap.
Questions
Q: Should we resolve linked contracts at contract parse time or at usage time?
A: Usage time (lazy resolution) is better because:
- Contracts may be used in different contexts
- Avoids circular dependency issues
- Consistent with how we just fixed slow render
Q: How do we handle the runtime ViewState for slow arrays with fast properties?
A: The fast phase needs to provide the array with fast properties for each item. The runtime
slowForEachItemalready supports this - it gets the item from the array and uses it for bindings. The issue is the fast render function isn't being typed/validated to return the array.Q: What about the component's fast render function - does it need to return the full array?
A: Yes, if a slow array has fast properties, the fast render must return that array with the fast properties populated. The slow properties can be omitted (they're already rendered).
Design
Phase 1: Fix Type Generation
Modify extractPropertyPathsAndArrays in phase-type-generator.ts to resolve linked contracts:
function extractPropertyPathsAndArrays(
tags: ContractTag[],
targetPhase: RenderingPhase,
parentPath: string[] = [],
parentPhase?: RenderingPhase,
parentTrackBy?: string,
importResolver?: JayImportResolver, // NEW
contractDir?: string, // NEW
): { paths: PropertyPath[]; arrays: ArrayInfo[]; asyncProps: AsyncInfo[] } {
// ...
if (tag.type.includes(ContractTagType.subContract)) {
let childTags: ContractTag[] = [];
let childContractDir = contractDir;
if (tag.tags) {
// Inline nested tags
childTags = tag.tags;
} else if (tag.link && importResolver && contractDir) {
// Linked contract - resolve and use its tags
const linkedContract = loadLinkedContract(tag.link, contractDir, importResolver);
if (linkedContract) {
childTags = linkedContract.tags;
childContractDir = getLinkedContractDir(tag.link, contractDir, importResolver);
}
}
if (childTags.length > 0) {
const result = extractPropertyPathsAndArrays(
childTags,
targetPhase,
currentPath,
effectivePhase,
trackByForChildren,
importResolver,
childContractDir,
);
// ... rest of logic
}
}
}
Phase 2: Update Contract Compiler Integration
Pass the import resolver to the phase type generator:
// In contract-compiler.ts
const phaseTypes = generateAllPhaseViewStateTypes(
contract,
baseTypeName,
importResolver, // NEW
contractPath, // NEW
);
Phase 3: Update Generated Types
After the fix, the generated types should be:
export type CategoryPageSlowViewState = Pick<CategoryPageViewState, 'products' | ...> & {
products: Array<Pick<CategoryPageViewState['products'][number], 'name' | 'thumbnail' | ...>>;
};
export type CategoryPageFastViewState = Pick<CategoryPageViewState, 'products' | 'loadedProducts' | ...> & {
products: Array<Pick<CategoryPageViewState['products'][number], 'isAddingToCart'>>;
};
export type CategoryPageInteractiveViewState = Pick<CategoryPageViewState, 'products' | ...> & {
products: Array<Pick<CategoryPageViewState['products'][number], 'isAddingToCart'>>;
};
Implementation Plan
Phase 1: Refactor Linked Contract Loading
- Extract
loadLinkedContracthelper to a shared location (currently in slow-render-transform.ts) - Create
contract/linked-contract-resolver.tswith shared logic
Phase 2: Update Phase Type Generator
- Add
importResolverandcontractDirparameters toextractPropertyPathsAndArrays - Handle
tag.linkby loading linked contract and processing its tags - Update
generatePhaseViewStateTypeto pass resolver - Update
generateAllPhaseViewStateTypesto pass resolver
Phase 3: Update Contract Compiler
- Pass import resolver to phase type generation
- Pass contract path for resolving relative links
Phase 4: Testing
- Add test for linked contract with mixed phases
- Verify generated types include slow arrays in fast ViewState when they have fast properties
- Test runtime behavior with whisky-store example
Examples
Before (Broken)
// category-page.jay-contract.d.ts
export type CategoryPageFastViewState = Pick<
CategoryPageViewState,
'loadedProducts' | 'hasMore' | 'loadedCount' | 'isLoading' | 'hasProducts'
>;
// Missing: products!
After (Fixed)
// category-page.jay-contract.d.ts
export type CategoryPageFastViewState = Pick<
CategoryPageViewState,
'loadedProducts' | 'hasMore' | 'loadedCount' | 'isLoading' | 'hasProducts'
> & {
products: Array<Pick<CategoryPageViewState['products'][number], 'isAddingToCart'>>;
};
Trade-offs
Approach A: Resolve at Parse Time (Not Recommended)
- Pro: Simpler type generator logic
- Con: Increases contract parse complexity
- Con: Potential circular dependency issues
- Con: Inconsistent with slow render approach
Approach B: Resolve at Generation Time (Recommended)
- Pro: Consistent with slow render fix
- Pro: Lazy resolution avoids circular issues
- Pro: Import resolver already exists
- Con: Need to pass resolver through call chain
Verification Criteria
- Type Generation:
CategoryPageFastViewStateincludesproductswithisAddingToCart - Compile-time: TypeScript error if fast render doesn't return products array
- Runtime:
{isAddingToCart ? is-adding}resolves correctly in slow-rendered arrays - Test: All existing tests pass + new test for linked contracts with mixed phases
Related Design Logs
- Design Log #50: Rendering Phases in Contracts - establishes phase rules for arrays
- Design Log #75: Slow Rendering - documents slow render transformation
- Design Log #78: Unified Condition Parsing - documents expression evaluation during slow render
Implementation Results
Completed: 2026-01-28
All implementation phases completed successfully.
Changes Made
Created
linked-contract-resolver.ts(lib/contract/linked-contract-resolver.ts)- Extracted
loadLinkedContractandgetLinkedContractDirto shared location - Reused by both slow-render-transform and phase-type-generator
- Extracted
Updated
phase-type-generator.ts- Added
LinkedContractContextinterface - Updated
extractPropertyPathsAndArraysto accept and use import resolver - Handle
tag.linkby loading linked contract and processing its tags - Special case for recursive links (
$/) - include property without recursing - Updated
generatePhaseViewStateTypeandgenerateAllPhaseViewStateTypesto pass resolver
- Added
Updated
contract-compiler.ts- Pass
jayImportResolverandcontractFilePathtogenerateAllPhaseViewStateTypes
- Pass
Updated
slow-render-transform.ts- Refactored to use shared
loadLinkedContractandgetLinkedContractDir
- Refactored to use shared
Updated test fixtures
named-counter.jay-contract.d.ts: Updated to reflect explicit Pick for linked contract propertiesrepeated-with-link.jay-contract.d.ts: Updated to show expanded linked contract propertiescontract-compiler.test.ts: Updated 3 test expectations for linked sub-contracts
Verified Output
After fix, CategoryPageFastViewState correctly includes products with fast+interactive properties:
export type CategoryPageFastViewState = Pick<
CategoryPageViewState,
'hasMore' | 'loadedCount' | 'isLoading' | 'hasProducts'
> & {
products: Array<
Pick<CategoryPageViewState['products'][number], '_id' | 'isAddingToCart'> & {
quickOption: {
choices: Array<
Pick<
CategoryPageViewState['products'][number]['quickOption']['choices'][number],
'choiceId' | 'inStock' | 'isSelected'
>
>;
};
}
>;
loadedProducts: Array<
Pick<CategoryPageViewState['loadedProducts'][number], '_id' | 'isAddingToCart'> & {
quickOption: {
choices: Array<
Pick<
CategoryPageViewState['loadedProducts'][number]['quickOption']['choices'][number],
'choiceId' | 'inStock' | 'isSelected'
>
>;
};
}
>;
};
Test Results
All 471 tests pass (4 skipped) in compiler-jay-html package.
Deviations from Design
Context object instead of separate parameters: Used
LinkedContractContextinterface to bundleimportResolverandcontractDirfor cleaner function signatures.Recursive link handling: Added explicit handling for
$/recursive references - these are included in phase ViewStates based on parent phase but not recursively processed (they reference the same type).
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.