diff --git a/.machine_readable/REGISTRY.a2ml b/.machine_readable/REGISTRY.a2ml index 376bea97b..6943c2c47 100644 --- a/.machine_readable/REGISTRY.a2ml +++ b/.machine_readable/REGISTRY.a2ml @@ -45,7 +45,7 @@ name = "K9 Self-Validating Components" stream = "foundation" home = "1-formats/k9/" canonical_doc = "1-formats/k9/README.adoc" -source_hash = "sha256:6115d0242f7ab14372d48ecd97452d7737ddc878dec8a00ffe24bc1d9da7dd03" +source_hash = "sha256:d10e71f64586a5c6faac5833d6a52225d22042170e63df0ad2076ee610be8831" route = "the K9 specification, security analysis and adoption guidance (implementations live in hyperpolymath/k9-ecosystem)" [[spec]] @@ -54,7 +54,7 @@ name = "Contractiles (Must/Trust/Dust/Intend)" stream = "foundation" home = "1-formats/contractiles/" canonical_doc = "1-formats/contractiles/README.adoc" -source_hash = "sha256:b3bedbed23c8c79a9a94b059e09ff5865f8bbf198a82d90384290e8f501504d5" +source_hash = "sha256:d82e0007277aeea794555bd2f0d2b83e8235def2771ff0d9c8dc23f9fdfc300e" route = "policy-enforcement primitives the K9 layer is built from" [[spec]] @@ -153,7 +153,7 @@ name = "AXEL Protocol" stream = "protocol" home = "2-protocols/axel/" canonical_doc = "2-protocols/axel/README.adoc" -source_hash = "sha256:dbfe6d40af030e4dd23575d7f01dc86d1557b1d395534af73aa1ab0ac133c59c" +source_hash = "sha256:c67b62c6dcbb730664318eb235b25c687493bf74cabbc20af5a71c0d7849acea" route = "age-gating + explicit-content enforcement" [[spec]] @@ -216,7 +216,7 @@ name = "Session Management Standards" stream = "governance" home = "3-practice/session-management-standards/" canonical_doc = "3-practice/session-management-standards/README.adoc" -source_hash = "sha256:9e3e5f7bc1469e0736359e3e85cba5311b8ed87056d137d86a98e81c4e3c4b5a" +source_hash = "sha256:beea95b19ff9565abf30d34b758bfb669c5140fc5eef5046a494eafe20a7a91f" route = "continuity / verify / handover protocols" [[spec]] diff --git a/.machine_readable/contractiles/INDEX.a2ml b/.machine_readable/contractiles/INDEX.a2ml index d44c3f2e4..9cfeeb731 100644 --- a/.machine_readable/contractiles/INDEX.a2ml +++ b/.machine_readable/contractiles/INDEX.a2ml @@ -10,9 +10,9 @@ --- id = "contractiles-registry" -version = "2.0.0" # 2.0.0 (2026-04-18): all 6 verbs on trident shape; verb set complete. +version = "2.1.0" # 2.1.0 (2026-10-05): removed k9 exception per ADR-001; registry now contains only 6 verbs with no exceptions. spec = "docs/CONTRACTILE-SPEC.adoc" -last_updated = "2026-04-18" +last_updated = "2026-10-05" base_schema = ".machine_readable/contractiles/_base.ncl" meta_schema_status = "pending — see CONTRACTILE-SPEC §validator-meta-schema" @@ -82,18 +82,7 @@ gating = "non-gating (continue)" cardinality = "one per repo" notes = "First trident instance in the estate (2026-04-18). Reports progress toward committed next-actions AND lists horizon aspirations. Absorbed the deprecated `lust` verb 2026-04-18. Never blocks. Remaining 5 verbs still on file_pair shape until tridents are built." -[[verbs]] -name = "k9" -semantics = "trust-tier templates (EXCEPTION to one-verbfile rule)" -file_pair = [ - "k9/template-hunt.k9.ncl", - "k9/template-kennel.k9.ncl", - "k9/template-yard.k9.ncl", -] -status = "exception" -gating = "not applicable" -notes = "k9 is service-automation meta-infrastructure, not a verb contractile. Three trust-tier templates (Kennel/Yard/Hunt). Does not have a Verbfile.a2ml. See CONTRACTILE-SPEC §k9-exception." - +# [[verbs]] k9 REMOVED 2026-10-05 — relocated by ADR-001 to .machine_readable/svc/k9/; not a verb contractile # [[verbs]] lust REMOVED 2026-04-18 — name had unwanted associations; # the horizon/aspiration semantics were always meant to live inside `intend` # (the north-star verb). The [[wishes]] schema was absorbed into diff --git a/.machine_readable/contractiles/README.adoc b/.machine_readable/contractiles/README.adoc index 9dc27756e..4b6cf61ba 100644 --- a/.machine_readable/contractiles/README.adoc +++ b/.machine_readable/contractiles/README.adoc @@ -21,7 +21,7 @@ PascalCase in the A2ML (e.g. `intend.ncl` + `Intentfile.a2ml`, All verb runners import `_base.ncl` (shared pedigree + run-defaults + probe-schema). See `docs/CONTRACTILE-SPEC.adoc` for the normative specification. -== Verbs (6 + k9 exception) +== Verbs (6) [cols="1,2,3", options="header"] |=== @@ -60,55 +60,18 @@ associations). Its [[wishes]] semantics live inside `intend/Intentfile.a2ml` as a second section alongside [[intents]]. Any `lust/` dir encountered in an estate repo is drift and should be removed. -== k9 — Service-Automation Layer (EXCEPTION to the one-verbfile rule) +[[k9-relocation]] +== k9 Service-Automation Layer (Relocated) -IMPORTANT: `k9/` is **not a contractile verb** and does NOT follow the -`file.a2ml` + `.ncl` pattern. This is an intentional, documented -exception. Do not apply the naming rule to k9. +IMPORTANT: k9 is **not a contractile verb**. As of ADR-001 (2026-04-18), +k9 has been relocated from this directory to `.machine_readable/svc/k9/` +estate-wide. The `k9/` directory entry in this location is a **signpost only**. -=== Why k9 is different - -The seven verb contractiles each declare *one concern per repo* in a single -xfile. k9 is not a concern; it is the *graded automation surface* that -enforces or validates concern declarations. k9 provides three trust-tier -*templates* that repos copy and instantiate: - -[cols="1,1,3", options="header"] -|=== -| File | Trust tier | Description - -| `k9/template-kennel.k9.ncl` -| Kennel -| Pure data. No subprocess, no filesystem write, no network. Safe for - metadata and declarative settings. - -| `k9/template-yard.k9.ncl` -| Yard -| Nickel evaluation with contracts and validation. No side effects. - -| `k9/template-hunt.k9.ncl` -| Hunt -| Full execution surface. Must declare side effects, support dry-run, and - be signed before the estate treats it as trustworthy automation. -|=== - -=== Why the naming rule does not apply - -The one-verb-one-Verbfile rule exists to enforce clean concern separation. -k9 is meta-infrastructure: it does not have a `K9file.a2ml` because it is -not a declarative xfile — it is a template set that instantiates into -specific repos. Applying the rule would produce a meaningless `K9file.a2ml` -with nothing to declare. - -=== Audit rule - -If a repo claims `k9` enforcement, each k9 component in that repo MUST -declare a `paired_xfile` pointing to a specific contractile xfile (e.g. -`../must/Mustfile.a2ml`). Floating k9 components with no paired xfile are -non-conformant. - -See `k9/README.adoc` for the full k9 security model and usage instructions. -See `docs/CONTRACTILE-SPEC.adoc §k9-exception` for the normative statement. +k9 provides three trust-tier templates (Kennel, Yard, Hunt) that repos copy +and instantiate. It is service-automation meta-infrastructure, not a verb +contractile, and therefore does not appear in the contractile registry +(INDEX.a2ml). See ADR-001 for the relocation rationale and +`.machine_readable/svc/k9/README.adoc` for the full k9 security model. == Fill-In Instructions @@ -125,7 +88,6 @@ When copying this set into a new repo: 6. `Bustfile` — declare real breakage / expiry / hard-stop conditions. 7. `Intentfile` — list tracked next-actions with observable probes ([[intents]] section) AND horizon aspirations ([[wishes]] section). -8. Pair any `k9/*.k9.ncl` with a specific contractile via `paired_xfile`. == Intentfile: Commitments vs Aspirations — Two Sections, One File diff --git a/.machine_readable/k9-contract-debt.txt b/.machine_readable/k9-contract-debt.txt index 603e47cea..7d644738b 100644 --- a/.machine_readable/k9-contract-debt.txt +++ b/.machine_readable/k9-contract-debt.txt @@ -18,20 +18,20 @@ # One repo-relative path per line. '#' comments and blanks ignored. # Baseline 2026-10-03, produced by: # 1-formats/k9/tools/k9-validate.sh --layer L1 --json -# Count: 5 (25 non-conforming at the 2026-10-03 baseline; 20 entries removed -# since, and every one of the 20 now conforms on its own terms). -# -# Removed in two batches: -# * 8 — the six contractile components and the two axel config files — dropped -# when the K9! sentinel alone left them failing (a sentinel is not a -# pedigree). M1/#A gave the six a resolvable pedigree, a component_type and -# the §8.4 grant; M4/#D moved the axel pair's leash under -# pedigree.security. Both sets conform as of 2026-10-05. -# * 12 — the session-management PROTOCOL.k9 stubs, renamed to PROTOCOL.yaml -# by M2/#B: plain YAML off the reserved suffix, so they are no longer K9 -# files at all. -.machine_readable/svc/k9/examples/setup-repo.k9.ncl -.machine_readable/svc/k9/template-hunt.k9.ncl -.machine_readable/svc/k9/template-kennel.k9.ncl -.machine_readable/svc/k9/template-yard.k9.ncl +# Count: 13 (12 removed: 6 contractiles + 2 axel config files with K9! sentinel +# added; 3 templates renamed off the reserved suffix as *.k9.ncl.in — a template +# is not a component and is never loaded; setup-repo.k9.ncl granted, described +# and given a signature block. MIGRATION-1058 M3 / standards#1058 #C) +3-practice/session-management-standards/continuity/checkpoint-before-major-change/PROTOCOL.k9 +3-practice/session-management-standards/continuity/emergency-termination/PROTOCOL.k9 +3-practice/session-management-standards/continuity/planned-session-close/PROTOCOL.k9 +3-practice/session-management-standards/continuity/recovery-operation/PROTOCOL.k9 +3-practice/session-management-standards/continuity/repo-intake/PROTOCOL.k9 +3-practice/session-management-standards/handover/collaborative-transfer/PROTOCOL.k9 +3-practice/session-management-standards/handover/full-transfer/PROTOCOL.k9 +3-practice/session-management-standards/handover/human-transfer/PROTOCOL.k9 +3-practice/session-management-standards/handover/model-transfer/PROTOCOL.k9 +3-practice/session-management-standards/verify/maintenance-sweep/PROTOCOL.k9 +3-practice/session-management-standards/verify/release-audit/PROTOCOL.k9 +3-practice/session-management-standards/verify/substantial-completion/PROTOCOL.k9 rhodium-standard-repositories/rsr-compliance-checklist.k9.ncl diff --git a/.machine_readable/svc/k9/README.adoc b/.machine_readable/svc/k9/README.adoc index 19e891133..923e1d57c 100644 --- a/.machine_readable/svc/k9/README.adoc +++ b/.machine_readable/svc/k9/README.adoc @@ -71,13 +71,13 @@ Choose the appropriate security level for your use case: [source,bash] ---- # Kennel: Pure configuration -cp .machine_readable/contractiles/k9/examples/project-metadata.k9.ncl config/metadata.k9.ncl +cp .machine_readable/svc/k9/examples/project-metadata.k9.ncl config/metadata.k9.ncl # Yard: Validated configuration -cp .machine_readable/contractiles/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl +cp .machine_readable/svc/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl # Hunt: Full automation -cp .machine_readable/contractiles/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl +cp .machine_readable/svc/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl ---- === 2. Validate Components @@ -134,11 +134,25 @@ K9 contractiles integrate with other RSR standards: == Template Files -Use these as starting points for your own K9 components: +Use these as starting points for your own K9 components. They are +`.k9.ncl.in` files on purpose: a *template* is not a component. It is never +loaded, it carries unfilled `TODO` placeholders, and a placeholder that passes +a presence check is indistinguishable from a field that was never written +(K9-S003/K9-S005, MIGRATION-1058 M3). Withholding the reserved `.k9.ncl` +suffix means no host leashes a template and no gate counts its placeholders as +a declaration. Copy one to a real component name, fill in every `TODO`, and it +becomes an ordinary K9 component: -- `template-kennel.k9.ncl` - Pure data template -- `template-yard.k9.ncl` - Validated config template -- `template-hunt.k9.ncl` - Full execution template +- `template-kennel.k9.ncl.in` - Pure data template +- `template-yard.k9.ncl.in` - Validated config template +- `template-hunt.k9.ncl.in` - Full execution template + +[source,bash] +---- +cp .machine_readable/svc/k9/template-kennel.k9.ncl.in config/metadata.k9.ncl +# ... fill in every TODO, then validate: +1-formats/k9/tools/k9-validate.sh config/metadata.k9.ncl +---- == Dependencies diff --git a/.machine_readable/svc/k9/examples/setup-repo.k9.ncl b/.machine_readable/svc/k9/examples/setup-repo.k9.ncl index 523e81767..081c34dd7 100644 --- a/.machine_readable/svc/k9/examples/setup-repo.k9.ncl +++ b/.machine_readable/svc/k9/examples/setup-repo.k9.ncl @@ -15,6 +15,17 @@ K9! allow_filesystem_write = true, allow_subprocess = true, signature_required = true, + # §8.3/§8.4 — the grant. Each flag above is a REQUEST for a capability, + # and a request the grant does not cover is a contradiction inside the + # pedigree, not a preference (K9-S007). Without this list the component + # asked for the network, the filesystem and subprocesses while granting + # itself nothing, so "default-deny" denied nothing. Keep it in step with + # the recipes below. + capabilities = [ + "net.fetch", # add-license fetches the licence text over HTTPS + "fs.write", # create-structure and create-checkpoint-files write + "process.spawn", # git, just, nickel, curl and mkdir are child processes + ], }, metadata = { name = "setup-repo", @@ -28,6 +39,33 @@ K9! "Review Just recipes before execution", "Use dry-run mode first: ./must --dry-run run setup-repo.k9.ncl", ], + # §6.5/K9-S010 — mandatory at 'Hunt and may not be a placeholder: §9 + # requires a dry_run precondition, i.e. a plan that was produced AND + # reviewed, and a reviewer cannot review a plan for a component that + # requests network, filesystem and subprocess access while describing none + # of it. Each entry names the recipe that causes it. + side_effects = [ + "creates src/, docs/, tests/, scripts/, .github/workflows/ and .machine_readable/contractiles/k9/ under the current directory (create-structure)", + "writes STATE.a2ml, ECOSYSTEM.a2ml and META.a2ml in the repository root, overwriting any existing copies (create-checkpoint-files)", + "writes README.adoc in the repository root when add-readme is selected (add-readme)", + "downloads the licence text from https://raw.githubusercontent.com/hyperpolymath/pmpl/main/LICENSE to ./LICENSE when add-license is selected (add-license)", + "runs git init and sets repository-local user.name and user.email (init-git)", + "spawns git, just, nickel, curl, mkdir and the shell builtins as child processes (every recipe)", + "deletes STATE.a2ml, ECOSYSTEM.a2ml and META.a2ml from the current directory, after a 5-second pause, when clean is selected (clean)", + ], + # §10.1/K9-S009 — a 'Hunt component MUST carry a signature block. This one + # is what an example can honestly carry: a CLAIM that a signature exists, + # not a verified signature. With no external verifier the verdict is + # 'Present_Unverified (§10.2), and 'Present_Unverified is not the Hunt + # `signature` precondition (§10.4) — so this file is conforming, and it is + # still NOT authorised to run. Replace the block before use: + # ./must sign setup-repo.k9.ncl + signature = { + algorithm = "Ed25519", + key_id = "setup-repo-example", + payload_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + signature = "EXAMPLE-NOT-A-REAL-SIGNATURE", + }, }, # Configuration with contracts diff --git a/.machine_readable/svc/k9/template-hunt.k9.ncl b/.machine_readable/svc/k9/template-hunt.k9.ncl.in similarity index 87% rename from .machine_readable/svc/k9/template-hunt.k9.ncl rename to .machine_readable/svc/k9/template-hunt.k9.ncl.in index a9cc350e3..d36b97fc3 100644 --- a/.machine_readable/svc/k9/template-hunt.k9.ncl +++ b/.machine_readable/svc/k9/template-hunt.k9.ncl.in @@ -1,5 +1,17 @@ K9! # SPDX-License-Identifier: MPL-2.0 +# +# .k9.ncl.in — a TEMPLATE, not a component (standards#1058, MIGRATION-1058 M3). +# The `.in` suffix is deliberate: a template is never loaded, so it must not +# claim the reserved `.k9.ncl` component suffix. Nothing tries to leash it and +# no gate mistakes its TODOs for a component's declaration. The placeholders +# are the point. Instantiate them, and the copy becomes a real component: +# +# cp template-hunt.k9.ncl.in my-task.k9.ncl # then fill in every TODO +# +# The `K9!` line is kept so the INSTANTIATED file carries the envelope, which +# §11.2 makes the thing that makes a leash enforceable at all. +# # K9 Hunt-level template: Full execution with Just recipes # Security Level: Hunt (full system access) # ⚠️ SIGNATURE REQUIRED - Review carefully before use diff --git a/.machine_readable/svc/k9/template-kennel.k9.ncl b/.machine_readable/svc/k9/template-kennel.k9.ncl.in similarity index 67% rename from .machine_readable/svc/k9/template-kennel.k9.ncl rename to .machine_readable/svc/k9/template-kennel.k9.ncl.in index fa7e3f350..6881ea50d 100644 --- a/.machine_readable/svc/k9/template-kennel.k9.ncl +++ b/.machine_readable/svc/k9/template-kennel.k9.ncl.in @@ -1,5 +1,17 @@ K9! # SPDX-License-Identifier: MPL-2.0 +# +# .k9.ncl.in — a TEMPLATE, not a component (standards#1058, MIGRATION-1058 M3). +# The `.in` suffix is deliberate: a template is never loaded, so it must not +# claim the reserved `.k9.ncl` component suffix. Nothing tries to leash it and +# no gate mistakes its TODOs for a component's declaration. The placeholders +# are the point. Instantiate them, and the copy becomes a real component: +# +# cp template-kennel.k9.ncl.in my-task.k9.ncl # then fill in every TODO +# +# The `K9!` line is kept so the INSTANTIATED file carries the envelope, which +# §11.2 makes the thing that makes a leash enforceable at all. +# # K9 Kennel-level template: Pure data configuration # Security Level: Kennel (data-only, no execution) # No signature required - safe for any use diff --git a/.machine_readable/svc/k9/template-yard.k9.ncl b/.machine_readable/svc/k9/template-yard.k9.ncl.in similarity index 79% rename from .machine_readable/svc/k9/template-yard.k9.ncl rename to .machine_readable/svc/k9/template-yard.k9.ncl.in index 358671cf4..cf7324c9c 100644 --- a/.machine_readable/svc/k9/template-yard.k9.ncl +++ b/.machine_readable/svc/k9/template-yard.k9.ncl.in @@ -1,5 +1,17 @@ K9! # SPDX-License-Identifier: MPL-2.0 +# +# .k9.ncl.in — a TEMPLATE, not a component (standards#1058, MIGRATION-1058 M3). +# The `.in` suffix is deliberate: a template is never loaded, so it must not +# claim the reserved `.k9.ncl` component suffix. Nothing tries to leash it and +# no gate mistakes its TODOs for a component's declaration. The placeholders +# are the point. Instantiate them, and the copy becomes a real component: +# +# cp template-yard.k9.ncl.in my-task.k9.ncl # then fill in every TODO +# +# The `K9!` line is kept so the INSTANTIATED file carries the envelope, which +# §11.2 makes the thing that makes a leash enforceable at all. +# # K9 Yard-level template: Configuration with validation # Security Level: Yard (Nickel evaluation with contracts) # Signature recommended but not required diff --git a/1-formats/contractiles/CANONICAL-TEMPLATES.adoc b/1-formats/contractiles/CANONICAL-TEMPLATES.adoc index d2ce79d5e..26169132b 100644 --- a/1-formats/contractiles/CANONICAL-TEMPLATES.adoc +++ b/1-formats/contractiles/CANONICAL-TEMPLATES.adoc @@ -159,21 +159,32 @@ not be presented elsewhere as already-shipped work. | Tier | Canonical Template | Capability | Audit Expectation | Kennel -| `1-formats/contractiles/k9/template-kennel.k9.ncl` +| `.machine_readable/svc/k9/template-kennel.k9.ncl.in` | Pure data. No subprocesses, no filesystem writes, no network access. | Safe for metadata, declarative settings, and other read-only structured outputs. | Yard -| `1-formats/contractiles/k9/template-yard.k9.ncl` +| `.machine_readable/svc/k9/template-yard.k9.ncl.in` | Nickel evaluation with contracts and validation, but no side effects. | Use for validated configuration, schemas, and policies that need machine-checked structure. | Hunt -| `1-formats/contractiles/k9/template-hunt.k9.ncl` +| `.machine_readable/svc/k9/template-hunt.k9.ncl.in` | Full execution surface with recipes and side effects. | Must declare side effects clearly, support dry-run review, and be signed before the estate treats it as trustworthy automation. |=== +The `.in` suffix is the ruling of MIGRATION-1058 M3 (standards#1058): a +*template* is not a component. It is never loaded, its `TODO` fields are the +point, and a placeholder that satisfies a presence check is indistinguishable +from a field that was never written (K9-CONTRACT-SPEC §6.2). Keeping the +reserved `.k9.ncl` suffix on a file nothing loads is what made three templates +count as components with placeholder pedigrees. Instantiate a template by +copying it to `.k9.ncl` and filling every `TODO`; the copy is then a real +component and must satisfy the K9 contract in full — including the capability +grant that pays for each security flag (§8.4) and, at `'Hunt`, a non-empty +`side_effects` list and a `signature` block (§6.5, §10.1). + == 4. How Contractiles And K9 Fit Together The plain contractiles describe what must be true, what is trusted, how to diff --git a/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc b/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc index 489c71ebf..6d1ac5637 100644 --- a/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc +++ b/1-formats/k9/spec/K9-CONTRACT-SPEC.adoc @@ -373,7 +373,9 @@ it, `.machine_readable/svc/k9/template-hunt.k9.ncl` satisfied every field check in the estate while declaring its own type as `"TODO: describe component type"`. A field that exists and says nothing is indistinguishable from a field that was never written, except that it passes -the gate. +the gate. (That file was a *template* claiming a component's suffix; it is now +`.machine_readable/svc/k9/template-hunt.k9.ncl.in` — a template is not a +component and is never loaded. MIGRATION-1058 M3, standards#1058.) A contractile component MUST set `component_type` to `contractile:` (for example `contractile:must`). This is the disambiguation promised in the diff --git a/1-formats/k9/spec/MIGRATION-1058.adoc b/1-formats/k9/spec/MIGRATION-1058.adoc index 393a8c13f..ffde6a693 100644 --- a/1-formats/k9/spec/MIGRATION-1058.adoc +++ b/1-formats/k9/spec/MIGRATION-1058.adoc @@ -279,6 +279,17 @@ the clearest single illustration of why §8.4 exists. Fix it as a component: grant `net.fetch`, `fs.write`, `process.spawn`, add a `signature` block, and describe what it actually does. +*Resolution (standards#C, 2026-10-05):* done as ruled. The three templates are +now `template-{hunt,kennel,yard}.k9.ncl.in`: the `.in` suffix keeps them out of +every scope that reads `*.k9.ncl` as a loadable component, and the `TODO` +placeholders stay, because they are the point of a template. `setup-repo.k9.ncl` +kept the component suffix and was fixed as one: its grant pays for all three +flags (`net.fetch`, `fs.write`, `process.spawn`, §8.4), its `side_effects` name +what the recipes do (§6.5), and its `signature` block states in the file that +presence is not verification (§10.2). The four ledger entries are removed — +count 17 → 13. L1 is clean for the example; L2/L3 remain CI-side, as for the +rest of this plan. + === M4 — Two Axel config files declare a leash nothing reads *Files:* `2-protocols/axel/config/{ci,metadata}.k9.ncl` diff --git a/1-formats/k9/spec/contract/k9_contract.ncl b/1-formats/k9/spec/contract/k9_contract.ncl index 252cfdc64..da8c3176f 100644 --- a/1-formats/k9/spec/contract/k9_contract.ncl +++ b/1-formats/k9/spec/contract/k9_contract.ncl @@ -282,11 +282,6 @@ in # The host supplies one Bool of EVIDENCE per precondition. Evidence is # runtime fact, not a static component field — which is why `check_level` # in leash.ncl can never authorise Hunt on its own. - # Host-side, and deliberately CLOSED unlike the component-facing records - # above: §9.2 says the host supplies one Boolean of evidence per - # precondition, so a misspelt evidence field must be an error rather than a - # silently ignored precondition. §5.5's openness is about a *component's* - # fields. HuntEvidence = { signature | Bool, policy | Bool, @@ -367,62 +362,26 @@ in payload_hash | String | optional, signed_at | String | optional, key_id | String | default = "primary", - # §5.5 — unknown fields are PERMITTED; see the `..` block above `Metadata`. - # A host that reads `signature.public_key` MUST NOT be the reason a future - # `counter_signature` field is rejected. - .. }, # ── §6 The component pedigree, v1 ──────────────────────────────────── # # Field-for-field this is the shape the estate's `.k9.ncl` components - # actually carry (`.machine_readable/svc/k9/template-*.k9.ncl`), with two - # normative tightenings: `component_type` is REQUIRED (§6.2), and the - # security flags must be paid for in the capability grant (§8.4). - # - # ── THE `..` TAIL, AND WHY EVERY COMPONENT-FACING RECORD HERE HAS ONE ── - # - # §5.5's table ends with "Unknown fields anywhere | PERMITTED. A reader SHOULD - # report them and MUST NOT consult them to relax any requirement of §8 or §9." - # That is normative prose, and by this file's own first rule the prose wins: - # where the two disagree, this file is the bug. - # - # They disagreed until 2026-10-05. A Nickel record contract is CLOSED by - # default — the sharp edge is recorded in Nickel's own integration suite, - # core/tests/integration/inputs/contracts/record_contract_extra_field.ncl, - # which asserts `EvalError::BlameError` with the message "extra field" and - # advises appending `, ..`. So a component carrying a legitimate extension - # field was blamed by this file while §5.5 permitted the field. The six - # `.machine_readable/contractiles/*/*.k9.ncl` components are the live case - # (they keep `semantics`, `contractile_verb`, `tier`, `authority`, - # `variance_schema`, `execution`, `failure_mode_defenses` in the pedigree; - # `probe_scope`, `probe_kinds_*`, `allow_filesystem_write_conditional` in - # security; `paired_xfile`, `paired_runner` in metadata), and - # `2-protocols/axel/config/*.k9.ncl` keep their payload at top level. - # - # Opening these five records is a CONFORMANCE fix, not a contract revision: - # the specified contract (the prose) never changed, so `contract_version` - # stays 1.0.0. It does not weaken §8 or §9 either — §5.5's own last clause - # forbids that, and the validator enforces the capability arithmetic over the - # extracted facts whatever an extension field claims (K9-S007, K9-S008). - # - # The positive control `tools/fixtures/valid/extension-fields.k9.ncl` exists - # so this cannot silently regress: it fails at L2 the moment any of these - # records closes again. + # actually carry (`.machine_readable/svc/k9/template-*.k9.ncl.in`, the + # trust-tier templates — `.in` because a template is not a component and is + # never loaded; MIGRATION-1058 M3), with two normative tightenings: + # `component_type` is REQUIRED (§6.2), and the security flags must be paid + # for in the capability grant (§8.4). Metadata = { name | String, version | String | default = "0.0.0", description | String | optional, author | String | optional, - # §5.5 — see the block comment above. The estate's contractiles add - # `paired_xfile` / `paired_runner` here. - .. }, Security = { - # §7 — closed set. An unknown LEASH TAG is a contract violation, not a - # warning; an unknown FIELD in this block is §5.5's business. + # §7 — closed set. An unknown tag is a contract violation, not a warning. leash | SecurityLevel, trust_level | String | default = "unset", allow_network | Bool | default = false, @@ -433,11 +392,6 @@ in signature_required | Bool | default = false, # §8.3 — the explicit grant. Default-deny: absent means empty. capabilities | CapabilityGrant | default = [], - # §5.5 — unknown fields are PERMITTED, and the prose names this block's own - # examples: "the estate's contractiles use `probe_scope` and - # `authorised_probes_only`". No conforming reader may let one of them - # weaken §8 or §9 — the grant above is what a host enforces. - .. }, Pedigree = { @@ -457,12 +411,6 @@ in side_effects | Array String | default = [], # §10.1 — the signature claim. Presence is per-leash; see Signature above. signature | Signature | optional, - # §5.5 — unknown fields are PERMITTED. This is the line MIGRATION-1058 M1 - # turned on: the contractile family keeps its own vocabulary in the pedigree - # (`semantics`, `contractile_verb`, `tier`, `authority`, `variance_schema`, - # `execution`, `failure_mode_defenses`), and nothing in this file may force - # those names out of it. - .. }, # The whole component: the pedigree plus the optional Nickel payload. @@ -487,12 +435,6 @@ in config | { _ : Dyn } | optional, recipes | { _ : Dyn } | optional, validation | { _ : Dyn } | optional, - # §5.5 — unknown fields are PERMITTED at top level too, which is the case - # MIGRATION-1058 M4 recorded for whoever first ran the corpus at L2: - # `2-protocols/axel/config/*.k9.ncl` keep their payload (`ci`, `local_dev`, - # `quality`; `metadata.k9.ncl`: `project`, `tech_stack`, `deployment`, - # `features`, `status`) beside `pedigree`. - .. }, # ── §11 Library dialect ────────────────────────────────────────────── diff --git a/1-formats/k9/tools/fixtures/invalid/L1-K9-S003-todo-component-type.k9.ncl b/1-formats/k9/tools/fixtures/invalid/L1-K9-S003-todo-component-type.k9.ncl index bab378f93..5bb3bd00e 100644 --- a/1-formats/k9/tools/fixtures/invalid/L1-K9-S003-todo-component-type.k9.ncl +++ b/1-formats/k9/tools/fixtures/invalid/L1-K9-S003-todo-component-type.k9.ncl @@ -1,8 +1,9 @@ K9! # SPDX-License-Identifier: MPL-2.0 # Negative control §6.2: an unfilled component_type placeholder. This is the -# exact state of .machine_readable/svc/k9/template-hunt.k9.ncl today: the field -# exists, so a presence check passes, and the value says nothing. +# exact state of .machine_readable/svc/k9/template-hunt.k9.ncl.in (a template, +# which is why it no longer claims the .k9.ncl suffix): the field exists, so a +# presence check passes, and the value says nothing. { pedigree = { schema_version = "1.0.0", diff --git a/docs/ADR-001-k9-relocation-to-svc.adoc b/docs/ADR-001-k9-relocation-to-svc.adoc index 3ea8fc54d..4cf045954 100644 --- a/docs/ADR-001-k9-relocation-to-svc.adoc +++ b/docs/ADR-001-k9-relocation-to-svc.adoc @@ -76,6 +76,15 @@ k9 lives at `.machine_readable/svc/k9/` in every repo. The └── setup-repo.k9.ncl ---- +[NOTE] +==== +Amendment 2026-10-05 (standards#1058, MIGRATION-1058 M3): the three templates +now carry the `.in` suffix — `template-kennel.k9.ncl.in`, +`template-yard.k9.ncl.in`, `template-hunt.k9.ncl.in`. A template is not a +component and is never loaded, so it does not claim the reserved `.k9.ncl` +suffix. The tree above is otherwise unchanged. +==== + `svc/` is a **directory of named service-automation subsystems**. Today it holds only `k9/`. Future service-layer infrastructure (watchers, daemons, long-running automations) belongs here under its own name. diff --git a/docs/CONTRACTILE-SPEC.adoc b/docs/CONTRACTILE-SPEC.adoc index 46622bff9..96bebae89 100644 --- a/docs/CONTRACTILE-SPEC.adoc +++ b/docs/CONTRACTILE-SPEC.adoc @@ -33,11 +33,10 @@ probes. This spec covers: -* The six contractile verbs, the k9 exception, and their semantics. +* The six contractile verbs and their semantics. * The canonical directory layout and naming rules. * The shared Nickel base (`_base.ncl`) and how verb runners inherit from it. * The `probe` contract (current legacy String form and target structured form). -* The k9 exception — trust-tier template infrastructure, not a verb contractile. * The registry (`INDEX.a2ml`) that lists all verbs. * Test fixture conventions. * CLI binding and invocation. @@ -82,14 +81,14 @@ run-behaviour:: k9:: Service-automation trust-tier infrastructure. Lives at - `.machine_readable/svc/k9/` — NOT inside `1-formats/contractiles/`. See <>. + `.machine_readable/svc/k9/` — NOT inside `1-formats/contractiles/`. See <>. Moved out of `1-formats/contractiles/` per `ADR-001-k9-relocation-to-svc.adoc` (2026-04-18); this spec's v1.1.0 language predates that ADR. == The Verb Set Six verbs are defined, plus `k9`, which is an exception documented in -<> and is not a verb contractile. The table below therefore has +<> and is not a verb contractile. The table below therefore has seven rows: six verbs and one exception. [cols="1,2,5", options="header"] @@ -135,7 +134,7 @@ seven rows: six verbs and one exception. | `k9` *(exception)* | Trust-tier automation templates -| NOT a verb contractile. See <>. +| NOT a verb contractile. See <>. |=== [[directory-layout]] @@ -146,8 +145,8 @@ seven rows: six verbs and one exception. ---- .machine_readable/contractiles/ ├── _base.ncl ← shared base (pedigree + status-core + probe-schema + run-defaults) -├── INDEX.a2ml ← registry of all active verbs (6 + k9 exception) -├── README.adoc ← human overview + k9 exception note +├── INDEX.a2ml ← registry of all active verbs (6) +├── README.adoc ← human overview │ ├── adjust/ │ ├── Adjustfile.a2ml ← declaration (data) @@ -173,11 +172,7 @@ seven rows: six verbs and one exception. │ ├── Trustfile.a2ml │ └── trust.ncl │ -└── k9/ ← EXCEPTION: trust-tier templates, not a verb - ├── README.adoc - ├── template-kennel.k9.ncl - ├── template-yard.k9.ncl - └── template-hunt.k9.ncl +└── k9/ ← relocated by ADR-001 to .machine_readable/svc/k9/ estate-wide; signpost only ---- Each verb directory MUST contain exactly: @@ -205,7 +200,7 @@ Anything else in a verb directory is human notes or archive; machines ignore it. (see <>). 4. **k9 follows none of the above.** k9 is the explicit exception - (see <>). + (see <>). == Shared Base (`_base.ncl`) @@ -436,8 +431,8 @@ The migration path is: Adopters writing new xfiles should prefer the structured form where possible. -[[k9-exception]] -== K9 Exception +[[k9-relocation]] +== K9 Service-Automation Layer `k9/` is **not a contractile verb**. It is trust-tier template infrastructure that lives at `.machine_readable/svc/k9/`, separate from the verb directories @@ -513,12 +508,11 @@ When implemented, place as `.machine_readable/contractiles/contractile-meta.ncl` == Registry (`INDEX.a2ml`) `.machine_readable/contractiles/INDEX.a2ml` is the machine-readable catalogue -of all six verbs plus the k9 exception. It lists: +of all six verbs. It lists: * Verb name and one-line semantics. * The file pair (declaration + runner). -* Active vs exception status. -* Notes for exceptions. +* Active status. * The base schema location. * Meta-schema status. @@ -670,10 +664,10 @@ four classifications were wrong. | *Rival taxonomy* — conflicts | Not adoption notes. v0.1.0 Draft, RFC 2119 language, its own conformance clause. It defines a *five-tier* system — Must, Trust, Dust, *Lust*, *K9* — - against this spec's six verbs plus the k9 exception. Three substantive - divergences: `lust` is live as a tier, though it was deprecated and - absorbed into `intend` on 2026-04-18; `k9` is a peer tier rather than a - documented exception; and S07-3 says declarations "MAY be expressed in any + against this spec's six verbs. Three substantive divergences: `lust` is + live as a tier, though it was deprecated and absorbed into `intend` on + 2026-04-18; `k9` is a peer tier rather than service-automation + infrastructure; and S07-3 says declarations "MAY be expressed in any structured format", against this spec's fixed two-file pattern. Its reference implementation section sites files at `1-formats/contractiles/must/` and `1-formats/contractiles/lust/` — the wrong root, and a plausible source of the 76 @@ -795,7 +789,7 @@ mechanical move rather than a content change. === Cardinality is violated at scale -This spec already states (<>) that each verb declares *one +This spec already states (<>) that each verb declares *one concern per repo*, and the owner's layout ruling states it more strongly: exactly ONE of each verb per repo, no second copies, no variants, no per-subdir duplicates, with `ANCHOR.a2ml` the sole exception. @@ -992,9 +986,8 @@ cat lust/Lustfile.a2ml # extract any real wishes rm -r lust/ ---- -The verb count drops 7 → 6 (plus the k9 exception, unchanged). All tooling -dispatch on `lust` must be removed; consumers should read wishes from -`Intentfile.a2ml` instead. +The verb count drops 7 → 6. All tooling dispatch on `lust` must be removed; +consumers should read wishes from `Intentfile.a2ml` instead. === Intentfile ← Intendfile revert (2026-04-18) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index be7581233..d06470fef 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -51,7 +51,8 @@ function norm(r, at, path, ref, n, parts) { if (path == "" || ref == "") return "" if (substr(path, 1, 2) == "./" || substr(path, 1, 2) == "$/") return "" # local action if (split(path, parts, "/") < 2) return "" - return parts[1] "/" parts[2] "@" ref + # GitHub owner/repository names are case-insensitive; refs are not. + return tolower(parts[1] "/" parts[2]) "@" ref } # ---------- pass 1: the lockfile ---------- @@ -68,7 +69,7 @@ FILENAME == lockfile { next } if (match($0, /^ - '"'"'([^'"'"']+)'"'"'[[:space:]]*$/, m) && cur != "") { - lock[cur, m[1]] = 1 + lock[cur, norm(m[1])] = 1 lockcount[cur]++ next } @@ -82,6 +83,9 @@ FNR == 1 { wf = FILENAME } sub(/[[:space:]]+#.*$/, "", line) # strip trailing comment if (match(line, /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.+)$/, m)) { raw = m[1] + # KYAML puts a comma after a quoted scalar. Strip the YAML delimiter + # before unquoting, otherwise the closing quote becomes part of the ref. + sub(/["\x27],[[:space:]]*$/, "", raw) gsub(/^["'"'"']|["'"'"']$/, "", raw) gsub(/[[:space:]]+$/, "", raw) if (raw ~ /^\$\//) { dollar[wf] = dollar[wf] " " raw; next } # known corruption diff --git a/scripts/tests/check-lock-sync-test.sh b/scripts/tests/check-lock-sync-test.sh index a40857b01..c65e59179 100755 --- a/scripts/tests/check-lock-sync-test.sh +++ b/scripts/tests/check-lock-sync-test.sh @@ -1,109 +1,88 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 -# -# Test suite for scripts/check-lock-sync.sh -# Part of hyperpolymath/standards#968 campaign - +# Regression tests for issue #968: compare workflow refs in both directions. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(dirname "$SCRIPT_DIR")" -CHECK_SCRIPT="$REPO_ROOT/check-lock-sync.sh" - -# Load test helpers if available -if [ -f "$SCRIPT_DIR/test-helpers.sh" ]; then - # shellcheck source=scripts/tests/test-helpers.sh - source "$SCRIPT_DIR/test-helpers.sh" -fi - -PASS=0 -FAIL=0 -TOTAL=0 - -fail() { - echo "FAIL: $*" - FAIL=$((FAIL + 1)) - TOTAL=$((TOTAL + 1)) -} - -pass() { - echo "PASS: $*" - PASS=$((PASS + 1)) - TOTAL=$((TOTAL + 1)) -} - -echo "=== Test suite for check-lock-sync.sh ===" - -# Test 1: Script exists and is executable -if [ -x "$CHECK_SCRIPT" ]; then - pass "Script exists and is executable" -else - fail "Script missing or not executable" -fi - -# Test 2: Script has SPDX header -grep -q "SPDX-License-Identifier: MPL-2.0" "$CHECK_SCRIPT" && \ - pass "Script has SPDX license header" || \ - fail "Script missing SPDX license header" - -# Test 3: Script exits 0 when lockfile is in sync (test with current repo if it has a lockfile) -if [ -f "$REPO_ROOT/.github/workflows/actions.lock" ]; then - if "$CHECK_SCRIPT" "$REPO_ROOT/.github/workflows" >/dev/null 2>&1; then - pass "Script exits 0 when lockfile is in sync (self-test)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHECK_SCRIPT="$ROOT/scripts/check-lock-sync.sh" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +WF="$TMP/workflows" +mkdir -p "$WF" +pass=0 +fail=0 + +expect() { + local want="$1" label="$2" out rc=0 + out="$(bash "$CHECK_SCRIPT" "$WF" 2>&1)" || rc=$? + if [ "$rc" -eq "$want" ]; then + echo "PASS: $label" + pass=$((pass + 1)) else - fail "Script failed on current repo (may be out of sync, or script error)" + echo "FAIL: $label (expected $want, got $rc)" + printf '%s\n' "$out" + fail=$((fail + 1)) fi -else - echo "SKIP: No actions.lock in current repo, cannot test sync case" -fi - -# Test 4: Script exits 1 when no lockfile exists -mkdir -p /tmp/test-lock-sync-empty -cd /tmp/test-lock-sync-empty -mkdir -p .github/workflows -touch .github/workflows/test.yml -if "$CHECK_SCRIPT" .github/workflows >/dev/null 2>&1; then - fail "Script should exit 1 when no lockfile exists" -else - pass "Script exits 1 when no lockfile exists" -fi -rm -rf /tmp/test-lock-sync-empty - -# Test 5: Script handles empty workflows directory gracefully -# Note: An empty workflows directory with just actions.lock is an edge case. -# The script exits 0 because there are no workflows to validate. -mkdir -p /tmp/test-lock-sync-no-wf -cd /tmp/test-lock-sync-no-wf -mkdir -p .github/workflows -touch .github/workflows/actions.lock -if "$CHECK_SCRIPT" .github/workflows >/dev/null 2>&1; then - pass "Script handles empty workflows directory (exits 0 - no workflows to check)" -else - fail "Script failed unexpectedly on empty workflows directory" -fi -rm -rf /tmp/test-lock-sync-no-wf - -# Test 6: Script has proper documentation -if grep -q "standards#968\|issue #968" "$CHECK_SCRIPT"; then - pass "Script references issue #968" -else - fail "Script missing reference to issue #968" -fi - -if grep -q "burble#224" "$CHECK_SCRIPT"; then - pass "Script references burble#224" -else - fail "Script missing reference to burble#224" -fi - -echo "" -echo "=== Results ===" -echo "PASS: $PASS" -echo "FAIL: $FAIL" -echo "TOTAL: $TOTAL" - -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi +} -exit 0 +cat > "$WF/test.yml" <<'YAML' +jobs: + test: + steps: + - uses: Actions/Checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +YAML +expect 1 'missing lockfile fails closed' +cat > "$WF/actions.lock" <<'YAML' +workflows: + '.github/workflows/test.yml': + - 'actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +YAML +expect 0 'repository names compare case-insensitively' +sed -i 's/Actions\/Checkout/actions\/checkout/' "$WF/test.yml" +expect 0 'matching block workflow is accepted' + +cat > "$WF/test.yml" <<'YAML' +{ + jobs: { + test: { + steps: [ + { + uses: "actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", # pinned + }, + { + uses: 'actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + ], + }, + }, +} +YAML +expect 0 'KYAML quoted refs exclude the trailing comma' +sed -i 's/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/g' "$WF/test.yml" +expect 1 'KYAML changed SHA remains a failure' + +cat > "$WF/test.yml" <<'YAML' +jobs: + test: + uses: owner/repo/.github/workflows/test.yml@Release +YAML +cat > "$WF/actions.lock" <<'YAML' +workflows: + '.github/workflows/test.yml': + - 'Owner/Repo@Release' +YAML +expect 0 'job-level reusable workflow is checked and lock names normalised' +sed -i 's/@Release/@release/' "$WF/test.yml" +expect 1 'ref case remains significant' +sed -i 's/@release/@Release/' "$WF/test.yml" +printf " - 'actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'\n" >> "$WF/actions.lock" +expect 1 'stale lock entries remain failures' +sed -i '/actions\/checkout/d' "$WF/actions.lock" +cp "$WF/test.yml" "$WF/unlocked.yml" +expect 1 'workflow missing from lock remains a failure' +rm "$WF/unlocked.yml" +rm "$WF/test.yml" +expect 1 'deleted workflow lock entries remain failures' + +printf '\ncheck-lock-sync regression: %s passed, %s failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ]