A practical walkthrough of the toolchain, end to end. Every output below is
real — produced by the CLI from Examples/service.hc
and Examples/service.hcs.
.hc + .hcs + --ctx ──▶ resolve ──▶ validate (contracts) ──▶ emit (IR v2) ──▶ your generator
│ │
explain (why is this value X?) diff (what changed?)
One structure, two contexts. service.hc is the what:
Service
Logger.console
Database#main-db
Connect
APIServer
Listen
service.hcs is the how — defaults, a production override block, and
invariants the cascade must respect in every context:
Logger:
level: "debug"
APIServer > Listen:
host: "127.0.0.1"
port: 5000
@env[production]:
Logger:
level: "info"
'#main-db':
driver: "postgres"
pool_size: 50
APIServer > Listen:
host: "0.0.0.0"
port: 8080
@contract:
APIServer > Listen:
port: int >= 1 <= 65535
host: string
'#main-db':
pool_size[?]: int >= 1 <= 100
$ hypercode resolve Examples/service.hc --hcs Examples/service.hcs --ctx env=production
Service
Logger (class: console)
- format: json [.console]
- level: info [Logger]
Database (id: main-db)
- driver: postgres [#main-db]
- file: dev.sqlite3 [Database]
- pool_size: 50 [#main-db]
Connect
APIServer
Listen
- host: 0.0.0.0 [APIServer > Listen]
- port: 8080 [APIServer > Listen]Drop --ctx and the same structure resolves to the development build
(sqlite, 127.0.0.1:5000, level: debug). The .hc never changes — that
is the white-label guarantee. Every value carries [the selector that won].
Contracts are invariants attached to selectors. Values cascade freely; contracts only accumulate and narrow (RFC §9.4). Two layers of checking:
Sheet-level (static): a more specific contract that weakens an inherited one — wider interval (HC2102), changed type (HC2101), required→optional (HC2103) — is rejected when the sheet is read.
Value-level (per context): the resolved values are checked against every applicable contract — type, bounds, required presence (HC2104):
$ hypercode validate app.hc --hcs app.hcs --ctx env=production
app.hcs:1:1: error[HC2104]: contract violation for 'port': 99999 exceeds upper bound 65535.0 from contract 'APIServer > Listen'
$ echo $?
1The classic failure — "the production override accidentally weakened a limit" — becomes a build error instead of an incident. The CI recipe is one line per context:
- name: Validate configuration for every context
run: |
hypercode validate app.hc --hcs app.hcs # development
hypercode validate app.hc --hcs app.hcs --ctx env=production
hypercode validate app.hc --hcs app.hcs --ctx env=stagingDiagnostics are also available LSP-shaped: --diagnostics json.
"Why is this value X in production?" — the eternal archaeology of layered configuration, answered in one command:
$ hypercode explain Examples/service.hc --hcs Examples/service.hcs --ctx env=production Logger level
Matched 1 node for selector 'Logger'
Node: Service > Logger.console
level
WINNER Logger { value: info }
file: Examples/service.hcs line: 16 specificity: (0,0,1) order: 4
────────────────────
losing Logger { value: debug }
file: Examples/service.hcs line: 1 specificity: (0,0,1) order: 0Winner and every losing rule, each with its file, line, specificity and
source order. Selectors work the same as in sheets: Logger, .console,
'#main-db', APIServer > Listen. Omit the property to trace all of them.
emit produces the canonical resolved-graph IR — the contract between
Hypercode and any consumer (schema,
ajv-validated in CI):
$ hypercode emit Examples/service.hc --hcs Examples/service.hcs --ctx env=production --format jsonWhat each piece is for:
- Typed values — consumers get
8080, not"8080". hashcovers only the stable resolved content (type/class/id, values, child hashes). A different rule winning with the same value does not change the hash — it is the invalidation signal for incremental regeneration: re-generate only nodes whose hashes changed.winner/losers— full provenance; an auditor or codegen validator can state which rule demanded a behavior.contracts— every contract governing the property, ascending specificity, so a consumer can re-derive the effective constraint without re-parsing the sheet.
--ir-version 1 keeps the legacy strings-only v1 for existing consumers;
v1 values round-trip byte-for-byte (1.10 stays 1.10).
The intended role (RFC §9.7): .hc/.hcs is the durable
specification, code is regenerated output.
.hc + .hcs ──▶ resolved IR ──▶ LLM generates code per node
│ │
node hashes validated against the same
(what changed?) contracts + provenance
- The generator consumes the IR — never the raw sheets — so it sees one unambiguous, typed, context-resolved graph.
- Node hashes scope regeneration to what actually changed.
- Contracts give the validator formal grounds to reject a generated artifact.
- Humans review the specification diff; machines expand it into code (review compression).
This loop is runnable today: Examples/codegen-demo/
generates a module per node, verifies freshness by node hash and contract
conformance of the generated values in CI. hypercode diff (§6) is the
productized form of that hash comparison.
The invalidation signal as a first-class command (HC-113). Emit two IRs, diff them — the output is the affected-node set with reasons:
$ hypercode emit app.hc --hcs app.hcs --ctx env=production > old.ir.json
$ sed 's/port: 8080/port: 9090/' app.hcs > edited.hcs
$ hypercode emit app.hc --hcs edited.hcs --ctx env=production > new.ir.json
$ hypercode diff old.ir.json new.ir.json
~ Service > APIServer > Listen
~ port: 8080 → 9090
was: APIServer > Listen @ app.hcs:26
now: APIServer > Listen @ edited.hcs:26
1 affected node(s)
$ echo $?
1- Hash-driven: unchanged subtrees are skipped wholesale, so cost is proportional to the change, not the tree. A provenance-only change (a different rule winning the same value) is invisible — by design.
- Resolved content only. Document metadata (
context,resolver) does not participate: two IRs that resolve to the same graph under different contexts are identical (exit0) — same graph means nothing to regenerate, whichever context produced it. When the contexts differ, the text output says so in a leadingnote:line. - Nodes are matched by selector identity (
type[.class][#id]); duplicate siblings pair by content hash first, so a duplicate that merely moved is a reorder, not two modifications. Added, removed and reordered nodes are reported as such. --format jsonemitshypercode.diff/v1(schema, ajv-validated in CI) — the machine-readable feed for incremental regeneration (feed it to your generator instead of re-running everything).- Exit code is
diff-like:0identical,1documents differ,2trouble (unreadable or non-v2 input) — usable as a CI gate ("spec changed → require regeneration").
Real configurations don't live in one file (HC-116). A tenant sheet starts
from the product baseline and overrides only what differs
(Examples/imports/acme.hcs):
@import "../service.hcs"
'#main-db':
pool_size: 80
APIServer > Listen:
port: 8443
Imports expand at the directive position, so the importer's rules come later
in source order and win specificity ties. explain shows the cross-file
cascade — every rule keeps the file it was defined in:
$ hypercode explain Examples/service.hc --hcs Examples/imports/acme.hcs \
--ctx env=production "APIServer > Listen" port
Node: Service > APIServer > Listen
port
WINNER APIServer > Listen { value: 8443 }
file: Examples/imports/acme.hcs line: 10 specificity: (0,0,2) order: 9
────────────────────
losing APIServer > Listen { value: 8080 }
file: Examples/service.hcs line: 26 specificity: (0,0,2) order: 7
losing APIServer > Listen { value: 5000 }
file: Examples/service.hcs line: 11 specificity: (0,0,2) order: 3The baseline's @contract: block still gates the tenant — contracts compose
across imports and only narrow:
$ hypercode validate Examples/service.hc --hcs bad-tenant.hcs --ctx env=production
bad-tenant.hcs:3:1: error[HC2104]: contract violation for 'port': 99999 exceeds upper bound 65535.0 from contract 'APIServer > Listen'Rules of the road: imports go first; paths resolve relative to the importing
sheet; a sheet expands once per resolution (diamonds are fine); a cycle is an
error naming the chain. Trust model: the CLI loader reads any path the
process can read (absolute and ../ included) — fine for local trusted
config; embedders resolving untrusted sheets supply a policy-bound loader
(RFC §8).
Written in .hcs |
Resolved as |
|---|---|
port: 8080 |
int 8080 |
ratio: 0.5 |
float 0.5 |
active: true |
bool true |
driver: sqlite |
string "sqlite" (bare strings are fine) |
zip: "00123" |
string "00123" — quoting forces string |
version: 1.10 |
float 1.1 in v2; v1 IR preserves the lexeme 1.10 |
{ "version": "hypercode.ir/v2", "context": { "env": "production" }, // echo of --ctx "resolver": { "name": "hypercode-swift", "version": "0.6.0-dev" }, "documentHash": "3be098179523f21c…", "nodes": [ /* … each node: */ { "type": "Listen", "hash": "c4ba5d08dcfca81b…", // stable content hash "properties": { "port": { "value": 8080, // typed, not stringly "winner": { "selector": "APIServer > Listen", "line": 26, "specificity": [0,0,2], "…": "…" }, "losers": [ { "value": 5000, "line": 11, "…": "…" } ], "contracts": [ { "selector": "APIServer > Listen", "type": "int", "min": 1.0, "max": 65535.0, "required": true } ] } } } ] }