Contract Props And Params Consistency
Design Log #124 — Contract Props and Params Consistency
Written for AI agents. See Log Methodology Note below for details.
Background
Contracts are the source of truth for the shape of a component. When a component accepts props (e.g., productId) or params (e.g., slug), these must be declared in the .jay-contract file. Without this, the agent-kit and validate tools cannot verify correctness, and AI agents generating pages may omit or mismatch props/params.
Related design logs: #38 (Contract File), #84 (headless props), #113 (explicit route params).
Problem
Components in consumer projects (e.g., the Wix mono repo) have contracts that are missing props and params declarations, even though the component implementation requires them. The agent-kit template already documents how to read and declare props/params in contracts, so the gap is in validation — nothing enforces consistency.
Concrete example: wix-stores-v1
// lib/components/product-page.ts
export interface ProductPageParams extends UrlParams { slug: string; }
export const productPage = makeJayStackComponent<ProductPageContract>()
.withProps<PageProps>()
.withLoadParams(loadProductParams) // yields ProductPageParams[]
...
# product-page.jay-contract — NO params section!
name: product-page
tags:
- { tag: productName, type: data, ... }
The component uses .withLoadParams() with { slug: string }, but the contract has no params. The agent-kit doesn't know about slug, so AI-generated pages may not provide it.
Gap: Validate commands don't check component-contract consistency for props/params
jay-stack validatechecks contract→route (does the route provide what contracts need), but not route→contract (are route params declared in some contract)jay-stack validate-pluginvalidates almost nothing about the component- Neither checks that
.withProps<>()/.withLoadParams<>()usage matches contract declarations
Files:
packages/jay-stack/stack-cli/lib/validate.tspackages/jay-stack/plugin-validator/lib/validate-plugin.ts
Questions
Q: What level of validation is feasible?
A: Static checks at two levels: (a) jay-html attributes vs contract props, and (b) single-file AST analysis of component source to detect
.withProps<>()/.withLoadParams<>()usage. The AST approach is proven by existing analyzers likesource-file-binding-resolver.ts.Q: Should validate-plugin check that the contract's props match the component's TypeScript signature?
A: Yes — single-file AST check using
typescript-bridge. This is the most important check (Phase 3). Detect builder method calls and extract type parameters to compare against contract.Q: Are there cases where a component has props but intentionally omits them from the contract?
A: No. Props must be in the contract. The
props="{.}"pass-through pattern in client-only jay is not a prop declaration — the validator should skippropsas an attribute name.
Design
Phase 1: Route→contract param consistency (checkRouteToContractParams)
What: If a page is on a dynamic route, check that for each route param, at least one contract on the page (page-level or any keyed headless) declares it. One param might be consumed by the page contract, another by a headless component.
Rule: Collect all route params from the path. Collect all declared params from all contracts (page + headless). For each route param not in the combined set → warning.
Edge cases:
- No contract on the page at all → skip (nothing to check against)
- No contracts declare any params → warn for all route params
Phase 2: Jay-html→contract prop consistency (checkHeadlessInstanceProps)
What: When a <jay:xxx> instance passes attributes, check that the resolved contract declares matching props. Also check that required contract props are present on the instance.
Skip attributes: if, forEach, trackBy, ref, slowForEach, jayIndex, jayTrackBy, jay-coordinate-base, jay-scope, when-resolved, when-loading, when-rejected, accessor, props, key
Phase 3: Component source→contract consistency (checkComponentPropsAndParams)
What: In validate-plugin, parse the component's TypeScript source and check:
- If it calls
.withProps<T>()with custom props → contract must declareprops - If it calls
.withLoadParams(...)→ contract must declareparams - Individual property names match between interface and contract
AST patterns to detect:
Builder chain:
makeJayStackComponent<ContractType>()
.withProps<PropsType>() ← detect this
.withLoadParams(loadFn) ← detect this
...
Props type resolution:
.withProps<PageProps>()→PagePropsis the framework base type ({ language, url }). No custom props. Skip..withProps<ProductCardProps>()→ custom props. Findinterface ProductCardProps { productId: string }in same file. Each property must be in contractprops..withProps<PageProps & CustomProps>()→ intersection. StripPageProps, extractCustomPropsproperties.
Params type resolution:
.withLoadParams(loadProductParams)→ find the function → look for the params interface it yields (e.g.,ProductPageParams extends UrlParams { slug: string })- Extract properties from the params interface (excluding inherited
UrlParamsfields) - Each property must be in contract
params
Framework types to skip:
PageProps— framework base type, not component propsUrlParams— base for params, provides inherited fields likeRecord<string, string>RequestQuery— fast-phase only, not user-defined
Implementation Plan
Phase 1: checkRouteToContractParams
File: packages/jay-stack/stack-cli/lib/validate.ts
- Add
checkRouteToContractParams(parsedFile, filePath, pagesBase):- Extract route params via
extractRouteParams - If no route params → return
[] - Collect all declared params from page contract + all headless import contracts
- If no contracts exist at all → return
[] - For each route param not in the combined declared set → emit warning
- Extract route params via
- Call from
validateJayFilesafter existingcheckRouteParams - Add test fixtures + tests
Phase 2: checkHeadlessInstanceProps
File: packages/jay-stack/stack-cli/lib/validate.ts
- Add
HEADLESS_SKIP_ATTRSset (union of directive attrs +props,key) - Add
checkHeadlessInstanceProps(jayHtml, file):- Walk body tree, find
<jay:xxx>elements - Match to headless import by contract name
- Collect non-skip attributes → check each exists in
contract.propsby name - Check each required
contract.propsentry has a matching attribute on the element
- Walk body tree, find
- Call from
validateJayFilesaftercheckRefElementTypes - Add test fixtures + tests
Phase 3: checkComponentPropsAndParams
File: packages/jay-stack/plugin-validator/lib/check-component-contract.ts (new)
- Parse the component source with
ts.createSourceFile()(single-file, no program needed) - Walk AST top-level statements:
- Collect all
interfacedeclarations by name (for later property extraction) - Find exported variable declarations with
makeJayStackComponentcall chains
- Collect all
- Walk the builder call chain:
.withProps<T>()→ extract type argument name.withLoadParams(fn)→ mark that params are used, find function to extract params type
- Resolve types to interfaces:
- If props type is
PagePropsalone → skip (framework type) - If props type is intersection
PageProps & CustomProps→ extractCustomProps - Find the matching interface in the file → extract property names
- For params: find the function, look for the params interface (extends UrlParams)
- If props type is
- Compare against contract:
- Props: each interface property → must exist in
contract.props[].name - Params: each interface property → must exist in
contract.params[].name - Reverse: contract prop/param not in interface → warning (contract is over-declared)
- Props: each interface property → must exist in
- Return errors for mismatches
Integrate into validate-plugin:
- In
validateComponent(), resolve the component source file path frommodulefield + component name - Load and parse the contract (already done)
- Call
checkComponentPropsAndParams(sourcePath, parsedContract) - Add results as errors
File resolution for component source:
- Local plugins:
pluginPath + modulefield → directory or file → find.tsfile exporting the component name - NPM packages: look for
lib/directory (source may be available alongside dist)
Phase order
Phase 3 is the most important — implement first. Phases 1 and 2 add complementary checks for the consumer side (page validation).
Trade-offs
- Phase 1: Straightforward — reuses
extractRouteParams, adds combined param collection - Phase 2: Uses same tree-walking pattern as existing
checkRefElementTypes - Phase 3: Single-file AST approach (no type checker needed) — lightweight but can't resolve cross-file types. Properties from imported types won't be checked, only interfaces defined in the same file. This covers the common case (component defines its own props/params interfaces).
Verification Criteria
- Existing validate tests still pass
- Phase 1: warns when route provides params no contract declares
- Phase 2: warns on undeclared props and missing required props
- Phase 3: detects
.withProps<>()/.withLoadParams<>()and validates against contract - Run against wix-stores-v1 → should flag missing
paramson product-page and category-page contracts yarn vitest runinpackages/jay-stack/stack-cliandpackages/jay-stack/plugin-validatorpass
Implementation Results
Test Results
- stack-cli: 104 passed (7 test files), including 6 new tests for Phases 1 and 2
- plugin-validator: 13 passed (1 test file), all new for Phase 3
Files Modified
| File | Change |
|---|---|
plugin-validator/lib/check-component-contract.ts |
New. Single-file AST checker using typescript-bridge |
plugin-validator/lib/validate-plugin.ts |
Added checkComponentContractConsistency, source/contract resolution |
plugin-validator/lib/types.ts |
Added 'component-contract-mismatch' error type |
plugin-validator/lib/index.ts |
Export checkComponentPropsAndParams |
plugin-validator/package.json |
Added @jay-framework/typescript-bridge dependency |
plugin-validator/test/check-component-contract.test.ts |
New. 13 tests for Phase 3 |
stack-cli/lib/validate.ts |
Added checkRouteToContractParams and checkHeadlessInstanceProps |
stack-cli/test/validate.test.ts |
Added 6 integration tests for Phases 1 and 2 |
stack-cli/test/fixtures/validate/route-to-contract-missing/ |
New fixture |
stack-cli/test/fixtures/validate/headless-props-undeclared/ |
New fixture |
stack-cli/test/fixtures/validate/headless-props-missing-required/ |
New fixture |
Deviations from Design
Phase 3 uses
parseContractfrom compiler-jay-html rather than raw YAML parsing, to get properContractProp[]andContractParam[]types with all fields (required, kind, etc.).Types imported from
.jay-contractfiles are skipped in Phase 3 — if a component uses.withProps<WidgetProps>()whereWidgetPropsis imported from the contract's generated.d.ts, the check is skipped because the types match by definition.Phase 3 integrated into
validateComponent()in validate-plugin.ts, callingcheckComponentContractConsistency()which resolves the source file and contract file independently.
Post-implementation improvements
Contract file resolution via
package.jsonexports chain. The originalvalidateContractguessed contract file locations (dist/,lib/, root). This failed for wix-stores where plugin.yaml sayscontract: product-page.jay-contractandpackage.jsonexports maps it to./dist/contracts/product-page.jay-contract. AddedresolveContractFile()which first checkspackage.jsonexports for"./<contractSpec>"→ follows the mapped path → falls back to guessing.Component source resolution via
index.tsexport chain. The original design guessed source file locations. This failed for wix-stores where components are re-exported fromlib/index.ts(e.g.,export { productPage } from './components/product-page'). AddedresolveComponentSourcePath()which parses the entry module's AST, finds the re-export matching the component name, and follows the module path to the actual.tsfile. Also handlesexport * from './module'by parsing each star-exported module to check if it exports the component name.Error messages prefixed with
[contractName]. All error/warning messages from Phase 3 now start with[contract-name](e.g.,[product-page] component uses .withLoadParams()...) to identify which component the error relates to when validating plugins with multiple contracts.Vite build externals. Added
@jay-framework/typescript-bridge,module, andtypescriptto the plugin-validator'svite.config.tsrollup externals. Without this, Vite tried to bundletypescript-bridge(which usescreateRequirefrom Node'smodulebuiltin) and failed with a browser compatibility error.
Verified against real plugins
Running validate-plugin against wix-stores:
[product-page]— flagged missingparams(component uses.withLoadParams())[product-search]— flagged missingprops({category, subcategory}) and missingparams[category-list]— flagged missingprops({parentCategory})
Running against wix-stores-v1:
[product-page]— flagged missingparams[category-page]— flagged missingparams
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.