Skip to main content

fuel_dispatch/fkc/
validate.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Build-time validators (the `V-FKC-*` battery, FKC §10).
3//!
4//! These run at import time, `Result`-returning, never panicking, never a
5//! silent fix-up (FKC G6, constitution: validate-at-build-time, never-panic).
6//! They operate on the **parsed/structured** form ([`FkcFile`] / [`FkcKernel`])
7//! — structural + coherence checks — NOT on a registered binding table. The
8//! register slice (`register.rs`) calls [`validate_file`] after parse+lower so a
9//! bad contract fails import; the CI lint (`tests`) calls it standalone over the
10//! whole checked-in corpus.
11//!
12//! ## Rule → check → error map (FKC §10, rules 1–16)
13//!
14//! | rule | check | `FkcError` |
15//! |------|-------|------------|
16//! | 1  | `fkc_version ∈ FKC_SUPPORTED_VERSIONS` | `UnsupportedVersion` |
17//! | 2  | required fields (kernel; EXACTLY ONE of op_kind/fused_op; blurb; entry_point; ≥1 input; ≥1 output/bundle; cost; precision; determinism). Describe-only (§3.10) EXEMPTS exactly-one-of-op_kind/fused_op AND the ≥1-input rule (a zero-operand documentation op is legitimate). | `MissingRequiredField` / `MissingBlurb` / `OpTargetAmbiguous` |
18//! | 3  | dtype validity; sub-byte ⇒ fdx.quant; ggml_dtype a real `GgmlDType` | `BadScalarType` / `QuantIncoherent` |
19//! | 4  | layout coherence (≥1 of contiguous/strided OK; broadcast/reverse ⇒ strided) | `LayoutIncoherent` |
20//! | 5  | awkward-layout coherence PER OPERAND | `AwkwardStrategyIncoherent` |
21//! | 6  | quant coherence + scale single-place + MX/AFFINE_BLOCK not-yet-registrable | `QuantIncoherent` / `ScaleDoubleDeclared` / `MxNotYetRegistrable` |
22//! | 7  | op-param variant in the correct namespace | `BadOpParamsVariant` |
23//! | 8  | cost expressions parse, in-scope | `CostExprParse` |
24//! | 8a | cost provenance present + non-placeholder | `CostProvenanceMissing` / `PlaceholderCost` |
25//! | 9  | precision coverage (UNAUDITED flagged; nondeterministic ⇒ audited+no-bitstable) | (note / `QuantIncoherent`-shaped) |
26//! | 10 | duplicate KernelRef — at register time (`finalize`), not here |
27//! | 11 | prose/structured blurb agreement — see §10.11 note below |
28//! | 13 | bundle slot rank ≤ 6 | `BundleSlotRankExceeded` |
29//! | 14 | gather (paged) coherence | `GatherIncoherent` / `GatherNotYetSupported` |
30//! | 15 | affine / symbolic extent coherence | `UnknownAdmissibilityEnum` |
31//! | 16 | FDX-subset drift-guard (every dtype/quant/granularity/ggml token ∈ FDX table) | `FdxTokenNotInTable` |
32//!
33//! ### §10.11 (prose blurb) — not feasible on the structured form
34//!
35//! The prose blurb is the first non-empty line of the markdown section, which
36//! the parser drops (it keeps only the structured [`FkcKernel`], FKC §3.1). So
37//! the structured-vs-prose comparison is not available to a validator that
38//! operates on the parsed form. We implement the *structured* half of the rule
39//! (the `blurb:` field must be a non-empty one-line string → `MissingBlurb`) and
40//! leave the prose-vs-structured re-render diff to the `fkc fmt --check` lint
41//! that has the raw markdown ([`FkcError::BlurbMismatch`] stays reserved for it).
42
43use crate::fkc::error::FkcError;
44use crate::fkc::lower;
45use crate::fkc::schema::{
46    AcceptBlock, CapsBlock, CostBlock, CostMemory, FdxSpec, FkcFile, FkcFrontMatter, FkcKernel,
47    FkcProvider, GatherSpec, LayoutSpec, OpParamFieldSpec, OpParamsSchema, OutputDesc,
48    PrecisionBlock, QuantSpec, ReturnBlock, TensorDesc,
49};
50// Rule 7's op-param allowlists are generated from — and held against — these
51// two enums by `variant_table!`; see the V-FKC-5 note above `variant_table!`.
52use crate::kernel::OpParams;
53use fuel_graph::registry::FusedOpParams;
54
55/// The exact set of `fkc_version` values this importer accepts (FKC §10.1 / §11;
56/// KISS #450, narrowing §6.1-0008).
57///
58/// **Membership, not a `<= max` bound.** A version is accepted IFF it is in this
59/// set; a reader MUST NOT accept a version merely because it is *lower* than one
60/// it supports. (GAP-292: the old gate `fkc_version > MAX` accepted `0`, so the
61/// accepted set was silently `{0, 1}` — and `0` is not a version.) A reader
62/// DECLARING its supported set here is exactly what KISS #450 requires.
63///
64/// # Two independent version axes (GAP-038 — KISS-CONTRACT §8-0001..0007)
65///
66/// `fkc_version` is the **wire/schema** axis: the format of the contract file.
67/// The **second** axis is the producing **crate's published semver** (in
68/// `Cargo.toml` and the registry) — deliberately *not* stamped in any contract
69/// (KISS pins the Provenance block to a closed 5-field set with no crate-version
70/// field). The bump rule, transcribed from KISS-CONTRACT §8 and umbrella §5.1
71/// (verify at the KISS tree; this is a pointer, not the authority):
72///
73/// | change                                             | bumps |
74/// |----------------------------------------------------|-------|
75/// | a pure code refactor that preserves the bytes      | crate semver ONLY (§8-0001) |
76/// | a front-matter/section **schema** change (§8-0002) | the schema version — a new entry here |
77/// | an upstream **vocabulary** change — a new KISS-Ops  | NEITHER axis (§8-0003) |
78/// |   op name, a KISS-Grammar shape                    |       |
79///
80/// §8-0001: the two axes are INDEPENDENT — a crate-semver change MUST NOT imply a
81/// schema change, so this set is a literal and MUST NOT be derived from
82/// `CARGO_PKG_VERSION`. ⚠️ §8-0003 is the counter-intuitive one: an upstream
83/// op-name change is NOT a schema change and MUST NOT add an entry here. The
84/// compile-time pin below enforces §8-0002 while, by pinning the schema STRUCTS
85/// rather than the op vocabulary, leaving §8-0003's vocabulary changes untouched.
86pub const FKC_SUPPORTED_VERSIONS: &[u32] = &[1];
87
88/// The single source of truth for "does this importer accept `fkc_version`?"
89/// (KISS #450 membership). Both the production gate in [`validate_file`] and the
90/// checked-in-corpus conformance test call THIS, so the two cannot diverge on the
91/// accepted set — the drift a duplicated inline `> MAX` check invited (GAP-292).
92pub(crate) fn fkc_version_is_supported(version: u32) -> bool {
93    FKC_SUPPORTED_VERSIONS.contains(&version)
94}
95
96/// GAP-038 + GAP-297 / §8-0002 — compile-time schema pin for the ENTIRE FKC
97/// contract schema. Adding, removing, or retyping a field on ANY
98/// `#[derive(Deserialize)]` struct in [`crate::fkc::schema`] fails to compile in
99/// the matching `_fkc_pin_*` fn below (every destructure is exhaustive), forcing
100/// the schema-vs-version decision to be made deliberately: a schema change is a
101/// §8-0002 change and MUST add a new entry to [`FKC_SUPPORTED_VERSIONS`] (and bump
102/// `PINNED_FKC_SCHEMA_VERSION` in the test below).
103///
104/// ⚠️ It pins STRUCTS, not the op vocabulary, so a §8-0003 vocabulary change does
105/// NOT trip it — the guard fires on schema drift, never vocab drift.
106///
107/// GAP-297 DISCHARGES the floor GAP-038 documented: the pin covers the top-level
108/// front-matter AND the `provider` sub-struct AND the whole kernel-block tree
109/// (`accept`/`return`/tensor/layout/fdx/quant/gather/caps/cost/precision). Pinning
110/// only the top level would just relocate the floor one struct down, so EVERY
111/// Deserialize struct is destructured. [`FkcFile`] is deliberately EXCLUDED — it is
112/// the parsed RESULT (`#[derive(Debug, Clone)]`, not `Deserialize`), so it is not
113/// part of the wire schema.
114///
115/// SPLIT INTO GROUPS by block (front-matter / kernel / accept / return / tensor /
116/// caps-cost-precision) so each fn stays under the method-length + parameter-count
117/// limits, coverage unchanged — all 17 structs are still exhaustively destructured
118/// across the six functions (GAP-297 Codacy follow-up). If a new schema struct is
119/// added, destructure it in the group it belongs to (or add a group) — a struct
120/// with no `_fkc_pin_*` arm is a pin the guard cannot enforce.
121///
122/// Never called: an unreferenced private fn is still fully type-checked (the
123/// `dead_code` lint runs *after* type-checking), so the destructures are enforced
124/// on every build; `/// arm binds every field to `_`, which reads (and thereby pins) the field set.
125fn _fkc_pin_front_matter(front_matter: FkcFrontMatter, provider: FkcProvider) {
126    let FkcFrontMatter {
127        fkc_version: _,
128        provider: _,
129    } = front_matter;
130    let FkcProvider {
131        name: _,
132        backend: _,
133        kernel_source: _,
134        link_registry: _,
135        revision_base: _,
136    } = provider;
137}
138
139fn _fkc_pin_kernel(kernel: FkcKernel) {
140    let FkcKernel {
141        kernel: _,
142        registrable: _,
143        op_kind: _,
144        fused_op: _,
145        blurb: _,
146        backend: _,
147        kernel_source: _,
148        entry_point: _,
149        kernel_revision_hash: _,
150        variant: _,
151        accept: _,
152        return_: _,
153        caps: _,
154        cost: _,
155        precision: _,
156        determinism: _,
157    } = kernel;
158}
159
160fn _fkc_pin_accept(
161    accept: AcceptBlock,
162    op_params: OpParamsSchema,
163    op_param_field: OpParamFieldSpec,
164) {
165    let AcceptBlock {
166        inputs: _,
167        op_params: _,
168    } = accept;
169    let OpParamsSchema {
170        variant: _,
171        fields: _,
172    } = op_params;
173    let OpParamFieldSpec {
174        kind: _,
175        constraint: _,
176        note: _,
177    } = op_param_field;
178}
179
180fn _fkc_pin_return(ret: ReturnBlock, output: OutputDesc) {
181    let ReturnBlock {
182        outputs: _,
183        bundle: _,
184    } = ret;
185    let OutputDesc {
186        name: _,
187        dtype_rule: _,
188        shape_rule: _,
189        layout_guarantee: _,
190        aliasing: _,
191    } = output;
192}
193
194fn _fkc_pin_tensor(
195    tensor: TensorDesc,
196    layout: LayoutSpec,
197    fdx: FdxSpec,
198    quant: QuantSpec,
199    gather: GatherSpec,
200) {
201    let TensorDesc {
202        name: _,
203        optional: _,
204        dtypes: _,
205        dtype_class: _,
206        layout: _,
207        rank: _,
208        shape_constraint: _,
209        fdx: _,
210        device: _,
211        substrate: _,
212    } = tensor;
213    let LayoutSpec {
214        contiguous: _,
215        strided: _,
216        broadcast_stride0: _,
217        broadcast_axes: _,
218        start_offset: _,
219        reverse_strides: _,
220        awkward_layout_strategy: _,
221    } = layout;
222    let FdxSpec {
223        requires_ext: _,
224        quant: _,
225        sub_byte: _,
226        symbolic_extent: _,
227        extent_kind: _,
228        gather: _,
229    } = fdx;
230    let QuantSpec {
231        family: _,
232        ggml_dtype: _,
233        granularity: _,
234        block_shape: _,
235        role: _,
236        scale_operand: _,
237    } = quant;
238    let GatherSpec {
239        kind: _,
240        block_table: _,
241        context_lens: _,
242    } = gather;
243}
244
245fn _fkc_pin_caps_cost_precision(
246    caps: CapsBlock,
247    cost: CostBlock,
248    cost_memory: CostMemory,
249    precision: PrecisionBlock,
250) {
251    let CapsBlock {
252        awkward_layout_strategy: _,
253        fast_paths: _,
254        in_place: _,
255        alignment_bytes: _,
256        access_granularity_bits: _,
257    } = caps;
258    let CostBlock {
259        provenance: _,
260        class: _,
261        cost_fn: _,
262        flops: _,
263        bytes_moved: _,
264        overhead_ns: _,
265        memory: _,
266    } = cost;
267    let CostMemory {
268        device_bytes: _,
269        host_bytes: _,
270        disk_bytes: _,
271    } = cost_memory;
272    let PrecisionBlock {
273        bit_stable_on_same_hardware: _,
274        max_ulp: _,
275        max_relative: _,
276        max_absolute: _,
277        audited: _,
278        notes: _,
279    } = precision;
280}
281
282// ===========================================================================
283// FDX normative token tables (§10.16 drift-guard)
284// ===========================================================================
285//
286// FKC re-numbers nothing; FDX owns the codes (§0). The dtype token resolves
287// through `lower::lower_dtype` (the same `DType` set FDX §6.1 owns); the
288// quant `family` / `granularity` / `ggml_dtype` tokens resolve through the
289// tables below, which mirror the FDX §6.2 symbol sets. A token absent from
290// these is `FdxTokenNotInTable` (rule 16) — so FKC's set stays a subset of
291// FDX's and the two specs cannot drift.
292
293/// FDX `FDXQuant.family` symbols (FDX §6.2): `NONE | GGML_BLOCK | MX |
294/// AFFINE_INT | AFFINE_FLOAT | AFFINE_BLOCK`. `"none"` is accepted in either
295/// case (the corpus writes `none`).
296fn is_fdx_quant_family(tok: &str) -> bool {
297    matches!(
298        tok,
299        "none" | "NONE" | "GGML_BLOCK" | "MX" | "AFFINE_INT" | "AFFINE_FLOAT" | "AFFINE_BLOCK"
300    )
301}
302
303/// FDX `FDXScaleGranularity` symbols (FDX §6.2): `PerTensor | PerToken |
304/// PerChannel | PerBlock`.
305fn is_fdx_granularity(tok: &str) -> bool {
306    matches!(tok, "PerTensor" | "PerToken" | "PerChannel" | "PerBlock")
307}
308
309/// The as-built `GgmlDType` variant set (`fuel-core-types/src/quantized.rs`),
310/// matched **by code** per FKC §3.4 — i.e. these are exactly the variant
311/// names, and `Q4_K_M` (a GGUF file-format name, NOT a `GgmlDType` variant) is
312/// deliberately absent (writing it fails §10.6 / rule 16).
313fn ggml_dtype_code(tok: &str) -> Option<u32> {
314    let code = match tok {
315        "F32" => 0,
316        "F16" => 1,
317        "Q4_0" => 2,
318        "Q4_1" => 3,
319        "Q5_0" => 6,
320        "Q5_1" => 7,
321        "Q8_0" => 8,
322        "Q8_1" => 9,
323        "Q2K" => 10,
324        "Q3K" => 11,
325        "Q4K" => 12,
326        "Q5K" => 13,
327        "Q6K" => 14,
328        "Q8K" => 15,
329        "BF16" => 30,
330        _ => return None,
331    };
332    Some(code)
333}
334
335/// The `ScaleGranularity` set that has an as-built `fuel-core-types`
336/// counterpart (`quant_scale.rs`): `{ PerTensor, PerToken, PerChannel }`.
337/// `PerBlock` is FDX/FKC-only with no target type yet (FKC §6 / §10.6).
338fn is_registrable_granularity(tok: &str) -> bool {
339    matches!(tok, "PerTensor" | "PerToken" | "PerChannel")
340}
341
342// ===========================================================================
343// Public entry points
344// ===========================================================================
345
346/// Run the full `V-FKC-*` battery over a parsed file. The caller (the lint /
347/// `import_bundle_str`) runs this after parse so a structurally-bad contract
348/// fails import. Stops at the first violation (a typed `FkcError`).
349pub fn validate_file(file: &FkcFile) -> Result<(), FkcError> {
350    // Rule 1: format version supported (membership, not `<= max` — KISS #450).
351    if !fkc_version_is_supported(file.front_matter.fkc_version) {
352        return Err(FkcError::UnsupportedVersion {
353            found: file.front_matter.fkc_version,
354            supported: FKC_SUPPORTED_VERSIONS,
355        });
356    }
357    for kernel in &file.kernels {
358        match validate_kernel(kernel) {
359            Ok(()) => {}
360            // §3.10 + §14/§6: a describe-only (`registrable: false`) section is
361            // documentation — it carries no dispatch target and is excluded from
362            // lowering/registration (`lower_file` filters it). It may legitimately
363            // trip a CONSUMER-AHEAD gate — an `fdx.gather` operand
364            // (`GatherNotYetSupported`, rule 14) or an MX/AFFINE_BLOCK quant
365            // (`MxNotYetRegistrable`, rule 6) — which is a CORRECT
366            // "describable-but-not-yet-registrable" outcome, NOT a defect. Such a
367            // describe-only gate MUST NOT block a bundle's importable sections, so
368            // it is swallowed HERE (the same "deferred" posture the corpus CI lint
369            // takes). Every OTHER error still fails import — a describe-only
370            // section's DESCRIPTIVE checks (bad dtype, incoherent layout, …) are
371            // still enforced, and a REGISTRABLE section's consumer-ahead gate still
372            // fails (it WOULD try to register).
373            Err(FkcError::GatherNotYetSupported { .. } | FkcError::MxNotYetRegistrable { .. })
374                if !kernel.registrable => {}
375            Err(e) => return Err(e),
376        }
377    }
378    Ok(())
379}
380
381/// Validate one parsed kernel section against the structural + coherence
382/// rules (every rule that operates on the parsed form). The duplicate-
383/// `KernelRef` rule (10) is a *register-time* check (`finalize`), not here.
384pub fn validate_kernel(kernel: &FkcKernel) -> Result<(), FkcError> {
385    let section = kernel.kernel.as_str();
386
387    // §3.10 (rule 17): a describe-only section is documentation. SKIP the
388    // dispatch-resolution checks (exactly-one-of op_kind/fused_op, op resolves,
389    // op-param namespace) but STILL run every descriptive check below.
390    let describe_only = !kernel.registrable;
391
392    // Rule 2: required fields (the describe-only exceptions are applied inside).
393    required_fields(kernel, section, describe_only)?;
394
395    // Determine the op target namespace (primitive vs fused) for rule 7. For a
396    // describe-only section there is no resolved dispatch target, so the
397    // namespace check is skipped (and the op token may be `~` / descriptive).
398    let is_fused = if describe_only {
399        // No dispatch resolution; op_kind/fused_op need not name a real target.
400        // (A descriptive token like `binary`, or `~`, is legal here — §3.10.)
401        false
402    } else {
403        match (kernel.op_kind.as_deref(), kernel.fused_op.as_deref()) {
404            (Some(op), None) => {
405                // Rule 2/3: op_kind token resolves (reuse the lower table).
406                lower::lower_op_kind(op, section)?;
407                false
408            }
409            (None, Some(fused)) => {
410                // fused_op token resolves through the SCREAMING_SNAKE table.
411                lower::lower_fused_op(fused, section)?;
412                true
413            }
414            (op, fused) => {
415                return Err(FkcError::OpTargetAmbiguous {
416                    section: section.to_string(),
417                    op_kind: op.map(String::from),
418                    fused_op: fused.map(String::from),
419                });
420            }
421        }
422    };
423
424    // Per-operand checks (rules 3, 4, 5, 6, 14, 15, 16) — descriptive, run for
425    // describe-only sections too.
426    if let Some(accept) = &kernel.accept {
427        for d in &accept.inputs {
428            let operand = d.name.as_deref().unwrap_or("<input>").to_string();
429            validate_operand(section, &operand, d, accept)?;
430        }
431        // Rule 7: op-param variant in the correct namespace — SKIPPED for a
432        // describe-only section (no resolved dispatch target to bind against).
433        if !describe_only
434            && let Some(op_params) = &accept.op_params
435            && let Some(variant) = op_params.variant.as_deref()
436        {
437            validate_op_params_namespace(section, variant, is_fused)?;
438        }
439    }
440
441    // Rule 16 also covers the OUTPUT dtype tokens (`fixed(DType)` rules) — and
442    // rule 13 covers bundle slot ranks. Returns are validated here.
443    if let Some(ret) = &kernel.return_ {
444        for out in &ret.outputs {
445            // `fixed(DType)` output dtype rules name a dtype token; resolve it
446            // through the lower table (also covers rule 16 dtype membership).
447            if let Some(rule) = out.dtype_rule.as_deref() {
448                check_output_dtype_rule(section, out.name.as_deref().unwrap_or("<output>"), rule)?;
449            }
450        }
451        // Rule 13: bundle slot rank ≤ 6 (when a static rank is declarable).
452        if let Some(bundle) = &ret.bundle {
453            check_bundle_ranks(section, bundle)?;
454        }
455    }
456
457    // Rule 8/8a: cost expressions parse + provenance present + non-placeholder.
458    validate_cost(kernel, section)?;
459
460    // Rule 9 (precision coverage): nondeterministic ⇒ audited + no bit-stable.
461    validate_precision_coherence(kernel, section)?;
462
463    Ok(())
464}
465
466// ===========================================================================
467// Rule 2 — required fields
468// ===========================================================================
469
470fn required_fields(kernel: &FkcKernel, section: &str, describe_only: bool) -> Result<(), FkcError> {
471    // kernel name is always present (it is the struct's required field).
472    // exactly-one-of op_kind/fused_op is checked in validate_kernel's match.
473
474    // blurb: a non-empty one-line string.
475    match kernel.blurb.as_deref() {
476        Some(b) if !b.trim().is_empty() => {}
477        _ => {
478            return Err(FkcError::MissingBlurb {
479                section: section.to_string(),
480            });
481        }
482    }
483
484    // entry_point.
485    require(kernel.entry_point.as_deref(), section, "entry_point")?;
486
487    // ≥1 accept.inputs — EXCEPT a describe-only section (§3.10): a
488    // documentation-only zero-operand op is legitimate (a zero-input random
489    // fill like `rand_uniform`/`rand_normal` whose only "input" is backend-
490    // private RNG state, not a graph tensor). The ≥1-input requirement is
491    // still enforced for every REGISTRABLE section (a real dispatch target
492    // must consume at least one graph operand).
493    if !describe_only {
494        let has_inputs = kernel
495            .accept
496            .as_ref()
497            .map(|a| !a.inputs.is_empty())
498            .unwrap_or(false);
499        if !has_inputs {
500            return Err(FkcError::MissingRequiredField {
501                section: section.to_string(),
502                field: "accept.inputs (≥1)".to_string(),
503            });
504        }
505    }
506
507    // ≥1 return.outputs OR a bundle.
508    let has_outputs = kernel
509        .return_
510        .as_ref()
511        .map(|r| !r.outputs.is_empty() || r.bundle.is_some())
512        .unwrap_or(false);
513    if !has_outputs {
514        return Err(FkcError::MissingRequiredField {
515            section: section.to_string(),
516            field: "return.outputs (≥1) or return.bundle".to_string(),
517        });
518    }
519
520    // a cost block.
521    if kernel.cost.is_none() {
522        return Err(FkcError::MissingRequiredField {
523            section: section.to_string(),
524            field: "cost".to_string(),
525        });
526    }
527    // a precision block.
528    if kernel.precision.is_none() {
529        return Err(FkcError::MissingRequiredField {
530            section: section.to_string(),
531            field: "precision".to_string(),
532        });
533    }
534    // determinism.
535    require(kernel.determinism.as_deref(), section, "determinism")?;
536
537    Ok(())
538}
539
540fn require(v: Option<&str>, section: &str, field: &str) -> Result<(), FkcError> {
541    match v {
542        Some(s) if !s.trim().is_empty() => Ok(()),
543        _ => Err(FkcError::MissingRequiredField {
544            section: section.to_string(),
545            field: field.to_string(),
546        }),
547    }
548}
549
550// ===========================================================================
551// Per-operand: rules 3, 4, 5, 6, 14, 15, 16
552// ===========================================================================
553
554fn validate_operand(
555    section: &str,
556    operand: &str,
557    d: &TensorDesc,
558    accept: &crate::fkc::schema::AcceptBlock,
559) -> Result<(), FkcError> {
560    // Rule 3 / Rule 16: every dtype token is a real DType (and ∈ FDX table).
561    for tok in &d.dtypes {
562        lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
563            section: section.to_string(),
564            field: format!("{operand}.dtypes"),
565            token: tok.clone(),
566        })?;
567    }
568
569    // Rule 4: layout coherence.
570    if let Some(layout) = &d.layout {
571        layout_coherence(section, operand, layout)?;
572    }
573
574    // Rule 5: awkward-layout coherence PER OPERAND (effective strategy).
575    awkward_strategy_coherence(section, operand, d, accept)?;
576
577    // Rule 3/6/16: quant coherence (incl. sub-byte ⇒ fdx.quant, scale single-
578    // place, ggml/family/granularity membership, MX/AFFINE_BLOCK gate).
579    if let Some(fdx) = &d.fdx {
580        // Rule 3: a sub-byte base dtype MUST carry an fdx.quant block.
581        let is_sub_byte = d
582            .dtypes
583            .iter()
584            .any(|t| matches!(t.as_str(), "F4" | "F6E2M3" | "F6E3M2"));
585        if is_sub_byte && fdx.quant.is_none() && fdx.sub_byte.is_none() {
586            return Err(FkcError::QuantIncoherent {
587                section: section.to_string(),
588                operand: operand.to_string(),
589                reason: "sub-byte dtype requires an fdx.sub_byte + fdx.quant block (no reliance \
590                         on size_in_bytes()==0)"
591                    .to_string(),
592            });
593        }
594
595        if let Some(quant) = &fdx.quant {
596            quant_coherence(section, operand, quant, accept)?;
597        }
598
599        // Rule 14: gather (paged) coherence.
600        if let Some(gather) = &fdx.gather {
601            gather_coherence(section, operand, gather, fdx, accept)?;
602        }
603
604        // Rule 15: affine / symbolic extent coherence.
605        extent_coherence(section, operand, fdx)?;
606    }
607
608    Ok(())
609}
610
611// --- Rule 4: layout coherence ---
612
613fn layout_coherence(
614    section: &str,
615    operand: &str,
616    layout: &crate::fkc::schema::LayoutSpec,
617) -> Result<(), FkcError> {
618    use crate::fkc::caps_map::{Tri, resolve_layout};
619    // Parse the five flags (also validates each value token; bad value →
620    // BadLayoutFlag from resolve_layout).
621    let r = resolve_layout(Some(layout), section, operand)?;
622
623    let contiguous_ok = matches!(r.contiguous, Tri::Required | Tri::Accepted);
624    let strided_ok = matches!(r.strided, Tri::Accepted);
625
626    // At least one of contiguous / strided must be acceptable.
627    if !contiguous_ok && !strided_ok {
628        return Err(FkcError::LayoutIncoherent {
629            section: section.to_string(),
630            operand: operand.to_string(),
631            reason: "neither `contiguous` (required|accepted) nor `strided: accepted` is set — \
632                     the kernel accepts no layout"
633                .to_string(),
634        });
635    }
636
637    // broadcast_stride0: accepted ⇒ strided: accepted.
638    if matches!(r.broadcast_stride0, Tri::Accepted) && !strided_ok {
639        return Err(FkcError::LayoutIncoherent {
640            section: section.to_string(),
641            operand: operand.to_string(),
642            reason: "`broadcast_stride0: accepted` requires `strided: accepted` (broadcast is a \
643                     stride-0 special case)"
644                .to_string(),
645        });
646    }
647    // reverse_strides: accepted ⇒ strided: accepted.
648    if matches!(r.reverse_strides, Tri::Accepted) && !strided_ok {
649        return Err(FkcError::LayoutIncoherent {
650            section: section.to_string(),
651            operand: operand.to_string(),
652            reason: "`reverse_strides: accepted` requires `strided: accepted` (a negative stride \
653                     is still a strided walk)"
654                .to_string(),
655        });
656    }
657
658    // broadcast_axes ⟺ broadcast_stride0: required (§6-additive mask). The
659    // mask names the iteration axes the operand must be stride-0 on; it is
660    // meaningful ONLY for a REQUIRED (baked) broadcast, and REQUIRED needs it
661    // — the shape-blind (op, dtypes, backend) binder can't tell a correctly-
662    // broadcast operand from a wrongly-shaped one without the axis set. So:
663    // present iff required, and non-empty when required.
664    match (
665        &layout.broadcast_axes,
666        matches!(r.broadcast_stride0, Tri::Required),
667    ) {
668        (Some(axes), true) if axes.is_empty() => {
669            return Err(FkcError::LayoutIncoherent {
670                section: section.to_string(),
671                operand: operand.to_string(),
672                reason: "`broadcast_stride0: required` with an EMPTY `broadcast_axes` — name the \
673                         iteration axes the operand must be stride-0 on"
674                    .to_string(),
675            });
676        }
677        (Some(_), false) => {
678            return Err(FkcError::LayoutIncoherent {
679                section: section.to_string(),
680                operand: operand.to_string(),
681                reason: "`broadcast_axes` set without `broadcast_stride0: required` — the mask is \
682                         meaningful only for a required (baked) broadcast"
683                    .to_string(),
684            });
685        }
686        (None, true) => {
687            return Err(FkcError::LayoutIncoherent {
688                section: section.to_string(),
689                operand: operand.to_string(),
690                reason: "`broadcast_stride0: required` needs `broadcast_axes` — the shape-blind \
691                         binder can't verify the broadcast without the axis mask"
692                    .to_string(),
693            });
694        }
695        _ => {}
696    }
697    Ok(())
698}
699
700// --- Rule 5: awkward-layout strategy coherence (per operand) ---
701
702fn awkward_strategy_coherence(
703    section: &str,
704    operand: &str,
705    d: &TensorDesc,
706    _accept: &crate::fkc::schema::AcceptBlock,
707) -> Result<(), FkcError> {
708    use crate::fkc::caps_map::{Tri, resolve_layout};
709
710    // Effective strategy: per-operand override, else (handled by caller for the
711    // kernel-wide default — but the per-operand check only needs the per-operand
712    // value when present, since the kernel-wide default is validated against
713    // each operand by the caps default below).
714    let per_operand = d
715        .layout
716        .as_ref()
717        .and_then(|l| l.awkward_layout_strategy.as_deref());
718
719    let Some(strategy) = per_operand else {
720        // No per-operand override: inheritance from the kernel-wide default is
721        // checked at the kernel level via the operands' flags; nothing to do.
722        return Ok(());
723    };
724
725    let r = resolve_layout(d.layout.as_ref(), section, operand)?;
726    match strategy {
727        "handles_strided" => {
728            if !matches!(r.strided, Tri::Accepted) {
729                return Err(FkcError::AwkwardStrategyIncoherent {
730                    section: section.to_string(),
731                    operand: operand.to_string(),
732                    strategy: strategy.to_string(),
733                    reason: "`handles_strided` requires `strided: accepted` on this operand"
734                        .to_string(),
735                });
736            }
737        }
738        "requires_contiguous" => {
739            if !matches!(r.contiguous, Tri::Required) {
740                return Err(FkcError::AwkwardStrategyIncoherent {
741                    section: section.to_string(),
742                    operand: operand.to_string(),
743                    strategy: strategy.to_string(),
744                    reason: "`requires_contiguous` requires `contiguous: required` on this operand"
745                        .to_string(),
746                });
747            }
748        }
749        "contiguize_internally" => {
750            // Folds this operand's copy into the kernel's bytes_moved; it must
751            // at least *accept* strided input (otherwise there is nothing to
752            // contiguize internally).
753            if !matches!(r.strided, Tri::Accepted) {
754                return Err(FkcError::AwkwardStrategyIncoherent {
755                    section: section.to_string(),
756                    operand: operand.to_string(),
757                    strategy: strategy.to_string(),
758                    reason: "`contiguize_internally` requires `strided: accepted` on this operand \
759                             (it accepts strided then copies internally)"
760                        .to_string(),
761                });
762            }
763        }
764        other => {
765            return Err(FkcError::AwkwardStrategyIncoherent {
766                section: section.to_string(),
767                operand: operand.to_string(),
768                strategy: other.to_string(),
769                reason: "unknown awkward_layout_strategy (meaning-bearing — §11.1)".to_string(),
770            });
771        }
772    }
773    Ok(())
774}
775
776// --- Rule 6 + 16: quant coherence ---
777
778fn quant_coherence(
779    section: &str,
780    operand: &str,
781    quant: &QuantSpec,
782    accept: &crate::fkc::schema::AcceptBlock,
783) -> Result<(), FkcError> {
784    let family = quant.family.as_deref().unwrap_or("none");
785
786    // Rule 16: family token ∈ FDX table.
787    if !is_fdx_quant_family(family) {
788        return Err(FkcError::FdxTokenNotInTable {
789            section: section.to_string(),
790            field: format!("{operand}.fdx.quant.family"),
791            token: family.to_string(),
792        });
793    }
794    // Rule 16: granularity token ∈ FDX table (when present, non-null).
795    if let Some(g) = quant.granularity.as_deref()
796        && !is_fdx_granularity(g)
797    {
798        return Err(FkcError::FdxTokenNotInTable {
799            section: section.to_string(),
800            field: format!("{operand}.fdx.quant.granularity"),
801            token: g.to_string(),
802        });
803    }
804    // Rule 16 + 3: ggml_dtype is a real GgmlDType variant (by code).
805    if let Some(g) = quant.ggml_dtype.as_deref()
806        && ggml_dtype_code(g).is_none()
807    {
808        // `Q4_K_M` (GGUF name, NOT a variant) lands here.
809        return Err(FkcError::QuantIncoherent {
810            section: section.to_string(),
811            operand: operand.to_string(),
812            reason: format!(
813                "ggml_dtype `{g}` is not a real GgmlDType variant (matched by code; \
814                     `Q4_K_M` is a GGUF file-format name → use `Q4K`)"
815            ),
816        });
817    }
818
819    // Scale single-place rule: a separate scale_operand XOR a sidecar scale,
820    // never both for the same scale. We model the "sidecar scale" as a
821    // sidecar `scale_buffer` (not in the schema today) — so we check that
822    // `scale_operand`, when present, names a REAL accept.inputs role and that
823    // it is not also a GGML INLINE family (GGML scales are baked, never a
824    // separate operand) — that is the double-declaration we can detect here.
825    if let Some(scale_role) = quant.scale_operand.as_deref() {
826        // The role must be a real input operand (single-place: it IS the
827        // authority, so it must exist).
828        let names_real = accept
829            .inputs
830            .iter()
831            .any(|i| i.name.as_deref() == Some(scale_role));
832        if !names_real {
833            return Err(FkcError::ScaleDoubleDeclared {
834                section: section.to_string(),
835                operand: operand.to_string(),
836                reason: format!(
837                    "scale_operand `{scale_role}` does not name a real accept.inputs role"
838                ),
839            });
840        }
841        // GGML_BLOCK scales are INLINE-baked; a separate scale_operand for a
842        // GGML family is the double-declaration §10.6 forbids.
843        if family == "GGML_BLOCK" {
844            return Err(FkcError::ScaleDoubleDeclared {
845                section: section.to_string(),
846                operand: operand.to_string(),
847                reason: "GGML_BLOCK bakes its scale INLINE (per ggml_dtype) — it must NOT also \
848                         declare a separate scale_operand"
849                    .to_string(),
850            });
851        }
852    }
853
854    // Per-family coherence (§10.6).
855    match family {
856        "none" | "NONE" => {
857            // Dense — no ggml_dtype, no granularity, no scale.
858        }
859        "GGML_BLOCK" => {
860            // Requires a real ggml_dtype; carries NO granularity / NO PerBlock.
861            if quant.ggml_dtype.is_none() {
862                return Err(FkcError::QuantIncoherent {
863                    section: section.to_string(),
864                    operand: operand.to_string(),
865                    reason: "GGML_BLOCK requires a `ggml_dtype` (the block format)".to_string(),
866                });
867            }
868            if quant.granularity.is_some() {
869                return Err(FkcError::QuantIncoherent {
870                    section: section.to_string(),
871                    operand: operand.to_string(),
872                    reason: "GGML_BLOCK carries NO granularity (the scale is baked INLINE per \
873                             ggml_dtype; no ScalePair, no PerBlock)"
874                        .to_string(),
875                });
876            }
877        }
878        "AFFINE_INT" | "AFFINE_FLOAT" => {
879            // granularity ∈ {PerTensor, PerToken, PerChannel} (NOT PerBlock).
880            match quant.granularity.as_deref() {
881                Some(g) if is_registrable_granularity(g) => {}
882                Some("PerBlock") => {
883                    return Err(FkcError::QuantIncoherent {
884                        section: section.to_string(),
885                        operand: operand.to_string(),
886                        reason: format!(
887                            "{family} granularity must be PerTensor|PerToken|PerChannel \
888                             (PerBlock is MX-only)"
889                        ),
890                    });
891                }
892                _ => {
893                    return Err(FkcError::QuantIncoherent {
894                        section: section.to_string(),
895                        operand: operand.to_string(),
896                        reason: format!(
897                            "{family} requires granularity ∈ PerTensor|PerToken|PerChannel"
898                        ),
899                    });
900                }
901            }
902        }
903        "MX" => {
904            // MX ⇒ granularity: PerBlock; parse-validates but NOT registrable.
905            if quant.granularity.as_deref() != Some("PerBlock") {
906                return Err(FkcError::QuantIncoherent {
907                    section: section.to_string(),
908                    operand: operand.to_string(),
909                    reason: "MX requires granularity: PerBlock (F8E8M0 per-block scale)"
910                        .to_string(),
911                });
912            }
913            return Err(FkcError::MxNotYetRegistrable {
914                section: section.to_string(),
915                family: "MX".to_string(),
916                reason: "no ScaleGranularity::PerBlock target type yet (§6)".to_string(),
917            });
918        }
919        "AFFINE_BLOCK" => {
920            // Block geometry present — EITHER `block_shape` OR a block-shaped
921            // SEPARATE scale operand (§10.6: "block geometry present
922            // (`block_shape` / a block-shaped separate scale operand)"). Both
923            // forms are legal; a contract that carries the per-block absmax as
924            // a separate `scale_operand` (the single-place SEPARATE_BUFFER
925            // form) does not also need an inline `block_shape`. Granularity is
926            // NOT PerBlock (its grain rides block_shape / the absmax operand).
927            // Parse-validates but is NOT registrable yet (no block-quant
928            // descriptor target).
929            let has_block_shape = quant.block_shape.is_some();
930            let has_separate_scale = quant.scale_operand.is_some();
931            if !has_block_shape && !has_separate_scale {
932                return Err(FkcError::QuantIncoherent {
933                    section: section.to_string(),
934                    operand: operand.to_string(),
935                    reason: "AFFINE_BLOCK requires block geometry — either `block_shape` or a \
936                             block-shaped separate `scale_operand` (§10.6)"
937                        .to_string(),
938                });
939            }
940            if quant.granularity.as_deref() == Some("PerBlock") {
941                return Err(FkcError::QuantIncoherent {
942                    section: section.to_string(),
943                    operand: operand.to_string(),
944                    reason: "AFFINE_BLOCK must NOT use granularity: PerBlock (PerBlock stays \
945                             MX-only; its grain rides block_shape)"
946                        .to_string(),
947                });
948            }
949            return Err(FkcError::MxNotYetRegistrable {
950                section: section.to_string(),
951                family: "AFFINE_BLOCK".to_string(),
952                reason: "no block-quant descriptor target type yet (§6)".to_string(),
953            });
954        }
955        _ => unreachable!("family membership already checked against the FDX table"),
956    }
957
958    Ok(())
959}
960
961// --- Rule 14: gather (paged) coherence ---
962
963fn gather_coherence(
964    section: &str,
965    operand: &str,
966    gather: &crate::fkc::schema::GatherSpec,
967    fdx: &crate::fkc::schema::FdxSpec,
968    accept: &crate::fkc::schema::AcceptBlock,
969) -> Result<(), FkcError> {
970    let Some(kind) = gather.kind.as_deref() else {
971        // `kind: ~` ⇒ no gather declared.
972        return Ok(());
973    };
974    match kind {
975        "paged_blocks" => {
976            // (a) requires_ext: true.
977            if fdx.requires_ext != Some(true) {
978                return Err(FkcError::GatherIncoherent {
979                    section: section.to_string(),
980                    operand: operand.to_string(),
981                    reason: "paged_blocks requires `fdx.requires_ext: true`".to_string(),
982                });
983            }
984            // (b) symbolic_extent: required.
985            if fdx.symbolic_extent.as_deref() != Some("required") {
986                return Err(FkcError::GatherIncoherent {
987                    section: section.to_string(),
988                    operand: operand.to_string(),
989                    reason: "paged_blocks requires `fdx.symbolic_extent: required`".to_string(),
990                });
991            }
992            // (c) block_table / context_lens, when non-~, name real input roles.
993            for (label, role) in [
994                ("block_table", gather.block_table.as_deref()),
995                ("context_lens", gather.context_lens.as_deref()),
996            ] {
997                if let Some(role) = role {
998                    let exists = accept
999                        .inputs
1000                        .iter()
1001                        .any(|i| i.name.as_deref() == Some(role));
1002                    if !exists {
1003                        return Err(FkcError::GatherIncoherent {
1004                            section: section.to_string(),
1005                            operand: operand.to_string(),
1006                            reason: format!(
1007                                "gather.{label} `{role}` does not name a real accept.inputs role"
1008                            ),
1009                        });
1010                    }
1011                }
1012            }
1013            // IMPORT-SIDE LIFT (FDX gather-sidecar arc, slice A). The `gather`
1014            // block is boundary METADATA that DESCRIBES what an FDX view of this
1015            // operand will someday carry (`FDXIndexedResidency`, FDX §6.9 —
1016            // "Description only: no cost, no decision"). Registration/dispatch of
1017            // the consuming op (`OpKind::PagedAttn`) does NOT depend on it: the
1018            // as-built ABI passes `block_table` / `context_lens` as ORDINARY U32
1019            // graph inputs (named by the coherence check above) + the geometry in
1020            // `OpParams::PagedAttn`; the kernel reads them directly, never through
1021            // an FDX gather view. So a COHERENT `paged_blocks` operand is
1022            // registrable NOW — we validated its internal coherence (kind /
1023            // requires_ext / symbolic_extent / real block_table+context_lens
1024            // roles); nothing further is a build-time gate. What remains
1025            // [consumer-ahead] is the FDX VIEW layer that would MATERIALIZE this
1026            // descriptor at the kernel boundary (`view_with_gather` +
1027            // `Capability::DlpackExtGather` direct-admission, FKC §3.9.1) — a
1028            // separate seam with no consumer yet, NOT the import gate. An
1029            // INCOHERENT gather still errors above (`GatherIncoherent`), never a
1030            // silent pass. (`GatherNotYetSupported` stays a reserved variant for
1031            // a future ragged/CSR `kind >= 2` that FKC cannot yet key.)
1032            Ok(())
1033        }
1034        other => Err(FkcError::UnknownAdmissibilityEnum {
1035            section: section.to_string(),
1036            field: format!("{operand}.fdx.gather.kind"),
1037            value: other.to_string(),
1038        }),
1039    }
1040}
1041
1042// --- Rule 15: affine / symbolic extent coherence ---
1043
1044fn extent_coherence(
1045    section: &str,
1046    operand: &str,
1047    fdx: &crate::fkc::schema::FdxSpec,
1048) -> Result<(), FkcError> {
1049    // symbolic_extent ∈ {rejected, tolerated, required} (when present).
1050    if let Some(se) = fdx.symbolic_extent.as_deref()
1051        && !matches!(se, "rejected" | "tolerated" | "required")
1052    {
1053        return Err(FkcError::UnknownAdmissibilityEnum {
1054            section: section.to_string(),
1055            field: format!("{operand}.fdx.symbolic_extent"),
1056            value: se.to_string(),
1057        });
1058    }
1059    // extent_kind ∈ {rejected, scalar, range, affine} (when present).
1060    if let Some(ek) = fdx.extent_kind.as_deref() {
1061        if !matches!(ek, "rejected" | "scalar" | "range" | "affine") {
1062            return Err(FkcError::UnknownAdmissibilityEnum {
1063                section: section.to_string(),
1064                field: format!("{operand}.fdx.extent_kind"),
1065                value: ek.to_string(),
1066            });
1067        }
1068        // extent_kind: range|affine ⇒ symbolic_extent: required.
1069        if matches!(ek, "range" | "affine") && fdx.symbolic_extent.as_deref() != Some("required") {
1070            return Err(FkcError::UnknownAdmissibilityEnum {
1071                section: section.to_string(),
1072                field: format!("{operand}.fdx.extent_kind"),
1073                value: format!(
1074                    "{ek} requires symbolic_extent: required (got {:?})",
1075                    fdx.symbolic_extent
1076                ),
1077            });
1078        }
1079    }
1080    Ok(())
1081}
1082
1083// ===========================================================================
1084// Rule 7 — op-param namespace
1085// ===========================================================================
1086
1087/// An `op_kind` contract names an `OpParams` variant; a `fused_op` contract
1088/// names a `FusedOpParams` variant (§3.7, §10.7). The two namespaces are
1089/// distinct; checking the wrong one is `BadOpParamsVariant`.
1090fn validate_op_params_namespace(
1091    section: &str,
1092    variant: &str,
1093    is_fused: bool,
1094) -> Result<(), FkcError> {
1095    let ok = if is_fused {
1096        is_fused_op_params_variant(variant)
1097    } else {
1098        is_op_params_variant(variant)
1099    };
1100    if ok {
1101        Ok(())
1102    } else {
1103        Err(FkcError::BadOpParamsVariant {
1104            section: section.to_string(),
1105            variant: variant.to_string(),
1106            namespace: if is_fused {
1107                "FusedOpParams"
1108            } else {
1109                "OpParams"
1110            }
1111            .to_string(),
1112        })
1113    }
1114}
1115
1116// --- V-FKC-5: the op-param allowlists, tied to the real enums -------------
1117//
1118// These two tables were hand-typed string literals with no link back to
1119// `OpParams`/`FusedOpParams`, and they drifted exactly as that shape
1120// predicts: `OpParams` grew `JitScalars` and `NonZeroIndices` and the
1121// allowlist stayed 42-of-44, so a contract naming the real `NonZeroIndices`
1122// variant was rejected as if it were a typo. A test asserting "the list is
1123// complete" would only have moved the drift from the list to the test, so
1124// the list is now GENERATED, from a declaration the compiler checks against
1125// the enum itself: `variant_table!` emits both an exhaustive `match` (adding,
1126// removing, or renaming a variant is a COMPILE error, not a silent
1127// divergence) and the `&[(name, exclusion)]` table the predicate reads. One
1128// source of truth, and drift is structurally impossible rather than
1129// test-detected.
1130//
1131// `None` in the second column = a contract may name this variant.
1132// `Some(reason)` = deliberately NOT contract-authorable, with the reason
1133// recorded at the entry — the distinction the old silent omission destroyed.
1134
1135/// Emit (a) a never-called exhaustive `match` over `$enum`, so the variant
1136/// list below cannot drift from the enum without failing to compile, and
1137/// (b) the `&[(&str, Option<&str>)]` table the `is_*_variant` predicate
1138/// reads. See the V-FKC-5 note above.
1139macro_rules! variant_table {
1140    ($enum:ident, $guard:ident, $table:ident, $($v:ident => $excluded:expr),+ $(,)?) => {
1141        /// Never called — it exists so the compiler checks the variant list
1142        /// below against the real enum. A new variant makes this match
1143        /// non-exhaustive; a removed or renamed one makes an arm unresolvable.
1144        fn $guard(p: &$enum) {
1145            match p { $( $enum::$v { .. } => (), )+ }
1146        }
1147
1148        /// `(variant name, why it is NOT contract-authorable)`.
1149        const $table: &[(&str, Option<&str>)] = &[ $( (stringify!($v), $excluded) ),+ ];
1150    };
1151}
1152
1153variant_table!(
1154    OpParams,
1155    _op_params_is_exhaustive,
1156    OP_PARAMS_VARIANTS,
1157    None => None,
1158    Reduce => None,
1159    Matmul => None,
1160    Conv1D => None,
1161    Conv2D => None,
1162    ConvTranspose1D => None,
1163    ConvTranspose2D => None,
1164    ReduceSumTo => None,
1165    ReduceMaxTo => None,
1166    ReduceMaxToBackward => None,
1167    FlashAttn => None,
1168    PagedAttn => None,
1169    Slice => None,
1170    Cast => None,
1171    Affine => None,
1172    Clamp => None,
1173    PowI => None,
1174    Concat => None,
1175    SoftmaxLastDim => None,
1176    NormLastDim => None,
1177    IndexSelect => None,
1178    Gather => None,
1179    Rope => None,
1180    QMatMul => None,
1181    IndexAdd => None,
1182    ScatterAdd => None,
1183    Flip => None,
1184    Roll => None,
1185    CumSum => None,
1186    Triangular => None,
1187    LogSoftmaxLastDim => None,
1188    MaskedFill => None,
1189    PadBackward => None,
1190    Pad => None,
1191    WriteSlice => None,
1192    WriteSliceRotating => None,
1193    WriteSliceDoff => None,
1194    FusedSoftmaxCrossEntropy => None,
1195    CausalConv1d => None,
1196    SelectiveScan => None,
1197    SsdChunkScan => None,
1198    Nf4Matmul => None,
1199    // A JIT-synthesized runtime op's live scalar args. `kernel.rs` pins the
1200    // producer: "Only produced by `compile_one`'s `is_runtime` arm" — the
1201    // kernel is synthesized at run time, so there is no checked-in source
1202    // file for a `.fkc.md` contract to describe.
1203    JitScalars => Some("runtime-JIT-only: no checked-in kernel to contract"),
1204    NonZeroIndices => None,
1205);
1206
1207variant_table!(
1208    FusedOpParams,
1209    _fused_op_params_is_exhaustive,
1210    FUSED_OP_PARAMS_VARIANTS,
1211    SoftmaxLastDim => None,
1212    FusedLinear => None,
1213    RmsNormLastDim => None,
1214    LayerNormLastDim => None,
1215    Rope => None,
1216    Conv2D => None,
1217    SoftmaxLastDimBackward => None,
1218    LayerNormLastDimBackward => None,
1219    RmsNormLastDimBackward => None,
1220    ReduceMaxToBackward => None,
1221    PowIBackward => None,
1222    ConvTranspose2D => None,
1223    FlashAttn => None,
1224    PagedAttn => None,
1225    QMatMul => None,
1226    InplaceAffine => None,
1227    SsdChunkScan => None,
1228    Nf4Matmul => None,
1229    AwqMatmul => None,
1230    FlashAttnBackward => None,
1231    SelectiveScan => None,
1232    CausalConv1d => None,
1233    FusedSoftmaxCrossEntropy => None,
1234    // A runtime-REGISTERED fused op (JIT-synthesized or import-time). Its
1235    // identity is a runtime `FusedOpId` and its recipe lives in the
1236    // `runtime_fused` sidecar, so it has no stable name a hand-authored
1237    // contract could bind to.
1238    Runtime => Some("runtime-registered: identity is a runtime FusedOpId"),
1239);
1240
1241/// Whether a contract may name `v` as an `OpParams` variant. Backed by
1242/// [`OP_PARAMS_VARIANTS`], which the compiler holds against the real
1243/// `OpParams` enum (`fuel-dispatch/src/kernel.rs`).
1244fn is_op_params_variant(v: &str) -> bool {
1245    contract_authorable(OP_PARAMS_VARIANTS, v)
1246}
1247
1248/// Whether a contract may name `v` as a `FusedOpParams` variant. Backed by
1249/// [`FUSED_OP_PARAMS_VARIANTS`], which the compiler holds against the real
1250/// `FusedOpParams` enum (`fuel-graph/src/registry.rs`).
1251fn is_fused_op_params_variant(v: &str) -> bool {
1252    contract_authorable(FUSED_OP_PARAMS_VARIANTS, v)
1253}
1254
1255/// `v` names a real variant in `table` AND that variant carries no exclusion
1256/// reason. An excluded variant is rejected exactly like an unknown one — the
1257/// contract still cannot name it — but the reason is recorded at the entry
1258/// rather than being an unexplained absence.
1259fn contract_authorable(table: &[(&str, Option<&str>)], v: &str) -> bool {
1260    table
1261        .iter()
1262        .any(|(name, excluded)| *name == v && excluded.is_none())
1263}
1264
1265// ===========================================================================
1266// Returns: rule 16 (output dtype) + rule 13 (bundle rank)
1267// ===========================================================================
1268
1269/// A `fixed(DType)` output dtype rule names a dtype token that must be a real
1270/// DType (rule 16). `passthrough(role)` / other rules carry no dtype literal.
1271fn check_output_dtype_rule(section: &str, operand: &str, rule: &str) -> Result<(), FkcError> {
1272    let rule = rule.trim();
1273    if let Some(inner) = rule
1274        .strip_prefix("fixed(")
1275        .and_then(|s| s.strip_suffix(")"))
1276    {
1277        let tok = inner.trim();
1278        lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
1279            section: section.to_string(),
1280            field: format!("{operand}.dtype_rule fixed(...)"),
1281            token: tok.to_string(),
1282        })?;
1283    }
1284    Ok(())
1285}
1286
1287/// Rule 13: every bundle slot's declared shape must be rank ≤ 6 when a static
1288/// shape is given (an inline `[d0, d1, ...]` literal). A `shape_rule:` string
1289/// has no statically-knowable rank without evaluating the rule, so it is not
1290/// rank-checked here — `fkc::return_check::check_slot_rank`, called from
1291/// `cross_check_fused_section` for every bundle slot whose `shape_rule`
1292/// evaluates against a real probe shape, covers that DERIVED case instead
1293/// (Finding 5.3).
1294fn check_bundle_ranks(section: &str, bundle: &serde_yaml_ng::Value) -> Result<(), FkcError> {
1295    let serde_yaml_ng::Value::Sequence(slots) = bundle else {
1296        return Ok(());
1297    };
1298    for (i, slot) in slots.iter().enumerate() {
1299        let serde_yaml_ng::Value::Mapping(map) = slot else {
1300            continue;
1301        };
1302        let slot_name = map
1303            .get(serde_yaml_ng::Value::String("name".into()))
1304            .and_then(|v| v.as_str())
1305            .map(str::to_string)
1306            .unwrap_or_else(|| format!("slot{i}"));
1307        // A static `shape:` literal list is rank-checkable.
1308        if let Some(serde_yaml_ng::Value::Sequence(dims)) =
1309            map.get(serde_yaml_ng::Value::String("shape".into()))
1310            && dims.len() > 6
1311        {
1312            return Err(FkcError::BundleSlotRankExceeded {
1313                section: section.to_string(),
1314                slot: slot_name,
1315                rank: dims.len(),
1316            });
1317        }
1318    }
1319    Ok(())
1320}
1321
1322// ===========================================================================
1323// Rule 8 / 8a — cost
1324// ===========================================================================
1325
1326fn validate_cost(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1327    let Some(cost) = &kernel.cost else {
1328        // required_fields already errored on an absent cost block.
1329        return Ok(());
1330    };
1331
1332    // Rule 8a: provenance present + ∈ {declared, judge_measured}.
1333    match cost.provenance.as_deref() {
1334        Some("declared") | Some("judge_measured") => {}
1335        other => {
1336            return Err(FkcError::CostProvenanceMissing {
1337                section: section.to_string(),
1338                found: other.map(String::from),
1339            });
1340        }
1341    }
1342
1343    // Rule 8: every present cost expression parses (reuse the cost_expr
1344    // parser). A field that is `~`/absent is fine (Unknown).
1345    use crate::fkc::cost_expr;
1346    let parse = |field: &str, src: Option<&str>| -> Result<(), FkcError> {
1347        cost_expr::compile_field(src)
1348            .map(|_| ())
1349            .map_err(|e| FkcError::CostExprParse {
1350                section: section.to_string(),
1351                field: field.to_string(),
1352                expr: src.unwrap_or("").to_string(),
1353                reason: e.to_string(),
1354            })
1355    };
1356    parse("flops", cost.flops.as_deref())?;
1357    parse("bytes_moved", cost.bytes_moved.as_deref())?;
1358    if let Some(serde_yaml_ng::Value::String(s)) = &cost.overhead_ns {
1359        parse("overhead_ns", Some(s))?;
1360    }
1361    if let Some(mem) = &cost.memory {
1362        for (field, v) in [
1363            ("memory.device_bytes", &mem.device_bytes),
1364            ("memory.host_bytes", &mem.host_bytes),
1365            ("memory.disk_bytes", &mem.disk_bytes),
1366        ] {
1367            if let Some(serde_yaml_ng::Value::String(s)) = v {
1368                parse(field, Some(s))?;
1369            }
1370        }
1371    }
1372
1373    // Rule 8a (placeholder): closed classifier (§2.3 / V-FKC-9,
1374    // `cost_compile::classify_cost`). `class: free` is the honest
1375    // metadata-only declaration (license for zero coefficients). A
1376    // `judge_measured` cost legitimately ships shape-hint expressions OR
1377    // all-`~` (the Judge populates it) — the corpus convention is
1378    // `judge_measured` + `~` coefficients, which is NOT a placeholder (it is
1379    // explicitly measured). Otherwise a `declared` cost needs a pinned
1380    // `cost.cost_fn` OR a usable `flops`/`bytes_moved` expression; a
1381    // NON-EMPTY-but-non-`free` `class` alone is NOT a license (closes the old
1382    // `class.is_empty()` escape hatch: a `declared` block with a class but no
1383    // flops/bytes/cost_fn is still a placeholder).
1384    let class = cost.class.as_deref().unwrap_or("");
1385    let has_any_expr = cost.flops.as_deref().is_some_and(|s| !s.trim().is_empty())
1386        || cost
1387            .bytes_moved
1388            .as_deref()
1389            .is_some_and(|s| !s.trim().is_empty());
1390    let has_cost_fn = cost
1391        .cost_fn
1392        .as_deref()
1393        .is_some_and(|s| !s.trim().is_empty());
1394    if cost.provenance.as_deref() == Some("declared")
1395        && crate::fkc::cost_compile::classify_cost("declared", class, has_any_expr, has_cost_fn)
1396            .is_none()
1397    {
1398        return Err(FkcError::PlaceholderCost {
1399            section: section.to_string(),
1400            reason: "provenance: declared with no usable cost path (no flops/bytes_moved \
1401                     expression, no pinned cost.cost_fn) and class is not `free`"
1402                .to_string(),
1403        });
1404    }
1405
1406    Ok(())
1407}
1408
1409// ===========================================================================
1410// Rule 9 — precision coherence (determinism cross-check)
1411// ===========================================================================
1412
1413fn validate_precision_coherence(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1414    let det = kernel.determinism.as_deref().unwrap_or("");
1415    if det == "nondeterministic" {
1416        // nondeterministic ⇒ bit_stable=false + audited:true (no silent
1417        // unaudited nondeterminism; §4.9 / §10.9).
1418        if let Some(p) = &kernel.precision {
1419            if p.bit_stable_on_same_hardware == Some(true) {
1420                return Err(FkcError::QuantIncoherent {
1421                    section: section.to_string(),
1422                    operand: "<precision>".to_string(),
1423                    reason: "determinism: nondeterministic requires bit_stable_on_same_hardware = \
1424                             false"
1425                        .to_string(),
1426                });
1427            }
1428            if p.audited != Some(true) {
1429                return Err(FkcError::QuantIncoherent {
1430                    section: section.to_string(),
1431                    operand: "<precision>".to_string(),
1432                    reason: "determinism: nondeterministic requires audited: true with a \
1433                             none(reason) precision (no silent unaudited nondeterminism)"
1434                        .to_string(),
1435                });
1436            }
1437        }
1438    }
1439    Ok(())
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use super::*;
1445    use crate::fkc::parse_file;
1446
1447    // --- A minimal VALID contract builder for unit tests. ---
1448
1449    /// A full, valid single-kernel bundle with the required fields, mutated by
1450    /// the negative tests via string replacement of the `__SLOT__` markers.
1451    fn valid_bundle() -> String {
1452        r#"---
1453fkc_version: 1
1454provider:
1455  name: test-provider
1456  backend: Cpu
1457  kernel_source: "test-cpu"
1458---
1459
1460# test bundle
1461
1462## demo
1463
1464A blurb.
1465
1466```fkc
1467kernel: demo
1468op_kind: AddElementwise
1469blurb: "demo blurb"
1470entry_point: "x::y"
1471accept:
1472  inputs:
1473    - name: lhs
1474      dtypes: [F32]
1475      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1476    - name: rhs
1477      dtypes: [F32]
1478      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1479  op_params: { variant: None }
1480return:
1481  outputs:
1482    - name: out
1483      dtype_rule: passthrough(lhs)
1484caps:
1485  awkward_layout_strategy: requires_contiguous
1486cost:
1487  provenance: declared
1488  class: cheap_elementwise
1489  flops: "n"
1490  bytes_moved: "3 * n * 4"
1491precision:
1492  bit_stable_on_same_hardware: true
1493  audited: true
1494determinism: same_hardware_bitwise
1495```
1496"#
1497        .to_string()
1498    }
1499
1500    fn validate_str(src: &str) -> Result<(), FkcError> {
1501        let file = parse_file(src).expect("fixture parses");
1502        validate_file(&file)
1503    }
1504
1505    #[test]
1506    fn valid_contract_passes() {
1507        validate_str(&valid_bundle()).expect("the valid fixture validates");
1508    }
1509
1510    // ===== Rule 1 =====
1511    #[test]
1512    fn version_too_new_is_unsupported() {
1513        let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 99");
1514        let err = validate_str(&src).expect_err("version 99 unsupported");
1515        assert!(
1516            matches!(err, FkcError::UnsupportedVersion { found: 99, .. }),
1517            "got {err:?}"
1518        );
1519    }
1520
1521    // ===== GAP-292: the accepted set is membership, not `<= max` (KISS #450) =====
1522
1523    /// GAP-292 born-red (LOW side): `fkc_version: 0` MUST be rejected. The pre-fix
1524    /// gate was a bare `fkc_version > FKC_VERSION_MAX`, so 0 (being ≤ max) was
1525    /// silently ACCEPTED — the accepted set was {0, 1} and 0 is not a version.
1526    /// KISS #450: a reader accepts a version IFF it is in its declared supported
1527    /// set, NEVER merely because it is lower than a supported one. This fails on
1528    /// the pre-fix gate (0 passes the `> max` test).
1529    #[test]
1530    fn fkc_version_zero_is_rejected() {
1531        let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 0");
1532        let err = validate_str(&src).expect_err("fkc_version 0 must be rejected");
1533        assert!(
1534            matches!(err, FkcError::UnsupportedVersion { found: 0, .. }),
1535            "expected UnsupportedVersion for fkc_version 0, got {err:?}"
1536        );
1537    }
1538
1539    /// GAP-292 fencepost guard (HIGH-but-sole side, the coverage the pre-fix test
1540    /// set never had): `fkc_version: 1`, the sole supported version, MUST still be
1541    /// accepted after the lower-bound fix. A bound written in haste lands a
1542    /// fencepost exactly here; paired with the low-side test it pins BOTH ends.
1543    #[test]
1544    fn fkc_version_one_is_still_accepted() {
1545        let result = validate_str(&valid_bundle());
1546        assert!(
1547            !matches!(result, Err(FkcError::UnsupportedVersion { .. })),
1548            "fkc_version 1 (sole supported) must pass the version gate, got {result:?}"
1549        );
1550    }
1551
1552    // ===== GAP-038: FKC two version axes (KISS-CONTRACT §8-0001..0007) =====
1553
1554    /// §8-0001: `fkc_version` (wire/schema) and the crate's published semver are
1555    /// INDEPENDENT axes. A crate release that bumps only `Cargo.toml`'s version
1556    /// leaves the wire schema untouched, so `FKC_SUPPORTED_VERSIONS` is a literal —
1557    /// never derived from `CARGO_PKG_VERSION`. If it were, publishing the crate
1558    /// would silently advance the accepted set and reject older contracts.
1559    #[test]
1560    fn fkc_version_is_decoupled_from_crate_semver() {
1561        // A literal evaluated at compile time; the module-scope pin is the
1562        // structural enforcement, this records the decoupling as an executable fact.
1563        assert_eq!(FKC_SUPPORTED_VERSIONS, &[1]);
1564    }
1565
1566    /// §8-0002: the front-matter SCHEMA and the schema version move together. The
1567    /// exhaustive-destructure pin at module scope compile-breaks on a field change;
1568    /// this test pins the *version number* that field set corresponds to — the
1569    /// LATEST supported version (the max of the set) — so the two cannot drift
1570    /// apart. Add a new entry to `FKC_SUPPORTED_VERSIONS` AND bump
1571    /// `PINNED_FKC_SCHEMA_VERSION` when the pin forces a new field.
1572    #[test]
1573    fn fkc_front_matter_schema_is_pinned_to_the_version() {
1574        const PINNED_FKC_SCHEMA_VERSION: u32 = 1;
1575        assert_eq!(
1576            FKC_SUPPORTED_VERSIONS.iter().copied().max(),
1577            Some(PINNED_FKC_SCHEMA_VERSION),
1578            "the latest supported fkc_version moved without updating the pinned \
1579             front-matter schema (or vice-versa) — a §8-0002 schema change bumps both"
1580        );
1581    }
1582
1583    // ===== Rule 2 =====
1584    #[test]
1585    fn missing_blurb_errors() {
1586        let src = valid_bundle().replace("blurb: \"demo blurb\"\n", "");
1587        let err = validate_str(&src).expect_err("no blurb");
1588        assert!(matches!(err, FkcError::MissingBlurb { .. }), "got {err:?}");
1589    }
1590
1591    #[test]
1592    fn missing_cost_block_errors() {
1593        // Remove the whole cost block.
1594        let src = valid_bundle();
1595        let cut = src.replace(
1596            "cost:\n  provenance: declared\n  class: cheap_elementwise\n  flops: \"n\"\n  bytes_moved: \"3 * n * 4\"\n",
1597            "",
1598        );
1599        let err = validate_str(&cut).expect_err("no cost block");
1600        assert!(
1601            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "cost"),
1602            "got {err:?}"
1603        );
1604    }
1605
1606    #[test]
1607    fn missing_determinism_errors() {
1608        let src = valid_bundle().replace("determinism: same_hardware_bitwise\n", "");
1609        let err = validate_str(&src).expect_err("no determinism");
1610        assert!(
1611            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "determinism"),
1612            "got {err:?}"
1613        );
1614    }
1615
1616    #[test]
1617    fn both_op_kind_and_fused_op_is_ambiguous() {
1618        let src = valid_bundle().replace(
1619            "op_kind: AddElementwise",
1620            "op_kind: AddElementwise\nfused_op: SOFTMAX_LAST_DIM",
1621        );
1622        let err = validate_str(&src).expect_err("both targets");
1623        assert!(
1624            matches!(err, FkcError::OpTargetAmbiguous { .. }),
1625            "got {err:?}"
1626        );
1627    }
1628
1629    // ===== Rule 3 / 16 — dtype =====
1630    #[test]
1631    fn bad_dtype_token_is_fdx_token_not_in_table() {
1632        let src = valid_bundle().replace("dtypes: [F32]", "dtypes: [F99]");
1633        let err = validate_str(&src).expect_err("F99 not a dtype");
1634        assert!(
1635            matches!(err, FkcError::FdxTokenNotInTable { .. }),
1636            "got {err:?}"
1637        );
1638    }
1639
1640    // ===== Rule 4 — layout coherence =====
1641    #[test]
1642    fn layout_no_acceptable_form_is_incoherent() {
1643        // contiguous: n/a + strided: rejected ⇒ accepts no layout.
1644        let src = valid_bundle().replace(
1645            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1646            "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1647        );
1648        let err = validate_str(&src).expect_err("no acceptable layout");
1649        assert!(
1650            matches!(err, FkcError::LayoutIncoherent { .. }),
1651            "got {err:?}"
1652        );
1653    }
1654
1655    #[test]
1656    fn broadcast_without_strided_is_incoherent() {
1657        let src = valid_bundle().replace(
1658            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1659            "layout: { contiguous: required, strided: rejected, broadcast_stride0: accepted, start_offset: rejected, reverse_strides: rejected }",
1660        );
1661        let err = validate_str(&src).expect_err("broadcast w/o strided");
1662        assert!(
1663            matches!(err, FkcError::LayoutIncoherent { .. }),
1664            "got {err:?}"
1665        );
1666    }
1667
1668    #[test]
1669    fn reverse_without_strided_is_incoherent() {
1670        let src = valid_bundle().replace(
1671            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1672            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: accepted }",
1673        );
1674        let err = validate_str(&src).expect_err("reverse w/o strided");
1675        assert!(
1676            matches!(err, FkcError::LayoutIncoherent { .. }),
1677            "got {err:?}"
1678        );
1679    }
1680
1681    // ===== Rule 4 — broadcast_axes mask (§6-additive, path 1a) =====
1682    #[test]
1683    fn required_broadcast_with_axes_is_valid() {
1684        // Baracuda's §6-additive spelling for a baked bias-add cell.
1685        let src = valid_bundle()
1686            .replace(
1687                "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1688                "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1689            )
1690            .replace(
1691                "awkward_layout_strategy: requires_contiguous",
1692                "awkward_layout_strategy: handles_strided",
1693            );
1694        validate_str(&src)
1695            .expect("required broadcast with a non-empty broadcast_axes mask validates");
1696    }
1697
1698    #[test]
1699    fn required_broadcast_without_axes_is_incoherent() {
1700        let src = valid_bundle().replace(
1701            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1702            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, start_offset: rejected, reverse_strides: rejected }",
1703        );
1704        let err = validate_str(&src).expect_err("required broadcast needs a broadcast_axes mask");
1705        assert!(
1706            matches!(err, FkcError::LayoutIncoherent { .. }),
1707            "got {err:?}"
1708        );
1709    }
1710
1711    #[test]
1712    fn broadcast_axes_without_required_is_incoherent() {
1713        let src = valid_bundle().replace(
1714            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1715            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: accepted, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1716        );
1717        let err = validate_str(&src).expect_err("broadcast_axes is meaningful only with required");
1718        assert!(
1719            matches!(err, FkcError::LayoutIncoherent { .. }),
1720            "got {err:?}"
1721        );
1722    }
1723
1724    #[test]
1725    fn required_broadcast_with_empty_axes_is_incoherent() {
1726        let src = valid_bundle().replace(
1727            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1728            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [], start_offset: rejected, reverse_strides: rejected }",
1729        );
1730        let err = validate_str(&src).expect_err("empty broadcast_axes on a required broadcast");
1731        assert!(
1732            matches!(err, FkcError::LayoutIncoherent { .. }),
1733            "got {err:?}"
1734        );
1735    }
1736
1737    // ===== Rule 5 — awkward strategy =====
1738    #[test]
1739    fn handles_strided_without_strided_accepted_is_incoherent() {
1740        let src = valid_bundle().replace(
1741            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1742            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: handles_strided }",
1743        );
1744        let err = validate_str(&src).expect_err("handles_strided but strided rejected");
1745        assert!(
1746            matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1747            "got {err:?}"
1748        );
1749    }
1750
1751    #[test]
1752    fn unknown_awkward_strategy_is_typed_error() {
1753        let src = valid_bundle().replace(
1754            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1755            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: teleports }",
1756        );
1757        let err = validate_str(&src).expect_err("unknown strategy");
1758        assert!(
1759            matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1760            "got {err:?}"
1761        );
1762    }
1763
1764    // ===== Rule 6 — quant =====
1765    fn quant_bundle(family: &str, extra: &str) -> String {
1766        // A QMatMul-shaped contract whose weight carries an fdx.quant block.
1767        format!(
1768            r#"---
1769fkc_version: 1
1770provider:
1771  name: test-provider
1772  backend: Cpu
1773  kernel_source: "test-cpu"
1774---
1775
1776# q
1777
1778## q
1779
1780blurb.
1781
1782```fkc
1783kernel: q
1784op_kind: QMatMul
1785blurb: "q"
1786entry_point: "x::q"
1787accept:
1788  inputs:
1789    - name: act
1790      dtypes: [F32]
1791      layout: {{ contiguous: required, strided: rejected }}
1792    - name: weight
1793      dtypes: [U8]
1794      layout: {{ contiguous: required, strided: rejected }}
1795      fdx:
1796        requires_ext: true
1797        quant:
1798          family: {family}
1799{extra}
1800  op_params: {{ variant: QMatMul }}
1801return:
1802  outputs:
1803    - name: out
1804      dtype_rule: fixed(F32)
1805cost:
1806  provenance: judge_measured
1807  class: gemm_like
1808precision:
1809  bit_stable_on_same_hardware: true
1810  audited: true
1811determinism: same_hardware_bitwise
1812```
1813"#
1814        )
1815    }
1816
1817    #[test]
1818    fn ggml_block_with_real_dtype_ok() {
1819        let src = quant_bundle(
1820            "GGML_BLOCK",
1821            "          ggml_dtype: Q4_0\n          role: weight",
1822        );
1823        validate_str(&src).expect("GGML_BLOCK Q4_0 weight validates");
1824    }
1825
1826    #[test]
1827    fn ggml_dtype_q4_k_m_is_quant_incoherent() {
1828        // The GGUF file-format name `Q4_K_M` is NOT a GgmlDType variant.
1829        let src = quant_bundle(
1830            "GGML_BLOCK",
1831            "          ggml_dtype: Q4_K_M\n          role: weight",
1832        );
1833        let err = validate_str(&src).expect_err("Q4_K_M bad");
1834        assert!(
1835            matches!(err, FkcError::QuantIncoherent { .. }),
1836            "got {err:?}"
1837        );
1838    }
1839
1840    #[test]
1841    fn ggml_block_with_granularity_is_incoherent() {
1842        let src = quant_bundle(
1843            "GGML_BLOCK",
1844            "          ggml_dtype: Q4_0\n          granularity: PerChannel\n          role: weight",
1845        );
1846        let err = validate_str(&src).expect_err("GGML carries no granularity");
1847        assert!(
1848            matches!(err, FkcError::QuantIncoherent { .. }),
1849            "got {err:?}"
1850        );
1851    }
1852
1853    #[test]
1854    fn mx_family_is_not_yet_registrable() {
1855        let src = quant_bundle(
1856            "MX",
1857            "          granularity: PerBlock\n          role: weight",
1858        );
1859        let err = validate_str(&src).expect_err("MX not registrable");
1860        assert!(
1861            matches!(err, FkcError::MxNotYetRegistrable { ref family, .. } if family == "MX"),
1862            "got {err:?}"
1863        );
1864    }
1865
1866    #[test]
1867    fn affine_block_without_block_shape_is_incoherent() {
1868        let src = quant_bundle(
1869            "AFFINE_BLOCK",
1870            "          role: weight\n          scale_operand: scl",
1871        );
1872        // scale_operand `scl` won't exist either, but block_shape missing OR the
1873        // scale-operand check fires; both are coherence errors.
1874        let err = validate_str(&src).expect_err("AFFINE_BLOCK needs block_shape / real scale");
1875        assert!(
1876            matches!(
1877                err,
1878                FkcError::QuantIncoherent { .. } | FkcError::ScaleDoubleDeclared { .. }
1879            ),
1880            "got {err:?}"
1881        );
1882    }
1883
1884    #[test]
1885    fn affine_int_bad_granularity_is_incoherent() {
1886        let src = quant_bundle(
1887            "AFFINE_INT",
1888            "          granularity: PerBlock\n          role: weight",
1889        );
1890        let err = validate_str(&src).expect_err("AFFINE_INT PerBlock bad");
1891        assert!(
1892            matches!(err, FkcError::QuantIncoherent { .. }),
1893            "got {err:?}"
1894        );
1895    }
1896
1897    #[test]
1898    fn unknown_family_is_fdx_token_not_in_table() {
1899        let src = quant_bundle("MADE_UP", "          role: weight");
1900        let err = validate_str(&src).expect_err("MADE_UP not an FDX family");
1901        assert!(
1902            matches!(err, FkcError::FdxTokenNotInTable { .. }),
1903            "got {err:?}"
1904        );
1905    }
1906
1907    #[test]
1908    fn ggml_with_separate_scale_operand_is_double_declared() {
1909        // GGML scales are INLINE; declaring a separate scale_operand is the
1910        // double-declaration §10.6 forbids. (act is a real input role.)
1911        let src = quant_bundle(
1912            "GGML_BLOCK",
1913            "          ggml_dtype: Q4_0\n          role: weight\n          scale_operand: act",
1914        );
1915        let err = validate_str(&src).expect_err("GGML + separate scale");
1916        assert!(
1917            matches!(err, FkcError::ScaleDoubleDeclared { .. }),
1918            "got {err:?}"
1919        );
1920    }
1921
1922    // ===== Rule 7 — op-param namespace =====
1923    #[test]
1924    fn op_kind_with_fused_only_variant_is_bad_namespace() {
1925        // `FusedLinear` is a FusedOpParams variant, NOT an OpParams variant; an
1926        // op_kind contract naming it must fail the namespace check.
1927        let src = valid_bundle().replace(
1928            "op_params: { variant: None }",
1929            "op_params: { variant: FusedLinear }",
1930        );
1931        let err = validate_str(&src).expect_err("FusedLinear not in OpParams");
1932        assert!(
1933            matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
1934            "got {err:?}"
1935        );
1936    }
1937
1938    // ===== V-FKC-5 — the op-param allowlists vs the real enums =====
1939
1940    /// The drift this closes, stated as behavior rather than as a list
1941    /// comparison: `OpParams::NonZeroIndices` is a real, kernel-backed variant
1942    /// (1 input, a `[indices; count]` bundle output — the data-dependent
1943    /// dynamic-shape seam), and a contract naming it was rejected as if it
1944    /// were a typo, because the hand-typed allowlist was two variants behind
1945    /// the enum.
1946    #[test]
1947    fn op_params_namespace_accepts_the_real_nonzeroindices_variant() {
1948        let src = valid_bundle().replace(
1949            "op_params: { variant: None }",
1950            "op_params: { variant: NonZeroIndices }",
1951        );
1952        validate_str(&src)
1953            .expect("NonZeroIndices is a real OpParams variant — a contract may name it");
1954    }
1955
1956    /// The predicate must be exactly the generated table, in both directions.
1957    /// The table itself is held against the real enum by the compiler (see
1958    /// `variant_table!`), so this is what closes the loop between "the list
1959    /// matches the enum" and "the checker reads the list".
1960    #[test]
1961    fn op_param_predicates_agree_with_the_generated_variant_tables() {
1962        // Positive control: a macro that expanded to nothing, or a table that
1963        // silently lost its entries, would make every assertion below vacuous.
1964        // These floors are deliberately loose — they catch an empty or
1965        // truncated table, not a single added variant.
1966        assert!(
1967            OP_PARAMS_VARIANTS.len() >= 40,
1968            "OP_PARAMS_VARIANTS looks truncated ({} entries) — the rest of this \
1969             test would pass vacuously",
1970            OP_PARAMS_VARIANTS.len()
1971        );
1972        assert!(
1973            FUSED_OP_PARAMS_VARIANTS.len() >= 20,
1974            "FUSED_OP_PARAMS_VARIANTS looks truncated ({} entries)",
1975            FUSED_OP_PARAMS_VARIANTS.len()
1976        );
1977
1978        for (name, excluded) in OP_PARAMS_VARIANTS {
1979            assert_eq!(
1980                is_op_params_variant(name),
1981                excluded.is_none(),
1982                "OpParams::{name} — excluded={excluded:?} but the predicate says \
1983                 {}",
1984                is_op_params_variant(name)
1985            );
1986        }
1987        for (name, excluded) in FUSED_OP_PARAMS_VARIANTS {
1988            assert_eq!(
1989                is_fused_op_params_variant(name),
1990                excluded.is_none(),
1991                "FusedOpParams::{name} — excluded={excluded:?} but the predicate \
1992                 says {}",
1993                is_fused_op_params_variant(name)
1994            );
1995        }
1996
1997        // An exclusion with no stated reason is the silent omission this whole
1998        // change exists to eliminate.
1999        for (name, excluded) in OP_PARAMS_VARIANTS.iter().chain(FUSED_OP_PARAMS_VARIANTS) {
2000            if let Some(reason) = excluded {
2001                assert!(
2002                    !reason.trim().is_empty(),
2003                    "{name} is excluded with an empty reason"
2004                );
2005            }
2006        }
2007    }
2008
2009    /// The two deliberate exclusions, pinned so a later reader doesn't "fix"
2010    /// them back in: both name kernels that are synthesized or registered at
2011    /// RUN time, so there is no checked-in kernel for a `.fkc.md` contract to
2012    /// describe. This is the one case where rejection is correct — and the
2013    /// reason is now recorded at the table entry instead of being an
2014    /// unexplained gap.
2015    #[test]
2016    fn runtime_only_op_param_variants_stay_out_of_contract_reach() {
2017        assert!(!is_op_params_variant("JitScalars"));
2018        assert!(!is_fused_op_params_variant("Runtime"));
2019
2020        let src = valid_bundle().replace(
2021            "op_params: { variant: None }",
2022            "op_params: { variant: JitScalars }",
2023        );
2024        let err = validate_str(&src).expect_err("JitScalars is runtime-JIT-only");
2025        assert!(
2026            matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
2027            "got {err:?}"
2028        );
2029    }
2030
2031    // ===== V-FKC-10 — FKC's quant tokens ⊆ FDX's normative code table =====
2032
2033    /// FDX's `codes` module doc names it "the normative owner of the shared
2034    /// dtype/quant/granularity/... codes (the FKC kernel-contract format
2035    /// references them by symbol)". FKC's [`is_fdx_quant_family`] /
2036    /// [`is_fdx_granularity`] hand-copy that table as string literals, and
2037    /// nothing checked the two still agreed — in either direction.
2038    ///
2039    /// The guard reads `codes.rs` as TEXT rather than by symbol, deliberately.
2040    /// FDX's consts live behind fuel-ir's non-default `dlpack` feature, so a
2041    /// symbol-level test would compile only under `--features dlpack` and
2042    /// would report `test result: ok` while running nothing under the default
2043    /// gate — the exact failure mode that let this drift survive. Reading the
2044    /// source text needs no feature, so this runs in a plain
2045    /// `cargo test -p fuel-dispatch`.
2046    ///
2047    /// Read at run time, not `include_str!`d: `fuel-ir` is a sibling crate, so the
2048    /// file is outside this crate's tarball, and an `include_str!` of it would make
2049    /// the published crate unbuildable (the packaged-crate check fails any `src/`
2050    /// include that leaves the crate). From a tarball the test fails loudly rather
2051    /// than skipping: it is a workspace consistency check.
2052    fn fdx_codes_src() -> String {
2053        let path =
2054            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../fuel-ir/src/dlpack/codes.rs");
2055        std::fs::read_to_string(&path).unwrap_or_else(|e| {
2056            panic!(
2057                "the FDX code table must be readable from the workspace (this test checks \
2058                 fuel-dispatch against fuel-ir's source text); read {path:?}: {e}"
2059            )
2060        })
2061    }
2062
2063    /// `(FDX const name, the token an FKC contract writes)`. Every const named
2064    /// here must exist in `codes.rs` and every `FDX_QUANT_*` const there must
2065    /// appear here — so an FDX addition, removal, or rename all fail loudly.
2066    const FDX_FAMILY_TOKENS: &[(&str, &str)] = &[
2067        ("FDX_QUANT_NONE", "NONE"),
2068        ("FDX_QUANT_GGML_BLOCK", "GGML_BLOCK"),
2069        ("FDX_QUANT_MX", "MX"),
2070        ("FDX_QUANT_AFFINE_INT", "AFFINE_INT"),
2071        ("FDX_QUANT_AFFINE_FLOAT", "AFFINE_FLOAT"),
2072        ("FDX_QUANT_AFFINE_BLOCK", "AFFINE_BLOCK"),
2073    ];
2074
2075    /// As [`FDX_FAMILY_TOKENS`], for `FDXScaleGranularity`.
2076    const FDX_GRAN_TOKENS: &[(&str, &str)] = &[
2077        ("FDX_SCALE_GRAN_PER_TENSOR", "PerTensor"),
2078        ("FDX_SCALE_GRAN_PER_TOKEN", "PerToken"),
2079        ("FDX_SCALE_GRAN_PER_CHANNEL", "PerChannel"),
2080        ("FDX_SCALE_GRAN_PER_BLOCK", "PerBlock"),
2081    ];
2082
2083    /// Every `pub const <prefix>… ` declared in FDX's code table.
2084    fn fdx_consts_with_prefix(prefix: &str) -> std::collections::BTreeSet<String> {
2085        fdx_codes_src()
2086            .lines()
2087            .filter_map(|l| l.trim().strip_prefix("pub const ")?.split(':').next())
2088            .map(|n| n.trim().to_string())
2089            .filter(|n| n.starts_with(prefix))
2090            .collect()
2091    }
2092
2093    #[test]
2094    fn fkc_quant_tokens_are_a_faithful_subset_of_the_fdx_code_table() {
2095        let families = fdx_consts_with_prefix("FDX_QUANT_");
2096        let grans = fdx_consts_with_prefix("FDX_SCALE_GRAN_");
2097
2098        // Positive control. A parse that matched nothing — `codes.rs` reshaped,
2099        // the consts moved, the `pub const` spelling changed — would make every
2100        // set comparison below trivially satisfiable in the *dangerous*
2101        // direction (FKC accepts tokens FDX no longer defines). Assert the
2102        // instrument works before trusting what it reports.
2103        assert!(
2104            families.contains("FDX_QUANT_GGML_BLOCK") && grans.contains("FDX_SCALE_GRAN_PER_BLOCK"),
2105            "parsed {} family / {} granularity consts out of {} lines of codes.rs — \
2106             the parse found neither known anchor, so it is broken, not the table",
2107            families.len(),
2108            grans.len(),
2109            fdx_codes_src().lines().count()
2110        );
2111
2112        let declared_families: std::collections::BTreeSet<String> = FDX_FAMILY_TOKENS
2113            .iter()
2114            .map(|(c, _)| c.to_string())
2115            .collect();
2116        assert_eq!(
2117            families, declared_families,
2118            "FDX's FDX_QUANT_* table and FKC's mirror of it have diverged — update \
2119             FDX_FAMILY_TOKENS *and* is_fdx_quant_family together"
2120        );
2121
2122        let declared_grans: std::collections::BTreeSet<String> =
2123            FDX_GRAN_TOKENS.iter().map(|(c, _)| c.to_string()).collect();
2124        assert_eq!(
2125            grans, declared_grans,
2126            "FDX's FDX_SCALE_GRAN_* table and FKC's mirror of it have diverged"
2127        );
2128
2129        // …and the tokens themselves round-trip through the FKC predicates.
2130        for (konst, token) in FDX_FAMILY_TOKENS {
2131            assert!(
2132                is_fdx_quant_family(token),
2133                "FKC rejects `{token}`, the token for the live FDX const {konst}"
2134            );
2135        }
2136        for (konst, token) in FDX_GRAN_TOKENS {
2137            assert!(
2138                is_fdx_granularity(token),
2139                "FKC rejects `{token}`, the token for the live FDX const {konst}"
2140            );
2141        }
2142
2143        // The corpus writes `none` lowercase; both spellings are accepted.
2144        assert!(is_fdx_quant_family("none"));
2145
2146        // Plausible-but-not-FDX tokens must NOT be accepted, or the subset
2147        // property above is satisfied by a predicate that accepts everything.
2148        for bogus in ["PerGroup", "AWQ", "GPTQ", "affine_int", "PERTENSOR", ""] {
2149            assert!(!is_fdx_quant_family(bogus), "family accepted `{bogus}`");
2150            assert!(!is_fdx_granularity(bogus), "granularity accepted `{bogus}`");
2151        }
2152    }
2153
2154    // ===== Rule 8 / 8a — cost =====
2155    #[test]
2156    fn missing_provenance_errors() {
2157        let src = valid_bundle().replace("  provenance: declared\n", "");
2158        let err = validate_str(&src).expect_err("no provenance");
2159        assert!(
2160            matches!(err, FkcError::CostProvenanceMissing { .. }),
2161            "got {err:?}"
2162        );
2163    }
2164
2165    #[test]
2166    fn bad_cost_expr_is_cost_expr_parse() {
2167        let src = valid_bundle().replace("flops: \"n\"", "flops: \"2 * * n\"");
2168        let err = validate_str(&src).expect_err("malformed flops");
2169        assert!(matches!(err, FkcError::CostExprParse { .. }), "got {err:?}");
2170    }
2171
2172    #[test]
2173    fn declared_with_no_coeffs_and_no_class_is_placeholder() {
2174        let src = valid_bundle()
2175            .replace("  class: cheap_elementwise\n", "")
2176            .replace("  flops: \"n\"\n", "")
2177            .replace("  bytes_moved: \"3 * n * 4\"\n", "");
2178        let err = validate_str(&src).expect_err("bare declared cost");
2179        assert!(
2180            matches!(err, FkcError::PlaceholderCost { .. }),
2181            "got {err:?}"
2182        );
2183    }
2184
2185    #[test]
2186    fn judge_measured_all_tilde_is_not_placeholder() {
2187        // The corpus convention: judge_measured + all-`~` coefficients is an
2188        // explicit measured cost, NOT a placeholder.
2189        let src = valid_bundle()
2190            .replace("provenance: declared", "provenance: judge_measured")
2191            .replace("  flops: \"n\"\n", "  flops: ~\n")
2192            .replace("  bytes_moved: \"3 * n * 4\"\n", "  bytes_moved: ~\n");
2193        validate_str(&src).expect("judge_measured + ~ is fine");
2194    }
2195
2196    #[test]
2197    fn placeholder_cost_class_field() {
2198        use crate::fkc::cost_compile::{CostClassKind, classify_cost};
2199        assert!(classify_cost("declared", "cheap_elementwise", false, false).is_none());
2200        assert!(matches!(
2201            classify_cost("declared", "free", false, false),
2202            Some(CostClassKind::Free)
2203        ));
2204        assert!(matches!(
2205            classify_cost("declared", "gemm_like", true, false),
2206            Some(CostClassKind::DeclaredFormula)
2207        ));
2208        assert!(matches!(
2209            classify_cost("declared", "gemm_like", false, true),
2210            Some(CostClassKind::VendorSpec)
2211        ));
2212        assert!(matches!(
2213            classify_cost("judge_measured", "gemm_like", false, false),
2214            Some(CostClassKind::JudgeMeasured)
2215        ));
2216        // integration: a declared+non-free class with NO flops/bytes/cost_fn is rejected on import
2217        use crate::fkc::register::import_bundle_str;
2218        let src = "---\nfkc_version: 1\nprovider:\n  name: ph-provider\n  backend: Cpu\n  kernel_source: \"ph-cpu\"\n---\n\n# ph\n\n## add_f32\n\nA.\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\nblurb: \"a\"\nentry_point: \"x::add_f32\"\naccept:\n  inputs:\n    - name: lhs\n      dtypes: [F32]\n      layout: { contiguous: required, strided: rejected }\n    - name: rhs\n      dtypes: [F32]\n      layout: { contiguous: required, strided: rejected }\n  op_params: { variant: None }\nreturn:\n  outputs:\n    - name: out\n      dtype_rule: passthrough(lhs)\ncost:\n  provenance: declared\n  class: cheap_elementwise\nprecision:\n  bit_stable_on_same_hardware: true\n  audited: true\ndeterminism: same_hardware_bitwise\n```\n";
2219        let err = import_bundle_str(src, &StubLink::new())
2220            .expect_err("declared + non-free class + no usable cost path must reject");
2221        assert!(
2222            matches!(err, FkcError::PlaceholderCost { .. }),
2223            "got {err:?}"
2224        );
2225    }
2226
2227    // ===== Rule 9 — determinism/precision =====
2228    #[test]
2229    fn nondeterministic_with_bitstable_true_is_incoherent() {
2230        let src = valid_bundle().replace(
2231            "determinism: same_hardware_bitwise",
2232            "determinism: nondeterministic",
2233        );
2234        let err = validate_str(&src).expect_err("nondeterministic + bit_stable true");
2235        assert!(
2236            matches!(err, FkcError::QuantIncoherent { .. }),
2237            "got {err:?}"
2238        );
2239    }
2240
2241    // ===== Rule 15 — extent =====
2242    #[test]
2243    fn unknown_extent_kind_is_typed_error() {
2244        let src = quant_bundle("none", "").replace(
2245            "      fdx:\n        requires_ext: true\n        quant:\n          family: none\n",
2246            "      fdx:\n        extent_kind: wobbly\n        symbolic_extent: required\n",
2247        );
2248        let err = validate_str(&src).expect_err("bad extent_kind");
2249        assert!(
2250            matches!(err, FkcError::UnknownAdmissibilityEnum { .. }),
2251            "got {err:?}"
2252        );
2253    }
2254
2255    // ===== Rule 17 / §3.10 — describe-only (non-registrable) sections =====
2256
2257    /// A describe-only chassis-umbrella bundle whose `op_kind:` is the
2258    /// DESCRIPTIVE token `binary` (NOT a real `OpKind`) — legal because
2259    /// `registrable: false` skips the dispatch-resolution checks (§3.10).
2260    fn describe_only_bundle() -> String {
2261        r#"---
2262fkc_version: 1
2263provider:
2264  name: test-provider
2265  backend: Cpu
2266  kernel_source: "test-cpu"
2267---
2268
2269# describe-only bundle
2270
2271## binary
2272
2273A chassis umbrella.
2274
2275```fkc
2276kernel: binary
2277registrable: false
2278op_kind: binary
2279blurb: "shared binary chassis (documentation umbrella)"
2280entry_point: "x::chassis"
2281accept:
2282  inputs:
2283    - name: lhs
2284      dtypes: [F32, F64, BF16, F16]
2285      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2286    - name: rhs
2287      dtypes: [F32, F64, BF16, F16]
2288      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2289return:
2290  outputs:
2291    - name: out
2292      dtype_rule: passthrough(lhs)
2293cost:
2294  provenance: judge_measured
2295  class: cheap_elementwise
2296precision:
2297  bit_stable_on_same_hardware: true
2298  audited: true
2299determinism: same_hardware_bitwise
2300```
2301"#
2302        .to_string()
2303    }
2304
2305    #[test]
2306    fn describe_only_with_descriptive_op_kind_validates() {
2307        // `op_kind: binary` is NOT a real OpKind, but registrable: false skips
2308        // the resolution check — the section validates as documentation.
2309        validate_str(&describe_only_bundle()).expect("describe-only section validates");
2310    }
2311
2312    #[test]
2313    fn describe_only_with_tilde_op_kind_validates() {
2314        // `op_kind: ~` (absent) is legal for a describe-only section — neither
2315        // op_kind nor fused_op need resolve.
2316        let src = describe_only_bundle().replace("op_kind: binary\n", "op_kind: ~\n");
2317        validate_str(&src).expect("describe-only with op_kind: ~ validates");
2318    }
2319
2320    #[test]
2321    fn non_describe_only_still_requires_real_op_kind_regression() {
2322        // REGRESSION: the SAME section WITHOUT registrable: false must still
2323        // fail — a descriptive `binary` token is not a real OpKind, so a
2324        // registrable section is rejected (no relaxation of the validator).
2325        let src = describe_only_bundle().replace("registrable: false\n", "");
2326        let err = validate_str(&src).expect_err("registrable section needs a real op_kind");
2327        assert!(matches!(err, FkcError::UnknownOpKind { .. }), "got {err:?}");
2328    }
2329
2330    #[test]
2331    fn describe_only_still_validates_descriptive_dtypes() {
2332        // A describe-only section's descriptive checks STILL run: a bogus dtype
2333        // token is rejected by the FDX-subset drift-guard even though dispatch
2334        // resolution is skipped.
2335        let src = describe_only_bundle().replace("dtypes: [F32, F64, BF16, F16]", "dtypes: [F99]");
2336        let err = validate_str(&src).expect_err("bad dtype in describe-only docs");
2337        assert!(
2338            matches!(err, FkcError::FdxTokenNotInTable { .. }),
2339            "got {err:?}"
2340        );
2341    }
2342
2343    #[test]
2344    fn describe_only_still_validates_layout_coherence() {
2345        // Layout coherence (rule 4) still runs for describe-only docs.
2346        let src = describe_only_bundle().replace(
2347            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2348            "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2349        );
2350        let err = validate_str(&src).expect_err("incoherent layout in describe-only docs");
2351        assert!(
2352            matches!(err, FkcError::LayoutIncoherent { .. }),
2353            "got {err:?}"
2354        );
2355    }
2356
2357    /// A describe-only zero-operand op (e.g. a `rand_uniform`/`rand_normal`
2358    /// random fill whose only "input" is backend-private RNG state, not a graph
2359    /// tensor): `registrable: false` + `accept.inputs: []`. Mirrors the
2360    /// `metal/sort-random.fkc.md` random-fill sections.
2361    fn describe_only_zero_input_bundle() -> String {
2362        r#"---
2363fkc_version: 1
2364provider:
2365  name: test-provider
2366  backend: Cpu
2367  kernel_source: "test-cpu"
2368---
2369
2370# describe-only zero-input bundle
2371
2372## rand_fill
2373
2374A zero-operand random fill.
2375
2376```fkc
2377kernel: rand_fill
2378registrable: false
2379op_kind: RandUniform
2380blurb: "fill a dense buffer with U(min,max); no input tensor (RNG state is backend-private)"
2381entry_point: "x::rand_fill"
2382accept:
2383  inputs: []
2384return:
2385  outputs:
2386    - name: out
2387      dtype_rule: cast(out)
2388cost:
2389  provenance: judge_measured
2390  class: cheap_elementwise
2391precision:
2392  bit_stable_on_same_hardware: false
2393  audited: true
2394determinism: nondeterministic
2395```
2396"#
2397        .to_string()
2398    }
2399
2400    #[test]
2401    fn describe_only_zero_input_fill_validates() {
2402        // §3.10 carve-out: a describe-only section with `accept.inputs: []` is a
2403        // legitimate zero-operand documentation op (random fill) — the ≥1-input
2404        // required-field check is EXEMPTED for describe-only sections.
2405        validate_str(&describe_only_zero_input_bundle())
2406            .expect("describe-only zero-input fill validates");
2407    }
2408
2409    #[test]
2410    fn registrable_zero_input_still_fails_missing_required_field() {
2411        // REGRESSION: the SAME zero-input section WITHOUT registrable: false must
2412        // still fail the ≥1-input rule — the carve-out applies ONLY to
2413        // describe-only sections (a real dispatch target must consume ≥1 graph
2414        // operand). No relaxation for registrable sections.
2415        let src = describe_only_zero_input_bundle()
2416            .replace("registrable: false\n", "")
2417            // Use a real OpKind so the missing-input rule (not the op-resolution
2418            // rule) is the failure under test. The required-field battery runs
2419            // before op resolution, so `accept.inputs (≥1)` fires first regardless.
2420            .replace("op_kind: RandUniform\n", "op_kind: ReluElementwise\n");
2421        let err = validate_str(&src).expect_err("registrable zero-input section is rejected");
2422        assert!(
2423            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "accept.inputs (≥1)"),
2424            "got {err:?}"
2425        );
2426    }
2427
2428    // =====================================================================
2429    // Rule 14 — gather (paged_blocks) coherence + the IMPORT-SIDE LIFT
2430    // (FDX gather-sidecar arc, slice A).
2431    // =====================================================================
2432
2433    /// A minimal but COHERENT `registrable: true` paged_blocks bundle: the
2434    /// PagedAttn-shaped operand carries `fdx.gather: paged_blocks` naming the
2435    /// separate `block_table` / `context_lens` U32 input roles (the as-built
2436    /// ABI). `__GATHER__` marks the pool operand's `fdx` block so the negative
2437    /// tests can mutate it into an incoherent form.
2438    fn paged_blocks_bundle() -> String {
2439        r#"---
2440fkc_version: 1
2441provider:
2442  name: test-provider
2443  backend: Cpu
2444  kernel_source: "test-cpu"
2445---
2446
2447# paged bundle
2448
2449## paged_demo
2450
2451A paged attention blurb.
2452
2453```fkc
2454kernel: paged_demo
2455op_kind: PagedAttn
2456blurb: "naive paged attention over a blocked KV cache"
2457entry_point: "x::paged_demo"
2458accept:
2459  inputs:
2460    - name: q
2461      dtypes: [F32]
2462      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2463    - name: k_cache
2464      dtypes: [F32]
2465      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2466      fdx:
2467        requires_ext: true
2468        symbolic_extent: required
2469        gather: { kind: paged_blocks, block_table: block_table, context_lens: context_lens }
2470    - name: block_table
2471      dtypes: [U32]
2472      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2473    - name: context_lens
2474      dtypes: [U32]
2475      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2476  op_params: { variant: PagedAttn }
2477return:
2478  outputs:
2479    - name: out
2480      dtype_rule: passthrough(q)
2481cost:
2482  provenance: declared
2483  class: attention
2484  flops: "4 * b * hq * sq * ctx * d"
2485  bytes_moved: "2 * b * hq * sq * d * 4"
2486precision:
2487  bit_stable_on_same_hardware: true
2488  audited: true
2489determinism: same_hardware_bitwise
2490```
2491"#
2492        .to_string()
2493    }
2494
2495    #[test]
2496    fn coherent_registrable_paged_blocks_validates() {
2497        // BORN-RED → GREEN (slice A). Before the import-side lift, a REGISTRABLE
2498        // section whose operand carries `fdx.gather: paged_blocks` failed import
2499        // with `GatherNotYetSupported` (validate_file re-raises it for a
2500        // registrable section — only describe-only ones are swallowed). After the
2501        // lift, the gather block is validated for COHERENCE (kind / requires_ext /
2502        // symbolic_extent / real block_table+context_lens roles) and then ACCEPTED
2503        // — registration of `OpKind::PagedAttn` does not depend on the FDX gather
2504        // descriptor (the block_table/context_lens ride as ordinary U32 operands).
2505        validate_str(&paged_blocks_bundle())
2506            .expect("a coherent registrable paged_blocks section validates + is registrable");
2507    }
2508
2509    #[test]
2510    fn incoherent_gather_missing_requires_ext_still_errors() {
2511        // GUARD: the lift must NOT be a blanket pass. An INCOHERENT gather (here:
2512        // no `requires_ext: true`) still fails with the typed `GatherIncoherent`,
2513        // never a silent pass (never-panic, no-silent-fixup).
2514        let src = paged_blocks_bundle().replace("        requires_ext: true\n", "");
2515        let err = validate_str(&src).expect_err("gather without requires_ext must error");
2516        assert!(
2517            matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("requires_ext")),
2518            "got {err:?}",
2519        );
2520    }
2521
2522    #[test]
2523    fn incoherent_gather_dangling_block_table_role_still_errors() {
2524        // GUARD: `gather.block_table` naming a role that is not a real
2525        // `accept.inputs` name is `GatherIncoherent` — the coherence cross-check
2526        // (a table is described in exactly one place + a consistency check, FDX
2527        // §6.9.3) is preserved by the lift.
2528        let src = paged_blocks_bundle().replace("block_table: block_table,", "block_table: nope,");
2529        let err = validate_str(&src).expect_err("dangling block_table role must error");
2530        assert!(
2531            matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("block_table")),
2532            "got {err:?}",
2533        );
2534    }
2535
2536    #[test]
2537    fn unknown_gather_kind_is_unknown_admissibility_enum() {
2538        // GUARD: an unknown `gather.kind` (a future ragged/CSR kind FKC cannot yet
2539        // key) is a typed `UnknownAdmissibilityEnum`, never a silent default.
2540        let src = paged_blocks_bundle().replace("kind: paged_blocks,", "kind: ragged_csr,");
2541        let err = validate_str(&src).expect_err("unknown gather kind must error");
2542        assert!(
2543            matches!(err, FkcError::UnknownAdmissibilityEnum { ref value, .. } if value == "ragged_csr"),
2544            "got {err:?}",
2545        );
2546    }
2547
2548    // =====================================================================
2549    // TASK 3 — the CI lint over the whole checked-in contract corpus.
2550    // =====================================================================
2551
2552    /// A permissive stub `LinkRegistry` that resolves any entry_point to a
2553    /// distinct dummy `KernelRef` (so lowering succeeds). The lint checks
2554    /// parse + lower + validate, NOT real registration.
2555    struct StubLink {
2556        seen: std::sync::Mutex<std::collections::HashMap<String, crate::kernel::KernelRef>>,
2557    }
2558    impl StubLink {
2559        fn new() -> Self {
2560            Self {
2561                seen: std::sync::Mutex::new(std::collections::HashMap::new()),
2562            }
2563        }
2564        fn resolve(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
2565            use std::sync::Arc;
2566            use std::sync::RwLock;
2567            // A family of distinct fn items so distinct symbols get distinct
2568            // pointers (avoids spurious duplicate-pointer collapse on lower —
2569            // lowering does not finalize, but keep them distinct anyway).
2570            fn k0(
2571                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2572                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2573                _l: &[fuel_ir::Layout],
2574                _p: &crate::kernel::OpParams,
2575            ) -> fuel_ir::Result<()> {
2576                Ok(())
2577            }
2578            fn k1(
2579                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2580                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2581                _l: &[fuel_ir::Layout],
2582                _p: &crate::kernel::OpParams,
2583            ) -> fuel_ir::Result<()> {
2584                Ok(())
2585            }
2586            fn k2(
2587                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2588                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2589                _l: &[fuel_ir::Layout],
2590                _p: &crate::kernel::OpParams,
2591            ) -> fuel_ir::Result<()> {
2592                Ok(())
2593            }
2594            fn k3(
2595                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2596                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2597                _l: &[fuel_ir::Layout],
2598                _p: &crate::kernel::OpParams,
2599            ) -> fuel_ir::Result<()> {
2600                Ok(())
2601            }
2602            let table: [crate::kernel::KernelRef; 4] = [k0, k1, k2, k3];
2603            let mut g = self.seen.lock().unwrap();
2604            if let Some(k) = g.get(symbol) {
2605                return Some(*k);
2606            }
2607            let k = table[g.len() % table.len()];
2608            g.insert(symbol.to_string(), k);
2609            Some(k)
2610        }
2611    }
2612    impl crate::fkc::lower::LinkRegistry for StubLink {
2613        fn resolve_primitive(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2614            self.resolve(s)
2615        }
2616        fn resolve_fused(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2617            self.resolve(s)
2618        }
2619        fn resolve_cost_fn(&self, _name: &str) -> Option<crate::kernel::CostFn> {
2620            // Permissive (§4.4 cost-fn trampoline): the corpus lint checks that a
2621            // NAMED `cost.cost_fn` parses + lowers, not that it resolves to a
2622            // specific production fn (that is each provider's real link registry's
2623            // job). Any name resolves to a dummy CostFn so a cost-fn-pinning
2624            // contract lowers cleanly.
2625            fn c(
2626                _s: &[fuel_ir::Shape],
2627                _d: &[fuel_ir::DType],
2628                _p: &crate::kernel::OpParams,
2629                _c: &fuel_ir::backend::BackendCapabilities,
2630            ) -> crate::fused::CostEstimate {
2631                crate::fused::CostEstimate::default()
2632            }
2633            Some(c)
2634        }
2635    }
2636
2637    /// Recursively collect every `*.fkc.md` under `dir`, excluding any path
2638    /// containing an `_inventory` component.
2639    fn collect_fkc_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
2640        // GAP-157: a corpus sweep that can fail must FAIL, not silently truncate.
2641        // A read_dir error here would drop `.fkc.md` files from the CI lint's
2642        // corpus, and the lint would report "all contracts validated" over an
2643        // incomplete set. (Test-only helper — panic is the correct hard failure.)
2644        let rd = std::fs::read_dir(dir).unwrap_or_else(|e| {
2645            panic!(
2646                "collect_fkc_files: read_dir({}) failed: {e} — the FKC file sweep must FAIL, not silently truncate",
2647                dir.display()
2648            )
2649        });
2650        for entry in rd.flatten() {
2651            let path = entry.path();
2652            if path.is_dir() {
2653                if path.file_name().and_then(|n| n.to_str()) == Some("_inventory") {
2654                    continue;
2655                }
2656                collect_fkc_files(&path, out);
2657            } else if path.to_string_lossy().ends_with(".fkc.md") {
2658                out.push(path);
2659            }
2660        }
2661    }
2662
2663    /// THE CI LINT (Task 3). Walk `fuel-dispatch/kernel-contracts/**/*.fkc.md`
2664    /// (excluding `_inventory/`), parse + lower + validate EVERY file with a
2665    /// permissive stub LinkRegistry, and report the file/section counts. A
2666    /// failing contract is CAPTURED and REPORTED (file + kernel + the rule it
2667    /// tripped), never silently relaxed.
2668    ///
2669    /// `MxNotYetRegistrable` / `GatherNotYetSupported` are
2670    /// describable-but-not-yet-registrable per spec §6/§3.9.1 — they are a
2671    /// CORRECT validate outcome (the contract is legal, the consumer is
2672    /// behind), so the lint records them separately as "deferred", not as
2673    /// hard failures. `FanoutDtypeMismatch` (§3.4) is the same posture at
2674    /// lower time: a MULTI-AXIS dtype contract (varying operands with
2675    /// DIFFERENT dtype lists) is legal but not-yet-fannable by the uniform
2676    /// fan-out importer — surfaced (never silently picked), recorded as
2677    /// deferred, and awaiting a cartesian / per-axis fan-out follow-up.
2678    ///
2679    /// This is a **real CI gate**: it runs as a normal unit test (no
2680    /// `#[ignore]`) and FAILS the build if any checked-in contract has a hard
2681    /// parse/lower/validate failure. The whole corpus is currently clean of hard
2682    /// failures; only the `MxNotYetRegistrable` / `GatherNotYetSupported`
2683    /// deferred (consumer-ahead) cases remain, which are a CORRECT outcome and
2684    /// are NOT counted as failures. Run it verbosely with
2685    /// `cargo test -p fuel-dispatch --lib -- \
2686    ///  ci_lint_corpus_parse_lower_validate --nocapture` to see the full
2687    /// file/section/deferred counts. It does NOT relax a validator or edit the
2688    /// corpus to hide defects — a new hard failure must be fixed (or the section
2689    /// marked describe-only per §3.10) to make this gate green again.
2690    #[test]
2691    fn ci_lint_corpus_parse_lower_validate() {
2692        // Locate the corpus relative to this crate (CARGO_MANIFEST_DIR =
2693        // .../fuel-dispatch). The corpus lives at kernel-contracts/ inside this crate.
2694        let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2695        let corpus = manifest.join("kernel-contracts");
2696        assert!(
2697            corpus.is_dir(),
2698            "corpus dir not found at {}",
2699            corpus.display()
2700        );
2701
2702        let mut files = Vec::new();
2703        collect_fkc_files(&corpus, &mut files);
2704        files.sort();
2705        assert!(!files.is_empty(), "no .fkc.md files found");
2706
2707        let link = StubLink::new();
2708        let mut file_count = 0usize;
2709        let mut section_count = 0usize;
2710        let mut hard_failures: Vec<String> = Vec::new();
2711        let mut deferred: Vec<String> = Vec::new();
2712
2713        for path in &files {
2714            file_count += 1;
2715            let rel = path
2716                .strip_prefix(manifest)
2717                .unwrap_or(path)
2718                .display()
2719                .to_string();
2720            let src = std::fs::read_to_string(path).expect("read corpus file");
2721
2722            // 1) parse.
2723            let file = match parse_file(&src) {
2724                Ok(f) => f,
2725                Err(e) => {
2726                    hard_failures.push(format!("{rel}: PARSE: {e}"));
2727                    continue;
2728                }
2729            };
2730            section_count += file.kernels.len();
2731
2732            // 2) validate (whole file) — but to attribute a failure to a
2733            // specific kernel + rule, validate each kernel individually too.
2734            // First the file-level (version) check.
2735            if !fkc_version_is_supported(file.front_matter.fkc_version) {
2736                hard_failures.push(format!(
2737                    "{rel}: fkc_version {} not in supported set {:?}",
2738                    file.front_matter.fkc_version, FKC_SUPPORTED_VERSIONS
2739                ));
2740                continue;
2741            }
2742            for kernel in &file.kernels {
2743                let kname = kernel.kernel.as_str();
2744                match validate_kernel(kernel) {
2745                    Ok(()) => {}
2746                    Err(
2747                        e @ (FkcError::MxNotYetRegistrable { .. }
2748                        | FkcError::GatherNotYetSupported { .. }),
2749                    ) => {
2750                        deferred.push(format!("{rel} :: {kname}: {e}"));
2751                    }
2752                    Err(e) => {
2753                        hard_failures.push(format!("{rel} :: {kname}: {e}"));
2754                        continue;
2755                    }
2756                }
2757                // 3) lower (with the stub link) — only attempt when validate
2758                // passed (or deferred). Lowering catches link/dtype/cost issues
2759                // validate doesn't; surface those too.
2760                if let Err(e) = lower_one_kernel(&file, kernel, &link) {
2761                    // MxNotYetRegistrable is a register-time gate, not lower —
2762                    // lowering an MX/AFFINE_BLOCK contract succeeds; any lower
2763                    // error here is a real finding.
2764                    match e {
2765                        FkcError::MxNotYetRegistrable { .. }
2766                        | FkcError::GatherNotYetSupported { .. } => {}
2767                        // Describable but NOT-YET-FANNABLE (§3.4 multi-dtype
2768                        // fan-out): a MULTI-AXIS dtype contract whose varying
2769                        // operands enumerate DIFFERENT dtype lists (e.g. a
2770                        // mixed-precision matmul, or an indexing op with an
2771                        // independent data-dtype axis and index-dtype axis).
2772                        // The uniform fan-out importer surfaces this as a typed
2773                        // error rather than silently picking one operand's list
2774                        // (never a silent pick); registering it needs a
2775                        // cartesian / per-axis fan-out follow-up. Recorded as
2776                        // deferred (consumer-behind), not a hard failure.
2777                        FkcError::FanoutDtypeMismatch { .. } => {
2778                            deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2779                        }
2780                        // Describable but NOT-YET-FANNABLE (§3.4 optional-operand
2781                        // fan-out): a section with MULTIPLE / chained trailing
2782                        // optional operands (e.g. Metal layernorm's `alpha` AND
2783                        // `beta`, so `alpha` is optional but not last). The
2784                        // key-builder supports a SINGLE optional LAST input
2785                        // (conv's `bias`); a chained-optional ABI needs a nested
2786                        // fan follow-up. The importer surfaces it as a typed error
2787                        // rather than silently mis-keying (before optional support
2788                        // it was silently coerced to an all-present required key).
2789                        // Recorded as deferred (consumer-behind), not a hard fail.
2790                        FkcError::OptionalOperandNotLast { .. } => {
2791                            deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2792                        }
2793                        _ => hard_failures.push(format!("{rel} :: {kname}: LOWER: {e}")),
2794                    }
2795                }
2796            }
2797        }
2798
2799        eprintln!(
2800            "FKC corpus lint: {file_count} files, {section_count} sections; {} deferred (MX/gather not-yet-registrable + multi-axis dtype / chained-optional not-yet-fannable), {} hard failures",
2801            deferred.len(),
2802            hard_failures.len()
2803        );
2804        if !deferred.is_empty() {
2805            eprintln!(
2806                "--- deferred (describable, not-yet-registrable / not-yet-fannable; spec §6/§3.9.1/§3.4) ---"
2807            );
2808            for d in &deferred {
2809                eprintln!("  {d}");
2810            }
2811        }
2812        if !hard_failures.is_empty() {
2813            eprintln!("--- HARD FAILURES (file :: kernel: rule) ---");
2814            for f in &hard_failures {
2815                eprintln!("  {f}");
2816            }
2817            panic!(
2818                "{} corpus contract(s) failed parse/lower/validate — see the list above \
2819                 (REPORT these, do not relax the validators)",
2820                hard_failures.len()
2821            );
2822        }
2823    }
2824
2825    /// Lower a single kernel within a file using the stub link, mirroring what
2826    /// V-FKC-3, corpus arm: **no two registrable contracts anywhere in the
2827    /// checked-in corpus may claim the same `(kernel_source, entry_point)`.**
2828    ///
2829    /// `register_into` already rejects a duplicate `KernelRef` — but only for
2830    /// the providers a given process actually imports, and the corpus lint
2831    /// above stops at *lower*, so cross-file collisions in the checked-in
2832    /// corpus had no safety net. That is the gap this closes.
2833    ///
2834    /// It deliberately does NOT go through `register_into`, and the reason is
2835    /// worth recording: the lint's `StubLink` hands out
2836    /// `table[g.len() % table.len()]` — **four** fn pointers, cycling — so the
2837    /// 5th distinct symbol aliases the 1st. Registering the corpus through it
2838    /// would report a flood of duplicates that are artifacts of the stub, not
2839    /// of the corpus. Synthesizing an unbounded number of distinct `fn`
2840    /// pointers at runtime is not possible, which is why this net could not be
2841    /// built the obvious way.
2842    ///
2843    /// So it asserts the property at its source instead: two contracts naming
2844    /// the same entry point in the same `kernel_source` are exactly the ones a
2845    /// real link registry resolves to one `KernelRef`. A *structural*
2846    /// assertion — it tests the property the duplicate gate is built on, needs
2847    /// no oracle, and diagnoses by NAME rather than by index.
2848    #[test]
2849    fn ci_lint_corpus_has_no_cross_file_entry_point_collisions() {
2850        let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2851        let corpus = manifest.join("kernel-contracts");
2852        assert!(
2853            corpus.is_dir(),
2854            "corpus dir not found at {}",
2855            corpus.display()
2856        );
2857
2858        let mut files = Vec::new();
2859        collect_fkc_files(&corpus, &mut files);
2860        files.sort();
2861        assert!(!files.is_empty(), "no .fkc.md files found");
2862
2863        // (kernel_source, entry_point) -> first "file :: kernel" that claimed it
2864        let mut claimed: std::collections::HashMap<(String, String), String> =
2865            std::collections::HashMap::new();
2866        // (kernel_source, entry_point, human description)
2867        let mut collisions: Vec<(String, String, String)> = Vec::new();
2868        let mut checked = 0usize;
2869
2870        for path in &files {
2871            let rel = path
2872                .strip_prefix(manifest)
2873                .unwrap_or(path)
2874                .display()
2875                .to_string();
2876            let src = std::fs::read_to_string(path).expect("read corpus file");
2877            let Ok(file) = parse_file(&src) else {
2878                // Parse failures are the other lint's business; skip rather
2879                // than double-report them here.
2880                continue;
2881            };
2882            let kernel_source = file.front_matter.provider.kernel_source.clone();
2883            for kernel in &file.kernels {
2884                // Describe-only sections (§3.10) never reach the binding table,
2885                // so they cannot collide.
2886                if !kernel.registrable {
2887                    continue;
2888                }
2889                let Some(entry) = kernel.entry_point.as_ref() else {
2890                    continue;
2891                };
2892                checked += 1;
2893                let key = (kernel_source.clone(), entry.clone());
2894                let who = format!("{rel} :: {}", kernel.kernel);
2895                match claimed.get(&key) {
2896                    Some(first) => collisions.push((
2897                        key.0.clone(),
2898                        key.1.clone(),
2899                        format!("claimed by BOTH {first} AND {who}"),
2900                    )),
2901                    None => {
2902                        claimed.insert(key, who);
2903                    }
2904                }
2905            }
2906        }
2907
2908        // Report what the instrument actually SAW — a loop that silently
2909        // examined nothing passes identically to one that examined everything.
2910        eprintln!(
2911            "FKC corpus entry-point uniqueness: {checked} registrable contracts across {} files, {} distinct (kernel_source, entry_point) keys",
2912            files.len(),
2913            claimed.len()
2914        );
2915        assert!(
2916            checked > 0,
2917            "the lint examined zero registrable contracts — it is not looking where it thinks"
2918        );
2919
2920        // --- Ratchet, not a cliff ---------------------------------------
2921        //
2922        // The corpus has 13 PRE-EXISTING collisions, all of the same shape: an
2923        // IMPORTED file colliding with an UN-IMPORTED orphan. Verified by
2924        // counting `include_str!` sites — `dispatch/reduce.fkc.md` and
2925        // `vulkan/conv-attn-rope.fkc.md` have ZERO, while their partners
2926        // `cpu/reduce{,-to}.fkc.md` and `vulkan/attention.fkc.md` have two
2927        // each. So none of them registers today and none is a live duplicate.
2928        //
2929        // They are still real debt: two contracts claiming one kernel can
2930        // DISAGREE about precision/cost, which is the divergence FKC exists to
2931        // prevent, and wiring either orphan up would fail registration. They
2932        // are listed rather than deleted because removing corpus contracts is a
2933        // content decision for their authors, not a lint's to make.
2934        //
2935        // The value here is the ratchet: a NEW collision fails immediately.
2936        const KNOWN: &[(&str, &str)] = &[
2937            // dispatch/reduce.fkc.md (orphan) vs cpu/reduce{,-to}.fkc.md
2938            (
2939                "portable-cpu",
2940                "fuel_cpu_backend::byte_kernels::sum_reduce_f32",
2941            ),
2942            (
2943                "portable-cpu",
2944                "fuel_cpu_backend::byte_kernels::max_reduce_f32",
2945            ),
2946            (
2947                "portable-cpu",
2948                "fuel_cpu_backend::byte_kernels::min_reduce_f32",
2949            ),
2950            (
2951                "portable-cpu",
2952                "fuel_cpu_backend::byte_kernels::mean_reduce_f32",
2953            ),
2954            (
2955                "portable-cpu",
2956                "fuel_cpu_backend::byte_kernels::reduce_sum_to_f32",
2957            ),
2958            (
2959                "portable-cpu",
2960                "fuel_cpu_backend::byte_kernels::reduce_max_to_f32",
2961            ),
2962            (
2963                "portable-cpu",
2964                "fuel_cpu_backend::byte_kernels::reduce_max_to_backward_f32",
2965            ),
2966            // vulkan/conv-attn-rope.fkc.md (orphan) vs vulkan/attention.fkc.md
2967            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f32"),
2968            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_bf16"),
2969            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f16"),
2970            (
2971                "vulkan-slang",
2972                "fuel_vulkan_backend::fkc::flash_attn_backward_q_f32",
2973            ),
2974            (
2975                "vulkan-slang",
2976                "fuel_vulkan_backend::fkc::flash_attn_backward_k_f32",
2977            ),
2978            (
2979                "vulkan-slang",
2980                "fuel_vulkan_backend::fkc::flash_attn_backward_v_f32",
2981            ),
2982        ];
2983        let is_known =
2984            |key: &(String, String)| KNOWN.iter().any(|(ks, ep)| *ks == key.0 && *ep == key.1);
2985
2986        let novel: Vec<&(String, String, String)> = collisions
2987            .iter()
2988            .filter(|c| !is_known(&(c.0.clone(), c.1.clone())))
2989            .collect();
2990
2991        // An allowlist that is never re-checked ROTS: a fixed collision would
2992        // sit here forever, and the next reader would believe the debt is
2993        // larger than it is. Fail on a stale entry too.
2994        let seen: std::collections::HashSet<(String, String)> = collisions
2995            .iter()
2996            .map(|c| (c.0.clone(), c.1.clone()))
2997            .collect();
2998        let stale: Vec<&(&str, &str)> = KNOWN
2999            .iter()
3000            .filter(|(ks, ep)| !seen.contains(&((*ks).to_string(), (*ep).to_string())))
3001            .collect();
3002
3003        if !stale.is_empty() {
3004            for (ks, ep) in &stale {
3005                eprintln!("  STALE ALLOWLIST ENTRY (no longer collides): {ks} :: {ep}");
3006            }
3007            panic!(
3008                "{} allowlisted collision(s) no longer occur — delete them from KNOWN so the list keeps reflecting the real debt",
3009                stale.len()
3010            );
3011        }
3012
3013        if !novel.is_empty() {
3014            for (ks, ep, who) in &novel {
3015                eprintln!("  NEW COLLISION: kernel_source `{ks}` entry_point `{ep}` — {who}");
3016            }
3017            panic!(
3018                "{} NEW cross-file entry-point collision(s) — each would resolve to one KernelRef and trip the duplicate gate at registration. Give the new contract its own entry point, or mark it `registrable: false` if it is documentation.",
3019                novel.len()
3020            );
3021        }
3022    }
3023
3024    /// `lower_file` does per-kernel (front-matter defaults applied).
3025    ///
3026    /// (See `ci_lint_corpus_has_no_cross_file_entry_point_collisions` below for
3027    /// the V-FKC-3 corpus arm.)
3028    fn lower_one_kernel(
3029        file: &FkcFile,
3030        kernel: &FkcKernel,
3031        link: &dyn crate::fkc::lower::LinkRegistry,
3032    ) -> Result<(), FkcError> {
3033        // Reuse the public lower_file by lowering the whole file would re-lower
3034        // everything; instead lower just this kernel via a one-kernel clone.
3035        let mut one = file.clone();
3036        one.kernels = vec![kernel.clone()];
3037        crate::fkc::lower::lower_file(&one, link, &mut Vec::new()).map(|_| ())
3038    }
3039}