Phase-Aware Contract Props
Design Log #152 — Phase-Aware Contract Props
Written for AI agents. See Log Methodology Note below for details.
Background
Headless component contracts define props — typed inputs passed via <jay:component prop="value"> attributes. Unlike tags (which have explicit phase annotations like slow, fast+interactive), props have no phase information. The framework doesn't know when a prop value needs to be available.
PR #182 added support for resolving {binding} expressions in instance props — e.g., <jay:category-products categorySlug="{p.categorySlug}">. The binding is resolved against the page's ViewState at render time. But there's a gap: if the binding references a fast-phase field, the slow render receives an empty string.
Related
- DL#84 — Headless component props and repeater support
- DL#124 — Contract props and params consistency
- DL#50 — Rendering phases in contracts
- PR #182 — Resolve jay params values for headless instances
Problem
# category-products.jay-contract
props:
- name: categorySlug
type: string
<!-- Page uses a keyed component `p` that provides categorySlug at fast phase -->
<jay:category-products categorySlug="{p.categorySlug}"> ... </jay:category-products>
The category-products component has slowlyRender(props) that queries the database by categorySlug. But p.categorySlug is only available after the page's fast render. During slow render:
slowRenderInstancesresolves{p.categorySlug}→phas nocategorySlugyet → returns""- Component's
slowlyRender({ categorySlug: "" })queries with empty slug → wrong/empty results - Fast render later resolves correctly, but the slow data is already cached with wrong results
No error or warning is produced. The designer has no way to know that this binding won't work at slow time.
Questions & Answers
Q1: Should props have a phase annotation like tags?
A1: Yes. Same system as tags.
Q2: What phases make sense for props? Tags use slow, fast, fast+interactive. Props are inputs from the template, not rendered outputs — do the same phases apply?
A2: Same phases as tags. slow (default), fast, fast+interactive. In practice fast === fast+interactive because interactive-phase props can change on the client.
Q3: Should the phase on a prop indicate when the VALUE is expected to be available, or when the component USES it?
A3: These are the same thing — the expectation that a value is available is defined by when the component uses it.
Q4: How should the framework validate that a prop binding provides data at the right phase?
A4: The binding's source tag phase must be ≤ the prop's phase. A slow tag can bind to a fast prop. A fast tag cannot bind to a slow prop.
Q5: Should this be a compile-time validation error, a runtime warning, or both?
A5: Compile error, like any other tag phase mismatch.
Q6: What about literal props like limit="4" — are they always available at all phases?
A6: Yes. Literal values and route params are always available at all phases.
Design
Prop Phase Annotation
Add a phase field to contract prop definitions, following the same rules as tag phases:
props:
- name: categorySlug
type: string
phase: slow # Default — must be available at build time
description: Category slug to filter products by
- name: productId
type: string
phase: fast # Only needs to be available at request time
description: Product ID to exclude from results
Phase values for props (same as tags):
| Phase | Default | Meaning | Binding source must be |
|---|---|---|---|
slow |
Yes | Available at build time | Literal, route param, or slow-phase tag |
fast |
No | Available at request time | Any of above, or fast-phase tag |
fast+interactive |
No | Can also change on client | Same as fast (fast === fast+interactive in practice) |
Default is slow — same as tags. This means existing contracts without phase on props will now be validated as slow, which may surface binding mismatches that were previously silent. This is a regression fix — the empty-prop-at-slow-time bug was always there, just undetected.
What Can Provide Slow-Phase Props
A phase: slow prop binding must resolve to a value available during slow render:
- Literal values — always available:
categorySlug="best-sellers" - Route params — always available:
categorySlug="{category}"(from URL segments) - Slow-phase keyed ViewState — available if the keyed component has
slowlyRender:categorySlug="{p.categorySlug}"wherep.categorySlugis a slow tag - Page slow ViewState — available:
slug="{pageSlug}"wherepageSlughasphase: slow
A phase: slow prop binding to a fast-phase field should produce a validation error.
What Can Provide Fast-Phase Props
A phase: fast prop can bind to anything available at request time — slow or fast ViewState, route params, query params, literals.
Validation
Compile-time error in jay-stack validate, same as tag phase mismatches.
When a <jay:component> instance has a prop binding, the validator:
- Resolves the binding path (e.g.,
p.categorySlug) - Finds the source tag in the page's contract or keyed component's contract
- Checks: source tag phase must be ≤ prop phase
Phase ordering: slow < fast ≤ fast+interactive
A slow source can bind to any prop phase. A fast source cannot bind to a slow prop.
❌ <jay:category-products> prop "categorySlug" (phase: slow) bound to
{p.categorySlug} which is phase: fast+interactive.
Suggestion: Use a slow-phase binding, a route param, or a literal value.
Literal values and route params are always considered slow (always available).
Default Behavior
Props default to phase: slow, same as tags. Existing contracts without explicit phase on props will now be validated — bindings to fast-phase fields that were silently producing empty values at slow render will surface as compile errors.
Contract Example
name: category-products
description: Shows products filtered by category
props:
- name: categorySlug
type: string
phase: slow
required: true
description: Category slug — must be available at build time for SSG
- name: productId
type: string
description: Product to exclude (no phase — works at any time)
- name: limit
type: number
description: Max products to show (no phase — literal value)
tags:
- tag: products
type: sub-contract
repeated: true
phase: fast+interactive
trackBy: _id
tags:
- tag: name
- tag: price
dataType: number
Implementation Plan
Phase 1: Contract schema
- Add
phasefield toContractProptype in compiler-shared (defaults toslow) - Update contract parser to accept
phaseon props - Update
validate-pluginto validate phase values
Phase 2: Compile-time validation
- In
jay-stack validate, when processing<jay:component>instances with{binding}props:- Resolve the binding path to the source contract tag
- Compare source tag phase vs prop phase (source must be ≤ prop)
- Flag mismatches as errors
Phase 3: Documentation
- Update designer guide with phase-aware prop examples and binding rules
- Update plugin developer contract guide
- Update agent-kit contract authoring guide
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Phase annotation on props (this design) | Explicit, validates at compile time, documents intent | New concept for plugin developers to learn |
| Infer from component usage | No schema change needed | Can't validate without analyzing component source code |
| Runtime-only warning | Simple, no schema change | Catches issues late (at build/serve time, not at validate time) |
| No validation | Zero effort | Silent bugs — empty props at slow render with no feedback |
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.