Skip to content

ci(k9): make a failing K9 gate legible (follow-up to #1143) - #1144

Merged
hyperpolymath merged 8 commits into
mainfrom
arena/01a10407-standards
Oct 4, 2026
Merged

hyperpolymath merged 8 commits into
mainfrom
arena/01a10407-standards

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Follow-up to #1143. The K9 fixtures step is red on main as of b3075e0 and nothing about that is currently readable: the log blob host is unreachable from the sandbox, the step summary is not exposed by the check-run API, and the report comment was pinned to a stale run because the publish step failed trying to PATCH it.

This PR changes no gate. It changes only whether a failure can be read.

  • One annotation per failure. A whole-report ::error never reached check-runs/{id}/annotations. Annotations are now emitted per FAIL/ERROR line (capped at 20), each short enough to survive the parser.
  • Always post the report. The publish step's PATCH branch failed and left the PR showing the report from two runs earlier. It now always creates a comment; each names its run.

Why this matters beyond convenience

The first CI run of #1143 was the first time any tool in this estate ran Nickel over a K9 file, and it immediately found two defects in that PR's own code:

  1. k9_contract.ncl named a Record type that does not exist in Nickel. The contract failed with unbound identifier, so every L2 verdict downstream was void and all 5 positive controls failed.
  2. Negative-control attribution grepped the human-readable finding line, which echoes the file path — and a control is named L2-K9-N001-…. So the assertion "rejected by K9-N001" succeeded no matter which rule fired. With the contract broken, both L2 controls were rejected by K9-N002 and both still printed ok. A gate that cannot fire is the defect class rsr-antipattern.yml: BUILTIN_GLOBS bash block stranded outside Python heredoc (exit 127) #49 and Hypatia dogfooding job red estate-wide — unresolvable setup-beam pins (companion to hypatia-side fix) #64 established; this suite was built to prevent it and contained one.

Both are fixed in #1143. What is not yet established is whether the fixtures pass at L2 with the contract repaired — the step is still red, and this PR is how that gets read.

self-test now asserts the attribution predicate itself (29 assertions): a rule id present only in a filename is not attributed, and a skipped finding cannot satisfy a control.

arena-agent and others added 7 commits October 4, 2026 01:20
… suite (#1058, D173)

Ruling D173: "K9 needs a Nickel contract, not an ABNF." Implements it.

Standards
- 1-formats/k9/spec/K9-CONTRACT-SPEC.adoc v1.0.0 — normative. File envelope,
  dialect rules, versioned contract, closed leash set, default-deny capability
  model, the five Hunt preconditions, signature semantics, four conformance
  layers. 23 rule ids, indexed in Appendix A.
- spec/contract/k9_contract.ncl — the machine-readable contract.
- SPEC.adoc — points at the contract as normative; names the component/repo
  pedigree collision instead of leaving two shapes called "pedigree".

Deliberately no k9.abnf: a component body IS a Nickel term, so a whole-file
grammar would be a drifting restatement of a language we do not own. The
envelope is specified as three octets plus a first-significant-line table.

Two distinctions made load-bearing rather than prose
- Presence is not verification (10.4): the Hunt `signature` precondition is
  satisfiable by 'Verified only. 'Present_Unverified is false. No input turns
  "no verifier ran" into "verified".
- A flag is a request that must be paid for (8.4): allow_network/fs_write/
  subprocess now REQUIRE net.fetch/fs.write/process.spawn in the grant. A
  component asking for the network while granting itself nothing is invalid.
Hunt is otherwise unchanged: all five preconditions, always, no subset.

Validators aligned
- tools/k9-validate.sh — canonical. Layered L0 envelope / L1 structural /
  L2 Nickel / L3 crypto, so a lexical check cannot report a higher layer's
  authority. A check that could not run is SKIPPED, never a pass; --strict
  fails the run rather than reporting green over nothing.
- .githooks/validate-k9.sh — was its own format: it grepped for a line
  beginning `contract`, which 0 of 30 tracked K9 files have, so it exited 1
  with 30 errors on a clean tree. Now delegates and owns only commit policy.
- .githooks/validate-lint-format.sh — excludes *.k9.ncl from the bare nickel
  typecheck, matching ci-pipeline.yml (2 staged .ncl in, 1 out).
- k9-contractile.yml — installs Nickel pinned+sha256 (same pin as
  ci-pipeline.yml) and runs --self-test, the fixtures --strict, and the corpus.

Fixtures: 5 positive, 21 negative. Each negative names its rule and layer and
the runner asserts it was rejected BY that rule AT that layer, so a fixture
cannot pass for the wrong reason. 20 of 23 rules have a control.

Migration: spec/MIGRATION-1058.adoc. Baseline measured — 30 tracked K9 files,
5 conforming, 25 not. .machine_readable/k9-contract-debt.txt grandfathers them
shrink-only: fixing a file forces its entry out, and editing a listed file
removes its protection.

Not yet run: L2. No nickel binary is obtainable in the preparation sandbox
(release-asset host TLS-refused, no cargo to build the codeload tarball), so
this commit's L2 result comes from the workflow_dispatch run of the job added
here.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The three K9 steps now tee into $GITHUB_STEP_SUMMARY. Step conclusions were
already readable through the check-run API; the step bodies were not, and the
log blob host is not reachable from every machine that needs the result. The
PR now carries the verdict itself.

No behavioural change to the gates: each step still exits with the validator's
own status via PIPESTATUS[0], and the run scripts drop -e so a failing
validator reaches the tee instead of aborting before it.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The step summaries written last commit turned out not to be readable: the
check-run API returns an empty output.summary for Actions jobs, and the log
blob hosts are unreachable from the sandbox that needs the result. So the
verdict is now posted to the PR itself, edited in place on re-runs.

Adds pull-requests:write at the job level (the workflow-level grant stays
contents:read) and runs with always(), because a failing gate is exactly when
the detail is needed.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…uld not fail

The first CI run of this branch produced the first Nickel verdict this corpus
has ever had, and it found two defects — both in this PR's own code.

1. `Record` is not a Nickel type. Component's three open fields named it, so
   k9_contract.ncl failed with `unbound identifier` and every L2 verdict
   downstream was void. All 5 positive controls failed. Now `{ _ : Any }`.

2. Negative-control attribution matched the rule id in the FILENAME. A control
   is named `L2-K9-N001-…`, and the human finding line echoes the path, so
   grepping that output for `K9-N001` succeeded no matter which rule fired.
   With the contract broken, both L2 controls were rejected by K9-N002 and
   both still reported `ok`. The suite exists to catch gates that cannot fire;
   this was one. Attribution now reads the structured findings and requires an
   `error`-severity finding whose rule AND layer both match the filename.

   A broken contract is also called out by name rather than reported as "wrong
   rule": when K9-N002 fires, no L2 control proved anything.

self-test gains a block asserting the predicate itself — a rule id present only
in a path is not attributed, and a skipped finding cannot satisfy a control.
24 assertions become 29. The first draft of that block asserted E001 fires
once; it fires twice (bad magic also leaves the body unclaimed), so the
well-formedness count is compared rather than fixed at 1.

Still unverified locally: L2. No nickel binary is obtainable in this sandbox,
so the fixtures' L2 half is asserted by the workflow run of this commit.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The publish step failed on the last run, so the PR comment stayed pinned to the
run before it. `gh pr view --json comments` returns API urls, not web urls, so
stripping a github.com prefix left a string that was not a resource path; the
id is now taken off the end and PATCHed as issues/comments/{id}. `(.body //
"")` guards a null body on a deleted comment.

Failing fixtures and corpus steps now also emit `::error`. Check-run
annotations are readable from the check-run API, which unlike the log blob host
is reachable from the sandbox diagnosing the run.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Two runs were spent on an update path that failed and could not be read back.
Creating the comment is the one branch observed to complete, so it is now the
only branch; stacked reports are cheaper than no result.

Also statically audited the contract for the identifier class that broke it:
every type-position identifier is now either defined in the file or a Nickel
builtin, and every std.* function it calls is one the estate's CI-passing .ncl
already uses with the same argument order.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Two follow-ups to what landed in #1143, both about being able to READ a
failure rather than about the gate:

- A whole-report ::error never reached the check-run API. Annotations are now
  emitted per FAIL/ERROR line, capped at 20, each short enough to survive.
- The publish step tried to PATCH its previous comment and failed, pinning the
  PR to a stale report for two runs. It now always posts; stacked reports are
  cheaper than no result.

The K9 fixtures step is red on main as of b3075e0 and this is the change that
makes the reason legible.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 30 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d69157bf-d233-461a-990e-1455406b8d66
📥 Commits

Reviewing files that changed from the base of the PR and between b3075e0 and 9c971da.

📒 Files selected for processing (1)
  • .github/workflows/k9-contractile.yml
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath marked this pull request as ready for review October 4, 2026 01:38
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 4, 2026 01:39
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

K9 contract conformance

run https://github.com/hyperpolymath/standards/actions/runs/37168693062

K9 contract self-test

== the bash mirrors cannot drift from the normative contract ==
ok   leash_levels mirrors k9_contract.ncl
ok   core_capabilities mirrors k9_contract.ncl
ok   contract_version mirrors k9_contract.ncl
ok   schema_major mirrors k9_contract.ncl
== capability arithmetic (§8) ==
ok   capability_ok fs.read accepted
ok   capability_ok rollback.apply accepted
ok   capability_ok x-acme.gpu.alloc accepted
ok   capability_ok x-acme rejected
ok   capability_ok x-.gpu rejected
ok   capability_ok fs.delete rejected
ok   capability_ok  rejected
== the extractor ==
ok   extracts pedigree.security.leash
ok   extracts pedigree.component_type
ok   extracts pedigree.metadata.name
ok   pedigree leash is not reported as top-level leash
ok   required_capabilities for a quiet component
ok   required_capabilities follows allow_network
== the envelope strip keeps line numbers (§3.6) ==
ok   line 1 becomes a comment
ok   line count is preserved
ok   schema_version stays on line 5
== L3: signature presence is not verification (§10) ==
ok   no verifier -> K9-C001 is SKIPPED, never a pass
ok   the skip states presence does not authorise 'Hunt
ok   verifier accepts -> verdict 'Verified, no K9-C001 finding
ok   verifier refuses -> K9-C001 error, verdict 'Rejected
== the fixture runner's attribution cannot be fooled by a filename ==
ok   every extracted finding is well-formed rule+layer
ok   the rule that really fired is attributed
ok   a rule named only in the filename is NOT attributed
ok   K9-C001 is present as a skipped finding
ok   and that same finding is NOT extractable as a rejection

self-test: all assertions passed

K9 conformance fixtures

== positive controls (must pass) ==
ERROR   K9-N002 [L2] 1-formats/k9/tools/fixtures/valid/extension-capability.k9.ncl: the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ 
FAIL extension-capability.k9.ncl should conform (exit 1)
ERROR   K9-N002 [L2] 1-formats/k9/tools/fixtures/valid/hunt-fully-granted.k9.ncl: the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ 
SKIPPED K9-C001 [L3] 1-formats/k9/tools/fixtures/valid/hunt-fully-granted.k9.ncl: signature block present but no verifier ran (set K9_SIG_VERIFIER); verdict is 'Present_Unverified, which does NOT authorise 'Hunt
FAIL hunt-fully-granted.k9.ncl should conform (exit 1)
ERROR   K9-N002 [L2] 1-formats/k9/tools/fixtures/valid/kennel-data.k9.ncl: the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ 
FAIL kennel-data.k9.ncl should conform (exit 1)
ERROR   K9-N002 [L2] 1-formats/k9/tools/fixtures/valid/library-base.ncl: the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ 
FAIL library-base.ncl should conform (exit 1)
ERROR   K9-N002 [L2] 1-formats/k9/tools/fixtures/valid/yard-typed-config.k9.ncl: the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ 
FAIL yard-typed-config.k9.ncl should conform (exit 1)

== negative controls (must fail, by the named rule) ==
ok   L0-K9-E001-bad-magic.k9.ncl (rejected by K9-E001 at L0)
FAIL L0-K9-E002-nul-byte.k9.ncl cannot assert K9-E002: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L0-K9-E002-nul-byte.k9.ncl","dialect":"","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-E002","layer":"L0","message":"file contains a NUL byte; not a text K9 component"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
ok   L0-K9-E003-crlf.k9.ncl (rejected by K9-E003 at L0)
FAIL L0-K9-E004-no-spdx.k9.ncl cannot assert K9-E004: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L0-K9-E004-no-spdx.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-E004","layer":"L0","message":"no SPDX-License-Identifier in the first 5 lines"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
ok   L0-K9-E005-unclaimed-body.k9.ncl (rejected by K9-E005 at L0)
FAIL L0-K9-S012-library-with-pedigree.ncl cannot assert K9-S012: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L0-K9-S012-library-with-pedigree.ncl","dialect":"library","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S012","layer":"L0","message":"no 'K9!' envelope but a top-level pedigree: a component without an envelope cannot be leashed"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L0-K9-S014-stray-leash.ncl cannot assert K9-S014: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L0-K9-S014-stray-leash.ncl","dialect":"library","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S014","layer":"L0","message":"no 'K9!' envelope but a top-level leash claim; a leash belongs in pedigree.security"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S001-no-pedigree.k9.ncl cannot assert K9-S001: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S001-no-pedigree.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S001","layer":"L1","message":"no top-level 'pedigree' block"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S002-wrong-major.k9.ncl cannot assert K9-S002: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S002-wrong-major.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S002","layer":"L1","message":"pedigree.schema_version '2.0.0' is not readable by contract v1.0.0 (needs major 1)"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S003-todo-component-type.k9.ncl cannot assert K9-S003: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S003-todo-component-type.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S003","layer":"L1","message":"pedigree.component_type is an unfilled placeholder: 'TODO: describe component type (e.g., 'deployment')'"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S004-unknown-leash.k9.ncl cannot assert K9-S004: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S004-unknown-leash.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S004","layer":"L1","message":"leash ''Paddock' is not in the closed set {'Kennel 'Yard 'Hunt}"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S005-missing-name.k9.ncl cannot assert K9-S005: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S005-missing-name.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S005","layer":"L1","message":"pedigree.metadata.name is missing"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S006-unknown-capability.k9.ncl cannot assert K9-S006: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S006-unknown-capability.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":3,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S006","layer":"L1","message":"capability 'fs.delete' is neither a core name nor an 'x-<vendor>.<path>' extension"},{"severity":"error","rule":"K9-S006","layer":"L1","message":"capability 'x-acme' is neither a core name nor an 'x-<vendor>.<path>' extension"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S007-ungranted-flag.k9.ncl cannot assert K9-S007: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S007-ungranted-flag.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S007","layer":"L1","message":"security flags request capabilities the grant does not cover: net.fetch (default-deny: list them in pedigree.security.capabilities)"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S008-hunt-signature-not-required.k9.ncl cannot assert K9-S008: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S008-hunt-signature-not-required.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":1,"findings":[{"severity":"error","rule":"K9-S008","layer":"L1","message":"leash is 'Hunt but pedigree.security.signature_required is not true"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "},{"severity":"skipped","rule":"K9-C001","layer":"L3","message":"signature block present but no verifier ran (set K9_SIG_VERIFIER); verdict is 'Present_Unverified, which does NOT authorise 'Hunt"}]}
FAIL L1-K9-S009-hunt-no-signature-block.k9.ncl cannot assert K9-S009: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S009-hunt-no-signature-block.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S009","layer":"L1","message":"leash is 'Hunt but no pedigree.signature block is present (presence required here; verification is K9-C001)"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S010-hunt-empty-side-effects.k9.ncl cannot assert K9-S010: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S010-hunt-empty-side-effects.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":1,"findings":[{"severity":"error","rule":"K9-S010","layer":"L1","message":"leash is 'Hunt but pedigree.side_effects is empty: full access must be described"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "},{"severity":"skipped","rule":"K9-C001","layer":"L3","message":"signature block present but no verifier ran (set K9_SIG_VERIFIER); verdict is 'Present_Unverified, which does NOT authorise 'Hunt"}]}
FAIL L1-K9-S011-recipes-at-yard.k9.ncl cannot assert K9-S011: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S011-recipes-at-yard.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S011","layer":"L1","message":"component declares a 'recipes' block at leash 'Yard; recipes are an execution surface and require 'Hunt"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L1-K9-S013-dangling-import.k9.ncl cannot assert K9-S013: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L1-K9-S013-dangling-import.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":2,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-S013","layer":"L1","message":"import \"does-not-exist.ncl\" does not resolve to 1-formats/k9/tools/fixtures/invalid/does-not-exist.ncl"},{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L2-K9-N001-two-segment-version.k9.ncl cannot assert K9-N001: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L2-K9-N001-two-segment-version.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":1,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}
FAIL L2-K9-N001-wrong-field-type.k9.ncl cannot assert K9-N001: the normative contract does not typecheck (K9-N002), so L2 rejected nothing on its own merits
{"file":"1-formats/k9/tools/fixtures/invalid/L2-K9-N001-wrong-field-type.k9.ncl","dialect":"component","contract_version":"1.0.0","verdict":"fail","errors":1,"warnings":0,"skipped":0,"findings":[{"severity":"error","rule":"K9-N002","layer":"L2","message":"the normative contract does not typecheck: error: unbound identifier `Any`     ┌─ /home/runner/work/standards/standards/1-formats/k9/spec/contract/k9_contract.ncl:422:20     │ "}]}

fixtures: 5 positive, 21 negative (0 needing nickel), 23 failure(s)

@hyperpolymath
hyperpolymath disabled auto-merge October 4, 2026 01:40
@hyperpolymath
hyperpolymath merged commit 2f8c3ee into main Oct 4, 2026
39 of 46 checks passed
@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath deleted the arena/01a10407-standards branch October 4, 2026 01:40
hyperpolymath pushed a commit that referenced this pull request Oct 4, 2026
main is red on the K9 gate. #1144 merged at 9c971da, one commit before this
fix, so `{ _ : Any }` is what shipped — and `Any` is not a Nickel type. The
annotations #1144 added name it: `unbound identifier 'Any'` at
k9_contract.ncl:422:20. The dynamic type is `Dyn`. `Record` was wrong before
it; both names were asserted from memory in a sandbox with no nickel binary,
and both voided every L2 verdict downstream, because a contract that does not
typecheck cannot judge anything.

The static audit meant to catch the first one did not: it scanned `| T`
positions only, and `{ _ : Any }` puts its type after a colon. It now also
scans `_ : T` and `Array T`, and its builtin whitelist is narrowed to the four
types this repo's CI-passing .ncl actually uses. It reports exactly one
identifier it cannot evidence from the repo: `Dyn`, lines 428-430.

k9-contractile.yml gains a `K9 normative contract typecheck` step ahead of the
fixtures. `nickel typecheck` stops at the first error, and a broken contract
presents as five non-conforming positive controls rather than one broken
contract — that misdirection cost two runs to see through.

Unverified here: whether `Dyn` typechecks. Nothing else in this repo uses it,
so the workflow run of this commit is the evidence.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants