From fab0dd8ea44a8d3ccd8f6c8dd729b47f6ceff5ea Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Thu, 11 Jun 2026 19:30:00 +0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(R9):=20HC2104=20=E2=80=94=20resolved?= =?UTF-8?q?=20values=20validated=20against=20contracts=20(PR-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContractValueValidator checks every node's resolved values against all applicable contracts (intersection semantics, Resolution §7.3): type conformance (int satisfies float, everything else exact), declared bounds, and required presence. Violations point at the winning rule; a missing required property points at the contract demanding it. Resolution is context-dependent, so the check runs on the resolved graph and `hypercode validate` gains --ctx — the same sheet can be clean in development and violating in production. - 10 new tests incl. context-dependent violation and accumulated-contract intersection (89 total) - Resolution §7.3 planned → implemented; DOCS/Workplan R9 marked done Co-Authored-By: Claude Sonnet 4.6 --- DOCS/Workplan.md | 5 +- EBNF/Hypercode_Resolution.md | 18 +- .../Validation/ContractValueValidator.swift | 140 ++++++++++++++ Sources/HypercodeCLI/main.swift | 20 +- Tests/HypercodeTests/ContractValueTests.swift | 182 ++++++++++++++++++ 5 files changed, 355 insertions(+), 10 deletions(-) create mode 100644 Sources/Hypercode/Validation/ContractValueValidator.swift create mode 100644 Tests/HypercodeTests/ContractValueTests.swift diff --git a/DOCS/Workplan.md b/DOCS/Workplan.md index 967872c..e431def 100644 --- a/DOCS/Workplan.md +++ b/DOCS/Workplan.md @@ -291,12 +291,13 @@ All findings reproduced against `feat/hc-111-contracts` (69a38f0). R1–R8 block ### B — Decided 2026-06-11, pending implementation -- ⬜ **R9 — contract value validation is not implemented.** Values are never checked +- ✅ **R9 — contract value validation is not implemented.** Values are never checked against contracts (`timeout: 999` under `int <= 300` passes; wrong-type values pass). **Decision:** separate **PR-5** — value validation against contracts with a new **HC2104** diagnostic (type mismatch, bounds violation, missing required property). #22 stays scoped to grammar + monotonicity; files table fixed (`Resolver` row moved - to PR-5). + to PR-5). **Done:** `ContractValueValidator` on `feat/hc-111-value-validation`; + `validate` gained `--ctx`; violations point at the winning rule. - ✅ **R10 — v1 emitter is now lossy for numeric-looking strings.** `version: 1.10` → `"1.1"`, `build: 0123` → `"123"` under `--ir-version 1` (regression vs pre-PR-1 raw strings). **Decision:** store the source lexeme alongside the typed value; v1 emits diff --git a/EBNF/Hypercode_Resolution.md b/EBNF/Hypercode_Resolution.md index ac99a23..377be1e 100644 --- a/EBNF/Hypercode_Resolution.md +++ b/EBNF/Hypercode_Resolution.md @@ -147,11 +147,17 @@ Bounds at equal specificity simply intersect and are not a conflict. All three are `error`-severity diagnostics; `hypercode validate` exits non-zero. -### 7.3 Value validation (planned) - -Checking the resolved values themselves against the effective contract -(type conformance, bounds, required presence) is diagnostic **HC2104**, -scheduled as PR-5 in [DOCS/Workplan.md](../DOCS/Workplan.md). +### 7.3 Value validation (HC2104) + +Resolved values are checked against every applicable contract +(`ContractValueValidator`): type conformance (an int value satisfies a +`float` contract, ℤ ⊂ ℝ; everything else must match exactly), declared +bounds, and required presence. Because resolution is context-dependent, +this check runs on the resolved graph — the same sheet can be clean under +one `--ctx` and violating under another, so `hypercode validate` accepts +`--ctx key=value`. Violations point at the winning rule (where the value +was written); a missing required property points at the contract that +demands it. ### 7.4 IR @@ -179,8 +185,6 @@ hypercode resolve Examples/service.hc --hcs Examples/service.hcs --ctx env=produ no override-file origin), so the current precedence key is `(specificity, source-order)`. When syntax is introduced, it becomes the most-significant component of `precedence`. -- **Contract value validation.** §7.3 — HC2104, planned as PR-5. - *(Typed scalars, previously deferred, landed with IR v2: bare scalars are type-inferred at parse time, with the source lexeme preserved for v1 round-tripping.)* diff --git a/Sources/Hypercode/Validation/ContractValueValidator.swift b/Sources/Hypercode/Validation/ContractValueValidator.swift new file mode 100644 index 0000000..15751a2 --- /dev/null +++ b/Sources/Hypercode/Validation/ContractValueValidator.swift @@ -0,0 +1,140 @@ +import SpecificationCore + +/// Validates resolved property values against the contracts that govern each +/// node (Resolution §7.3, diagnostic HC2104). Contracts accumulate by +/// intersection, so a value must satisfy **every** applicable contract +/// individually: type conformance, declared bounds, and required presence. +/// +/// Resolution is context-dependent, so unlike the static `ContractValidator` +/// this check runs on the resolved graph — the same `.hcs` can be clean under +/// one `--ctx` and violating under another. +public struct ContractValueValidator { + public init() {} + + public func validate( + resolved: [ResolvedNode], + commands: [Command], + contracts: [SelectorContract] + ) -> [Diagnostic] { + guard !contracts.isEmpty else { return [] } + var diagnostics: [Diagnostic] = [] + walk(commands: commands, resolved: resolved, ancestors: [], + contracts: contracts, into: &diagnostics) + return diagnostics + } + + // MARK: - Tree walk + + private func walk( + commands: [Command], + resolved: [ResolvedNode], + ancestors: [Command], + contracts: [SelectorContract], + into diagnostics: inout [Diagnostic] + ) { + for (cmd, node) in zip(commands, resolved) { + let context = NodeContext(node: cmd, ancestors: ancestors) + let applicable = contracts.filter { selectorSpec($0.selector).isSatisfiedBy(context) } + for contract in applicable { + checkNode(cmd, node, against: contract, into: &diagnostics) + } + walk(commands: cmd.children, resolved: node.children, + ancestors: ancestors + [cmd], contracts: contracts, into: &diagnostics) + } + } + + private func checkNode( + _ cmd: Command, + _ node: ResolvedNode, + against contract: SelectorContract, + into diagnostics: inout [Diagnostic] + ) { + for key in contract.properties.keys.sorted() { + let constraint = contract.properties[key]! + guard let resolvedValue = node.properties[key] else { + if constraint.required { + diagnostics.append(Diagnostic( + severity: .error, code: "HC2104", + message: "node '\(label(cmd))' is missing required property '\(key)' — required by contract '\(contract.selector)'", + file: contract.file, + range: SourceRange(SourcePosition(line: contract.line, column: 1)) + )) + } + continue + } + diagnostics += check( + resolvedValue, key: key, + constraint: constraint, selector: contract.selector + ) + } + } + + // MARK: - Value checks + + private func check( + _ rv: ResolvedValue, + key: String, + constraint: PropertyContract, + selector: Selector + ) -> [Diagnostic] { + // Violations point at the winning rule — where the value was written — + // not at the contract declaration. + let winner = rv.winner + let range = SourceRange(SourcePosition(line: winner.line, column: 1)) + + // Type conformance. An int satisfies a float contract (ℤ ⊂ ℝ); + // everything else must match exactly. + let numeric: Double? + switch (rv.value.kind, constraint.type) { + case (.int(let i), .int), (.int(let i), .float): + numeric = Double(i) + case (.double(let d), .float): + numeric = d + case (.string, .string), (.bool, .bool): + numeric = nil + default: + return [Diagnostic( + severity: .error, code: "HC2104", + message: "contract violation for '\(key)': value '\(rv.value.rawString)' (\(kindName(rv.value.kind))) does not satisfy type '\(constraint.type.rawValue)' from contract '\(selector)'", + file: winner.file, range: range + )] + } + + var diags: [Diagnostic] = [] + if let n = numeric { + if let min = constraint.min, n < min { + diags.append(Diagnostic( + severity: .error, code: "HC2104", + message: "contract violation for '\(key)': \(rv.value.rawString) is below lower bound \(min) from contract '\(selector)'", + file: winner.file, range: range + )) + } + if let max = constraint.max, n > max { + diags.append(Diagnostic( + severity: .error, code: "HC2104", + message: "contract violation for '\(key)': \(rv.value.rawString) exceeds upper bound \(max) from contract '\(selector)'", + file: winner.file, range: range + )) + } + } + return diags + } + + // MARK: - Helpers + + private func kindName(_ kind: TypedValue.Kind) -> String { + switch kind { + case .string: return "string" + case .int: return "int" + case .double: return "float" + case .bool: return "bool" + } + } + + private func label(_ cmd: Command) -> String { + var text = cmd.type + if let c = cmd.className { text += ".\(c)" } + if let i = cmd.id { text += "#\(i)" } + return text + } +} diff --git a/Sources/HypercodeCLI/main.swift b/Sources/HypercodeCLI/main.swift index 9c934d8..bd8afda 100644 --- a/Sources/HypercodeCLI/main.swift +++ b/Sources/HypercodeCLI/main.swift @@ -4,7 +4,7 @@ import Hypercode let usage = """ usage: hypercode parse - hypercode validate [--hcs ] + hypercode validate [--hcs ] [--ctx key=value]... hypercode resolve --hcs [--ctx key=value]... hypercode emit [--hcs ] [--ctx key=value]... [--format json|yaml] [--ir-version 1|2] hypercode explain --hcs [--ctx key=value]... [property] @@ -77,6 +77,7 @@ func runResolve(_ args: [String]) throws { func runValidate(_ args: [String]) throws { var hcPath: String? var hcsPath: String? + var context: ResolutionContext = [:] var index = 0 while index < args.count { @@ -85,6 +86,14 @@ func runValidate(_ args: [String]) throws { index += 1 guard index < args.count else { fail("error: --hcs needs a path") } hcsPath = args[index] + case "--ctx": + index += 1 + guard index < args.count else { fail("error: --ctx needs key=value") } + let pair = args[index] + guard let equals = pair.firstIndex(of: "=") else { + fail("error: --ctx expects key=value, got '\(pair)'") + } + context[String(pair[.. [Diagnostic] { + let forest = try Parser(source: hc).parse() + let sheet = try CascadeSheetReader().read(hcs) + let resolved = Resolver(sheet: sheet, context: context).resolve(forest) + return ContractValueValidator().validate( + resolved: resolved, commands: forest, contracts: sheet.contracts + ) + } + + func testUpperBoundViolation() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + timeout: 999 + + @contract: + service: + timeout: int <= 300 + """ + ) + XCTAssertEqual(diags.count, 1) + XCTAssertEqual(diags[0].code, "HC2104") + XCTAssertTrue(diags[0].message.contains("exceeds upper bound")) + } + + func testLowerBoundViolation() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + replicas: 1 + + @contract: + service: + replicas: int >= 2 + """ + ) + XCTAssertEqual(diags.count, 1) + XCTAssertTrue(diags[0].message.contains("below lower bound")) + } + + func testTypeMismatch() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + timeout: fast + + @contract: + service: + timeout: int + """ + ) + XCTAssertEqual(diags.count, 1) + XCTAssertEqual(diags[0].code, "HC2104") + XCTAssertTrue(diags[0].message.contains("does not satisfy type 'int'")) + } + + func testMissingRequiredProperty() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + @contract: + service: + timeout: int + """ + ) + XCTAssertEqual(diags.count, 1) + XCTAssertTrue(diags[0].message.contains("missing required property 'timeout'")) + XCTAssertTrue(diags[0].message.contains("'service'")) + } + + func testMissingOptionalPropertyIsClean() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + @contract: + service: + timeout[?]: int + """ + ) + XCTAssertTrue(diags.isEmpty) + } + + func testValueWithinBoundsIsClean() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + timeout: 30 + + @contract: + service: + timeout: int >= 1 <= 300 + """ + ) + XCTAssertTrue(diags.isEmpty) + } + + func testIntersectionOfAccumulatedContracts() throws { + // service caps at 300; .primary raises the floor to 10. + // A value of 5 satisfies the service contract but violates .primary's. + let diags = try diagnostics( + hc: "App\n service.primary\n", + hcs: """ + service: + timeout: 5 + + @contract: + service: + timeout: int <= 300 + .primary: + timeout: int >= 10 + """ + ) + XCTAssertEqual(diags.count, 1) + XCTAssertTrue(diags[0].message.contains("below lower bound 10.0 from contract '.primary'")) + } + + func testIntSatisfiesFloatContract() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + ratio: 2 + + @contract: + service: + ratio: float >= 0.5 + """ + ) + XCTAssertTrue(diags.isEmpty, "an int value satisfies a float contract") + } + + func testContextDependentViolation() throws { + let hc = "App\n service\n" + let hcs = """ + service: + timeout: 30 + + @env[production]: + service: + timeout: 999 + + @contract: + service: + timeout: int <= 300 + """ + let dev = try diagnostics(hc: hc, hcs: hcs) + XCTAssertTrue(dev.isEmpty, "development context resolves to 30 — clean") + + let prod = try diagnostics(hc: hc, hcs: hcs, context: ["env": "production"]) + XCTAssertEqual(prod.count, 1, "production context resolves to 999 — violation") + XCTAssertEqual(prod[0].code, "HC2104") + } + + func testViolationPointsAtWinningRule() throws { + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + timeout: 999 + + @contract: + service: + timeout: int <= 300 + """ + ) + // Provenance records the winning rule's header line (line 1: "service:"), + // consistent with v1 IR "line" — not the property line within the block. + XCTAssertEqual(diags[0].range?.start.line, 1, + "diagnostic points at the winning rule, not the contract declaration") + } +} From 7e1b49151e98924389a427284058ffc6b51b9c9d Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Fri, 12 Jun 2026 00:50:56 +0300 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20address=20#23=20review=20=E2=80=94?= =?UTF-8?q?=20integer=20bounds=20compare=20without=20Double=20rounding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double(i) rounds beyond 2^53, so 9007199254740993 under int <= 9007199254740992 passed silently. Integer values now compare in the Int domain whenever the bound is an exactly representable integer, falling back to Double comparison only for fractional or out-of-Int-range bounds. Regression test with the 2^53 + 1 example. Co-Authored-By: Claude Sonnet 4.6 --- .../Validation/ContractValueValidator.swift | 38 ++++++++++++++++--- Tests/HypercodeTests/ContractValueTests.swift | 19 ++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Sources/Hypercode/Validation/ContractValueValidator.swift b/Sources/Hypercode/Validation/ContractValueValidator.swift index 15751a2..cbe5f6b 100644 --- a/Sources/Hypercode/Validation/ContractValueValidator.swift +++ b/Sources/Hypercode/Validation/ContractValueValidator.swift @@ -84,12 +84,12 @@ public struct ContractValueValidator { // Type conformance. An int satisfies a float contract (ℤ ⊂ ℝ); // everything else must match exactly. - let numeric: Double? + let numeric: NumericValue? switch (rv.value.kind, constraint.type) { case (.int(let i), .int), (.int(let i), .float): - numeric = Double(i) + numeric = .int(i) case (.double(let d), .float): - numeric = d + numeric = .double(d) case (.string, .string), (.bool, .bool): numeric = nil default: @@ -102,14 +102,14 @@ public struct ContractValueValidator { var diags: [Diagnostic] = [] if let n = numeric { - if let min = constraint.min, n < min { + if let min = constraint.min, n.isBelow(min) { diags.append(Diagnostic( severity: .error, code: "HC2104", message: "contract violation for '\(key)': \(rv.value.rawString) is below lower bound \(min) from contract '\(selector)'", file: winner.file, range: range )) } - if let max = constraint.max, n > max { + if let max = constraint.max, n.isAbove(max) { diags.append(Diagnostic( severity: .error, code: "HC2104", message: "contract violation for '\(key)': \(rv.value.rawString) exceeds upper bound \(max) from contract '\(selector)'", @@ -122,6 +122,34 @@ public struct ContractValueValidator { // MARK: - Helpers + /// A resolved numeric value compared against `Double` bounds without + /// losing integer precision: `Double(i)` rounds beyond 2^53, which could + /// hide a violation like `9007199254740993` under `int <= 9007199254740992`. + /// Integer values compare in the `Int` domain whenever the bound is an + /// exactly representable integer. + private enum NumericValue { + case int(Int) + case double(Double) + + func isBelow(_ bound: Double) -> Bool { + switch self { + case .double(let d): return d < bound + case .int(let i): + if let exact = Int(exactly: bound) { return i < exact } + return Double(i) < bound + } + } + + func isAbove(_ bound: Double) -> Bool { + switch self { + case .double(let d): return d > bound + case .int(let i): + if let exact = Int(exactly: bound) { return i > exact } + return Double(i) > bound + } + } + } + private func kindName(_ kind: TypedValue.Kind) -> String { switch kind { case .string: return "string" diff --git a/Tests/HypercodeTests/ContractValueTests.swift b/Tests/HypercodeTests/ContractValueTests.swift index 7da9f8e..af030bc 100644 --- a/Tests/HypercodeTests/ContractValueTests.swift +++ b/Tests/HypercodeTests/ContractValueTests.swift @@ -162,6 +162,25 @@ final class ContractValueTests: XCTestCase { XCTAssertEqual(prod[0].code, "HC2104") } + func testIntPrecisionBeyondDoubleRange() throws { + // 9007199254740993 = 2^53 + 1 rounds to the same Double as 2^53, so a + // Double-domain comparison would miss the violation. Integer bounds + // must compare in the Int domain. + let diags = try diagnostics( + hc: "App\n service\n", + hcs: """ + service: + big: 9007199254740993 + + @contract: + service: + big: int <= 9007199254740992 + """ + ) + XCTAssertEqual(diags.count, 1, "2^53 + 1 must violate an upper bound of 2^53") + XCTAssertTrue(diags[0].message.contains("exceeds upper bound")) + } + func testViolationPointsAtWinningRule() throws { let diags = try diagnostics( hc: "App\n service\n", From 92c02e40aa91f83416f89045c584abac089ec7d0 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Fri, 12 Jun 2026 00:52:05 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20#23=20review=20round=202?= =?UTF-8?q?=20=E2=80=94=20fail-fast=20walk,=20safe=20test=20indexing,=20R9?= =?UTF-8?q?=20tense?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ContractValueValidator.walk asserts command/resolved arrays stay parallel instead of silently skipping trailing nodes via zip - testViolationPointsAtWinningRule asserts the diagnostic count before inspecting it (clear failure instead of a crash) - DOCS/Workplan.md R9 reworded to past tense to match its ✅ status Co-Authored-By: Claude Sonnet 4.6 --- DOCS/Workplan.md | 4 ++-- Sources/Hypercode/Validation/ContractValueValidator.swift | 5 +++++ Tests/HypercodeTests/ContractValueTests.swift | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DOCS/Workplan.md b/DOCS/Workplan.md index d93c5b9..a66f6b6 100644 --- a/DOCS/Workplan.md +++ b/DOCS/Workplan.md @@ -294,8 +294,8 @@ All findings reproduced against `feat/hc-111-contracts` (69a38f0). R1–R8 block ### B — Decided 2026-06-11, pending implementation -- ✅ **R9 — contract value validation is not implemented.** Values are never checked - against contracts (`timeout: 999` under `int <= 300` passes; wrong-type values pass). +- ✅ **R9 — contract value validation was not implemented.** Values were never checked + against contracts (`timeout: 999` under `int <= 300` passed; wrong-type values passed). **Decision:** separate **PR-5** — value validation against contracts with a new **HC2104** diagnostic (type mismatch, bounds violation, missing required property). #22 stays scoped to grammar + monotonicity; files table fixed (`Resolver` row moved diff --git a/Sources/Hypercode/Validation/ContractValueValidator.swift b/Sources/Hypercode/Validation/ContractValueValidator.swift index cbe5f6b..93350ad 100644 --- a/Sources/Hypercode/Validation/ContractValueValidator.swift +++ b/Sources/Hypercode/Validation/ContractValueValidator.swift @@ -32,6 +32,11 @@ public struct ContractValueValidator { contracts: [SelectorContract], into diagnostics: inout [Diagnostic] ) { + // The resolver preserves tree shape, so the two arrays are parallel by + // construction — fail fast in debug builds rather than silently skip + // validation of trailing nodes. + assert(commands.count == resolved.count, + "command/resolved tree shape mismatch (\(commands.count) vs \(resolved.count))") for (cmd, node) in zip(commands, resolved) { let context = NodeContext(node: cmd, ancestors: ancestors) let applicable = contracts.filter { selectorSpec($0.selector).isSatisfiedBy(context) } diff --git a/Tests/HypercodeTests/ContractValueTests.swift b/Tests/HypercodeTests/ContractValueTests.swift index af030bc..5c199e7 100644 --- a/Tests/HypercodeTests/ContractValueTests.swift +++ b/Tests/HypercodeTests/ContractValueTests.swift @@ -193,9 +193,10 @@ final class ContractValueTests: XCTestCase { timeout: int <= 300 """ ) + XCTAssertEqual(diags.count, 1) // Provenance records the winning rule's header line (line 1: "service:"), // consistent with v1 IR "line" — not the property line within the block. - XCTAssertEqual(diags[0].range?.start.line, 1, + XCTAssertEqual(diags.first?.range?.start.line, 1, "diagnostic points at the winning rule, not the contract declaration") } } From 32277347655988b8dee8889842bf79677f39b0e5 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Fri, 12 Jun 2026 10:07:07 +0300 Subject: [PATCH 4/4] docs: usage guide with real CLI outputs; contract in the reference fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DOCS/Usage.md — every command end to end with genuine outputs: contexts / white-label, contracts as a CI gate (HC2104 walkthrough), explain trace, IR v2 anatomy (typed values, stable hashes, winner/losers, contracts[]), the AI-codegen pipeline, and a scalar-typing cheat-sheet - Examples/service.hcs gains a @contract: block (port/host/pool_size), so the reference fixture exercises HC-111 and is contract-validated in CI for both contexts - README: pipeline line and CLI block reflect v2/explain/validate --ctx; contracts and hashed IR added to "Why"; usage guide linked - workplan.md: new tasks HC-116 (@import for .hcs), HC-124 (end-to-end AI codegen demo); HC-092 usage guide marked done 92 tests green; ajv validates the contract-bearing fixture IR in both contexts. Co-Authored-By: Claude Sonnet 4.6 --- DOCS/Usage.md | 213 +++++++++++++++++++++++++++++++++++++++++++ Examples/service.hcs | 11 +++ README.md | 20 ++-- workplan.md | 3 + 4 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 DOCS/Usage.md diff --git a/DOCS/Usage.md b/DOCS/Usage.md new file mode 100644 index 0000000..0f3e52f --- /dev/null +++ b/DOCS/Usage.md @@ -0,0 +1,213 @@ +# Using Hypercode + +A practical walkthrough of the toolchain, end to end. Every output below is +real — produced by the CLI from [`Examples/service.hc`](../Examples/service.hc) +and [`Examples/service.hcs`](../Examples/service.hcs). + +``` +.hc + .hcs + --ctx ──▶ resolve ──▶ validate (contracts) ──▶ emit (IR v2) ──▶ your generator + │ + explain (why is this value X?) +``` + +## The running example + +One structure, two contexts. `service.hc` is the *what*: + +```hypercode +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: + +```hcs +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 +``` + +## 1. One structure, many builds (white-label / environments) + +```console +$ 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]`. + +## 2. Guardrails: contracts as a CI gate + +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): + +```console +$ 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 $? +1 +``` + +The 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: + +```yaml +- 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=staging +``` + +Diagnostics are also available LSP-shaped: `--diagnostics json`. + +## 3. Debugging the cascade: `explain` + +"Why is this value X in production?" — the eternal archaeology of layered +configuration, answered in one command: + +```console +$ 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: 0 +``` + +Winner *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. + +## 4. Machine-readable output: IR v2 + +`emit` produces the canonical resolved-graph IR — the contract between +Hypercode and any consumer ([schema](../Schema/hypercode-ir-v2.schema.json), +ajv-validated in CI): + +```console +$ hypercode emit Examples/service.hc --hcs Examples/service.hcs --ctx env=production --format json +``` + +```jsonc +{ + "version": "hypercode.ir/v2", + "context": { "env": "production" }, // echo of --ctx + "resolver": { "name": "hypercode-swift", "version": "0.5.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 } ] + } + } + } + ] +} +``` + +What each piece is for: + +- **Typed values** — consumers get `8080`, not `"8080"`. +- **`hash`** covers 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`). + +## 5. The target pipeline: specification for AI code generation + +The intended role ([RFC §9.7](../RFC/Hypercode.md)): `.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). + +The missing piece for this loop is `hypercode diff ` — +HC-113 in the [work plan](../workplan.md). + +## Scalar typing cheat-sheet + +| 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` | diff --git a/Examples/service.hcs b/Examples/service.hcs index 90286fb..eb5b971 100644 --- a/Examples/service.hcs +++ b/Examples/service.hcs @@ -26,3 +26,14 @@ APIServer > Listen: APIServer > Listen: host: "0.0.0.0" port: 8080 + +# Invariants the cascade must respect in every context (HC-111). +# A more specific selector may narrow these, never weaken them. +@contract: + APIServer > Listen: + port: int >= 1 <= 65535 + host: string + '#main-db': + pool_size[?]: int >= 1 <= 100 + Logger: + level: string diff --git a/README.md b/README.md index 2150ebe..00bb979 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ build **without touching the structure**. 📖 **API documentation:** ``` -.hc + .hcs ──[resolve]──▶ resolved graph ──[emit]──▶ canonical IR (hypercode.ir/v1) +.hc + .hcs ──[resolve]──▶ resolved graph ──[validate contracts]──▶ canonical IR (hypercode.ir/v2) ``` ## Why @@ -18,8 +18,13 @@ build **without touching the structure**. context-aware `@rules`. - **Context switching / white-label** — one structure, many contexts. Swap `--ctx env=production` (or `client=acme`) → different output, same `.hc`. -- **Provenance** — every resolved value records the selector and source line it - came from. +- **Provenance** — every resolved value records the selector, file and source + line it came from; `hypercode explain` shows the winner *and* every losing rule. +- **Contracts that only narrow** — `@contract:` blocks attach invariants to + selectors; values cascade, safety doesn't. A production override that breaks + a bound is a build error (`HC2104`), not an incident. +- **Hashed, typed IR** — per-node SHA-256 over stable resolved content: the + invalidation signal for incremental (re)generation. ## Install @@ -50,9 +55,11 @@ swift run hypercode resolve Examples/service.hc --hcs Examples/service.hcs --ctx ``` hypercode parse -hypercode validate [--hcs ] +hypercode validate [--hcs ] [--ctx key=value]... # incl. contract checks hypercode resolve --hcs [--ctx key=value]... -hypercode emit [--hcs ] [--ctx key=value]... [--format json|yaml] +hypercode emit [--hcs ] [--ctx key=value]... [--format json|yaml] [--ir-version 1|2] +hypercode explain --hcs [--ctx key=value]... [property] +hypercode lsp # LSP over stdio ``` The same structure, two contexts: @@ -64,12 +71,13 @@ hypercode resolve Examples/service.hc --hcs Examples/service.hcs --ctx env=produ ## Documentation +- **[Usage guide](DOCS/Usage.md)** — every command with real outputs: contexts, contracts, explain, IR v2 - **API docs (DocC):** - [Conceptual overview](OVERVIEW.md) - [RFC — the paradigm](RFC/Hypercode.md) - Formal specs: [`.hc` syntax (BNF)](EBNF/Hypercode_Syntax.md) · [resolution semantics](EBNF/Hypercode_Resolution.md) - Architecture: [overview](DOCS/Architecture.md) · [backends & adapters](DOCS/Backends.md) · [core vs dialects](DOCS/Dialects.md) · [positioning](DOCS/Positioning.md) -- [Resolved-graph IR schema](Schema/hypercode-ir-v1.schema.json) — the cross-implementation contract +- Resolved-graph IR schemas — the cross-implementation contract: [v2](Schema/hypercode-ir-v2.schema.json) · [v1 (legacy)](Schema/hypercode-ir-v1.schema.json) - [Lean 4 cascade oracle](SPEC/lean/) — machine-checked agreement with the resolver - [Work plan](workplan.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) diff --git a/workplan.md b/workplan.md index 0c0c31b..8fe4e98 100644 --- a/workplan.md +++ b/workplan.md @@ -99,6 +99,7 @@ P1 — built on v2: - ⬜ HC-113 `hypercode diff ` — affected nodes/properties with reasons (which rule changed), node-hash based; the invalidation signal for incremental (re)generation - ⬜ HC-114 Runtime resolver boundary — document default build/generation-time mode vs. optional embedded runtime resolver (per-request context: caching, latency, provenance); decide library API or explicit out-of-scope — RFC §9.8 - 🅿️ HC-115 OpenFeature bridge for the runtime mode *(only if HC-114 decides "in scope")* +- ⬜ HC-116 `@import` for `.hcs` — sheet modularity; real configurations don't live in one file (resolution order, cycle detection, provenance keeps the importing file) ## M9 — Validation & adoption (DOCS/Positioning.md) @@ -106,8 +107,10 @@ P1 — built on v2: - ⬜ HC-121 Dogfooding as the primary adoption path — Hyperprompt / Ontology consume the resolved IR (consumer-side dialects & backends per `DOCS/Dialects.md` / `DOCS/Backends.md`) - 🅿️ HC-122 SLSA-like generation attestation chain — signed `.hc`/`.hcs` → IR hash → generator identity/version → artifact hashes → validator report (RFC §8, §9.8) - 🅿️ HC-123 Agent Passport / 0AL integration — attestation chain plugs into 0AL's signed-agent model +- ⬜ HC-124 End-to-end AI codegen demo — one `.hc`/`.hcs` service spec → IR v2 → LLM generates code per node → generated artifacts validated against the same contracts; node hashes scope regeneration after a spec edit; the "review compression" story (RFC §9.7) made runnable ## Cross-cutting - [x] HC-090 Swift CI workflow (build + test) — `.github/workflows/swift.yml` - [x] HC-091 Repo layout documented (root Swift package) — `DOCS/Architecture.md` +- [x] HC-092 Usage guide with real CLI outputs (contexts, contracts as a CI gate, `explain`, IR v2, AI-codegen pipeline) — `DOCS/Usage.md`; `Examples/service.hcs` gained a `@contract:` block so the reference fixture exercises HC-111 *(PR #23)* - [x] HC-092 ANTLR/Java relabeled as a conformance oracle — `DOCS/Architecture.md`