feat(cli): answer the component contract per component, not per catalog - #85
Open
vltansky wants to merge 7 commits into
Open
feat(cli): answer the component contract per component, not per catalog#85vltansky wants to merge 7 commits into
vltansky wants to merge 7 commits into
Conversation
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
force-pushed
the
vladta/components-cli-first
branch
from
July 28, 2026 13:00
e34bcb2 to
1e8e740
Compare
Contributor
PreviewBuilt from |
--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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The agent-facing guidance named
dist/components.jsonas the source of truth:That is an instruction to read 13,634 tokens (
gpt-tokenizer,o200k_base) to learn about a handful of components. Where it goes:examplepropspurposename,body,sourceOver 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 componentslists the catalog in 1,600 tokens,htmdx components Calloutin 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.skill/authoring.md,skill/components.md, both READMEs) name the CLI, and split it by task: the full list when authoring,--usedwhen editing.components.jsonkeeps itshtmdx@2envelope and exact-version guarantee as bulk data for tools, and stays documented where it is consumed as data.adr/answer-the-component-contract-from-the-cli.md, status proposed — see What this does not establish.--usedscans 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:
Measured across every shipped example:
--usedtokensdiagrams.htmldegraded.htmlblank-layout.htmlindex.htmldecision-brief.htmlcomponent-tour.htmlcomponent-tour.htmlis 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 tokensDoes 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 ownvalidate(): 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:
components.jsonhtmdx componentshtmdx components --used--usedmatches 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.--usedasked all 6 times.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.mdnow 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 inyarn testand not in CI. Adapters drive theclaudeandcodexCLIs 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:--usedoutput varies per file and caches nothing. Cheaper in tokens is not automatically cheaper in billed compute.npxis not free.npx @wix/htmdx@4.11.0 components Calloutmeasured 2.85s cold against ~0.24s for a localdist/cli.jsrun. Naming several components in one call is what keeps that from multiplying.RiskTableidentically regardless of read, so that cell measures a model ceiling, not the contract.htmdx lintand 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 jsonstill emits the bare entry for a single name; several names and--usedemit 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--useddiscovery pointer is pretty-format only, so JSON output still parses.Not visual — no preview needed. Before/after output is the eval table above.
README.mdalso 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 test821 passed / 29 files ·yarn lint0 errors ·yarn fmt:checkclean ·yarn build:libraryclean, 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.