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/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/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 ]