feat: HC-111 monotonic selector contracts#22
Conversation
Adds @contract: block syntax to .hcs sheets. Each selector block declares property constraints (type, required/optional, numeric bounds). ContractValidator enforces monotonicity: more-specific selectors may only narrow — never widen — the interval, type, or required status inherited from less-specific ones. - CascadeSheet: ContractType, PropertyContract, SelectorContract, updated CascadeSheet - CascadeSheetReader: parses @contract: blocks; [?] optional marker; >= / <= bounds - ContractValidator: pairwise HC2101/HC2102/HC2103 diagnostics - Validator: integrates ContractValidator into sheet validation - Emitter v2: contracts[] field per property; accepts commands + contracts params - CLI emit: passes commands and sheet.contracts to Emitter - EBNF/Hypercode_Syntax.md: v0.2 — .hcs and @contract: grammar documented - 14 new tests (Contract parsing, ContractValidator, Validator integration); 74 total Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69a38f03c8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR introduces HC-111 “monotonic selector contracts” for .hcs cascade sheets, wires contract data through IR v2 emission, and adds supporting infrastructure (typed scalar values, cascade trace retention, hashing, and an explain CLI command) to make contract enforcement and debugging observable.
Changes:
- Add
.hcs@contract:parsing plusContractValidatormonotonicity enforcement (HC2101/HC2102/HC2103) and integrate intoValidator. - Extend resolver + IR emission to support typed values, winner/losers trace, IR v2 output (hashes, context echo, contracts[]), and CLI
--ir-version. - Add
hypercode explainimplementation and tests; update schema + grammar/workplan docs.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/HypercodeTests/WhiteLabelTests.swift | Update assertions for typed scalar values (TypedValue). |
| Tests/HypercodeTests/SHA256Tests.swift | Add SHA-256 test vectors for hashing correctness. |
| Tests/HypercodeTests/HCSReaderTests.swift | Update reader tests for typed parsing. |
| Tests/HypercodeTests/ExplainTests.swift | Add end-to-end tests for cascade trace via Explainer. |
| Tests/HypercodeTests/EmitterTests.swift | Add IR v2 structure/hash determinism tests. |
| Tests/HypercodeTests/ContractTests.swift | Add contract parsing + monotonicity + validator integration tests. |
| Tests/HypercodeTests/CascadeResolverTests.swift | Update typed assertions + verify losers retention. |
| Sources/HypercodeCLI/main.swift | Add explain subcommand; add --ir-version to emit; pass .hcs file for provenance. |
| Sources/Hypercode/Validation/Validator.swift | Run contract checks as part of .hcs validation. |
| Sources/Hypercode/Validation/ContractValidator.swift | New monotonicity validator for selector contracts. |
| Sources/Hypercode/HCS/Resolver.swift | Track provenance file; retain winner/losers for explain + IR v2. |
| Sources/Hypercode/HCS/CascadeSheetReader.swift | Add @contract: parsing; typed scalar inference; expose selector parsing for CLI. |
| Sources/Hypercode/HCS/CascadeSheet.swift | Add TypedValue, Match, and contract model types; extend CascadeSheet. |
| Sources/Hypercode/Explain/Explainer.swift | New resolved-tree walker to build per-node property traces and render text. |
| Sources/Hypercode/Emit/Emitter.swift | Add IR v2 emission, typed encoding, per-node/document hashing, and version selection. |
| Sources/Hypercode/Crypto/SHA256.swift | Add CryptoKit-based SHA-256 wrapper used by IR hashing. |
| Schema/hypercode-ir-v2.schema.json | Add/define IR v2 schema including contracts[]/trace/hash fields. |
| EBNF/Hypercode_Syntax.md | Update grammar doc to v0.2 and document .hcs + @contract: syntax. |
| DOCS/Workplan.md | Add detailed implementation plan for HC-110..112/111 sequence. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- New "Review follow-ups" section: R1-R8 blocking PR #22 (child-node hash schema violation, emitter Int-overflow crash, contract validator false positives and equal-specificity gap, Foundation leak in Explainer, compiler warning, test-framework consistency, undelivered plan items), R9-R13 pending owner decisions (contract value validation, lossy v1, D4/CryptoKit deviation, absent-bound semantics, public API construction gaps) - Fix PR-4 header: #22 is open, not merged; link PR numbers on all four headers - Annotate D4 decision row with the CryptoKit deviation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- R9: contract value validation -> separate PR-5 with HC2104; files table fixed - R10: store source lexeme alongside typed value; v1 emits byte-for-byte - R11: switch SHA-256 to swift-crypto (D4 superseded; Linux-capable) - R12: omitted bound = inheritance via interval intersection; specify in RFC - R13: public inits for Match/PropertyTrace/NodeTrace + derived ResolvedValue init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Package.swift: swift-crypto dependency (Linux-capable, same CryptoKit API); 0.5.0-dev version comment (R8) - SHA256.swift: import Crypto; hand-rolled hexString (String(format:) was a Foundation leak via member lookup) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R10: TypedValue is now a struct {kind, lexeme} — the typed kind drives v2 IR
and hashing, the source lexeme drives v1 IR and explain text, so v1 round-trips
byte-for-byte ("1.10" stays "1.10", "0123" stays "0123"). Static factories keep
existing .string()/.int() call sites compiling unchanged.
R13: explicit public inits for Match, PropertyTrace, NodeTrace;
ResolvedValue.init(winner:losers:) derives value/provenance from winner —
an inconsistent instance can no longer be constructed. PropertyCascade.decide
simplified accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R1: nodeV2 now returns (ir, hash) recursively — child hashes are computed once and embedded, replacing the root-only zip-insert that violated the schema's required "hash" on every resolvedNode. Command arrays are padded, never zip-truncated, so a caller-side mismatch cannot drop nodes. R2: whole-number doubles render via Int(exactly:) and a finiteness guard — a 26-digit numeral emits as 1e+26 instead of trapping in Int(_:). Also (R8): contracts[] accumulate in ascending specificity with declaration order as the stable tie-breaker. Per-node contract matching hoisted out of the per-property loop. 3 regression tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ty conflicts R3: like the CSS cascade, specificity only relates contracts that can govern the same node — a pair is checked only when some node in the document matches both selectors. Kills the false-positive HC2102 on disjoint selectors that made legitimate sheets unwritable. ContractValidator.validate now takes the forest; Validator passes it through. R4: contracts at equal specificity apply with equal force — a type conflict makes the intersection unsatisfiable and is now reported as HC2101 (was silently skipped by the strict `<` guard). Bounds at equal specificity intersect and stay legal. R7 (bundled — same file rewrite): ContractTests converted from Swift Testing to XCTest, matching the other 14 test files. Shared-key iteration is sorted for deterministic diagnostic order. 79 tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
padding(toLength:withPad:startingAt:) is Foundation/NSString API that compiled only via Swift 5 leaky member lookup (no import in file; Foundation loaded transitively) and would break under MemberImportVisibility. Replaced with hand-rolled padding — the core target stays Foundation-free. Also fixes the never-mutated `var line1` warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EBNF/Hypercode_Resolution.md 0.2: new §7 Contracts — accumulation by intersection (omitted bound inherits, decision R12), overlap-gated monotonicity with HC2101-HC2103 table, HC2104 planned as PR-5, IR echo ordering; "typed scalars" un-deferred (landed with IR v2) - EBNF/Hypercode_Syntax.md: <scalar> grammar fixed — bare unquoted strings are valid (driver: sqlite); type-inference and lexeme preservation noted; stale Date fixed; semantics table moved out (syntax doc covers syntax only) - RFC/Hypercode.md 0.2: contracts in the language model (§9.4 refined with intersection + overlap rules), explain and IR v2 marked shipped (§9.4, §9.7), type-system maturity limitation updated (§9.8), changelog entry - workplan.md M8: HC-110/111/112 annotated with open PR links (boxes get checked on merge) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…schema
The PR-2 plan promised an ajv validation step — now the Swift workflow emits
IR v2 from Examples/service.{hc,hcs} (dev + production contexts) and validates
it with ajv (draft 2020-12).
Wiring this up immediately caught a schema bug the review missed: typed
values used oneOf [string|integer|number|boolean], and every int value
matches both "integer" and "number", failing "exactly one". Switched to
anyOf in resolvedProperty.value and matchEntry.value. Both example outputs
now validate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HypercodeVersion.current is the single source of truth for resolver.version in IR v2 (was a hardcoded "0.5.0" drifting from CHANGELOG's 0.4.0); set to 0.5.0-dev until release - Schema descriptions no longer claim contracts[] is "empty until HC-111" — documented as accumulated ascending-specificity contracts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
@contract:block syntax to.hcscascade sheets — selectors declare property constraints (type, required/optional, numeric bounds>= / <=)ContractValidatorenforces monotonicity: a more-specific selector may only narrow constraints (HC2101 type mismatch, HC2102 interval widening, HC2103 optional weakening). Pairs are compared only when some node matches both selectors (CSS-style), and type conflicts at equal specificity are caughtEmitter.emitacceptscommands:andcontracts:Strict-review follow-ups included (R1–R8, R10–R13)
This branch also carries the fixes from the 2026-06-11 strict review (see
DOCS/Workplan.md§ Review follow-ups):hash(child nodes were missing it)Int.max(1e+26instead of a crash)String(format:)); warning-free buildoneOf→anyOfschema bug); EBNF/RFC/workplan items delivered;contracts[]sorted;0.5.0-devcommentTypedValuekeeps the source lexeme — v1 IR round-trips byte-for-byte (1.10,0123)Match/PropertyTrace/NodeTrace;ResolvedValue.init(winner:losers:)derives value/provenanceR9 (value validation against contracts, HC2104) lands as a stacked PR-5.
Test plan
swift build— zero warningsajv validategreen for dev + production example IR🤖 Generated with Claude Code