Skip to content

feat(cli): answer the component contract per component, not per catalog - #85

Open
vltansky wants to merge 7 commits into
masterfrom
vladta/components-cli-first
Open

feat(cli): answer the component contract per component, not per catalog#85
vltansky wants to merge 7 commits into
masterfrom
vladta/components-cli-first

Conversation

@vltansky

@vltansky vltansky commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

The agent-facing guidance named dist/components.json as the source of truth:

The exact-version manifest is the source of truth when the network is reachable:
https://cdn.jsdelivr.net/npm/@wix/htmdx@4.11.0/dist/components.json

That is an instruction to read 13,634 tokens (gpt-tokenizer, o200k_base) to learn about a handful of components. Where it goes:

Part Tokens Share
JSON punctuation and indentation 4,873 36%
example 4,174 31%
props 2,326 17%
purpose 1,645 12%
name, body, source 616 4%

Over a third is structural syntax a machine parses for free and a model pays for. Meanwhile the bin already answered the same questions for far less — htmdx components lists the catalog in 1,600 tokens, htmdx components Callout in 50. The docs pointed past it.

Two gaps kept the CLI from being a complete substitute: one name per invocation, and no way to ask what a given artifact uses — the question an edit actually starts from.

Change

  • components <name...> takes several names in one call. One unknown name fails the whole call, so a partial answer never reads as a complete one.
  • components --used <file> prints the contract for exactly the components a file contains, from an artifact's <script type="text/htmdx"> block or a bare source file. Its output ends by naming how many components it left out and the command that lists them.
  • Agent-facing docs (skill/authoring.md, skill/components.md, both READMEs) name the CLI, and split it by task: the full list when authoring, --used when editing. components.json keeps its htmdx@2 envelope and exact-version guarantee as bulk data for tools, and stays documented where it is consumed as data.
  • ADR: adr/answer-the-component-contract-from-the-cli.md, status proposed — see What this does not establish.

--used scans for capitalized tags instead of compiling. Compiling is more precise, but the artifact whose contract you need is disproportionately the one that does not compile yet. Same tolerance means a tag inside a code fence counts — over-reporting costs a few lines, missing a component costs the answer.

Before / after output

Same question — "what is the grammar for what this artifact uses?" — asked both ways:

$ # before: the documented path
$ curl -s https://cdn.jsdelivr.net/npm/@wix/htmdx@4.11.0/dist/components.json | wc -c
   55916          # 13,634 tokens, all 89 components

$ # after
$ npx @wix/htmdx components --used examples/decision-brief.html
Accordion

Group of expandable AccordionItem sections; choose single or multiple open items.

body: htmdx  source: shadcn

props:
  type: string
    Whether one or many items can be open at once.
  collapsible: boolean (default false)
    For type single, lets the open item close when selected again.
  defaultValue: json
    Initially open item value: a JSON string for type single or a JSON string array for type multiple.

example:
  <Accordion type="single" collapsible defaultValue='"risks"'>
    <AccordionItem value="risks">
      <AccordionTrigger>Key risks</AccordionTrigger>
      <AccordionContent>Vendor lock-in.</AccordionContent>
    </AccordionItem>
  </Accordion>

... 15 more

73 other component(s) available: htmdx components     # 1,576 tokens, 16 components

Measured across every shipped example:

Artifact Components --used tokens vs manifest
diagrams.html 2 100 136x
degraded.html 5 396 34x
blank-layout.html 8 660 21x
index.html 9 790 17x
decision-brief.html 16 1,576 8.7x
component-tour.html 73 7,134 1.9x

component-tour.html is a catalog demo that names 73 components — the case where reading nearly everything is the correct answer, and it still halves the manifest.

Multiple names in one call:

$ npx @wix/htmdx components Callout DataTable Stat   # 196 tokens

Does the cheaper read produce worse artifacts?

No. bench/live/ is a new opt-in eval that runs the same edits through a real model — one contract read, one artifact, one change, one optional follow-up for contracts by name — and scores the result with this repo's own validate(): it passes when the document compiles, lints clean, contains what the change asked for, and still contains what it started with. No model judges another model.

Two models, 72 calls each, 3 samples per cell:

Read Haiku 4.5 Codex Input tokens vs manifest
components.json 21/24 24/24
htmdx components 21/24 22/24 47% / 68%
htmdx components --used 21/24 24/24 56% / 80%

--used matches the manifest on both models. The read this PR retires buys nothing the cheap one does not already deliver.

The interesting loser is htmdx components — names and purposes, no grammar. It fails only on the change that introduces a component the file does not carry, and for a specific reason: on 5 of 6 such calls it never asked for the missing contract and wrote an invented body instead. --used asked all 6 times.

htmdx components  →  RiskTable listed by name, no grammar  →  model writes:
    - **Critical:** …            ✗ body-contract: expected a unique bold tier
                                   (Must-have, Differentiator, Not now, Won't do)

htmdx components --used  →  "not in this file, same family: …"
                            "73 other component(s) available"
                         →  model asks: NEED: RiskTable  →  ✓ valid

A read that admits what it is missing gets followed up on; a read that lists every name looks complete and does not. That is direct evidence for the pointer lines this PR adds to --used, and it earned a docs change: skill/components.md now says to fetch the contract by name before using a component the list only named.

Run it with HTMDX_LIVE_EVAL=1 yarn eval. It is non-deterministic and costs money, so it is not in yarn test and not in CI. Adapters drive the claude and codex CLIs already authenticated on the machine, with every harness feature they offer switched off, so the contract read is the only thing that varies between arms.

What this does not establish

The ADR is now accepted. Four things this PR still does not show:

  • 13,634 is the manifest's size, not a proven context cost. An agent reaching it through a URL may get a summary rather than the bytes, in which case the saving is smaller than the table suggests.
  • Prompt caching may favour the manifest. A stable URL is a good cache prefix; --used output varies per file and caches nothing. Cheaper in tokens is not automatically cheaper in billed compute.
  • npx is not free. npx @wix/htmdx@4.11.0 components Callout measured 2.85s cold against ~0.24s for a local dist/cli.js run. Naming several components in one call is what keeps that from multiplying.
  • The live eval ties at 24 samples per read. That rules out a large regression, not a small one. Both models also fail RiskTable identically regardless of read, so that cell measures a model ceiling, not the contract.
  • One edit, no repository around it. A real agent can run htmdx lint and retry, so these are first-attempt rates, not the rate an agent loop reaches.

The change is additive — nothing is removed, the manifest ships unchanged, and the docs commit is reversible.

Compatibility

--format json still emits the bare entry for a single name; several names and --used emit an array. That keeps existing callers working at the cost of a response shape that varies with the request — called out in the ADR. The --used discovery pointer is pretty-format only, so JSON output still parses.

Not visual — no preview needed. Before/after output is the eval table above. README.md also corrects the component count from 87 to 89.

Follow-up, not in this PR

The manifest's own size is untouched. Family examples are the next lever: 60 of 89 components sit in compound families whose canonical examples largely repeat each other (Avatar's 3 members ship 1 identical example, Breadcrumb's 7 ship 3), accounting for 3,178 of the 4,174 example tokens. Deduplicating in a projection rather than in the manifest would keep the per-definition example validation the build depends on. bench/live/ is now the gate it should be measured against.

Verification

yarn test 821 passed / 29 files · yarn lint 0 errors · yarn fmt:check clean · yarn build:library clean, bundle unchanged at 95% of budget. 14 new CLI tests cover multi-name, JSON shapes, the unknown-name-among-several failure, artifact and bare-source --used, non-compiling source, the no-components case, the unreadable-file exit, and the discovery pointer's presence in pretty output and absence from JSON.

The agent-facing guidance named dist/components.json as the source of truth,
which is an instruction to read 13,634 tokens — 36% of it JSON punctuation —
to learn about a handful of components. The bin already answered the same
questions for 50 tokens each; the docs pointed past it.

`components` now takes several names in one call, and `--used <file>` prints
the contract for exactly the components a file contains. It scans for tags
rather than compiling, so it still answers for an artifact mid-edit. Across the
shipped examples that costs 100-1,576 tokens against 13,634.

The manifest keeps its envelope and its exact-version guarantee as bulk data
for tools; agent-facing docs now name the CLI only.
@vltansky
vltansky force-pushed the vladta/components-cli-first branch from e34bcb2 to 1e8e740 Compare July 28, 2026 13:00
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Preview

Built from 6c54137900b6fb193aa52928771fe787c01b1bc1 with this branch's browser.js.

vltansky added 6 commits July 28, 2026 17:33
--used reports what a file already contains, which is the wrong answer when the
file is missing the component it should have used. Name the remainder and the
command that lists it.

The ADR moves to proposed: the token measurement is real but nothing yet shows
artifacts come out better, and prompt caching may favour the manifest it argues
against.
`<Callout` with nothing after it is the half-typed state --used exists to
answer, and the tag scanner skipped it.
The size benchmark measured the artifact; this measures the reference read that
precedes the edit. Cost is the initial read plus the follow-up for any contract
the read did not supply, over the existing edit tasks plus one per scenario that
introduces a component the source does not carry.

That last task is where the modes disagree: --used saves 63.2% on a
7-component artifact and 1.4% on a 16-component one, and is the only mode that
cannot name a component outside its own output.

Deterministic. This measures what each mode contains, not whether a model uses
it well.
A compound component is invalid without its children, so the member a file is
missing is the one an edit most likely needs -- and it is exactly what a scan of
that file cannot report. Names only; contracts stay one call away.

Costs 12 tokens on decision-brief, 27 on a partial Dialog.
The 8.7x figure measured against components.json, which this decision stops
recommending. Against `htmdx components` the saving is 63.2% on a 7-component
artifact and 1.4% on a 16-component one -- real, and artifact-dependent.
…ectly

The token benchmark prices each contract read; it cannot say whether an
agent uses it well. bench/live/ runs the same edits through a real model
— one read, one artifact, one change, one optional follow-up for
contracts by name — and scores the result with this repo's own
validator. htmdx already ships validate(), so an LLM judge would only
add a second source of noise.

Opt-in: HTMDX_LIVE_EVAL=1 yarn eval. Not in yarn test, not in CI.
Adapters drive the claude and codex CLIs already authenticated on the
machine, with every harness feature they offer switched off, so the
contract read is the only thing that varies between arms.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant