From 8f11ecdedf4657a04ff3c64a7e7176639a48dc53 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Fri, 12 Jun 2026 20:56:03 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20HC-120=20=E2=80=94=20Kustomize=20co?= =?UTF-8?q?mparison=20demo=20(3=20tenants=20=C3=97=203=20envs,=20twice)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same checkout service maintained in two source trees: - kustomize/: idiomatic — shared base, reusable tenant and env components, 9 leaf overlays; every overlay builds with 'kubectl kustomize'. Tenant×env knobs (Acme's prod DB endpoint and pool) have no home in either component and leak into leaf overlays — the place N×M trees rot. - hypercode/: one 5-line .hc topology, base.hcs with @env blocks and contracts, one sheet per tenant via @import (HC-116). metrics.py computes what humans maintain (28 files / 278 lines / 86% duplicated-structure share vs 5 / 57 / 30%) and with --check validates all 9 tenant×env targets through the binary; wired into CI. README walks three scenarios with real outputs: provenance archaeology ('why is pool_size 80?' — grep three files and replay merge order vs one explain), affected-target precision (rebuild 9 and diff YAML vs 'hypercode diff' naming one node), and — per the workplan requirement — Hypercode's own failure mode: specificity beats source order, a tenant's type-selector override silently loses to the baseline's id selector, diagnosed by the same explain command. Co-Authored-By: Claude Fable 5 --- .github/workflows/swift.yml | 5 +- Examples/kustomize-comparison/README.md | 161 ++++++++++++++++++ .../kustomize-comparison/hypercode/base.hcs | 39 +++++ .../hypercode/checkout.hc | 5 + .../hypercode/tenants/acme.hcs | 10 ++ .../hypercode/tenants/globex.hcs | 9 + .../hypercode/tenants/initech.hcs | 9 + .../kustomize/base/configmap.yaml | 7 + .../kustomize/base/deployment.yaml | 26 +++ .../kustomize/base/kustomization.yaml | 6 + .../kustomize/base/service.yaml | 10 ++ .../components/envs/dev/kustomization.yaml | 7 + .../components/envs/dev/patch-env.yaml | 13 ++ .../components/envs/prod/kustomization.yaml | 7 + .../components/envs/prod/patch-env.yaml | 15 ++ .../envs/staging/kustomization.yaml | 7 + .../components/envs/staging/patch-env.yaml | 13 ++ .../tenants/acme/kustomization.yaml | 7 + .../tenants/acme/patch-branding.yaml | 7 + .../tenants/globex/kustomization.yaml | 7 + .../tenants/globex/patch-branding.yaml | 7 + .../tenants/initech/kustomization.yaml | 7 + .../tenants/initech/patch-branding.yaml | 7 + .../overlays/acme-dev/kustomization.yaml | 8 + .../overlays/acme-prod/kustomization.yaml | 13 ++ .../overlays/acme-prod/patch-db.yaml | 14 ++ .../overlays/acme-staging/kustomization.yaml | 8 + .../overlays/globex-dev/kustomization.yaml | 8 + .../overlays/globex-prod/kustomization.yaml | 13 ++ .../overlays/globex-prod/patch-db.yaml | 12 ++ .../globex-staging/kustomization.yaml | 8 + .../overlays/initech-dev/kustomization.yaml | 8 + .../overlays/initech-prod/kustomization.yaml | 13 ++ .../overlays/initech-prod/patch-db.yaml | 12 ++ .../initech-staging/kustomization.yaml | 8 + Examples/kustomize-comparison/metrics.py | 111 ++++++++++++ workplan.md | 2 +- 37 files changed, 627 insertions(+), 2 deletions(-) create mode 100644 Examples/kustomize-comparison/README.md create mode 100644 Examples/kustomize-comparison/hypercode/base.hcs create mode 100644 Examples/kustomize-comparison/hypercode/checkout.hc create mode 100644 Examples/kustomize-comparison/hypercode/tenants/acme.hcs create mode 100644 Examples/kustomize-comparison/hypercode/tenants/globex.hcs create mode 100644 Examples/kustomize-comparison/hypercode/tenants/initech.hcs create mode 100644 Examples/kustomize-comparison/kustomize/base/configmap.yaml create mode 100644 Examples/kustomize-comparison/kustomize/base/deployment.yaml create mode 100644 Examples/kustomize-comparison/kustomize/base/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/base/service.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/dev/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/dev/patch-env.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/prod/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/prod/patch-env.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/staging/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/envs/staging/patch-env.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/acme/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/acme/patch-branding.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/globex/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/globex/patch-branding.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/initech/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/components/tenants/initech/patch-branding.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/acme-dev/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/acme-prod/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/acme-prod/patch-db.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/acme-staging/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/globex-dev/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/globex-prod/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/globex-prod/patch-db.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/globex-staging/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/initech-dev/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/initech-prod/kustomization.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/initech-prod/patch-db.yaml create mode 100644 Examples/kustomize-comparison/kustomize/overlays/initech-staging/kustomization.yaml create mode 100644 Examples/kustomize-comparison/metrics.py diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index f2a1113..d43ae68 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -32,4 +32,7 @@ jobs: npm exec --yes --package=ajv-cli@5 -- ajv validate --spec=draft2020 -s Schema/hypercode-diff-v1.schema.json -d /tmp/diff.json - name: Codegen demo — generated artifacts fresh & contract-conformant - run: python3 Examples/codegen-demo/check.py \ No newline at end of file + run: python3 Examples/codegen-demo/check.py + + - name: Kustomize comparison — metrics + all tenant×env targets validate + run: python3 Examples/kustomize-comparison/metrics.py --check \ No newline at end of file diff --git a/Examples/kustomize-comparison/README.md b/Examples/kustomize-comparison/README.md new file mode 100644 index 0000000..6adf24a --- /dev/null +++ b/Examples/kustomize-comparison/README.md @@ -0,0 +1,161 @@ +# HC-120 — One app, two source trees: Kustomize vs Hypercode + +The same product — a `checkout` web service shipped to **3 tenants × 3 +environments = 9 build targets** — maintained twice: + +- [`kustomize/`](kustomize/): an *idiomatic* Kustomize tree — shared `base/`, + reusable tenant and environment [components], 9 leaf overlays. Not a straw + man: components are exactly the tool Kustomize offers against tenant × env + combinatorics, and every overlay builds with `kubectl kustomize`. +- [`hypercode/`](hypercode/): one `.hc` topology, a `base.hcs` baseline with + `@env[…]` blocks and contracts, and one sheet per tenant via `@import` + (HC-116). 9 targets = 3 sheets × 3 `--ctx` values. + +The comparison is about **the layer humans edit and review**. Rendering K8s +manifests from the resolved IR is a consumer backend +([DOCS/Backends.md](../../DOCS/Backends.md)) and out of scope here. + +[components]: https://kubectl.docs.kubernetes.io/guides/config_management/components/ + +## Metrics + +`python3 metrics.py --check` (runs in CI; numbers are computed, not claimed): + +```console +3 tenants x 3 environments = 9 build targets + + kustomize hypercode +------------------------------------------------ +files 28 5 +meaningful lines 278 57 +duplicated lines 240 17 +duplication share 86% 30% + +all 9 hypercode targets validate (contracts enforced per context) +``` + +A line counts as *duplicated structure* when the same normalized line occurs +in more than one file of the same tree. The Kustomize number is dominated by +patch envelopes — every patch restates `apiVersion`/`kind`/`metadata`/the +container path before it can change one value. That envelope is not noise: +it is text a reviewer must read to know *what* the patch touches. + +The structural difference behind the numbers: tenant × env knobs (Acme's +production DB endpoint and pool size) have no home in either a tenant +component or an env component — they leak into **leaf overlays**, one +directory per combination, which is where N × M trees rot. In Hypercode the +same knob is one block in the tenant's sheet, scoped by `@env[prod]`. + +## Scenario 1 — "Why is the pool size 80 in Acme prod?" + +**Kustomize.** The value is assembled from three files; you find them by +search, then mentally replay patch order (base → components → overlay +patches): + +```console +$ grep -rln "DB_POOL_SIZE" kustomize/ +kustomize/overlays/acme-prod/patch-db.yaml # 80 ← wins (overlay patch, last) +kustomize/components/envs/prod/patch-env.yaml # 50 +kustomize/base/deployment.yaml # 10 +``` + +Nothing in the tree *states* which one wins — you must know the merge +semantics, and `kubectl kustomize` outputs the final YAML without the why. + +**Hypercode.** The cascade is a first-class object; ask it: + +```console +$ hypercode explain hypercode/checkout.hc --hcs hypercode/tenants/acme.hcs \ + --ctx env=prod "'#main-db'" pool_size +Node: Checkout > Database#main-db + pool_size + WINNER #main-db { value: 80 } + file: hypercode/tenants/acme.hcs line: 8 specificity: (1,0,0) order: 8 + ──────────────────── + losing #main-db { value: 50 } + file: hypercode/base.hcs line: 29 specificity: (1,0,0) order: 6 + losing #main-db { value: 10 } + file: hypercode/base.hcs line: 12 specificity: (1,0,0) order: 2 +``` + +One command, every contender, file:line each, and the tie-break rule +(equal specificity → later source order → the tenant sheet) is visible +instead of implied. + +## Scenario 2 — One-line change: which targets are affected? + +Bump Acme's production pool from 80 to 90. + +**Kustomize**: the change lives in `overlays/acme-prod/`, but proving the +blast radius means rebuilding all 9 targets and diffing rendered YAML — +`kubectl kustomize` has no semantic diff. + +**Hypercode**: emit and diff the resolved documents: + +```console +$ hypercode diff old.ir.json new.ir.json +~ Checkout > Database#main-db + ~ pool_size: 80 → 90 + was: #main-db @ hypercode/tenants/acme.hcs:8 + now: #main-db @ hypercode/tenants/acme.hcs:8 + +1 affected node(s) +``` + +One affected node, named, with the rule that did it — the invalidation feed +a regeneration pipeline consumes directly +([codegen demo](../codegen-demo/)). + +## Scenario 3 — Hypercode's own failure mode, honestly + +The cascade has a sharp edge: **specificity beats source order**. A tenant +author tries to override the DB host with a *type* selector: + +```hcs +@import "../base.hcs" + +@env[prod]: + Database: # ← (0,0,1) — type selector + host: db.initech.internal +``` + +It silently loses — the baseline's `'#main-db'` rule is an *id* selector, +`(1,0,0)`, and ids outrank source order. Production resolves to +`host: localhost`. In CSS this class of bug is debugged with devtools; in a +YAML overlay tree, with despair. Here, the same one command pinpoints it: + +```console +$ hypercode explain hypercode/checkout.hc --hcs hypercode/tenants/initech-broken.hcs \ + --ctx env=prod "Database" host +Node: Checkout > Database#main-db + host + WINNER #main-db { value: localhost } + file: hypercode/base.hcs line: 12 specificity: (1,0,0) order: 2 + ──────────────────── + losing Database { value: db.initech.internal } + file: hypercode/tenants/initech-broken.hcs line: 7 specificity: (0,0,1) order: 8 +``` + +The loser is listed with the reason it lost — fix is to target `'#main-db'`, +as [`tenants/acme.hcs`](hypercode/tenants/acme.hcs) does. The honest summary: +Hypercode does not remove override complexity; it makes every override +**explainable** and gates it with contracts (`pool_size: 99999` in any tenant +sheet fails `validate` with HC2104 before anything ships — try it). + +## Reproduce + +```console +$ python3 metrics.py --check # metrics + validate all 9 targets +$ kubectl kustomize kustomize/overlays/acme-prod # any overlay builds +``` + +| File | Role | +|---|---| +| `kustomize/base/` | shared manifests (Deployment, Service, ConfigMap) | +| `kustomize/components/tenants/*` | reusable per-tenant patches (branding) | +| `kustomize/components/envs/*` | reusable per-env patches (replicas, logging, pool) | +| `kustomize/overlays/-/` | 9 leaf targets; tenant × env knobs leak here | +| `hypercode/checkout.hc` | the topology (5 lines, never changes per target) | +| `hypercode/base.hcs` | defaults + `@env[…]` blocks + contracts | +| `hypercode/tenants/*.hcs` | one sheet per tenant, `@import "../base.hcs"` | +| `metrics.py` | computes the table above; `--check` validates all targets | diff --git a/Examples/kustomize-comparison/hypercode/base.hcs b/Examples/kustomize-comparison/hypercode/base.hcs new file mode 100644 index 0000000..d4d314a --- /dev/null +++ b/Examples/kustomize-comparison/hypercode/base.hcs @@ -0,0 +1,39 @@ +# Product baseline: defaults, environment blocks, and the invariants every +# tenant and environment must respect. + +Web: + replicas: 1 + log_level: debug + image_tag: latest + +Web > Listen: + port: 8080 + +'#main-db': + host: localhost + pool_size: 10 + +Branding: + brand_name: "Default" + theme_color: "#cccccc" + +@env[staging]: + Web: + replicas: 2 + log_level: info + +@env[prod]: + Web: + replicas: 5 + log_level: warn + '#main-db': + pool_size: 50 + +@contract: + Web: + replicas: int >= 1 <= 20 + log_level: string + Web > Listen: + port: int >= 1 <= 65535 + '#main-db': + pool_size[?]: int >= 1 <= 100 diff --git a/Examples/kustomize-comparison/hypercode/checkout.hc b/Examples/kustomize-comparison/hypercode/checkout.hc new file mode 100644 index 0000000..71d19a3 --- /dev/null +++ b/Examples/kustomize-comparison/hypercode/checkout.hc @@ -0,0 +1,5 @@ +Checkout + Web + Listen + Database#main-db + Branding diff --git a/Examples/kustomize-comparison/hypercode/tenants/acme.hcs b/Examples/kustomize-comparison/hypercode/tenants/acme.hcs new file mode 100644 index 0000000..4619dd7 --- /dev/null +++ b/Examples/kustomize-comparison/hypercode/tenants/acme.hcs @@ -0,0 +1,10 @@ +@import "../base.hcs" + +Branding: + brand_name: "Acme" + theme_color: "#0044ff" + +@env[prod]: + '#main-db': + host: db.acme.internal + pool_size: 80 diff --git a/Examples/kustomize-comparison/hypercode/tenants/globex.hcs b/Examples/kustomize-comparison/hypercode/tenants/globex.hcs new file mode 100644 index 0000000..d731b60 --- /dev/null +++ b/Examples/kustomize-comparison/hypercode/tenants/globex.hcs @@ -0,0 +1,9 @@ +@import "../base.hcs" + +Branding: + brand_name: "Globex" + theme_color: "#11aa55" + +@env[prod]: + '#main-db': + host: db.globex.internal diff --git a/Examples/kustomize-comparison/hypercode/tenants/initech.hcs b/Examples/kustomize-comparison/hypercode/tenants/initech.hcs new file mode 100644 index 0000000..9ab7acd --- /dev/null +++ b/Examples/kustomize-comparison/hypercode/tenants/initech.hcs @@ -0,0 +1,9 @@ +@import "../base.hcs" + +Branding: + brand_name: "Initech" + theme_color: "#ff6600" + +@env[prod]: + '#main-db': + host: db.initech.internal diff --git a/Examples/kustomize-comparison/kustomize/base/configmap.yaml b/Examples/kustomize-comparison/kustomize/base/configmap.yaml new file mode 100644 index 0000000..d97f5ad --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/base/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: checkout-branding +data: + brand_name: Default + theme_color: "#cccccc" diff --git a/Examples/kustomize-comparison/kustomize/base/deployment.yaml b/Examples/kustomize-comparison/kustomize/base/deployment.yaml new file mode 100644 index 0000000..227e89f --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/base/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + replicas: 1 + selector: + matchLabels: + app: checkout-web + template: + metadata: + labels: + app: checkout-web + spec: + containers: + - name: web + image: registry.example.com/checkout-web:latest + ports: + - containerPort: 8080 + env: + - name: LOG_LEVEL + value: debug + - name: DB_HOST + value: localhost + - name: DB_POOL_SIZE + value: "10" diff --git a/Examples/kustomize-comparison/kustomize/base/kustomization.yaml b/Examples/kustomize-comparison/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..8d19601 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml + - service.yaml + - configmap.yaml diff --git a/Examples/kustomize-comparison/kustomize/base/service.yaml b/Examples/kustomize-comparison/kustomize/base/service.yaml new file mode 100644 index 0000000..a40e141 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/base/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: checkout-web +spec: + selector: + app: checkout-web + ports: + - port: 8080 + targetPort: 8080 diff --git a/Examples/kustomize-comparison/kustomize/components/envs/dev/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/envs/dev/kustomization.yaml new file mode 100644 index 0000000..ca3f334 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/dev/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-env.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/components/envs/dev/patch-env.yaml b/Examples/kustomize-comparison/kustomize/components/envs/dev/patch-env.yaml new file mode 100644 index 0000000..0308d34 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/dev/patch-env.yaml @@ -0,0 +1,13 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + replicas: 1 + template: + spec: + containers: + - name: web + env: + - name: LOG_LEVEL + value: debug diff --git a/Examples/kustomize-comparison/kustomize/components/envs/prod/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/envs/prod/kustomization.yaml new file mode 100644 index 0000000..ca3f334 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/prod/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-env.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/components/envs/prod/patch-env.yaml b/Examples/kustomize-comparison/kustomize/components/envs/prod/patch-env.yaml new file mode 100644 index 0000000..9154fe9 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/prod/patch-env.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + replicas: 5 + template: + spec: + containers: + - name: web + env: + - name: LOG_LEVEL + value: warn + - name: DB_POOL_SIZE + value: "50" diff --git a/Examples/kustomize-comparison/kustomize/components/envs/staging/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/envs/staging/kustomization.yaml new file mode 100644 index 0000000..ca3f334 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/staging/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-env.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/components/envs/staging/patch-env.yaml b/Examples/kustomize-comparison/kustomize/components/envs/staging/patch-env.yaml new file mode 100644 index 0000000..9eef089 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/envs/staging/patch-env.yaml @@ -0,0 +1,13 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + replicas: 2 + template: + spec: + containers: + - name: web + env: + - name: LOG_LEVEL + value: info diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/acme/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/acme/kustomization.yaml new file mode 100644 index 0000000..cbd1721 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/acme/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-branding.yaml + target: + kind: ConfigMap + name: checkout-branding diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/acme/patch-branding.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/acme/patch-branding.yaml new file mode 100644 index 0000000..27ca430 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/acme/patch-branding.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: checkout-branding +data: + brand_name: Acme + theme_color: "#0044ff" diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/globex/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/globex/kustomization.yaml new file mode 100644 index 0000000..cbd1721 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/globex/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-branding.yaml + target: + kind: ConfigMap + name: checkout-branding diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/globex/patch-branding.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/globex/patch-branding.yaml new file mode 100644 index 0000000..85f7114 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/globex/patch-branding.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: checkout-branding +data: + brand_name: Globex + theme_color: "#11aa55" diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/initech/kustomization.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/initech/kustomization.yaml new file mode 100644 index 0000000..cbd1721 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/initech/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch-branding.yaml + target: + kind: ConfigMap + name: checkout-branding diff --git a/Examples/kustomize-comparison/kustomize/components/tenants/initech/patch-branding.yaml b/Examples/kustomize-comparison/kustomize/components/tenants/initech/patch-branding.yaml new file mode 100644 index 0000000..c7ea54e --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/components/tenants/initech/patch-branding.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: checkout-branding +data: + brand_name: Initech + theme_color: "#ff6600" diff --git a/Examples/kustomize-comparison/kustomize/overlays/acme-dev/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/acme-dev/kustomization.yaml new file mode 100644 index 0000000..ddaaf56 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/acme-dev/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: acme-dev- +resources: + - ../../base +components: + - ../../components/tenants/acme + - ../../components/envs/dev diff --git a/Examples/kustomize-comparison/kustomize/overlays/acme-prod/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/acme-prod/kustomization.yaml new file mode 100644 index 0000000..3bd8f2c --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/acme-prod/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: acme-prod- +resources: + - ../../base +components: + - ../../components/tenants/acme + - ../../components/envs/prod +patches: + - path: patch-db.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/overlays/acme-prod/patch-db.yaml b/Examples/kustomize-comparison/kustomize/overlays/acme-prod/patch-db.yaml new file mode 100644 index 0000000..3a161e0 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/acme-prod/patch-db.yaml @@ -0,0 +1,14 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + template: + spec: + containers: + - name: web + env: + - name: DB_HOST + value: db.acme.internal + - name: DB_POOL_SIZE + value: "80" diff --git a/Examples/kustomize-comparison/kustomize/overlays/acme-staging/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/acme-staging/kustomization.yaml new file mode 100644 index 0000000..48e3ed9 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/acme-staging/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: acme-staging- +resources: + - ../../base +components: + - ../../components/tenants/acme + - ../../components/envs/staging diff --git a/Examples/kustomize-comparison/kustomize/overlays/globex-dev/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/globex-dev/kustomization.yaml new file mode 100644 index 0000000..8ec03e4 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/globex-dev/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: globex-dev- +resources: + - ../../base +components: + - ../../components/tenants/globex + - ../../components/envs/dev diff --git a/Examples/kustomize-comparison/kustomize/overlays/globex-prod/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/globex-prod/kustomization.yaml new file mode 100644 index 0000000..562ad94 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/globex-prod/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: globex-prod- +resources: + - ../../base +components: + - ../../components/tenants/globex + - ../../components/envs/prod +patches: + - path: patch-db.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/overlays/globex-prod/patch-db.yaml b/Examples/kustomize-comparison/kustomize/overlays/globex-prod/patch-db.yaml new file mode 100644 index 0000000..ff02e42 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/globex-prod/patch-db.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + template: + spec: + containers: + - name: web + env: + - name: DB_HOST + value: db.globex.internal diff --git a/Examples/kustomize-comparison/kustomize/overlays/globex-staging/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/globex-staging/kustomization.yaml new file mode 100644 index 0000000..59ce216 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/globex-staging/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: globex-staging- +resources: + - ../../base +components: + - ../../components/tenants/globex + - ../../components/envs/staging diff --git a/Examples/kustomize-comparison/kustomize/overlays/initech-dev/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/initech-dev/kustomization.yaml new file mode 100644 index 0000000..7fb68fe --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/initech-dev/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: initech-dev- +resources: + - ../../base +components: + - ../../components/tenants/initech + - ../../components/envs/dev diff --git a/Examples/kustomize-comparison/kustomize/overlays/initech-prod/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/initech-prod/kustomization.yaml new file mode 100644 index 0000000..0f91e17 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/initech-prod/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: initech-prod- +resources: + - ../../base +components: + - ../../components/tenants/initech + - ../../components/envs/prod +patches: + - path: patch-db.yaml + target: + kind: Deployment + name: checkout-web diff --git a/Examples/kustomize-comparison/kustomize/overlays/initech-prod/patch-db.yaml b/Examples/kustomize-comparison/kustomize/overlays/initech-prod/patch-db.yaml new file mode 100644 index 0000000..76136b6 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/initech-prod/patch-db.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: checkout-web +spec: + template: + spec: + containers: + - name: web + env: + - name: DB_HOST + value: db.initech.internal diff --git a/Examples/kustomize-comparison/kustomize/overlays/initech-staging/kustomization.yaml b/Examples/kustomize-comparison/kustomize/overlays/initech-staging/kustomization.yaml new file mode 100644 index 0000000..2212b91 --- /dev/null +++ b/Examples/kustomize-comparison/kustomize/overlays/initech-staging/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: initech-staging- +resources: + - ../../base +components: + - ../../components/tenants/initech + - ../../components/envs/staging diff --git a/Examples/kustomize-comparison/metrics.py b/Examples/kustomize-comparison/metrics.py new file mode 100644 index 0000000..0a633ef --- /dev/null +++ b/Examples/kustomize-comparison/metrics.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""HC-120: measure the two source trees of the same 3-tenants x 3-envs app. + +Counts what humans maintain and review on each side: files, meaningful lines +(non-blank, non-comment), and duplicated lines (a normalized line occurring +in more than one file of the same tree — restated structure, the thing that +drifts). With --check it also validates every hypercode tenant x env target +through the compiled binary, so CI fails when the comparison stops being +honest. + +The comparison is about the layer humans edit. Rendering manifests from the +resolved IR is a consumer backend (DOCS/Backends.md) and out of scope here. +""" +import argparse +import os +import subprocess +import sys +from collections import Counter + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.abspath(os.path.join(HERE, "..", "..")) + +TENANTS = ["acme", "globex", "initech"] +ENVS = ["dev", "staging", "prod"] + + +def tree_files(root, suffixes): + out = [] + for dirpath, _, names in os.walk(os.path.join(HERE, root)): + for name in sorted(names): + if name.endswith(suffixes): + out.append(os.path.join(dirpath, name)) + return sorted(out) + + +def meaningful_lines(path): + lines = [] + for raw in open(path): + line = raw.strip() + if line and not line.startswith("#"): + lines.append(line) + return lines + + +def measure(root, suffixes): + files = tree_files(root, suffixes) + per_file = {f: meaningful_lines(f) for f in files} + total = sum(len(v) for v in per_file.values()) + # A line is "duplicated structure" if it appears in more than one file. + seen_in = Counter() + for f, lines in per_file.items(): + for line in set(lines): + seen_in[line] += 1 + duplicated = sum( + sum(1 for line in lines if seen_in[line] > 1) + for lines in per_file.values() + ) + return {"files": len(files), "lines": total, "duplicated": duplicated} + + +def check_targets(): + binary = os.environ.get( + "HYPERCODE_BIN", os.path.join(REPO, ".build", "debug", "hypercode")) + hc = os.path.join(HERE, "hypercode", "checkout.hc") + failures = 0 + for tenant in TENANTS: + for env in ENVS: + sheet = os.path.join(HERE, "hypercode", "tenants", f"{tenant}.hcs") + proc = subprocess.run( + [binary, "validate", hc, "--hcs", sheet, "--ctx", f"env={env}"], + capture_output=True, text=True) + if proc.returncode != 0: + failures += 1 + print(f"FAIL {tenant}/{env}:\n{proc.stdout}{proc.stderr}", + file=sys.stderr) + return failures + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", + help="also validate all tenant x env targets (CI)") + args = parser.parse_args() + + kustomize = measure("kustomize", (".yaml",)) + hypercode = measure("hypercode", (".hc", ".hcs")) + + print(f"{len(TENANTS)} tenants x {len(ENVS)} environments " + f"= {len(TENANTS) * len(ENVS)} build targets\n") + header = f"{'':24}{'kustomize':>12}{'hypercode':>12}" + print(header) + print("-" * len(header)) + for key, label in [("files", "files"), ("lines", "meaningful lines"), + ("duplicated", "duplicated lines")]: + print(f"{label:24}{kustomize[key]:>12}{hypercode[key]:>12}") + share_k = kustomize["duplicated"] / kustomize["lines"] * 100 + share_h = hypercode["duplicated"] / hypercode["lines"] * 100 + print(f"{'duplication share':24}{share_k:>11.0f}%{share_h:>11.0f}%") + + if args.check: + failures = check_targets() + if failures: + print(f"\n{failures} target(s) failed validation", file=sys.stderr) + return 1 + print(f"\nall {len(TENANTS) * len(ENVS)} hypercode targets validate " + "(contracts enforced per context)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/workplan.md b/workplan.md index b999c1a..93fefb7 100644 --- a/workplan.md +++ b/workplan.md @@ -103,7 +103,7 @@ P1 — built on v2: ## M9 — Validation & adoption (DOCS/Positioning.md) -- ⬜ HC-120 One deep Kustomize comparison demo — N tenants × M envs; metrics: duplicated structure, time-to-answer "why is this value here?", affected-module precision via IR diff; **must include** an own failure mode (specificity conflict) resolved via `explain` +- [x] HC-120 Kustomize comparison demo — `Examples/kustomize-comparison/`: 3 tenants × 3 envs maintained twice (idiomatic Kustomize components vs `.hc` + `@import`ed tenant sheets); computed metrics (28 files/278 lines/86% duplication vs 5/57/30%), "why is this value here" via `explain`, affected-target precision via `hypercode diff`, and the own failure mode (specificity beats source order) diagnosed via `explain`; `metrics.py --check` validates all 9 targets in CI - ⬜ 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 From 4042f80272c21d7e2d417d3baa7f765fbde4d603 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Fri, 12 Jun 2026 21:20:58 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20#29=20review=20=E2=80=94?= =?UTF-8?q?=20context-managed=20UTF-8=20reads=20in=20metrics.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- Examples/kustomize-comparison/metrics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Examples/kustomize-comparison/metrics.py b/Examples/kustomize-comparison/metrics.py index 0a633ef..9cae6cb 100644 --- a/Examples/kustomize-comparison/metrics.py +++ b/Examples/kustomize-comparison/metrics.py @@ -35,10 +35,11 @@ def tree_files(root, suffixes): def meaningful_lines(path): lines = [] - for raw in open(path): - line = raw.strip() - if line and not line.startswith("#"): - lines.append(line) + with open(path, encoding="utf-8") as handle: + for raw in handle: + line = raw.strip() + if line and not line.startswith("#"): + lines.append(line) return lines