diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e45a37d78..445081a10 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -63,7 +63,7 @@ Each has a normative grammar in this repository: |---|---|---|---| | Repo facts: identity, state, meta, ecosystem, agentic, neurosym, playbook, and the `(bot-directives …)` and `(contractiles …)` clauses | `_chora.deed` at the repo root, one per repo (D312) | `1-formats/deed/spec/abnf/deed.abnf` (D308; prose: `1-formats/deed/spec/DEED-GRAMMAR-SPEC.adoc`) | `bun 1-formats/deed/tools/deed_lint.js FILE.deed` | | Contracts | `*.k9.ncl` | `1-formats/k9/spec/K9-CONTRACT-SPEC.adoc` | `1-formats/k9/tools/k9-validate.sh --strict FILE` | -| Agent coordination | `coordination.k9` at the repo root: first line `K9!`, `schema_version: 1.1.1` | `2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf` (prose: `2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc`) | No validator script exists yet, and a generic YAML parser does not conform. The generator is `2-protocols/k9-coordination/tools/k9-init/`. | +| Agent coordination | `coordination.k9` at the repo root: first line `K9!`, `schema_version: 1.2.0` | `2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf` (prose: `2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc`) | No validator script exists yet, and a generic YAML parser does not conform. The generator is `2-protocols/k9-coordination/tools/k9-init/`. | - **Translating old content.** The mapping specs in `1-formats/deed/mappings/`, one per former A2ML family, decide where its content goes. Anything a diff --git a/.github/actions/signed-push/README.md b/.github/actions/signed-push/README.md index ee0c65afa..c91783be6 100644 --- a/.github/actions/signed-push/README.md +++ b/.github/actions/signed-push/README.md @@ -12,9 +12,11 @@ merge** (e.g. `mergeStateStatus: BLOCKED`). The only reliable way for automation produce Verified commits is the GraphQL [`createCommitOnBranch`] mutation authenticated as a **GitHub App installation** — GitHub signs those commits itself. -This action mints an App installation token and delegates the push to -[`Asana/push-signed-commits`], which diffs local vs. remote and replays the local -commits through `createCommitOnBranch`. +This action mints an App installation token and runs +[`push-verified.sh`](push-verified.sh) (bash, `gh` and `jq`), which diffs local +vs. remote and replays the local commits through `createCommitOnBranch`, oldest +first, each call naming the remote head it expects so that a concurrent push +fails closed. On success the local branch is reset to the remote. ## Prerequisites (one-time, account owner) @@ -54,10 +56,15 @@ jobs: ## Notes -- Pinned to SHAs (`create-github-app-token` v3.2.0, `push-signed-commits` v1) per - estate supply-chain policy. +- `create-github-app-token` is SHA-pinned (v3.2.0) per estate supply-chain policy. + The push itself is `push-verified.sh`, so no other third-party action runs. (It + replaced `Asana/push-signed-commits`, a Python action with no licence file.) +- The remote branch must already exist: `createCommitOnBranch` commits onto an + existing ref. +- `createCommitOnBranch` cannot express merge commits, symlinks, submodules or file + modes, so a push containing any of them is refused before anything is sent, and + the pushed tree is checked against the local one. - Run **after** local commits exist in the job — the action pushes the delta between your local branch and its remote. [`createCommitOnBranch`]: https://github.blog/changelog/2021-09-13-a-simpler-api-for-authoring-commits/ -[`Asana/push-signed-commits`]: https://github.com/Asana/push-signed-commits diff --git a/.github/actions/signed-push/action.yml b/.github/actions/signed-push/action.yml index 465173f5e..88a4eb721 100644 --- a/.github/actions/signed-push/action.yml +++ b/.github/actions/signed-push/action.yml @@ -2,9 +2,10 @@ name: Signed push (verified commits via GitHub App) description: > Re-push the current branch's local commits as GitHub-"Verified" commits using a GitHub App installation token and the createCommitOnBranch GraphQL mutation (via - Asana/push-signed-commits). This satisfies `required_signatures` branch rulesets - that a PAT or the default GITHUB_TOKEN cannot. Run this AFTER you have made local - commits in the job (checkout -> edit -> git commit -> this action). + push-verified.sh beside this file: bash, gh and jq, no third-party action). This + satisfies `required_signatures` branch rulesets that a PAT or the default + GITHUB_TOKEN cannot. Run this AFTER you have made local commits in the job + (checkout -> edit -> git commit -> this action). inputs: app-id: @@ -39,8 +40,8 @@ runs: # and auto-marks the resulting commits as "Verified". A PAT would leave them # Unverified — which is why this must be an App token, not GITHUB_TOKEN/PAT. - name: Push local commits as Verified - uses: Asana/push-signed-commits@d615ca88d8e1a946734c24970d1e7a6c56f34897 # v1.3 - with: - github-token: ${{ steps.app-token.outputs.token }} - local_branch_name: ${{ steps.branch.outputs.name }} - remote_branch_name: ${{ steps.branch.outputs.name }} + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + BRANCH: ${{ steps.branch.outputs.name }} + run: bash "$GITHUB_ACTION_PATH/push-verified.sh" "$BRANCH" diff --git a/.github/actions/signed-push/push-verified.sh b/.github/actions/signed-push/push-verified.sh new file mode 100755 index 000000000..e16d4328c --- /dev/null +++ b/.github/actions/signed-push/push-verified.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# push-verified.sh — re-push a branch's local commits as GitHub-"Verified" +# commits through the GraphQL createCommitOnBranch mutation. +# +# Replaces Asana/push-signed-commits, which was Python (banned here), had no +# licence file, pulled the tag-pinned actions/setup-python@v2 and ran an +# unpinned `pip install requests` at run time. The contract is the same: +# +# * the remote branch must already exist and must not have diverged: every +# commit on it must already be in the local branch; +# * each local commit not on the remote is recreated, oldest first, with +# its message, by one createCommitOnBranch call that names the remote +# head it expects (expectedHeadOid), so a concurrent push fails closed; +# * on success the local branch is reset to the remote, whose commits now +# carry GitHub's authorship and verification. +# +# Two things are stricter than the Python action: +# * file contents are base64'd from the raw blob, so binary files survive +# (the Python action decoded them as UTF-8 text); +# * createCommitOnBranch can express neither file modes nor symlinks nor +# submodules, so a commit that adds or changes one is REFUSED rather than +# silently flattened, and the pushed tree is compared with the local one. +# +# Usage: push-verified.sh BRANCH +# Env: GH_TOKEN (App installation token), GITHUB_REPOSITORY (owner/repo), +# REMOTE (default origin), GH_BIN (default gh; the test suite stubs it). +set -euo pipefail + +BRANCH="${1:?usage: push-verified.sh BRANCH}" +REMOTE="${REMOTE:-origin}" +GH_BIN="${GH_BIN:-gh}" +REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY (owner/repo) is required}" + +# die: report an error in Actions annotation form and stop. +die() { echo "::error::signed-push: $*" >&2; exit 1; } + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +git fetch --quiet "$REMOTE" "$BRANCH" +REMOTE_HEAD="$(git rev-parse "$REMOTE/$BRANCH")" +LOCAL_HEAD="$(git rev-parse --verify "refs/heads/$BRANCH")" + +[ "$(git merge-base "$LOCAL_HEAD" "$REMOTE_HEAD")" = "$REMOTE_HEAD" ] \ + || die "$REMOTE/$BRANCH has commits that $BRANCH lacks; refusing to push over them" + +mapfile -t COMMITS < <(git rev-list --reverse "$REMOTE_HEAD..$LOCAL_HEAD") +if [ "${#COMMITS[@]}" -eq 0 ]; then + echo "signed-push: $BRANCH has no commits that $REMOTE/$BRANCH lacks; nothing to push" + exit 0 +fi + +# changes_json: write the commit's file changes as {additions, deletions} +# JSON to $2. Renames arrive as a deletion plus an addition (--no-renames). +changes_json() { + local c="$1" out="$2" meta path old_mode new_mode new_blob status n=0 + : > "$out.adds"; : > "$out.dels" + while IFS= read -r -d '' meta && IFS= read -r -d '' path; do + read -r old_mode new_mode _ new_blob status <<<"${meta#:}" + case "$status" in + D) jq -nc --arg p "$path" '{path: $p}' >> "$out.dels" ;; + A|M|T) + case "$new_mode" in + 100644|100755) ;; + 120000) die "$c adds or changes the symlink $path; createCommitOnBranch cannot express symlinks" ;; + 160000) die "$c adds or changes the submodule $path; createCommitOnBranch cannot express submodules" ;; + *) die "$c gives $path the unsupported mode $new_mode" ;; + esac + if [ "$status" = A ] && [ "$new_mode" != 100644 ]; then + die "$c adds $path as executable ($new_mode); createCommitOnBranch would drop the mode" + fi + if [ "$status" != A ] && [ "$old_mode" != "$new_mode" ]; then + die "$c changes the mode of $path ($old_mode -> $new_mode); createCommitOnBranch cannot express modes" + fi + n=$((n + 1)) + git cat-file blob "$new_blob" | base64 -w0 > "$out.blob.$n" + jq -nc --arg p "$path" --rawfile b "$out.blob.$n" '{path: $p, contents: $b}' >> "$out.adds" + ;; + *) die "$c has an unsupported change ($status) to $path" ;; + esac + done < <(git diff --raw -z --no-renames --no-abbrev "$c^" "$c") + jq -n --slurpfile a "$out.adds" --slurpfile d "$out.dels" \ + '{additions: $a, deletions: $d}' > "$out" +} + +# Validate and prepare EVERY commit before sending any, so a commit that +# cannot be expressed refuses the whole push instead of leaving it partial. +i=0 +for c in "${COMMITS[@]}"; do + [ "$(git rev-list --parents -n 1 "$c" | wc -w)" -eq 2 ] \ + || die "$c is a merge commit; createCommitOnBranch can only create single-parent commits" + i=$((i + 1)) + changes_json "$c" "$WORK/changes.$i.json" +done + +expected="$REMOTE_HEAD" +pushed=0 +i=0 +for c in "${COMMITS[@]}"; do + i=$((i + 1)) + jq -n \ + --arg repo "$REPO" --arg branch "$BRANCH" --arg oid "$expected" \ + --arg headline "$(git log -1 --format=%s "$c")" \ + --arg body "$(git log -1 --format=%b "$c")" \ + --slurpfile fc "$WORK/changes.$i.json" \ + '{query: "mutation($in: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $in) { commit { oid } } }", + variables: {in: { + branch: {repositoryNameWithOwner: $repo, branchName: $branch}, + expectedHeadOid: $oid, + message: {headline: $headline, body: $body}, + fileChanges: $fc[0]}}}' > "$WORK/payload.json" + new="$("$GH_BIN" api graphql --input "$WORK/payload.json" --jq '.data.createCommitOnBranch.commit.oid' 2>"$WORK/gh.err" || true)" + if ! [[ "$new" =~ ^[0-9a-f]{40}$ ]]; then + msg="createCommitOnBranch failed for $c: $(tr '\n' ' ' < "$WORK/gh.err")" + if [ "$pushed" -gt 0 ]; then + msg="$msg -- PARTIAL PUSH: $pushed of ${#COMMITS[@]} commit(s) are on $REMOTE/$BRANCH; reconcile by hand" + fi + die "$msg" + fi + expected="$new" + pushed=$((pushed + 1)) + echo "signed-push: $c -> $new" +done + +git fetch --quiet "$REMOTE" "$BRANCH" +[ "$(git rev-parse "$REMOTE/$BRANCH")" = "$expected" ] \ + || die "$REMOTE/$BRANCH moved after the push (expected $expected)" +[ "$(git rev-parse "$REMOTE/$BRANCH^{tree}")" = "$(git rev-parse "$LOCAL_HEAD^{tree}")" ] \ + || die "the pushed tree differs from the local one; $REMOTE/$BRANCH needs inspection" + +if [ "$(git symbolic-ref --quiet --short HEAD || true)" = "$BRANCH" ]; then + git reset --quiet --hard "$REMOTE/$BRANCH" +else + git branch --quiet --force "$BRANCH" "$REMOTE/$BRANCH" +fi +echo "signed-push: pushed $pushed commit(s) to $REMOTE/$BRANCH as Verified; $BRANCH now matches it" diff --git a/.meta/REQUIRED-FILES.adoc b/.meta/REQUIRED-FILES.adoc index 81f931d65..01cc72f8c 100644 --- a/.meta/REQUIRED-FILES.adoc +++ b/.meta/REQUIRED-FILES.adoc @@ -43,8 +43,8 @@ reserved for Guix files (`guix.scm`, `.guix-channel`). `1-formats/k9/tools/k9-validate.sh --strict FILE`. | `coordination.k9` (repo root) -| Agent coordination rules. First line `K9!`, `schema_version: 1.1.1`. -| link:../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`] +| Agent coordination rules. First line `K9!`, `schema_version: 1.2.0`. +| link:../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`] (prose: link:../2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc[COORDINATION-K9-SPEC]). No validator script exists yet, and a generic YAML parser does not conform. |=== diff --git a/1-formats/k9/IANA-MEDIA-TYPE-APPLICATION.adoc b/1-formats/k9/IANA-MEDIA-TYPE-APPLICATION.adoc index fa14ba9db..f14689bbb 100644 --- a/1-formats/k9/IANA-MEDIA-TYPE-APPLICATION.adoc +++ b/1-formats/k9/IANA-MEDIA-TYPE-APPLICATION.adoc @@ -2,20 +2,19 @@ ____ Prepared for submission to IANA per RFC 6838 (Vendor Tree) Submission -URL: https://www.iana.org/form/media-types Revision 3 – 2026-10-13 +URL: https://www.iana.org/form/media-types Revision 4 – 2026-10-13 ____ ''''' -[IMPORTANT] -.Not filed: blocked on an open decision +[NOTE] +.File together with application/vnd.k9.coordination ==== -Do not submit this application yet. Its *File extension(s)* field claims -`.k9`, and a second format, the k9-coordination dialect -(`coordination.k9`), uses that extension and the `K9!` magic with a -different body language. Whether that dialect gets its own subtype or a -parameter of this one has not been ruled on. See -<>. +The k9-coordination dialect (`coordination.k9`) shares the `K9!` magic but +is a different format, so it is registered as its own subtype, +link:../../2-protocols/k9-coordination/docs/iana/application-vnd.k9.coordination-registration.txt[`application/vnd.k9.coordination`] +(owner ruling, 2026-10-13). This application claims only `.k9.ncl`. See +<>. ==== === Important: Change from +nickel suffix @@ -217,17 +216,18 @@ grammar of its own beyond the envelope. Standard Nickel tooling can parse a K9 body only after the envelope-strip rule has removed the `+K9!+` line; unstripped, `nickel typecheck` fails at line 1, column 3. -[[open-decision]] -==== Open decision: coordination.k9 +[[companion-type]] +==== Companion type: application/vnd.k9.coordination -A second format, the k9-coordination dialect, also uses the `.k9` -extension and the `+K9!+` magic. Its body is a YAML-like language with its -own ABNF grammar, it never executes anything, and it is versioned -separately. Readers MUST classify a file by its first significant line -before validating it (K9 Component Contract Specification, K9-E005). -Whether that dialect is labelled with its own subtype or with a parameter -of this one has not been ruled on. Until it is, this application MUST NOT -be filed. +The k9-coordination dialect also uses the `+K9!+` magic. Its body is a +YAML-like language with its own ABNF grammar, it never executes anything, +and it is versioned separately, so it is registered as its own subtype +rather than as a parameter of this one (owner ruling, 2026-10-13). +Parameters are routinely dropped by file-type detectors, which would label +coordination files as Nickel components. Files carrying the magic MUST be +classified by their first significant line before validation (K9 +Component Contract Specification, K9-E005); a coordination file is never +`application/vnd.k9` content. ''''' @@ -299,9 +299,8 @@ signature verification and explicit user consent. |Field |Value |*Deprecated alias names* |None |*Magic number(s)* |`+K9!+` (0x4B 0x39 0x21) at byte offset 0, followed by -LF. Shared with the k9-coordination dialect (see <>). -|*File extension(s)* |`+.k9.ncl+` (components); `+.k9+` (shared with the -k9-coordination dialect, see <>) +LF. Shared with `application/vnd.k9.coordination` (see <>). +|*File extension(s)* |`+.k9.ncl+` (`+.k9+` is `application/vnd.k9.coordination`) |*Macintosh file type code(s)* |None |*Object Identifier(s) / OID(s)* |None |*Intended usage* |COMMON @@ -364,7 +363,8 @@ https://github.com/hyperpolymath/standards/blob/main/1-formats/k9/SPEC.adoc * [x] Remove unregistered +nickel suffix (RFC 6838 Section 4.2.8) * [x] Unify file extensions under base type * [x] Align with the K9 Component Contract Specification v1.0.0 (Revision 3) -* [ ] Settle the coordination.k9 open decision (subtype or parameter) +* [x] Settle the coordination.k9 question: own subtype, application/vnd.k9.coordination (Revision 4) +* [ ] File application/vnd.k9.coordination in the same submission round * [ ] Submit via IANA web form at https://www.iana.org/form/media-types * [ ] Monitor IANA email for review feedback * [ ] Update specification with assigned media type upon approval @@ -386,5 +386,5 @@ independent media types - Community consensus on suffix semantics ''''' -_Prepared: 2026-01-30_ _Revised: 2026-10-13 (Revision 3)_ _Status: Draft -– blocked on the coordination.k9 open decision_ +_Prepared: 2026-01-30_ _Revised: 2026-10-13 (Revision 4)_ _Status: Draft +– ready for submission with application/vnd.k9.coordination_ diff --git a/1-formats/k9/SPEC.adoc b/1-formats/k9/SPEC.adoc index 319c034b5..1b0a40ea7 100644 --- a/1-formats/k9/SPEC.adoc +++ b/1-formats/k9/SPEC.adoc @@ -480,21 +480,29 @@ conformance by link:tools/k9-validate.sh[`tools/k9-validate.sh`]. + + + + K9 Coordination File + + + + + + ---- Location: `/usr/share/mime/packages/k9.xml` -[WARNING] -==== -The `*.k9` glob and the `K9!` magic also match `coordination.k9`, which is -a different format (the k9-coordination dialect, with a YAML-like body). This -entry therefore labels coordination files as Nickel components. How the -dialect should be labelled is an open decision that blocks the IANA filing. -See link:IANA-MEDIA-TYPE-APPLICATION.adoc#open-decision[the media type -application]. -==== +Both types open with `K9!`, so the magic alone cannot tell them apart. +The globs decide first (`*.k9.ncl` is a component, `*.k9` a coordination +file), and for a file without a recognised name the coordination entry adds +a nested match: a line starting `metadata:` within the first kilobyte, which +is how every coordination body begins and no Nickel component does. The +authoritative classification remains the K9-E005 discriminator in +link:spec/K9-CONTRACT-SPEC.adoc[the contract specification]. === macOS (UTI) @@ -506,15 +514,22 @@ application]. application/vnd.k9 ---- +The coordination type gets its own UTI, `org.k9-svc.coordination`, with +`public.mime-type` `application/vnd.k9.coordination`. + === Minix Static mapping in `/etc/mime.types`: [source] ---- -application/vnd.k9 k9 +application/vnd.k9.coordination k9 ---- +`/etc/mime.types` keys on a file's last extension only, so it cannot express +`.k9.ncl` (that would be read as plain `.ncl`). Components are left to +content detection there. + == Philosophy The `.k9` format treats documents as **Living Entities**. diff --git a/1-formats/k9/docs/iana/application-vnd.k9-registration.txt b/1-formats/k9/docs/iana/application-vnd.k9-registration.txt index 16e44ae7b..30123addb 100644 --- a/1-formats/k9/docs/iana/application-vnd.k9-registration.txt +++ b/1-formats/k9/docs/iana/application-vnd.k9-registration.txt @@ -10,12 +10,10 @@ structured syntax suffix is not registered with IANA. Per RFC 6838 Section 4.2.8, unregistered suffixes SHOULD NOT be used. This registration uses "application/vnd.k9" as the base type. -STATUS: NOT FILED. Do not submit this until the open decision below -(OPEN DECISION: coordination.k9) is settled, because the File -extension(s) field claims ".k9", which a second, differently-bodied -format also uses. +STATUS: NOT FILED; ready to file together with its companion +application/vnd.k9.coordination (see "Interoperability considerations"). -Prepared: 2026-10-13 (Revision 3) +Prepared: 2026-10-13 (Revision 4) Prepared By: Jonathan D.A. Jewell --- @@ -102,15 +100,17 @@ Interoperability considerations: body syntax is Nickel (https://nickel-lang.org), which is normative by reference; K9 defines no grammar of its own beyond the envelope. - OPEN DECISION: coordination.k9. A second format, the k9-coordination - dialect, also uses the ".k9" extension and the "K9!" magic, but its - body is a YAML-like language with its own ABNF grammar, it never - executes anything, and it is versioned separately. Readers MUST - classify a file by its first significant line before validating it - (K9 Component Contract Specification, "K9-E005 - the discriminator"). - Whether that dialect is labelled with its own subtype or with a - parameter of this one has not been ruled on. Until it is, this - registration MUST NOT be filed. + Companion type: application/vnd.k9.coordination. The k9-coordination + dialect (coordination.k9) shares the "K9!" magic but has a YAML-like + body with its own ABNF grammar, never executes anything, and is + versioned separately, so it is registered as its own subtype rather + than as a parameter of this one (owner ruling, 2026-10-13). + Parameters are routinely dropped by file-type detectors, which would + label coordination files as Nickel components. Files carrying the + magic MUST be classified by their first significant line before + validation (K9 Component Contract Specification, + "K9-E005 - the discriminator"); a coordination file is never + application/vnd.k9 content. Published specification: K9 Component Contract Specification v1.0.0 (normative): @@ -157,10 +157,9 @@ Additional information: Deprecated alias names for this type: None Magic number(s): "K9!" (0x4B 0x39 0x21) at byte offset 0, followed - by LF. Shared with the k9-coordination dialect; - see OPEN DECISION above. - File extension(s): .k9.ncl (components); .k9 (shared with the - k9-coordination dialect; see OPEN DECISION above) + by LF. Shared with application/vnd.k9.coordination; + see "Interoperability considerations". + File extension(s): .k9.ncl (".k9" is application/vnd.k9.coordination) Macintosh file type code(s): None Object Identifier(s) or OID(s): None @@ -223,3 +222,8 @@ Change Log: shared ".k9" extension and "K9!" magic with the k9-coordination dialect as an OPEN DECISION that blocks filing. +2026-10-13: Revision 4 -- the open decision is settled by owner + ruling: the k9-coordination dialect gets its own subtype, + application/vnd.k9.coordination, registered alongside this + one. This type now claims only ".k9.ncl"; ".k9" belongs to + the companion type. No longer blocked. diff --git a/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc b/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc index fe9911e43..94fd4bed0 100644 --- a/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc +++ b/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc @@ -99,7 +99,7 @@ and would produce a grammar that drifts from the implementation on the first upstream release. The `k9-coordination` dialect:: Named in §4 so a reader can route it, but governed by -link:../../../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`]. +link:../../../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`]. That format is YAML-bodied and has its own ABNF, which is correct for it: it *does* have a syntax of its own. The repository pedigree:: link:../pedigree.ncl[`pedigree.ncl`] defines a @@ -224,7 +224,7 @@ line are preserved. |Dialect |Envelope |Body |Governed by |`k9-svc-component` |`K9!` + SPDX |Nickel term with a top-level `pedigree` |*this document* |`k9-svc-library` |none |Nickel term, no `pedigree`, no `leash` |§11 of this document -|`k9-coordination` |`K9!` + SPDX |YAML |`coordination-k9-grammar_v1.1.abnf` +|`k9-coordination` |`K9!` + SPDX |YAML |`coordination-k9-grammar_v1.2.abnf` |*unclaimed* |any |anything else |nothing — and that is the defect |=== @@ -233,6 +233,12 @@ header but not the body language; it is a different format that happens to have been given the same scent. Naming it here is what lets a reader route it instead of misreporting it as a broken component. +The difference carries through to the media types. A component is +`application/vnd.k9`; a coordination file is its own subtype, +`application/vnd.k9.coordination` (owner ruling, 2026-10-13). It is not a +parameter of `vnd.k9`, because file-type detectors drop parameters and would +then label coordination files as Nickel components. + === Extension reservation The `.k9` and `.k9.ncl` extensions are *reserved* to the dialects above. A @@ -1121,7 +1127,7 @@ so the decision does not have to be re-litigated by the next reader: request" or "presence is not verification"; a contract can, and does. . *The sibling format that does need a grammar has one.* `k9-coordination` is YAML-bodied with its own syntax, and - `coordination-k9-grammar_v1.1.abnf` is the right artefact for it. The + `coordination-k9-grammar_v1.2.abnf` is the right artefact for it. The difference is not inconsistency; it is that one format has a syntax and the other borrows one. @@ -1137,7 +1143,7 @@ to infer a grammar from prose. * RFC 8017 / RFC 8032 — Ed25519 * link:../SPEC.adoc[K9 SVC Specification] * link:../IANA-MEDIA-TYPE-APPLICATION.adoc[`application/vnd.k9` media type] -* link:../../../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[K9 Coordination File Grammar v1.1.1] +* link:../../../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[K9 Coordination File Grammar v1.2.0] * standards#1058 (grammar artifacts), ruling D173; standards#837, #856, #752, #49, #64 [appendix] diff --git a/1-formats/k9/spec/contract/k9_contract.ncl b/1-formats/k9/spec/contract/k9_contract.ncl index bb38dad32..95aeb043c 100644 --- a/1-formats/k9/spec/contract/k9_contract.ncl +++ b/1-formats/k9/spec/contract/k9_contract.ncl @@ -145,7 +145,7 @@ in # executed. In scope for §11 only. library = "k9-svc-library", # `K9!` envelope + YAML body. Governed by - # 2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf, + # 2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf, # NOT by this contract. Named here so a validator can route it instead of # misreporting it as a broken component. coordination = "k9-coordination", diff --git a/1-formats/k9/tools/k9-validate.sh b/1-formats/k9/tools/k9-validate.sh index 20bee7086..ff68747f1 100755 --- a/1-formats/k9/tools/k9-validate.sh +++ b/1-formats/k9/tools/k9-validate.sh @@ -343,7 +343,7 @@ check_l0() { fi if [ "$DIALECT" = "coordination" ]; then - note "$f: dialect k9-coordination — governed by coordination-k9-grammar_v1.1.abnf, out of scope here" + note "$f: dialect k9-coordination — governed by coordination-k9-grammar_v1.2.abnf, out of scope here" return 2 fi @@ -864,7 +864,7 @@ EOF fi echo "== the dialect discriminator agrees with the coordination grammar (§4.3) ==" - # coordination-k9-grammar_v1.1.abnf requires `K9!` and has no `---` + # coordination-k9-grammar_v1.2.abnf requires `K9!` and has no `---` # production (D318). So no file without the magic may be routed there, and # one with it is routed only to be rejected by that grammar. local disc diff --git a/2-protocols/k9-coordination/README.adoc b/2-protocols/k9-coordination/README.adoc index e3c9035a8..e31567214 100644 --- a/2-protocols/k9-coordination/README.adoc +++ b/2-protocols/k9-coordination/README.adoc @@ -4,7 +4,7 @@ = K9 Coordination Protocol Jonathan D.A. Jewell -:version: 1.1.1 +:version: 1.2.0 :status: STABLE :revdate: 2026-10-06 :toc: @@ -15,7 +15,7 @@ Jonathan D.A. Jewell The K9 Coordination File Specification is *STABLE* at version {version}. The normative parsing grammar is -link:spec/abnf/coordination-k9-grammar_v1.1.abnf[K9 Coordination ABNF v1.1.1]. If this README or the prose +link:spec/abnf/coordination-k9-grammar_v1.2.abnf[K9 Coordination ABNF v1.2.0]. If this README or the prose specification disagrees with the ABNF about syntax, the ABNF wins. The implementation roadmap is currently at *Phase 1*. The grammar already @@ -35,7 +35,7 @@ K9 is a *Kennel-level* format: pure data with no execution, functions, or evaluation. K9 is YAML-like, but it is *not YAML*. Implementations MUST use a K9-aware -parser conforming to link:spec/abnf/coordination-k9-grammar_v1.1.abnf[coordination-k9-grammar_v1.1.abnf], not a generic YAML +parser conforming to link:spec/abnf/coordination-k9-grammar_v1.2.abnf[coordination-k9-grammar_v1.2.abnf], not a generic YAML parser. == The Problem @@ -217,7 +217,7 @@ K9! metadata: schema: k9-coordination - schema_version: 1.1.1 + schema_version: 1.2.0 generated_by: k9-init last_updated: 2026-04-09 @@ -413,7 +413,7 @@ If `deed_ref` is present: == See Also * link:spec/COORDINATION-K9-SPEC.adoc[K9 Coordination File Specification] -* link:spec/abnf/coordination-k9-grammar_v1.1.abnf[K9 Coordination ABNF Grammar v1.1.1] +* link:spec/abnf/coordination-k9-grammar_v1.2.abnf[K9 Coordination ABNF Grammar v1.2.0] * link:examples/[Example K9 files] * link:generator/[Generator tool] * link:https://www.rfc-editor.org/rfc/rfc7405[RFC 7405 — Case-Sensitive ABNF Strings] diff --git a/2-protocols/k9-coordination/docs/iana/application-vnd.k9.coordination-registration.txt b/2-protocols/k9-coordination/docs/iana/application-vnd.k9.coordination-registration.txt new file mode 100644 index 000000000..7d57e4985 --- /dev/null +++ b/2-protocols/k9-coordination/docs/iana/application-vnd.k9.coordination-registration.txt @@ -0,0 +1,156 @@ +IANA Media Type Registration: application/vnd.k9.coordination +========================================================= +SPDX-License-Identifier: MPL-2.0 + +This file is the canonical registration document for +application/vnd.k9.coordination. It lives beside the specification it +registers; there is no second copy. + +NOTE: The k9-coordination dialect shares the "K9!" magic with +application/vnd.k9 but is a different format, so it is registered as its +own subtype rather than as a parameter of vnd.k9 (owner ruling, +2026-10-13). Parameters are routinely dropped by file-type detectors, +which would label coordination files as Nickel components; a subtype +survives that, and lets each type state its own encoding and security +considerations. + +STATUS: NOT FILED; file together with application/vnd.k9. + +Prepared: 2026-10-13 (Revision 1) +Prepared By: Jonathan D.A. Jewell + +--- + +Type name: + application + +Subtype name: + vnd.k9.coordination + +Required parameters: + None + +Optional parameters: + None + +Encoding considerations: + 8bit + + UTF-8 text with no NUL octet. Line 1 is exactly the three octets + "K9!" (0x4B 0x39 0x21), then one or more "# SPDX-" comment lines, + then the body. The body is a line-oriented, YAML-like language with + its own ABNF grammar; it is NOT YAML and MUST NOT be handed to a + general YAML parser. Indentation is exactly two spaces per level; + tabs are invalid. Line endings are LF or CR+LF. + +Security considerations: + A coordination file is pure data. The grammar admits no executable + content, no include or import directive, no evaluation form and no + opaque payload. Processors MUST NOT execute anything found in it. + + Coordination files are read by automated agents (AI coding assistants) + to decide what they may and may not change in a repository, so: + + (a) Processors MUST treat a coordination file as untrusted input. Its + invariants, protected paths and "do not create" rules are claims + by whoever last edited the file, not grants of authority; an agent + MUST NOT infer any privilege from them. + + (b) A coordination file can steer an agent away from a change, so a + tampered file can be used to suppress a legitimate fix. Consumers + that act on it SHOULD read it from the reviewed default branch, + not from an unreviewed change. + + (c) Generators that turn a coordination file into tool-specific + instruction files SHOULD escape or quote its strings for the target + format, so that text in the file cannot become instructions the + file's author did not write. + + Privacy: coordination files name agents, work areas and dates, and + usually live in public repositories. They SHOULD NOT carry secrets, + credentials or personal data. + +Interoperability considerations: + Companion type: application/vnd.k9. Both types begin with the "K9!" + magic. A reader MUST classify a magic-bearing file by its first + significant line before validating it (K9 Component Contract + Specification, "K9-E005 - the discriminator"): a bare "key:" line is + this type; a Nickel term is application/vnd.k9. A "---" line is + routed to this type and then rejected by its grammar. + + The conventional filename is "coordination.k9" at the repository + root, which file-type detectors can match literally. + +Published specification: + K9 Coordination File Specification v1.2.0: + https://github.com/hyperpolymath/standards/blob/main/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc + + Formal grammar (ABNF, normative for parsing): + https://github.com/hyperpolymath/standards/blob/main/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf + +Applications which use this media type: + - The k9-coordination generator, which emits per-tool AI-agent + instruction files from a coordination.k9 + - k9-init, which scaffolds coordination.k9 files + - The K9 reference validator, k9-validate.sh, which routes these files + to the coordination grammar + + Reference implementation: + https://github.com/hyperpolymath/standards/tree/main/2-protocols/k9-coordination + +Fragment identifier considerations: + None defined. Processors encountering a fragment SHOULD treat it as + unresolvable rather than raise an error. + +Restrictions on usage: + None. + +Additional information: + + Deprecated alias names for this type: None + Magic number(s): "K9!" (0x4B 0x39 0x21) at byte offset 0, followed by + a line terminator. Shared with application/vnd.k9; + see "Interoperability considerations". + File extension(s): .k9 (".k9.ncl" is application/vnd.k9) + Macintosh file type code(s): None + Object Identifier(s) or OID(s): None + +Person & email address to contact for further information: + Jonathan D.A. Jewell + j.d.a.jewell@open.ac.uk + The Open University + Milton Keynes, MK7 6AA, United Kingdom + +Intended usage: + COMMON + +Author/Change controller: + Jonathan D.A. Jewell + The Open University + j.d.a.jewell@open.ac.uk + +Provisional registration: No (Vendor-tree registration.) + +--- + +References: + +[1] RFC 6838 - Media Type Specifications and Registration Procedures + https://www.rfc-editor.org/rfc/rfc6838.html + +[2] RFC 3629 - UTF-8, a transformation format of ISO 10646 + https://www.rfc-editor.org/rfc/rfc3629.html + +[3] K9 Coordination File Specification v1.2.0 + https://github.com/hyperpolymath/standards/blob/main/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc + +[4] application/vnd.k9 registration (companion type) + https://github.com/hyperpolymath/standards/blob/main/1-formats/k9/docs/iana/application-vnd.k9-registration.txt + +--- + +Change Log: + +2026-10-13: Revision 1 -- initial registration, following the owner + ruling that the k9-coordination dialect is its own subtype + rather than a parameter of application/vnd.k9. diff --git a/2-protocols/k9-coordination/examples/minimal.k9 b/2-protocols/k9-coordination/examples/minimal.k9 index 5e678684f..a3f2e6979 100644 --- a/2-protocols/k9-coordination/examples/minimal.k9 +++ b/2-protocols/k9-coordination/examples/minimal.k9 @@ -8,7 +8,7 @@ K9! metadata: schema: k9-coordination - schema_version: 1.1.1 + schema_version: 1.2.0 last_updated: 2026-04-03 project: diff --git a/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc b/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc index 23bbd5149..46c238e97 100644 --- a/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc +++ b/2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc @@ -1,18 +1,20 @@ // SPDX-License-Identifier: CC-BY-SA-4.0 = K9 Coordination File Specification Jonathan D.A. Jewell -:revdate: 2026-10-06 -:revnumber: 1.1.1 +:revdate: 2026-10-13 +:revnumber: 1.2.0 :description: Schema specification for coordination.k9 files :toc: == Status -Stable — Version 1.1.1. Owner ruling D311 (2026-10-06) made the 1.1 line -canonical; rulings D317 and D318 made the 1.1.1 grammar patch below. +Stable — Version 1.2.0. Owner ruling D311 (2026-10-06) made the 1.1 line +canonical; rulings D317 and D318 made the 1.1.1 grammar patch; an owner +ruling of 2026-10-13 added the two optional sections of 1.2.0 below. The normative grammar is -link:abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`]. +link:abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`]. +The v1.1 file is kept, marked superseded, so that existing links resolve. Where this prose, its examples or a generator disagree with the ABNF about syntax, the ABNF wins. @@ -21,6 +23,11 @@ Every example in this document parses under that ABNF. Read them closely: `alternatives_rejected:` items are quoted strings. Blank lines appear between sections, never inside one. +NOTE: *Changes in 1.2.0.* Two optional sections, additive, so every 1.1.1 +file is a 1.2.0 file: `build_commands` (the commands an agent may run, each +with its purpose) and `innervation` (the signals a repository emits and +responds to, and the local guards it enforces). + NOTE: *Changes in 1.1.1.* Two grammar fixes, both owner rulings of 2026-10-06. (1) D317: `languages:` items are indented one level deeper than their key (four spaces), as every other list already was; 1.1.0 placed them @@ -36,6 +43,7 @@ section, and a `---` separator is still invalid. | Location | Repository root | K9 Level | Kennel (pure data, no execution) | Magic Number | `K9!` (mandatory first line) +| Media Type | `application/vnd.k9.coordination` (link:../docs/iana/application-vnd.k9.coordination-registration.txt[registration draft]; distinct from `application/vnd.k9`, which is K9 SVC components) | SPDX | `PMPL-1.0-or-later` (or project license) |=== @@ -51,7 +59,7 @@ Identity and versioning for the coordination file itself. ---- metadata: schema: k9-coordination - schema_version: 1.1.1 + schema_version: 1.2.0 generated_by: k9-coordination-generator last_updated: 2026-04-03 ---- @@ -97,8 +105,23 @@ Severity levels: `critical` (never violate), `high` (ask user first), === Optional Sections Optional sections appear in this fixed order, each at most once: -`protected`, `architecture`, `do_not_create`, `terminology`, `ecosystem`, -`coordination`. +`build_commands` (between `project` and `invariants`), then after +`invariants`: `protected`, `architecture`, `innervation`, `do_not_create`, +`terminology`, `ecosystem`, `coordination`. + +==== `build_commands` (1.2.0) + +The commands an agent may run in this repository, each with its purpose. An +agent should prefer these to commands it infers. + +[source,yaml] +---- +build_commands: + - command: "just build" + purpose: "Build all sub-project artefacts" + - command: "just test" + purpose: "Run every sub-project test suite" +---- ==== `protected` @@ -135,6 +158,37 @@ architecture: reason: "ReScript compiler requires npm — this is the only exception" ---- +==== `innervation` (1.2.0) + +How the repository takes part in the wider estate. `afferent` lists the +signals it emits and who consumes them; `efferent` the signals it responds +to, and whether the response fixes things itself; `reflex` the local guards +that need no central processing. Each subsection is optional, but the +section needs at least one. + +[source,yaml] +---- +innervation: + afferent: + - signal: "spec.updated" + description: "A specification document changed" + triggers_on: "push to spec/ directories" + consumers: + - hypatia + - verisimdb + efferent: + - signal: "hypatia.finding" + action: "Log the finding" + auto_fix: false + reflex: + - trigger: "pre-commit" + guard: "panic-attack assail" + action: "Block the commit on critical findings" +---- + +`consumers` items are bare tokens, one per line; `auto_fix` is `true` or +`false`, unquoted. + ==== `do_not_create` Things that must NOT be introduced. Prevents well-meaning agents from @@ -224,8 +278,7 @@ rules a grammar cannot express (11–13). 2. One or more `# SPDX-` header lines MUST immediately follow the magic line. 3. `metadata`, `project` and `invariants` MUST appear, in that order. 4. Optional sections MUST appear in the fixed order given above, each at - most once. `protected`, `architecture`, `do_not_create`, `terminology`, - `ecosystem` and `coordination` each need at least one entry when present; + most once. Every optional section needs at least one entry when present; `invariants` may be empty. 5. Indentation is exactly two spaces per level; tabs are invalid. 6. Blank lines and full-line `#` comments are permitted between sections, diff --git a/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf b/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf index 6b1bdc5a6..16ef01e9b 100644 --- a/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf +++ b/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf @@ -3,7 +3,11 @@ ; SPDX-License-Identifier: CC-BY-SA-4.0 ; ; K9 Coordination File Grammar, v1.1.1 -; Normative for parsing coordination.k9 files +; +; SUPERSEDED by coordination-k9-grammar_v1.2.abnf (v1.2.0, 2026-10-13). +; v1.2.0 is additive, so every file valid here is valid there. This file +; is kept so that existing links resolve; it is no longer normative. +; Was normative for parsing coordination.k9 files until v1.2.0 ; Prose specification: ../COORDINATION-K9-SPEC.adoc ; ; K9 is YAML-like but NOT standard YAML. It is a domain-specific diff --git a/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf b/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf new file mode 100644 index 000000000..dcfa0d7ab --- /dev/null +++ b/2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf @@ -0,0 +1,265 @@ +; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +; SPDX-License: MPL-2.0 +; SPDX-License-Identifier: CC-BY-SA-4.0 +; +; K9 Coordination File Grammar, v1.2.0 +; Normative for parsing coordination.k9 files +; Prose specification: ../COORDINATION-K9-SPEC.adoc +; +; K9 is YAML-like but NOT standard YAML. It is a domain-specific +; coordination language with strict schema validation. +; +; Requires RFC 7405 (%s"..." case-sensitive string literals). +; +; CHANGELOG (v1.1.1 -> v1.2.0), owner ruling 2026-10-13: +; - Two optional sections, additive: every v1.1.1 file is a v1.2.0 file. +; build_commands (after project): the commands an agent may run, +; each with its purpose. innervation (after architecture): the +; signals the repo emits (afferent), responds to (efferent) and the +; local guards it enforces (reflex). +; - indent4 (8 spaces) for the consumer lists inside innervation. +; - Comments are ASCII, as RFC 5234 requires, apart from the SPDX +; copyright line (the earlier files also carried +; non-ASCII arrows and comparison signs). +; +; CHANGELOG (v1.1.0 -> v1.1.1), owner rulings D317 + D318: +; - lang-list items are indented one level under "languages:" (indent2), +; the same nesting every other list follows (was: indent, a defect) +; - Full-line "#" comments are permitted between sections (rule gap); +; still not within a section; "---" separators remain invalid +; +; CHANGELOG (v1.0.0 -> v1.1.0): +; - Added %s prefix for case-sensitive literals (RFC 7405) +; - Unified line endings to line-end (CRLF or LF) throughout +; - Added blank-line support between sections +; - Added bare-version to value rule (unquoted 1.0.0 etc.) +; - Expanded iso8601-datetime to full RFC 3339 (frac-sec, tz offsets) +; - Added escape sequences to strings (\" \\ \n \t) +; - Added indent2 / indent3 aliases for nested indentation +; - Expanded glob-pattern with ?, [], {} +; - Required >=1 entry in ecosystem / coordination sections +; - Fixed optional sections: explicit order, no duplicates + +k9-file = magic-line line-end header *gap body + +; --- Magic Number (REQUIRED first line) ---------------------------- +magic-line = %s"K9!" + +; --- SPDX Header (REQUIRED) ---------------------------------------- +header = 1*spdx-line +spdx-line = "#" SP %s"SPDX-" 1*text-char line-end + +; --- Gaps between sections (not permitted within a section) -------- +; A gap is a blank line or a full-line comment starting in column 0. +gap = blank-line / comment-line +blank-line = *SP line-end +comment-line = "#" *text-char line-end + +; --- Body Structure ------------------------------------------------ +; Required sections MUST appear in this order. +; Optional sections MUST appear in the order shown; each at most once. +body = metadata-section *gap + project-section *gap + [build-commands-section *gap] + invariants-section *gap + [protected-section *gap] + [architecture-section *gap] + [innervation-section *gap] + [do-not-create-section *gap] + [terminology-section *gap] + [ecosystem-section *gap] + [coordination-section *gap] + +; --- Required Sections --------------------------------------------- + +metadata-section = %s"metadata:" line-end + 1*metadata-field + +metadata-field = indent %s"schema:" SP value line-end + / indent %s"schema_version:" SP value line-end + / indent %s"generated_by:" SP value line-end + / indent %s"last_updated:" SP date line-end + +project-section = %s"project:" line-end + 1*project-field + +project-field = indent %s"name:" SP string line-end + / indent %s"description:" SP string line-end + / indent %s"license:" SP spdx-id line-end + / indent %s"repo:" SP repo-path line-end + / indent %s"deed_ref:" SP deed-filename line-end + / indent %s"languages:" line-end lang-list + / indent %s"build_system:" SP symbol line-end + / indent %s"runtime:" SP symbol line-end + +lang-list = 1*(indent2 "- " symbol line-end) + +invariants-section = %s"invariants:" line-end + *invariant-entry ; May be empty + +invariant-entry = indent "- " %s"id:" SP symbol line-end + indent2 %s"rule:" SP string line-end + indent2 %s"reason:" SP string line-end + [indent2 %s"severity:" SP severity line-end] + +severity = %s"critical" / %s"high" / %s"moderate" + +; --- Optional Sections --------------------------------------------- + +build-commands-section = %s"build_commands:" line-end + 1*build-command-entry + +build-command-entry = indent "- " %s"command:" SP string line-end + indent2 %s"purpose:" SP string line-end + +protected-section = %s"protected:" line-end + 1*protected-entry + +protected-entry = indent "- " %s"path:" SP file-path line-end + indent2 %s"reason:" SP string line-end + +architecture-section = %s"architecture:" line-end + 1*arch-entry + +arch-entry = indent "- " %s"id:" SP symbol line-end + indent2 %s"decision:" SP string line-end + indent2 %s"reason:" SP string line-end + [indent2 %s"alternatives_rejected:" line-end alt-list] + +alt-list = 1*(indent3 "- " string line-end) + +innervation-section = %s"innervation:" line-end + 1*innervation-entry ; >=1 required + +innervation-entry = indent %s"afferent:" line-end afferent-list + / indent %s"efferent:" line-end efferent-list + / indent %s"reflex:" line-end reflex-list + +; Afferent: a signal this repo emits, and who consumes it. +afferent-list = 1*(indent2 "- " %s"signal:" SP string line-end + indent3 %s"description:" SP string line-end + indent3 %s"triggers_on:" SP string line-end + indent3 %s"consumers:" line-end consumer-list) + +consumer-list = 1*(indent4 "- " symbol line-end) + +; Efferent: a signal this repo responds to, and whether it fixes itself. +efferent-list = 1*(indent2 "- " %s"signal:" SP string line-end + indent3 %s"action:" SP string line-end + indent3 %s"auto_fix:" SP boolean line-end) + +; Reflex: a local guard that needs no central processing. +reflex-list = 1*(indent2 "- " %s"trigger:" SP string line-end + indent3 %s"guard:" SP string line-end + indent3 %s"action:" SP string line-end) + +boolean = %s"true" / %s"false" + +do-not-create-section = %s"do_not_create:" line-end + 1*dnc-entry + +dnc-entry = indent "- " %s"pattern:" SP glob-pattern line-end + indent2 %s"reason:" SP string line-end + / indent "- " %s"description:" SP string line-end + indent2 %s"reason:" SP string line-end + +terminology-section = %s"terminology:" line-end + 1*term-entry + +term-entry = indent "- " %s"correct:" SP symbol line-end + indent2 %s"incorrect:" line-end incorrect-list + [indent2 %s"context:" SP string line-end] + +incorrect-list = 1*(indent3 "- " symbol line-end) + +ecosystem-section = %s"ecosystem:" line-end + 1*ecosystem-entry ; >=1 required + +ecosystem-entry = indent %s"depends_on:" line-end dep-list + / indent %s"consumed_by:" line-end dep-list + / indent %s"related:" line-end dep-list + +dep-list = 1*(indent2 "- " %s"name:" SP symbol line-end + indent3 %s"role:" SP string line-end) + +coordination-section = %s"coordination:" line-end + 1*coordination-entry ; >=1 required + +coordination-entry = indent %s"active:" line-end active-list + / indent %s"completed:" line-end completed-list + / indent %s"available:" line-end available-list + +active-list = 1*(indent2 "- " %s"agent:" SP symbol line-end + indent3 %s"area:" SP string line-end + indent3 %s"started:" SP date line-end + indent3 %s"status:" SP status line-end + [indent3 %s"blocker:" SP string line-end]) + +completed-list = 1*(indent2 "- " %s"agent:" SP symbol line-end + indent3 %s"area:" SP string line-end + indent3 %s"completed:" SP date line-end) + +available-list = 1*(indent2 "- " string line-end) + +status = %s"in-progress" / %s"blocked" / %s"paused" + +; --- Primitives ---------------------------------------------------- + +indent = " " ; Exactly 2 spaces (not tabs) +indent2 = indent indent ; Exactly 4 spaces +indent3 = indent indent indent ; Exactly 6 spaces +indent4 = indent3 indent ; Exactly 8 spaces + +value = string / symbol / bare-version +bare-version = 1*DIGIT *("." 1*DIGIT) ; e.g., 1.0.0, 2.3, 42 + +string = DQUOTE *string-char DQUOTE +string-char = %x20-21 / %x23-5B / %x5D-7E + / UTF8-2 / UTF8-3 / UTF8-4 + / escape +escape = "\" (DQUOTE / "\" / %s"n" / %s"t") + +symbol = ALPHA *(ALPHA / DIGIT / "-" / "_" / ".") +spdx-id = 1*(ALPHA / DIGIT / "-" / ".") +repo-path = 1*(ALPHA / DIGIT / "-" / "_" / "/") +deed-filename = 1*(ALPHA / DIGIT / "-" / "_" / ".") %s".deed" +file-path = 1*(ALPHA / DIGIT / "-" / "_" / "/" / ".") +glob-pattern = 1*(ALPHA / DIGIT / "-" / "_" / "/" / "." + / "*" / "?" / "[" / "]" / "{" / "}") + +date = 4DIGIT "-" 2DIGIT "-" 2DIGIT ; ISO 8601 calendar date + / iso8601-datetime ; or full RFC 3339 datetime + +iso8601-datetime = 4DIGIT "-" 2DIGIT "-" 2DIGIT "T" + 2DIGIT ":" 2DIGIT ":" 2DIGIT + ["." 1*DIGIT] + ("Z" / ("+" / "-") 2DIGIT ":" 2DIGIT) + +; --- UTF-8 Support (RFC 3629) ------------------------------------- + +UTF8-2 = %xC2-DF UTF8-tail +UTF8-3 = %xE0 %xA0-BF UTF8-tail + / %xE1-EC 2UTF8-tail + / %xED %x80-9F UTF8-tail + / %xEE-EF 2UTF8-tail +UTF8-4 = %xF0 %x90-BF 2UTF8-tail + / %xF1-F3 3UTF8-tail + / %xF4 %x80-8F 2UTF8-tail +UTF8-tail = %x80-BF + +text-char = VCHAR / SP / UTF8-2 / UTF8-3 / UTF8-4 + +line-end = CRLF / LF + +; --- Notes --------------------------------------------------------- +; 1. Indentation is EXACTLY 2 spaces per level. Tabs are INVALID. +; 2. The magic number "K9!" MUST be the first line (no leading ws). +; 3. SPDX header MUST appear before the first section. +; 4. metadata, project, and invariants sections are REQUIRED. +; 5. Optional sections appear in fixed order; each at most once. +; 6. deed_ref links to a .deed file for typed identity. +; 7. Line endings may be CRLF or LF (mixing is permitted). +; 8. Blank lines and full-line "#" comments are permitted between +; sections, not within them. +; 9. Strings support escape sequences: \" \\ \n \t +; 10. Bare version numbers (e.g., 1.0.0) are valid unquoted values. diff --git a/AGENTS.adoc b/AGENTS.adoc index d3a4a032a..29e127a9e 100644 --- a/AGENTS.adoc +++ b/AGENTS.adoc @@ -22,7 +22,7 @@ A2ML is retired (D99, D269c, D312, D313). Never create, restore or extend a `+.a2ml+` file. Repo metadata goes in `+_chora.deed+` clauses (grammar `+1-formats/deed/spec/abnf/deed.abnf+`), contracts in `+*.k9.ncl+`, and agent coordination in `+coordination.k9+` (grammar -`+2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf+`). +`+2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf+`). `+1-formats/deed/mappings/+` says where former A2ML content goes. Run the relevant `+just+`/`+must+` targets, deed validation diff --git a/README.adoc b/README.adoc index 4288e09f1..a524de377 100644 --- a/README.adoc +++ b/README.adoc @@ -118,8 +118,8 @@ normative grammar in this repository. |link:1-formats/k9/spec/K9-CONTRACT-SPEC.adoc[K9 contract spec]. Check with `1-formats/k9/tools/k9-validate.sh --strict FILE`. |`coordination.k9`, at the repo root -|Agent coordination: first line `K9!`, `schema_version: 1.1.1` -|link:2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`] and link:2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc[COORDINATION-K9-SPEC]. No validator script exists yet. +|Agent coordination: first line `K9!`, `schema_version: 1.2.0` +|link:2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`] and link:2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc[COORDINATION-K9-SPEC]. No validator script exists yet. |=== The worked model is `rsr-template-repo_chora.deed` in diff --git a/REGISTRY.adoc b/REGISTRY.adoc index 749e9f613..54073faad 100644 --- a/REGISTRY.adoc +++ b/REGISTRY.adoc @@ -16,7 +16,7 @@ The registry was a generated A2ML file, and A2ML is retired (owner rulings D99, D269c, D312 and D313). Descriptive metadata now lives in deed clauses (grammar: link:1-formats/deed/spec/abnf/deed.abnf[`deed.abnf`]), k9 contracts and the k9 coordination protocol (grammar: -link:2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`]). +link:2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`]). A generator that kept writing A2ML, and a required check that failed whenever that output drifted, kept a retired format alive. They went together. diff --git a/coordination.k9 b/coordination.k9 index 571e54b14..2b3d318a7 100644 --- a/coordination.k9 +++ b/coordination.k9 @@ -7,79 +7,83 @@ K9! # and guards (reflex). It is also the reference implementation of the # coordination.k9 format — this repo defines the format AND uses it. # +# Conforms to 2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf. +# Repository facts that are not agent coordination were moved out 2026-10-13: +# - the VeriSimDB port (8097) is set in .verisimdb/config.toml; +# - the CRG grade (B, self-declared) belongs in a root READINESS.md, per +# component-readiness-grades/COMPONENT-READINESS-GRADES.adoc, which this +# repo does not have yet. A2ML is retired, so neither went there. +# # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) ---- metadata: schema: k9-coordination - schema_version: 1.0.0 + schema_version: 1.2.0 generated_by: manual - last_updated: 2026-04-04 + last_updated: 2026-10-13 project: name: "standards" description: "Constitutional authority for 290+ hyperpolymath repos. Defines CRG grades, RSR compliance, A2ML formats, K9 components, contractiles, protocol specs (Groove, AVOW, AXEL, Consent-Aware HTTP), and the innervation architecture that binds them." license: MPL-2.0 - repo: "hyperpolymath/standards" + repo: hyperpolymath/standards languages: - Rust - Zig - - ReScript + - AffineScript - Idris2 - - Agda - JavaScript - - Elixir - Nickel + - Julia build_system: just - crg_grade: B - verisimdb_port: 8097 build_commands: - just build: "Build all sub-project artefacts (Rust, Zig)" - just test: "Run all 5 sub-project test suites (158+ tests)" - just lint: "Lint Rust (clippy) and Deno sub-projects" - just fmt: "Format Rust and Deno source" - just clean: "Clean build artefacts" - just assail: "Run panic-attack pre-commit scan" - just v1-audit: "Run release pre-flight audit" - just doctor: "Self-diagnostic" + - command: "just build" + purpose: "Build the in-tree sub-project artefacts" + - command: "just test" + purpose: "Run the repository test suite (scripts/run-shell-test-suite.sh)" + - command: "just lint" + purpose: "Run the lint steps" + - command: "just fmt" + purpose: "Format sources" + - command: "just clean" + purpose: "Clean build artefacts" + - command: "just assail" + purpose: "Run panic-attack pre-commit scan" + - command: "just v1-audit" + purpose: "Run release pre-flight audit" + - command: "just doctor" + purpose: "Self-diagnostic" invariants: - id: a2ml-in-machine-readable rule: "A2ML files (STATE, META, ECOSYSTEM, AGENTIC, NEUROSYM, PLAYBOOK) MUST be in .machine_readable/ only — NEVER in root" reason: "Core structural invariant. Root-level A2ML files are out-of-date duplicates." severity: critical - - id: no-banned-languages - rule: "No TypeScript, Python, Go, Java, Kotlin, Swift, npm, Bun in any sub-project" + rule: "No TypeScript, ReScript, Python, Go, Java, Kotlin, Swift, Deno or yarn in any sub-project; Bun is the JS runtime and package manager" reason: "Language policy is the first standard this repo defines. Violating it here destroys credibility." severity: critical - - id: specs-are-authoritative rule: "Specification documents in spec/ directories are the canonical truth. Implementations must conform to specs, not vice versa." reason: "Standards repo is spec-first. If code disagrees with spec, the code is wrong." severity: critical - - id: self-dogfooding rule: "This repo MUST use every standard it defines: CRG grades, RSR compliance, K9 coordination, Hypatia scanning, A2ML metadata" reason: "A constitution that exempts itself from its own laws has no authority." severity: critical - - id: ietf-draft-names-frozen rule: "IETF Internet-Draft names (draft-jewell-*) must NOT be renamed even if the project name changes" reason: "Draft names are formally registered identifiers. Renaming them breaks citation chains." severity: critical - - id: pmpl-license-only rule: "All original code uses PMPL-1.0-or-later. MPL-2.0 fallback only where platform requires OSI approval." reason: "License consistency across the ecosystem." severity: high - - id: no-postulate-inflation rule: "Do not add new postulates/axioms without proof that they cannot be constructively proven" reason: "Postulates are technical debt in formal systems. Each one weakens the trust chain." severity: high - - id: sha-pinned-actions rule: "All GitHub Actions must be SHA-pinned, not version-tagged" reason: "Version tags are mutable. SHA pins are immutable. This is a security invariant." @@ -92,8 +96,6 @@ protected: reason: "RESIDUAL — only actions-allowlist/ remains here. It is a live published interface: allowed-actions.json is fetched by raw URL from allowlist-preflight-reusable.yml and governance-reusable.yml, so moving it breaks consumers outside this repo. Needs its own deprecation/redirect plan." - path: 9-archive/ reason: "FROZEN ARCHIVE — the LAST COPY of dead upstream trees (rhodium-standard-repositories and its satellites). Not maintained, not to be edited for conformance: a change made here to satisfy a gate would falsify the historical record. Exempt from K9 validation and the debt ledger by .githooks/validate-k9.sh." - - path: a2ml/ - reason: "A2ML format specification and reference implementation" - path: 1-formats/k9/ reason: "K9 self-validating component specification" - path: 2-protocols/k9-coordination/ @@ -104,8 +106,6 @@ protected: reason: "AVOW spam prevention protocol" - path: 2-protocols/axel/ reason: "AXEL age-gating protocol" - - path: lol/ - reason: "LOL multilingual corpus — active, production-grade" - path: 2-protocols/0-ai-gatekeeper/ reason: "AI manifest enforcement system" - path: .machine_readable/ @@ -121,15 +121,12 @@ architecture: - "Keep all 6 A2ML files per repo (too much overhead)" - "Single monolithic metadata file (loses semantic separation)" - "Pure database with no repo-local files (loses git auditability)" - - id: specs-then-implementations decision: "This repo contains both specifications and reference implementations" reason: "The 'standards only, no code' non-goal was superseded. Specs without implementations are untestable." - - id: monorepo-with-absorbed-satellites decision: "All 7 A2ML format specs are absorbed into this monorepo (since 2026-02-08)" reason: "Eliminates cross-repo synchronisation overhead. Single PR for spec changes." - - id: k9-as-universal-receptor decision: "coordination.k9 replaces per-repo AGENTIC.a2ml + NEUROSYM.a2ml + parts of META.a2ml" reason: "K9 already has Must/Trust/Dust/Intent semantics. Extending it to carry agent constraints is natural." @@ -139,70 +136,67 @@ architecture: # ───────────────────────────────────────────────────────────── innervation: - # Afferent: what this repo emits to the nervous system afferent: - signal: "spec.updated" description: "A specification document changed" triggers_on: "push to spec/ directories" - consumers: ["hypatia", "verisimdb", "panll"] - + consumers: + - hypatia + - verisimdb + - panll - signal: "proof.changed" description: "An Agda or Idris2 proof file was modified" triggers_on: "push to **/proofs/ or **/src/abi/" - consumers: ["echidna", "verisimdb"] - + consumers: + - echidna + - verisimdb - signal: "crg.grade.assessed" description: "A CRG grade was assigned or changed" triggers_on: "change to component-readiness-grades/" - consumers: ["verisimdb", "panll"] - + consumers: + - verisimdb + - panll - signal: "test.results" description: "Test suite results from just test" triggers_on: "CI completion" - consumers: ["verisimdb", "hypatia"] - - # Efferent: what this repo responds to + consumers: + - verisimdb + - hypatia efferent: - signal: "hypatia.finding" action: "Log finding, update compliance scan octad in VeriSimDB" auto_fix: false - - signal: "echidna.proof.stale" action: "Re-verify proof, update proof-status octad" auto_fix: true - - signal: "ecosystem.link.stale" action: "Re-derive from imports, update Graph modality" auto_fix: true - - # Reflex: local guards that don't need central processing reflex: - trigger: "pre-commit" guard: "panic-attack assail" action: "Block commit if critical findings" - - trigger: "file-create" guard: "No A2ML files in root directory" action: "Reject and suggest .machine_readable/" - - trigger: "file-create" - guard: "No banned language files (*.ts, *.py, *.go, *.java, *.kt)" + guard: "No banned language files (*.ts, *.res, *.py, *.go, *.java, *.kt)" action: "Reject with language policy link" do_not_create: - - pattern: "**/*.ts" - reason: "TypeScript is banned — use ReScript" - - pattern: "**/*.py" + - pattern: **/*.ts + reason: "TypeScript is banned — use AffineScript" + - pattern: **/*.py reason: "Python is banned" - - pattern: "**/*.go" + - pattern: **/*.go reason: "Go is banned — use Rust" - - pattern: "Dockerfile" + - pattern: Dockerfile reason: "Use Containerfile (Podman, not Docker)" - - pattern: "Makefile" + - pattern: Makefile reason: "Use Justfile/Mustfile" - - pattern: "STATE.a2ml" + - pattern: STATE.a2ml reason: "A2ML files go in .machine_readable/ only" - - pattern: "META.a2ml" + - pattern: META.a2ml reason: "A2ML files go in .machine_readable/ only" terminology: @@ -213,18 +207,15 @@ terminology: context: "The metadata format was renamed from SCM to A2ML. All references must use the new name." - correct: coordination.k9 incorrect: - - config file - - settings file - context: "The per-repo AI coordination file is coordination.k9 — it is a nerve receptor, not a config." + - config-file + - settings-file + context: "The per-repo AI coordination file is coordination.k9 — it is a nerve receptor, not a config file or a settings file." - correct: innervation incorrect: - - integration layer + - integration-layer - middleware - - glue code - context: "The standards architecture is innervation — the nervous system runs through everything, not on top." - -ports: - verisimdb: 8097 + - glue-code + context: "The standards architecture is innervation — the nervous system runs through everything, not on top. Not an integration layer, middleware or glue code." ecosystem: depends_on: @@ -237,8 +228,8 @@ ecosystem: - name: panic-attack role: "Pre-commit security scanning" consumed_by: - - name: "290+ hyperpolymath repos" - role: "Follow RSR, CRG, A2ML, K9, language policy, and other standards defined here" + - name: hyperpolymath-estate + role: "The 290+ hyperpolymath repos: follow RSR, CRG, A2ML, K9, language policy, and other standards defined here" - name: panll role: "Visualises standards compliance via CRG Dashboard, Compliance Monitor, Proof Hub panels" - name: gitbot-fleet diff --git a/scripts/tests/signed-push-test.sh b/scripts/tests/signed-push-test.sh new file mode 100755 index 000000000..212ae792a --- /dev/null +++ b/scripts/tests/signed-push-test.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# signed-push-test.sh — fixture suite for .github/actions/signed-push/push-verified.sh. +# +# A stub `gh` stands in for GitHub: it applies each createCommitOnBranch payload +# to a local bare "remote", refusing it when expectedHeadOid is not the branch +# head, exactly as the real mutation does. The suite checks that local commits +# arrive in order with their messages and byte-identical contents (binary +# included), that deletions and renames survive, and that every refusal +# (divergence, a moved head, merges, modes, symlinks) happens before anything +# is pushed or is reported as a partial push. +# +# Run: bash scripts/tests/signed-push-test.sh +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET="${TARGET:-$SCRIPT_DIR/../../.github/actions/signed-push/push-verified.sh}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +pass=0; fail=0 + +# ok: record and print a passing assertion. +ok() { echo "PASS: $1"; pass=$((pass + 1)); } +# bad: record and print a failing assertion. +bad() { echo "FAIL: $1"; fail=$((fail + 1)); } + +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid +export GITHUB_REPOSITORY=o/r + +# The stub: apply a createCommitOnBranch payload to $STUB_REMOTE, print the oid. +cat > "$WORK/gh" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +[ "$1 $2" = "api graphql" ] || { echo "stub: unexpected call: $*" >&2; exit 2; } +payload="$4" +echo x >> "$STUB_CALLS" +in=$(jq -c '.variables.in' "$payload") +branch=$(jq -r '.branch.branchName' <<<"$in") +want=$(jq -r '.expectedHeadOid' <<<"$in") +head=$(git -C "$STUB_REMOTE" rev-parse "refs/heads/$branch") +[ -n "${STUB_FAIL_ON_CALL:-}" ] && [ "$(wc -l < "$STUB_CALLS")" -eq "$STUB_FAIL_ON_CALL" ] && { + echo "stub: simulated outage" >&2; exit 1; } +[ "$want" = "$head" ] || { echo "Expected branch to point to $want but it did not" >&2; exit 1; } +wt=$(mktemp -d); git -C "$STUB_REMOTE" worktree add -q --detach "$wt" "$head" +jq -r '.fileChanges.deletions[].path' <<<"$in" | while IFS= read -r p; do rm -f "$wt/$p"; done +n=$(jq '.fileChanges.additions | length' <<<"$in") +for i in $(seq 0 $((n - 1))); do + p=$(jq -r ".fileChanges.additions[$i].path" <<<"$in"); mkdir -p "$wt/$(dirname "$p")" + jq -r ".fileChanges.additions[$i].contents" <<<"$in" | base64 -d > "$wt/$p" +done +msg=$(jq -r '.message.headline' <<<"$in"); body=$(jq -r '.message.body' <<<"$in") +git -C "$wt" add -A +git -C "$wt" -c user.name=github -c user.email=noreply@github.com commit -q --allow-empty -m "$msg" ${body:+-m "$body"} +new=$(git -C "$wt" rev-parse HEAD) +git -C "$STUB_REMOTE" update-ref "refs/heads/$branch" "$new" "$head" +git -C "$STUB_REMOTE" worktree remove --force "$wt" +echo "$new" +STUB +chmod +x "$WORK/gh" +export GH_BIN="$WORK/gh" + +# fresh: a bare remote with branch b, and a clone with b checked out. +fresh() { + rm -rf "$WORK/remote.git" "$WORK/clone"; : > "$WORK/calls" + git init -q --bare -b main "$WORK/remote.git" + git clone -q "$WORK/remote.git" "$WORK/clone" 2>/dev/null + ( cd "$WORK/clone" && printf 'keep\n' > keep.txt && printf 'old\n' > gone.txt && printf 'move me\n' > from.txt \ + && git add -A && git commit -q -m base && git push -q origin HEAD:refs/heads/b 2>/dev/null && git checkout -q -b b origin/b 2>/dev/null ) + export STUB_REMOTE="$WORK/remote.git" STUB_CALLS="$WORK/calls" +} +# calls: how many mutations the stub has received since the last fresh. +calls() { wc -l < "$WORK/calls" | tr -d " "; } +# run: push branch b from the clone, capturing all output in $WORK/out. +run() { ( cd "$WORK/clone" && bash "$TARGET" b ) > "$WORK/out" 2>&1; } + +# 1. The planted positive: three commits, every change shape. +fresh +( cd "$WORK/clone" || exit 1 + printf 'hello\n' > keep.txt && head -c 300 /dev/urandom > bin.dat && git add -A && git commit -q -m "one: text and binary" -m "a body line" + git rm -q gone.txt && git commit -q -m "two: delete" + git mv from.txt sub/to.txt 2>/dev/null || { mkdir sub; git mv from.txt sub/to.txt; } + git commit -q -m "three: rename" ) +LOCAL_TREE=$(git -C "$WORK/clone" rev-parse 'b^{tree}') +run; st=$? +[ "$st" = 0 ] && ok "three commits pushed" || bad "three commits pushed — $(cat "$WORK/out")" +[ "$(calls)" = 3 ] && ok "one mutation per commit" || bad "one mutation per commit (got $(calls))" +[ "$(git -C "$WORK/remote.git" rev-parse 'b^{tree}')" = "$LOCAL_TREE" ] && ok "remote tree identical, binary included" || bad "remote tree identical" +[ "$(git -C "$WORK/remote.git" log --format=%s b -3 | tr '\n' '|')" = "three: rename|two: delete|one: text and binary|" ] \ + && ok "messages kept, in order" || bad "messages kept, in order" +git -C "$WORK/remote.git" log --format=%b -1 'b~2' | grep -qx "a body line" && ok "commit body kept" || bad "commit body kept" +[ "$(git -C "$WORK/clone" rev-parse b)" = "$(git -C "$WORK/remote.git" rev-parse b)" ] && ok "local branch reset to the remote" || bad "local branch reset to the remote" +run && grep -q "nothing to push" "$WORK/out" && [ "$(calls)" = 3 ] && ok "second run pushes nothing" || bad "second run pushes nothing" + +# 2. Diverged remote: refused before any mutation. +fresh +( cd "$WORK/clone" && printf 'x\n' > a.txt && git add a.txt && git commit -q -m local ) +( d=$(mktemp -d) && git clone -q -b b "$WORK/remote.git" "$d" 2>/dev/null && cd "$d" && printf 'y\n' > other.txt && git add other.txt && git commit -q -m elsewhere && git push -q origin b 2>/dev/null ) +run; st=$? +[ "$st" != 0 ] && grep -q "has commits that b lacks" "$WORK/out" && [ "$(calls)" = 0 ] && ok "diverged remote refused, nothing sent" || bad "diverged remote refused — $(cat "$WORK/out")" + +# 3. The head moves mid-push: the second mutation is refused, reported as partial. +fresh +( cd "$WORK/clone" && for i in 1 2; do printf '%s\n' "$i" > n.txt && git add n.txt && git commit -q -m "c$i"; done ) +STUB_FAIL_ON_CALL=2 run; st=$? +[ "$st" != 0 ] && grep -q "PARTIAL PUSH: 1 of 2" "$WORK/out" && ok "failure after one commit reported as a partial push" || bad "partial push reported — $(cat "$WORK/out")" +unset STUB_FAIL_ON_CALL + +# 4. Shapes createCommitOnBranch cannot express are refused before any mutation. +for shape in exec symlink modechange merge; do + fresh + ( cd "$WORK/clone" || exit 1 + case $shape in + exec) printf '#!/bin/sh\n' > run.sh && chmod +x run.sh && git add run.sh && git commit -q -m exec ;; + symlink) ln -s keep.txt link && git add link && git commit -q -m symlink ;; + modechange) chmod +x keep.txt && git add keep.txt && git commit -q -m mode ;; + merge) git checkout -q -b side && printf 's\n' > s.txt && git add s.txt && git commit -q -m side \ + && git checkout -q b && printf 't\n' > t.txt && git add t.txt && git commit -q -m main-side \ + && git merge -q --no-edit side >/dev/null ;; + esac ) + run; st=$? + [ "$st" != 0 ] && [ "$(calls)" = 0 ] && [ "$(git -C "$WORK/remote.git" rev-parse b)" = "$(git -C "$WORK/clone" rev-parse origin/b)" ] \ + && ok "$shape refused before anything is pushed" || bad "$shape refused — $(cat "$WORK/out")" +done + +echo "signed-push-test: $pass passed, $fail failed" +[ "$fail" -eq 0 ]