Validate Tag Coverage Report
Validate Tag Coverage Report
Written for AI agents. See Log Methodology Note below for details.
Date: February 27, 2026 Related: Design Log #73 (jay-stack validate command), #38 (Contract File), #85 (agent kit)
Background
The jay-stack validate command validates .jay-html and .jay-contract files for parse and compile errors. AI agents run this command after generating pages to verify correctness. However, validation currently only checks for errors — it doesn't tell the agent whether it used the contract's data effectively.
An agent generating a product page might successfully compile a page that only uses 3 of 12 available contract tags, missing important data like price, images, or stock status. The agent has no feedback that it left data on the table.
Problem
After generating a jay-html page, an agent needs a sanity check:
- Which contract tags did I use?
- Which tags did I miss?
- Are any required tags missing?
This feedback lets the agent decide whether to iterate on the page or move on.
Questions and Answers
Q1: Should this be a separate command or part of validate?
Answer: Part of validate — always runs. The agent gets errors + coverage in one command.
Q2: How do we detect which tags are "used" in a jay-html file?
Answer: HTML tree traversal. Walk the parsed DOM tree returned by parseJayFile() (a node-html-parser HTMLElement), tracking the current scope (key + forEach nesting). Extract tag references from:
{key.tagName}text expressionsif="key.tagName"condition attributesref="key.tagName"ref attributesforEach="key.tagName"forEach attributes- Attribute values with
{expression}bindings
Source text search was considered but rejected — it can't distinguish same-named child tags under different forEach scopes (e.g., name under options vs name under variants).
Q3: How should we handle sub-contract tags inside forEach scopes?
Answer: Track forEach scopes. When entering forEach="key.items", record that we're now inside items scope. Child references like name map to contract path items.name. The contract tells us which tags are repeated sub-contracts, so we know when a forEach introduces a new scope.
Q4: What should the output look like?
Tag Coverage:
src/pages/products/[slug]/page.jay-html
productPage (product-page): 8/12 tags used
Unused: ribbons, breadcrumbs, sku, additionalInfo
cartIndicator (cart-indicator): 3/3 tags used
When there are required unused tags:
⚠ Required unused: productName, price
Q5: Should we report on jay:instance tags (no key) or only key-based headless components?
Answer: Both. Key-based use key.tagPath references. Instance tags (<jay:contract-name>) use bare tag names inside their inline template. We can detect these by walking <jay:*> elements and checking child content against the contract.
Q6: What counts as a "required" tag?
Answer: Only explicit required: true on contract tags.
Design
Tree traversal approach
Walk the parsed body: HTMLElement tree. Maintain a scope stack that tracks the current variable context:
- Start with root scope — tag references are prefixed with headless keys (e.g.,
productPage.name) - When entering
forEach="key.items", push a new scope where bare names resolve toitems.childTag - When entering
<jay:contract-name>, push a scope for that contract's tags (bare names) - When entering
<with-data accessor="path">, push scope for nested sub-contract
At each element, extract tag references from:
forEachattribute value — marks the repeated tag as usedifattribute value — parse the condition expression for tag referencesrefattribute value — marks the interactive tag as used- Text content
{expr}— parse for tag references - Any attribute value containing
{expr}— parse for tag references
Collecting used tags
For each headless import (with or without key), maintain a Set<string> of used tag paths. After traversal, compare against the flattened contract tag list.
Flattening contract tags
Recursively flatten Contract.tags into a list of paths:
{ tag: "name", type: data }→ path"name"{ tag: "options", type: sub-contract, tags: [{ tag: "_id" }, { tag: "name" }] }→ paths"options","options._id","options.name"
Output
Add coverage: FileCoverage[] to ValidationResult. Print after errors/warnings.
Implementation Plan
Phase 1: Tag coverage analysis function
- Add
analyzeTagCoverage(jayHtml: JayHtmlSourceFile)function invalidate.ts - Implement contract tag flattening:
flattenContractTags(tags: ContractTag[], prefix?: string): TagInfo[] - Implement DOM tree walker that collects used tag paths per headless import
- Return
FileCoveragewith used/unused/required-unused per contract
Phase 2: Integration with validate
- Add
FileCoverageand related types toValidationResult - Call
analyzeTagCoverage()for each successfully parsed jay-html file - Update
printJayValidationResult()to print coverage report - Include coverage in
--jsonoutput
Verification Criteria
- Running
jay-stack validateon the whisky-store shows tag coverage per page per contract - Unused tags are listed by name
- Required unused tags are highlighted with ⚠
- Existing validation behavior (errors/warnings) is unchanged
- Output is clear enough for an AI agent to act on
Implementation Results
What was implemented
All changes in packages/jay-stack/stack-cli/lib/validate.ts:
- Types:
ContractCoverage,FileCoverage(exported),TagInfo,TagScope(internal). Addedcoverage: FileCoverage[]toValidationResult. flattenContractTags(tags, prefix?): Recursively flattens contract tags into{path, required}entries. Sub-contract children get dotted paths (e.g.,priceData.formatted.price).extractExpressions(text): Extracts{expr}patterns from text/attribute values.extractTagPath(expr): Parses expressions and conditions (!tag,tag === value) into dotted tag paths.collectUsedTags(jayHtml): Walks DOM tree with scope tracking. Handles key-based (key.tagPath), instance-based (<jay:contract-name>), forEach, and with-data scoping. ReturnsMap<importIndex, Set<tagPath>>.analyzeTagCoverage(jayHtml, file): Orchestrates flatten → collect → compare. Marks parent sub-contract paths as implicitly used when children are used.- Integration: Called after successful parse, before generate. Coverage printed after errors/warnings. Included in
--jsonoutput automatically.
Deviations from design
- Used import index (
Map<number, Set<string>>) instead of direct object references for tracking used tags per headless import — avoids Map identity issues. ifandrefattributes on elements resolve in the parent scope (before forEach), while text content and child elements resolve in the child scope (after forEach). This matches the semantic meaning:ifconditions andrefbindings on an element apply to the element itself in its parent context.- Attributes on
<jay:*>elements (props likeproductId="{_id}") resolve in the parent scope, not the instance scope — correct since prop values come from the outer context.
Test results
All existing tests pass: 61/61 (6 test files). Type checking passes.
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.