From 7499e5b5c325c5f8788478270a59d6f1d8f6c6ed Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:55:06 +0100 Subject: [PATCH 1/2] docs(policy): yq for reading now, KYAML sequenced behind proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner ruling 2026-09-22, in response to "is it sensible to make the estate rule kyaml not yaml, and invest in using yq wherever it comes into things?" Measured first. 23,199 YAML files across the estate at depth 6 (excluding node_modules/target/.git/vendor): 9,261 in .github/workflows, 11,238 elsewhere under .github, 2,700 outside it. The split that matters is that the 9,261 are written by BOTS — dependabot and gh actions-lock — so a style rule imposed on them is violated on a schedule, by us, forever. Y-1, IN FORCE: every gate that READS YAML parses it with yq, never grep/sed. This is not hygiene, it is a measured recurring defect class — a grep-read key is not what the parser sees. Recorded instances: a name/uuid nested under a [package] table that grep read correctly while the loader returned nothing; gh actions-lock's extractor seeing step-level `uses:` only and calling 71 job-level reusable refs fatal orphans, turning a green repo red; allowlist globs keyed to a depth a sparse checkout had changed. Enforceable today — yq v4.53.3 is already installed. Y-2, SEQUENCED: yq for WRITING waits on a comment-preservation proof. The estate pins actions as `uses: # v7.0.1` and that trailing comment is load-bearing — a bare 40-hex string is unreviewable, and the comment is the only place the tag survives. A rewrite that strips it still parses, still runs, and is no longer reviewable, so no parse check would catch it. Y-3, SEQUENCED, scope deliberately undecided: KYAML as target authoring dialect. It is genuinely attractive — a strict subset of YAML per KEP-5295, so no new parser anywhere, and flow style with quoted values kills the Norway problem, sexagesimal coercion, 1.0-becomes-float and indentation ambiguity at source. Three things must be true first: GitHub Actions must be PROVEN to parse KYAML workflows (assuming it does is how workflows here die at startup with jobs=0, which is not a failing job and shows no honest red square); a formatter must exist, as none does outside `kubectl -o kyaml`; and comment handling must be proven — KEP-5295 is explicit that automated reformatting is lossy, go-yaml "does not always handle comments properly" so comments may be "formatted wrongly, or lost entirely". The structural point: that last precondition and Y-2's obstacle are the SAME problem. An automated rewrite risks destroying load-bearing comments whether the rewriter is `yq -i` or a KYAML formatter. So the proof is written once, against the real pin-comment corpus, and satisfies both. It also fixes the acceptance bar for both: parse-clean is not the bar — a migration producing valid, running, comment-stripped workflows has failed. Adoption order is a dependency order, six steps, one issue each: probe -> comment proof -> formatter -> owner scope ruling -> estate-authored YAML -> workflows (only if ruled in scope, and only once bot drift is resolved). Nothing in the estate is out of compliance with Y-2 or Y-3 today, because neither imposes anything today. Gates must not cite them yet. standards-map.toml gains the entry and entry_count goes 123 -> 124; Gate D caught the stale count before this commit, which is the gate working. Refs: #787 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f --- 3-practice/YAML-POLICY.adoc | 294 ++++++++++++++++++++++++++++++++++++ standards-map.toml | 15 +- 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 3-practice/YAML-POLICY.adoc diff --git a/3-practice/YAML-POLICY.adoc b/3-practice/YAML-POLICY.adoc new file mode 100644 index 000000000..1241409b3 --- /dev/null +++ b/3-practice/YAML-POLICY.adoc @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Hyperpolymath YAML, KYAML and yq Policy +Jonathan D.A. Jewell +:toc: +:toc-placement: preamble + +Canonical policy for how YAML is *read*, *written* and *authored* across all +hyperpolymath and metadatastician repositories. + +Ruled by the owner on 2026-09-22. This document distinguishes sharply between +rules that are *in force now* and rules that are *sequenced behind a proof*. +A rule that is not yet in force must not be cited by a gate, and must not be +described anywhere as though it were settled. + +NOTE: The estate's recurring failure is not the absence of rules. It is rules +with no tool behind them — see `TRUST-DEFAULTS-POLICY.adoc` and the standing +"a default is not a gate" finding. Every rule below states explicitly whether +it is enforceable today, and by what. + +== 0. The measurement this rests on + +Taken 2026-09-22 over `/home/hyperpolymath/developer` at depth 6, excluding +`node_modules`, `target`, `.git` and `vendor`: + +[cols="6,2,8",options="header"] +|=== +| Population | Files | Who writes them + +| `.github/workflows/**` +| 9,261 +| *Bots.* Dependabot opens PRs that write plain YAML; `gh actions-lock` + rewrites `uses:` lines. The estate is not the sole author. + +| `.github/**` excluding workflows +| 11,238 +| *The estate.* Mostly composite `action.yml`, plus `dependabot.yml` and + issue templates. Stable, hand-authored. + +| Everything else +| 2,700 +| *The estate.* `.machine_readable/` descriptors, protocol and config files. + +| *Total* +| *23,199* +| +|=== + +The split between *bot-written* and *estate-written* YAML is the single fact +that shapes this policy. A style rule imposed on a population your own +automation rewrites is violated on a schedule, by you, forever. + +Tooling present on the estate workstation at the time of ruling: `yq` v4.53.3 +(mikefarah), `actionlint`, `kubectl`. + +== 1. Rule Y-1 — gates READ YAML with `yq`, never with `grep` + +*Status: IN FORCE. Enforceable today. `yq` is already installed.* + +Any gate, check script, audit or census that needs a value out of a YAML file +MUST obtain it with `yq`. Reading YAML with `grep`, `sed`, `awk` or a regex is +forbidden in gate code. + +.Normative +[source,bash] +---- +# CORRECT — ask the parser +ref="$(yq -r '.jobs.build.steps[0].uses // ""' "$wf")" + +# FORBIDDEN in gate code — a grep impersonating a parser +ref="$(grep -m1 'uses:' "$wf" | sed 's/.*uses: *//')" +---- + +=== 1.1 Why this rule exists + +This is not hygiene. It is a measured, recurring, estate-wide defect class: +*a grep-read key is not what the parser sees.* Recorded instances include — + +* A `name`/`uuid` pair nested under a `[package]` table read correctly by grep + while the real loader returned `name=nothing`. Three candidate blockers were + found by reading key names; the actual blocker needed the loader. +* `gh actions-lock`'s extractor sees *step-level* `uses:` only, so it + classified every *job-level* reusable-workflow ref as a fatal orphan pin. + 71 such refs were added across 14 branches on the strength of that reading, + turning a green repository red. +* Allowlist globs keyed to a path depth that a sparse checkout had changed, + so the launcher gate found 16 defects in its own source. + +In each case a line-oriented reader gave a confident, well-formed answer about +a structure it could not actually see. `yq` cannot make that class of error +because it resolves the document, not the text. + +=== 1.2 What this rule does NOT cover + +Y-1 governs *reading in gate code*. It does not govern: + +* *Human inspection.* `grep` at a terminal to find a file is fine. +* *Writing.* See Y-2 — writing is explicitly NOT yet covered. +* *Existence checks.* `test -f`, and "does this file mention X at all" as a + cheap pre-filter before a `yq` read, remain acceptable — provided the + *verdict* comes from `yq`. A pre-filter that decides the outcome is a grep + gate wearing a costume. + +== 2. Rule Y-2 — `yq` for WRITING is deferred behind a comment-preservation proof + +*Status: NOT IN FORCE. Sequenced. Do not cite this as a rule.* + +The owner's ruling is "go for Y-1 now, Y-2 next, and sort the proof as part of +this work". Y-2 becomes policy when, and only when, the proof below passes. + +=== 2.1 The obstacle: the pin comment is load-bearing + +The estate pins actions by full commit SHA with the human-readable tag in a +trailing comment: + +[source,yaml] +---- +uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 +---- + +That comment is not decoration. Humans read it to know what the SHA *is* — +a bare 40-hex string is unreviewable — and it is the only place the tag +survives, because a tag can be moved while a commit SHA cannot. Losing or +relocating these comments across 9,261 workflows would be a silent, +large-scale degradation that no parse check would catch: the file still +parses, still runs, and is no longer reviewable. + +=== 2.2 The proof obligation + +Before Y-2 is in force, demonstrate on a representative corpus that a `yq` +read-modify-write round trip: + +. preserves every comment, +. preserves each comment's *association* with its line, not merely its + presence somewhere in the file, +. is idempotent — a second round trip is a no-op (`cmp` clean), and +. is verified by a *mutant*: deliberately drop one comment and confirm the + check goes red naming the right file. A passing check proves nothing until + a mutant dies. + +Until that proof lands, workflow files are written by the tool that owns them +(`gh actions-lock`, Dependabot) or by hand. Do not introduce blind `yq -i` +into gate or migration code. + +== 3. Rule Y-3 — KYAML as the target authoring dialect, adopted in sequence + +*Status: SEQUENCED. Scope NOT YET DECIDED — the probe in §5 decides it.* + +=== 3.1 What KYAML is + +KYAML is a strict subset of YAML defined by Kubernetes KEP-5295 and shipped in +`kubectl` from Kubernetes 1.34 (alpha, `KUBECTL_KYAML=true`; beta and default +from 1.35). Its rules: + +* `{}` for every map and `[]` for every list — flow style throughout; +* every string *value* double-quoted; keys unquoted where unambiguous; +* trailing commas permitted; +* two-space indentation; +* a `---` document header. + +The decisive property is that *KYAML is a subset of YAML, not a new format*. +KEP-5295: "Since KYAML is a subset of YAML, it should always be accepted as +input." Adopting it therefore requires **no new parser anywhere** — every +existing reader, including GitHub's, already handles the syntax in principle. + +=== 3.2 Why it is attractive here + +Flow style with quoted values removes YAML's implicit typing traps at the +source rather than defending against them per-consumer: + +* the Norway problem — bare `no` coercing to boolean `false`; +* sexagesimal and octal coercion of unquoted numerics; +* `1.0` becoming a float and losing its trailing zero as a version string; +* indentation ambiguity, which is what makes YAML hostile to templating and + text patching. + +For an estate whose configuration is machine-generated and machine-patched at +scale, non-whitespace-sensitivity is a real structural win, not a style +preference. + +=== 3.3 The three things that must be true before any mandate + +[cols="1,7",options="header"] +|=== +| # | Precondition + +| 1 +| *GitHub Actions must be proven to parse KYAML workflows.* It should, because + flow style is valid YAML — but "the parser will surely accept it" is exactly + how workflows in this estate die at startup with `jobs=0`, which is a + *startup death*, not a failing job, and so does not show up as an honest red + square. This must be probed, not assumed. See §5 step 1. + +| 2 +| *A formatter or linter must exist.* There is none outside `kubectl -o kyaml`, + which emits Kubernetes resources, not arbitrary YAML. A style rule with no + tool behind it cannot be gated — only asserted — and an unenforceable rule + in this estate has a known outcome. + +| 3 +| *Comment handling must be proven.* KEP-5295 is explicit that KYAML *allows* + comments when hand-authored, but that automated reformatting is lossy: + go-yaml "does not always handle comments properly", so comments may be + "formatted wrongly, or lost entirely". This is the *same* obstacle as Y-2 §2.1 + — see §4. +|=== + +=== 3.4 Scope is deliberately undecided + +The owner considered mandating KYAML estate-wide including workflows, and +chose instead to *probe workflows first and decide after*, with estate-wide +adoption booked as ordered issues. Accordingly this document does **not** +mandate KYAML anywhere yet. Nothing in the estate is out of compliance with +Y-3 today, because Y-3 imposes nothing today. + +== 4. One proof obligation sits under two rules + +The comment-preservation problem in §2.2 and precondition 3 in §3.3 are the +same problem: *an automated rewrite of YAML risks silently destroying +semantically load-bearing comments.* It does not matter whether the rewriter +is `yq -i` or a KYAML formatter. + +Therefore the proof is written **once**, against the estate's real pin-comment +corpus, and satisfies both rules. Whichever of Y-2 or Y-3 reaches it first +pays for it; the other inherits it. Do not commission two proofs. + +This also fixes the acceptance criterion for both: *parse-clean is not the +bar.* A migration that produces valid, running, comment-stripped workflows has +failed. The bar is comment-preserving and idempotent, with a dead mutant. + +== 5. Adoption order + +Each step is a separate issue. The order is a dependency order, not a +preference: no step may start before its predecessor has landed evidence. + +[cols="1,6,6",options="header"] +|=== +| Step | Work | Gate to pass + +| 1 +| Probe whether GitHub Actions parses a KYAML workflow. +| One real workflow converted to KYAML on a scratch branch spawns `jobs >= 1` + and completes. `jobs=0` is a *failure of the probe*, not a flaky run. + +| 2 +| Comment-preservation proof (§4), shared by Y-2 and Y-3. +| Round trip preserves every comment and its line association; idempotent + under `cmp`; a dropped-comment mutant dies. + +| 3 +| A KYAML formatter/linter for arbitrary YAML, or an adopted third-party one. +| Formats and *checks* a file without `kubectl`; passes step 2's proof. + +| 4 +| Decide KYAML scope on the evidence from steps 1–3. +| An owner ruling recorded as a decision row. Not an inference from the probe. + +| 5 +| Migrate estate-authored, non-bot YAML (the 11,238 + 2,700 populations). +| Gate green per step 3; no comment loss; reversible in one revert. + +| 6 +| Workflows (9,261) — only if step 4 ruled them in scope. +| Additionally: Dependabot and `gh actions-lock` either emit KYAML or their + drift is explicitly accepted in writing. Unresolved, this step does not start. +|=== + +== 6. What this document does not rule + +* It does not ban YAML. KYAML *is* YAML. +* It does not require reformatting anything today. +* It does not apply to lockfiles (`pnpm-lock.yaml` and friends), which are + tool-owned artefacts and must never be hand-edited or reformatted. +* It does not change `LANGUAGE-POLICY.adoc`. YAML is a data format here, not a + programming language, and no language ranking is affected. + +== 7. Sources + +* KEP-5295, KYAML — https://www.kubernetes.dev/resources/keps/5295/ +* KYAML reference — https://www.kubernetes.io/docs/reference/encodings/kyaml/ +* "How to Pretty-Print Your Kubernetes YAML as KYAML and Why You'd Want To" — + https://kubernetes.io/blog/2026/08/11/how-to-pretty-print-kubernetes-yaml-as-kyaml/ + +== 8. Change log + +[cols="2,2,8",options="header"] +|=== +| Date | Who | Change + +| 2026-09-22 +| Owner ruling +| Document created. Y-1 in force. Y-2 sequenced behind the comment proof. + Y-3 sequenced, scope deferred to the GitHub Actions probe; estate-wide + adoption booked as ordered issues per §5. +|=== diff --git a/standards-map.toml b/standards-map.toml index 3797185c4..699a3802b 100644 --- a/standards-map.toml +++ b/standards-map.toml @@ -32,7 +32,7 @@ [map] repo = "hyperpolymath/standards" version = "1.0.0" -entry_count = 123 +entry_count = 124 generated = false # hand-curated; the district index.adoc files ARE generated checked_by = "scripts/check-standards-map.sh" @@ -812,6 +812,19 @@ canonical_doc = "3-practice/TRUST-DEFAULTS-POLICY.adoc" gate = "canon-self-conformance" note = "SPDX/licence policy is owner-only, flag-never-edit" +[[entry]] +from = "3-practice/YAML-POLICY.adoc" +target = "3-practice/YAML-POLICY.adoc" +district = "3-practice" +kind = "policy" +files = 1 +lifecycle = "versioned" +canonical = true +canon_slot = "" +canonical_doc = "3-practice/YAML-POLICY.adoc" +gate = "canon-self-conformance" +note = "YAML read/write/author policy. Y-1 (yq for reading) in force; Y-2 and Y-3 sequenced behind a shared comment-preservation proof - do not gate on them yet" + [[entry]] from = "3-practice/ZIGZAG-TESTING.adoc" target = "3-practice/ZIGZAG-TESTING.adoc" From c07e523dc4013ee3770e5953be6cb30f4c997624 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:06:31 +0100 Subject: [PATCH 2/2] docs(policy): cite the first gate test written to Rule Y-1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cicd-suite PR #32 extracts a composite's embedded shell script with `yq -r '.runs.steps[0].run'` in order to execute it under the real CI shell. A grep/sed extraction would have had to guess the block scalar's indentation and could silently have produced a different script from the one GitHub runs — the exact failure class Y-1 exists to prevent. The defect that test was written for belongs to the same family: the gate's behaviour depended on `-e`, supplied by the harness and absent from the file, so reading the file alone gave a confident verdict in the wrong direction. Refs: #787 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f --- 3-practice/YAML-POLICY.adoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/3-practice/YAML-POLICY.adoc b/3-practice/YAML-POLICY.adoc index 1241409b3..0e0e9a13c 100644 --- a/3-practice/YAML-POLICY.adoc +++ b/3-practice/YAML-POLICY.adoc @@ -100,6 +100,25 @@ Y-1 governs *reading in gate code*. It does not govern: *verdict* comes from `yq`. A pre-filter that decides the outcome is a grep gate wearing a costume. +=== 1.3 A landed exemplar + +`cicd-suite` PR #32 (2026-09-22) is the first gate test written to this rule. +To prove that a composite action's embedded shell script behaves correctly, it +extracts that script with + +[source,bash] +---- +yq -r '.runs.steps[0].run' "actions//action.yml" +---- + +and executes the result. A `grep`/`sed` extraction would have had to guess the +block scalar's indentation, and would have silently produced a *different* +script from the one GitHub runs — which is precisely the class of error Y-1 +exists to prevent. The defect that test was written for is itself of this +family: the gate's behaviour depended on a shell flag (`-e`) supplied by the +*harness* and absent from the file, so reading the file alone gave a confident +verdict in the wrong direction. + == 2. Rule Y-2 — `yq` for WRITING is deferred behind a comment-preservation proof *Status: NOT IN FORCE. Sequenced. Do not cite this as a rule.*