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    AwqMatmul => None,
1200    // A JIT-synthesized runtime op's live scalar args. `kernel.rs` pins the
1201    // producer: "Only produced by `compile_one`'s `is_runtime` arm" — the
1202    // kernel is synthesized at run time, so there is no checked-in source
1203    // file for a `.fkc.md` contract to describe.
1204    JitScalars => Some("runtime-JIT-only: no checked-in kernel to contract"),
1205    NonZeroIndices => None,
1206);
1207
1208variant_table!(
1209    FusedOpParams,
1210    _fused_op_params_is_exhaustive,
1211    FUSED_OP_PARAMS_VARIANTS,
1212    SoftmaxLastDim => None,
1213    FusedLinear => None,
1214    RmsNormLastDim => None,
1215    LayerNormLastDim => None,
1216    Rope => None,
1217    Conv2D => None,
1218    SoftmaxLastDimBackward => None,
1219    LayerNormLastDimBackward => None,
1220    RmsNormLastDimBackward => None,
1221    ReduceMaxToBackward => None,
1222    PowIBackward => None,
1223    ConvTranspose2D => None,
1224    FlashAttn => None,
1225    PagedAttn => None,
1226    QMatMul => None,
1227    InplaceAffine => None,
1228    SsdChunkScan => None,
1229    Nf4Matmul => None,
1230    AwqMatmul => None,
1231    FlashAttnBackward => None,
1232    SelectiveScan => None,
1233    CausalConv1d => None,
1234    FusedSoftmaxCrossEntropy => None,
1235    // A runtime-REGISTERED fused op (JIT-synthesized or import-time). Its
1236    // identity is a runtime `FusedOpId` and its recipe lives in the
1237    // `runtime_fused` sidecar, so it has no stable name a hand-authored
1238    // contract could bind to.
1239    Runtime => Some("runtime-registered: identity is a runtime FusedOpId"),
1240);
1241
1242/// Whether a contract may name `v` as an `OpParams` variant. Backed by
1243/// [`OP_PARAMS_VARIANTS`], which the compiler holds against the real
1244/// `OpParams` enum (`fuel-dispatch/src/kernel.rs`).
1245fn is_op_params_variant(v: &str) -> bool {
1246    contract_authorable(OP_PARAMS_VARIANTS, v)
1247}
1248
1249/// Whether a contract may name `v` as a `FusedOpParams` variant. Backed by
1250/// [`FUSED_OP_PARAMS_VARIANTS`], which the compiler holds against the real
1251/// `FusedOpParams` enum (`fuel-graph/src/registry.rs`).
1252fn is_fused_op_params_variant(v: &str) -> bool {
1253    contract_authorable(FUSED_OP_PARAMS_VARIANTS, v)
1254}
1255
1256/// `v` names a real variant in `table` AND that variant carries no exclusion
1257/// reason. An excluded variant is rejected exactly like an unknown one — the
1258/// contract still cannot name it — but the reason is recorded at the entry
1259/// rather than being an unexplained absence.
1260fn contract_authorable(table: &[(&str, Option<&str>)], v: &str) -> bool {
1261    table
1262        .iter()
1263        .any(|(name, excluded)| *name == v && excluded.is_none())
1264}
1265
1266// ===========================================================================
1267// Returns: rule 16 (output dtype) + rule 13 (bundle rank)
1268// ===========================================================================
1269
1270/// A `fixed(DType)` output dtype rule names a dtype token that must be a real
1271/// DType (rule 16). `passthrough(role)` / other rules carry no dtype literal.
1272fn check_output_dtype_rule(section: &str, operand: &str, rule: &str) -> Result<(), FkcError> {
1273    let rule = rule.trim();
1274    if let Some(inner) = rule
1275        .strip_prefix("fixed(")
1276        .and_then(|s| s.strip_suffix(")"))
1277    {
1278        let tok = inner.trim();
1279        lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
1280            section: section.to_string(),
1281            field: format!("{operand}.dtype_rule fixed(...)"),
1282            token: tok.to_string(),
1283        })?;
1284    }
1285    Ok(())
1286}
1287
1288/// Rule 13: every bundle slot's declared shape must be rank ≤ 6 when a static
1289/// shape is given (an inline `[d0, d1, ...]` literal). A `shape_rule:` string
1290/// has no statically-knowable rank without evaluating the rule, so it is not
1291/// rank-checked here — `fkc::return_check::check_slot_rank`, called from
1292/// `cross_check_fused_section` for every bundle slot whose `shape_rule`
1293/// evaluates against a real probe shape, covers that DERIVED case instead
1294/// (Finding 5.3).
1295fn check_bundle_ranks(section: &str, bundle: &serde_yaml_ng::Value) -> Result<(), FkcError> {
1296    let serde_yaml_ng::Value::Sequence(slots) = bundle else {
1297        return Ok(());
1298    };
1299    for (i, slot) in slots.iter().enumerate() {
1300        let serde_yaml_ng::Value::Mapping(map) = slot else {
1301            continue;
1302        };
1303        let slot_name = map
1304            .get(serde_yaml_ng::Value::String("name".into()))
1305            .and_then(|v| v.as_str())
1306            .map(str::to_string)
1307            .unwrap_or_else(|| format!("slot{i}"));
1308        // A static `shape:` literal list is rank-checkable.
1309        if let Some(serde_yaml_ng::Value::Sequence(dims)) =
1310            map.get(serde_yaml_ng::Value::String("shape".into()))
1311            && dims.len() > 6
1312        {
1313            return Err(FkcError::BundleSlotRankExceeded {
1314                section: section.to_string(),
1315                slot: slot_name,
1316                rank: dims.len(),
1317            });
1318        }
1319    }
1320    Ok(())
1321}
1322
1323// ===========================================================================
1324// Rule 8 / 8a — cost
1325// ===========================================================================
1326
1327fn validate_cost(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1328    let Some(cost) = &kernel.cost else {
1329        // required_fields already errored on an absent cost block.
1330        return Ok(());
1331    };
1332
1333    // Rule 8a: provenance present + ∈ {declared, judge_measured}.
1334    match cost.provenance.as_deref() {
1335        Some("declared") | Some("judge_measured") => {}
1336        other => {
1337            return Err(FkcError::CostProvenanceMissing {
1338                section: section.to_string(),
1339                found: other.map(String::from),
1340            });
1341        }
1342    }
1343
1344    // Rule 8: every present cost expression parses (reuse the cost_expr
1345    // parser). A field that is `~`/absent is fine (Unknown).
1346    use crate::fkc::cost_expr;
1347    let parse = |field: &str, src: Option<&str>| -> Result<(), FkcError> {
1348        cost_expr::compile_field(src)
1349            .map(|_| ())
1350            .map_err(|e| FkcError::CostExprParse {
1351                section: section.to_string(),
1352                field: field.to_string(),
1353                expr: src.unwrap_or("").to_string(),
1354                reason: e.to_string(),
1355            })
1356    };
1357    parse("flops", cost.flops.as_deref())?;
1358    parse("bytes_moved", cost.bytes_moved.as_deref())?;
1359    if let Some(serde_yaml_ng::Value::String(s)) = &cost.overhead_ns {
1360        parse("overhead_ns", Some(s))?;
1361    }
1362    if let Some(mem) = &cost.memory {
1363        for (field, v) in [
1364            ("memory.device_bytes", &mem.device_bytes),
1365            ("memory.host_bytes", &mem.host_bytes),
1366            ("memory.disk_bytes", &mem.disk_bytes),
1367        ] {
1368            if let Some(serde_yaml_ng::Value::String(s)) = v {
1369                parse(field, Some(s))?;
1370            }
1371        }
1372    }
1373
1374    // Rule 8a (placeholder): closed classifier (§2.3 / V-FKC-9,
1375    // `cost_compile::classify_cost`). `class: free` is the honest
1376    // metadata-only declaration (license for zero coefficients). A
1377    // `judge_measured` cost legitimately ships shape-hint expressions OR
1378    // all-`~` (the Judge populates it) — the corpus convention is
1379    // `judge_measured` + `~` coefficients, which is NOT a placeholder (it is
1380    // explicitly measured). Otherwise a `declared` cost needs a pinned
1381    // `cost.cost_fn` OR a usable `flops`/`bytes_moved` expression; a
1382    // NON-EMPTY-but-non-`free` `class` alone is NOT a license (closes the old
1383    // `class.is_empty()` escape hatch: a `declared` block with a class but no
1384    // flops/bytes/cost_fn is still a placeholder).
1385    let class = cost.class.as_deref().unwrap_or("");
1386    let has_any_expr = cost.flops.as_deref().is_some_and(|s| !s.trim().is_empty())
1387        || cost
1388            .bytes_moved
1389            .as_deref()
1390            .is_some_and(|s| !s.trim().is_empty());
1391    let has_cost_fn = cost
1392        .cost_fn
1393        .as_deref()
1394        .is_some_and(|s| !s.trim().is_empty());
1395    if cost.provenance.as_deref() == Some("declared")
1396        && crate::fkc::cost_compile::classify_cost("declared", class, has_any_expr, has_cost_fn)
1397            .is_none()
1398    {
1399        return Err(FkcError::PlaceholderCost {
1400            section: section.to_string(),
1401            reason: "provenance: declared with no usable cost path (no flops/bytes_moved \
1402                     expression, no pinned cost.cost_fn) and class is not `free`"
1403                .to_string(),
1404        });
1405    }
1406
1407    Ok(())
1408}
1409
1410// ===========================================================================
1411// Rule 9 — precision coherence (determinism cross-check)
1412// ===========================================================================
1413
1414fn validate_precision_coherence(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1415    let det = kernel.determinism.as_deref().unwrap_or("");
1416    if det == "nondeterministic" {
1417        // nondeterministic ⇒ bit_stable=false + audited:true (no silent
1418        // unaudited nondeterminism; §4.9 / §10.9).
1419        if let Some(p) = &kernel.precision {
1420            if p.bit_stable_on_same_hardware == Some(true) {
1421                return Err(FkcError::QuantIncoherent {
1422                    section: section.to_string(),
1423                    operand: "<precision>".to_string(),
1424                    reason: "determinism: nondeterministic requires bit_stable_on_same_hardware = \
1425                             false"
1426                        .to_string(),
1427                });
1428            }
1429            if p.audited != Some(true) {
1430                return Err(FkcError::QuantIncoherent {
1431                    section: section.to_string(),
1432                    operand: "<precision>".to_string(),
1433                    reason: "determinism: nondeterministic requires audited: true with a \
1434                             none(reason) precision (no silent unaudited nondeterminism)"
1435                        .to_string(),
1436                });
1437            }
1438        }
1439    }
1440    Ok(())
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445    use super::*;
1446    use crate::fkc::parse_file;
1447
1448    // --- A minimal VALID contract builder for unit tests. ---
1449
1450    /// A full, valid single-kernel bundle with the required fields, mutated by
1451    /// the negative tests via string replacement of the `__SLOT__` markers.
1452    fn valid_bundle() -> String {
1453        r#"---
1454fkc_version: 1
1455provider:
1456  name: test-provider
1457  backend: Cpu
1458  kernel_source: "test-cpu"
1459---
1460
1461# test bundle
1462
1463## demo
1464
1465A blurb.
1466
1467```fkc
1468kernel: demo
1469op_kind: AddElementwise
1470blurb: "demo blurb"
1471entry_point: "x::y"
1472accept:
1473  inputs:
1474    - name: lhs
1475      dtypes: [F32]
1476      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1477    - name: rhs
1478      dtypes: [F32]
1479      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1480  op_params: { variant: None }
1481return:
1482  outputs:
1483    - name: out
1484      dtype_rule: passthrough(lhs)
1485caps:
1486  awkward_layout_strategy: requires_contiguous
1487cost:
1488  provenance: declared
1489  class: cheap_elementwise
1490  flops: "n"
1491  bytes_moved: "3 * n * 4"
1492precision:
1493  bit_stable_on_same_hardware: true
1494  audited: true
1495determinism: same_hardware_bitwise
1496```
1497"#
1498        .to_string()
1499    }
1500
1501    fn validate_str(src: &str) -> Result<(), FkcError> {
1502        let file = parse_file(src).expect("fixture parses");
1503        validate_file(&file)
1504    }
1505
1506    #[test]
1507    fn valid_contract_passes() {
1508        validate_str(&valid_bundle()).expect("the valid fixture validates");
1509    }
1510
1511    // ===== Rule 1 =====
1512    #[test]
1513    fn version_too_new_is_unsupported() {
1514        let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 99");
1515        let err = validate_str(&src).expect_err("version 99 unsupported");
1516        assert!(
1517            matches!(err, FkcError::UnsupportedVersion { found: 99, .. }),
1518            "got {err:?}"
1519        );
1520    }
1521
1522    // ===== GAP-292: the accepted set is membership, not `<= max` (KISS #450) =====
1523
1524    /// GAP-292 born-red (LOW side): `fkc_version: 0` MUST be rejected. The pre-fix
1525    /// gate was a bare `fkc_version > FKC_VERSION_MAX`, so 0 (being ≤ max) was
1526    /// silently ACCEPTED — the accepted set was {0, 1} and 0 is not a version.
1527    /// KISS #450: a reader accepts a version IFF it is in its declared supported
1528    /// set, NEVER merely because it is lower than a supported one. This fails on
1529    /// the pre-fix gate (0 passes the `> max` test).
1530    #[test]
1531    fn fkc_version_zero_is_rejected() {
1532        let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 0");
1533        let err = validate_str(&src).expect_err("fkc_version 0 must be rejected");
1534        assert!(
1535            matches!(err, FkcError::UnsupportedVersion { found: 0, .. }),
1536            "expected UnsupportedVersion for fkc_version 0, got {err:?}"
1537        );
1538    }
1539
1540    /// GAP-292 fencepost guard (HIGH-but-sole side, the coverage the pre-fix test
1541    /// set never had): `fkc_version: 1`, the sole supported version, MUST still be
1542    /// accepted after the lower-bound fix. A bound written in haste lands a
1543    /// fencepost exactly here; paired with the low-side test it pins BOTH ends.
1544    #[test]
1545    fn fkc_version_one_is_still_accepted() {
1546        let result = validate_str(&valid_bundle());
1547        assert!(
1548            !matches!(result, Err(FkcError::UnsupportedVersion { .. })),
1549            "fkc_version 1 (sole supported) must pass the version gate, got {result:?}"
1550        );
1551    }
1552
1553    // ===== GAP-038: FKC two version axes (KISS-CONTRACT §8-0001..0007) =====
1554
1555    /// §8-0001: `fkc_version` (wire/schema) and the crate's published semver are
1556    /// INDEPENDENT axes. A crate release that bumps only `Cargo.toml`'s version
1557    /// leaves the wire schema untouched, so `FKC_SUPPORTED_VERSIONS` is a literal —
1558    /// never derived from `CARGO_PKG_VERSION`. If it were, publishing the crate
1559    /// would silently advance the accepted set and reject older contracts.
1560    #[test]
1561    fn fkc_version_is_decoupled_from_crate_semver() {
1562        // A literal evaluated at compile time; the module-scope pin is the
1563        // structural enforcement, this records the decoupling as an executable fact.
1564        assert_eq!(FKC_SUPPORTED_VERSIONS, &[1]);
1565    }
1566
1567    /// §8-0002: the front-matter SCHEMA and the schema version move together. The
1568    /// exhaustive-destructure pin at module scope compile-breaks on a field change;
1569    /// this test pins the *version number* that field set corresponds to — the
1570    /// LATEST supported version (the max of the set) — so the two cannot drift
1571    /// apart. Add a new entry to `FKC_SUPPORTED_VERSIONS` AND bump
1572    /// `PINNED_FKC_SCHEMA_VERSION` when the pin forces a new field.
1573    #[test]
1574    fn fkc_front_matter_schema_is_pinned_to_the_version() {
1575        const PINNED_FKC_SCHEMA_VERSION: u32 = 1;
1576        assert_eq!(
1577            FKC_SUPPORTED_VERSIONS.iter().copied().max(),
1578            Some(PINNED_FKC_SCHEMA_VERSION),
1579            "the latest supported fkc_version moved without updating the pinned \
1580             front-matter schema (or vice-versa) — a §8-0002 schema change bumps both"
1581        );
1582    }
1583
1584    // ===== Rule 2 =====
1585    #[test]
1586    fn missing_blurb_errors() {
1587        let src = valid_bundle().replace("blurb: \"demo blurb\"\n", "");
1588        let err = validate_str(&src).expect_err("no blurb");
1589        assert!(matches!(err, FkcError::MissingBlurb { .. }), "got {err:?}");
1590    }
1591
1592    #[test]
1593    fn missing_cost_block_errors() {
1594        // Remove the whole cost block.
1595        let src = valid_bundle();
1596        let cut = src.replace(
1597            "cost:\n  provenance: declared\n  class: cheap_elementwise\n  flops: \"n\"\n  bytes_moved: \"3 * n * 4\"\n",
1598            "",
1599        );
1600        let err = validate_str(&cut).expect_err("no cost block");
1601        assert!(
1602            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "cost"),
1603            "got {err:?}"
1604        );
1605    }
1606
1607    #[test]
1608    fn missing_determinism_errors() {
1609        let src = valid_bundle().replace("determinism: same_hardware_bitwise\n", "");
1610        let err = validate_str(&src).expect_err("no determinism");
1611        assert!(
1612            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "determinism"),
1613            "got {err:?}"
1614        );
1615    }
1616
1617    #[test]
1618    fn both_op_kind_and_fused_op_is_ambiguous() {
1619        let src = valid_bundle().replace(
1620            "op_kind: AddElementwise",
1621            "op_kind: AddElementwise\nfused_op: SOFTMAX_LAST_DIM",
1622        );
1623        let err = validate_str(&src).expect_err("both targets");
1624        assert!(
1625            matches!(err, FkcError::OpTargetAmbiguous { .. }),
1626            "got {err:?}"
1627        );
1628    }
1629
1630    // ===== Rule 3 / 16 — dtype =====
1631    #[test]
1632    fn bad_dtype_token_is_fdx_token_not_in_table() {
1633        let src = valid_bundle().replace("dtypes: [F32]", "dtypes: [F99]");
1634        let err = validate_str(&src).expect_err("F99 not a dtype");
1635        assert!(
1636            matches!(err, FkcError::FdxTokenNotInTable { .. }),
1637            "got {err:?}"
1638        );
1639    }
1640
1641    // ===== Rule 4 — layout coherence =====
1642    #[test]
1643    fn layout_no_acceptable_form_is_incoherent() {
1644        // contiguous: n/a + strided: rejected ⇒ accepts no layout.
1645        let src = valid_bundle().replace(
1646            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1647            "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1648        );
1649        let err = validate_str(&src).expect_err("no acceptable layout");
1650        assert!(
1651            matches!(err, FkcError::LayoutIncoherent { .. }),
1652            "got {err:?}"
1653        );
1654    }
1655
1656    #[test]
1657    fn broadcast_without_strided_is_incoherent() {
1658        let src = valid_bundle().replace(
1659            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1660            "layout: { contiguous: required, strided: rejected, broadcast_stride0: accepted, start_offset: rejected, reverse_strides: rejected }",
1661        );
1662        let err = validate_str(&src).expect_err("broadcast w/o strided");
1663        assert!(
1664            matches!(err, FkcError::LayoutIncoherent { .. }),
1665            "got {err:?}"
1666        );
1667    }
1668
1669    #[test]
1670    fn reverse_without_strided_is_incoherent() {
1671        let src = valid_bundle().replace(
1672            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1673            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: accepted }",
1674        );
1675        let err = validate_str(&src).expect_err("reverse w/o strided");
1676        assert!(
1677            matches!(err, FkcError::LayoutIncoherent { .. }),
1678            "got {err:?}"
1679        );
1680    }
1681
1682    // ===== Rule 4 — broadcast_axes mask (§6-additive, path 1a) =====
1683    #[test]
1684    fn required_broadcast_with_axes_is_valid() {
1685        // Baracuda's §6-additive spelling for a baked bias-add cell.
1686        let src = valid_bundle()
1687            .replace(
1688                "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1689                "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1690            )
1691            .replace(
1692                "awkward_layout_strategy: requires_contiguous",
1693                "awkward_layout_strategy: handles_strided",
1694            );
1695        validate_str(&src)
1696            .expect("required broadcast with a non-empty broadcast_axes mask validates");
1697    }
1698
1699    #[test]
1700    fn required_broadcast_without_axes_is_incoherent() {
1701        let src = valid_bundle().replace(
1702            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1703            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, start_offset: rejected, reverse_strides: rejected }",
1704        );
1705        let err = validate_str(&src).expect_err("required broadcast needs a broadcast_axes mask");
1706        assert!(
1707            matches!(err, FkcError::LayoutIncoherent { .. }),
1708            "got {err:?}"
1709        );
1710    }
1711
1712    #[test]
1713    fn broadcast_axes_without_required_is_incoherent() {
1714        let src = valid_bundle().replace(
1715            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1716            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: accepted, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1717        );
1718        let err = validate_str(&src).expect_err("broadcast_axes is meaningful only with required");
1719        assert!(
1720            matches!(err, FkcError::LayoutIncoherent { .. }),
1721            "got {err:?}"
1722        );
1723    }
1724
1725    #[test]
1726    fn required_broadcast_with_empty_axes_is_incoherent() {
1727        let src = valid_bundle().replace(
1728            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1729            "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [], start_offset: rejected, reverse_strides: rejected }",
1730        );
1731        let err = validate_str(&src).expect_err("empty broadcast_axes on a required broadcast");
1732        assert!(
1733            matches!(err, FkcError::LayoutIncoherent { .. }),
1734            "got {err:?}"
1735        );
1736    }
1737
1738    // ===== Rule 5 — awkward strategy =====
1739    #[test]
1740    fn handles_strided_without_strided_accepted_is_incoherent() {
1741        let src = valid_bundle().replace(
1742            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1743            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: handles_strided }",
1744        );
1745        let err = validate_str(&src).expect_err("handles_strided but strided rejected");
1746        assert!(
1747            matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1748            "got {err:?}"
1749        );
1750    }
1751
1752    #[test]
1753    fn unknown_awkward_strategy_is_typed_error() {
1754        let src = valid_bundle().replace(
1755            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1756            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: teleports }",
1757        );
1758        let err = validate_str(&src).expect_err("unknown strategy");
1759        assert!(
1760            matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1761            "got {err:?}"
1762        );
1763    }
1764
1765    // ===== Rule 6 — quant =====
1766    fn quant_bundle(family: &str, extra: &str) -> String {
1767        // A QMatMul-shaped contract whose weight carries an fdx.quant block.
1768        format!(
1769            r#"---
1770fkc_version: 1
1771provider:
1772  name: test-provider
1773  backend: Cpu
1774  kernel_source: "test-cpu"
1775---
1776
1777# q
1778
1779## q
1780
1781blurb.
1782
1783```fkc
1784kernel: q
1785op_kind: QMatMul
1786blurb: "q"
1787entry_point: "x::q"
1788accept:
1789  inputs:
1790    - name: act
1791      dtypes: [F32]
1792      layout: {{ contiguous: required, strided: rejected }}
1793    - name: weight
1794      dtypes: [U8]
1795      layout: {{ contiguous: required, strided: rejected }}
1796      fdx:
1797        requires_ext: true
1798        quant:
1799          family: {family}
1800{extra}
1801  op_params: {{ variant: QMatMul }}
1802return:
1803  outputs:
1804    - name: out
1805      dtype_rule: fixed(F32)
1806cost:
1807  provenance: judge_measured
1808  class: gemm_like
1809precision:
1810  bit_stable_on_same_hardware: true
1811  audited: true
1812determinism: same_hardware_bitwise
1813```
1814"#
1815        )
1816    }
1817
1818    #[test]
1819    fn ggml_block_with_real_dtype_ok() {
1820        let src = quant_bundle(
1821            "GGML_BLOCK",
1822            "          ggml_dtype: Q4_0\n          role: weight",
1823        );
1824        validate_str(&src).expect("GGML_BLOCK Q4_0 weight validates");
1825    }
1826
1827    #[test]
1828    fn ggml_dtype_q4_k_m_is_quant_incoherent() {
1829        // The GGUF file-format name `Q4_K_M` is NOT a GgmlDType variant.
1830        let src = quant_bundle(
1831            "GGML_BLOCK",
1832            "          ggml_dtype: Q4_K_M\n          role: weight",
1833        );
1834        let err = validate_str(&src).expect_err("Q4_K_M bad");
1835        assert!(
1836            matches!(err, FkcError::QuantIncoherent { .. }),
1837            "got {err:?}"
1838        );
1839    }
1840
1841    #[test]
1842    fn ggml_block_with_granularity_is_incoherent() {
1843        let src = quant_bundle(
1844            "GGML_BLOCK",
1845            "          ggml_dtype: Q4_0\n          granularity: PerChannel\n          role: weight",
1846        );
1847        let err = validate_str(&src).expect_err("GGML carries no granularity");
1848        assert!(
1849            matches!(err, FkcError::QuantIncoherent { .. }),
1850            "got {err:?}"
1851        );
1852    }
1853
1854    #[test]
1855    fn mx_family_is_not_yet_registrable() {
1856        let src = quant_bundle(
1857            "MX",
1858            "          granularity: PerBlock\n          role: weight",
1859        );
1860        let err = validate_str(&src).expect_err("MX not registrable");
1861        assert!(
1862            matches!(err, FkcError::MxNotYetRegistrable { ref family, .. } if family == "MX"),
1863            "got {err:?}"
1864        );
1865    }
1866
1867    #[test]
1868    fn affine_block_without_block_shape_is_incoherent() {
1869        let src = quant_bundle(
1870            "AFFINE_BLOCK",
1871            "          role: weight\n          scale_operand: scl",
1872        );
1873        // scale_operand `scl` won't exist either, but block_shape missing OR the
1874        // scale-operand check fires; both are coherence errors.
1875        let err = validate_str(&src).expect_err("AFFINE_BLOCK needs block_shape / real scale");
1876        assert!(
1877            matches!(
1878                err,
1879                FkcError::QuantIncoherent { .. } | FkcError::ScaleDoubleDeclared { .. }
1880            ),
1881            "got {err:?}"
1882        );
1883    }
1884
1885    #[test]
1886    fn affine_int_bad_granularity_is_incoherent() {
1887        let src = quant_bundle(
1888            "AFFINE_INT",
1889            "          granularity: PerBlock\n          role: weight",
1890        );
1891        let err = validate_str(&src).expect_err("AFFINE_INT PerBlock bad");
1892        assert!(
1893            matches!(err, FkcError::QuantIncoherent { .. }),
1894            "got {err:?}"
1895        );
1896    }
1897
1898    #[test]
1899    fn unknown_family_is_fdx_token_not_in_table() {
1900        let src = quant_bundle("MADE_UP", "          role: weight");
1901        let err = validate_str(&src).expect_err("MADE_UP not an FDX family");
1902        assert!(
1903            matches!(err, FkcError::FdxTokenNotInTable { .. }),
1904            "got {err:?}"
1905        );
1906    }
1907
1908    #[test]
1909    fn ggml_with_separate_scale_operand_is_double_declared() {
1910        // GGML scales are INLINE; declaring a separate scale_operand is the
1911        // double-declaration §10.6 forbids. (act is a real input role.)
1912        let src = quant_bundle(
1913            "GGML_BLOCK",
1914            "          ggml_dtype: Q4_0\n          role: weight\n          scale_operand: act",
1915        );
1916        let err = validate_str(&src).expect_err("GGML + separate scale");
1917        assert!(
1918            matches!(err, FkcError::ScaleDoubleDeclared { .. }),
1919            "got {err:?}"
1920        );
1921    }
1922
1923    // ===== Rule 7 — op-param namespace =====
1924    #[test]
1925    fn op_kind_with_fused_only_variant_is_bad_namespace() {
1926        // `FusedLinear` is a FusedOpParams variant, NOT an OpParams variant; an
1927        // op_kind contract naming it must fail the namespace check.
1928        let src = valid_bundle().replace(
1929            "op_params: { variant: None }",
1930            "op_params: { variant: FusedLinear }",
1931        );
1932        let err = validate_str(&src).expect_err("FusedLinear not in OpParams");
1933        assert!(
1934            matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
1935            "got {err:?}"
1936        );
1937    }
1938
1939    // ===== V-FKC-5 — the op-param allowlists vs the real enums =====
1940
1941    /// The drift this closes, stated as behavior rather than as a list
1942    /// comparison: `OpParams::NonZeroIndices` is a real, kernel-backed variant
1943    /// (1 input, a `[indices; count]` bundle output — the data-dependent
1944    /// dynamic-shape seam), and a contract naming it was rejected as if it
1945    /// were a typo, because the hand-typed allowlist was two variants behind
1946    /// the enum.
1947    #[test]
1948    fn op_params_namespace_accepts_the_real_nonzeroindices_variant() {
1949        let src = valid_bundle().replace(
1950            "op_params: { variant: None }",
1951            "op_params: { variant: NonZeroIndices }",
1952        );
1953        validate_str(&src)
1954            .expect("NonZeroIndices is a real OpParams variant — a contract may name it");
1955    }
1956
1957    /// The predicate must be exactly the generated table, in both directions.
1958    /// The table itself is held against the real enum by the compiler (see
1959    /// `variant_table!`), so this is what closes the loop between "the list
1960    /// matches the enum" and "the checker reads the list".
1961    #[test]
1962    fn op_param_predicates_agree_with_the_generated_variant_tables() {
1963        // Positive control: a macro that expanded to nothing, or a table that
1964        // silently lost its entries, would make every assertion below vacuous.
1965        // These floors are deliberately loose — they catch an empty or
1966        // truncated table, not a single added variant.
1967        assert!(
1968            OP_PARAMS_VARIANTS.len() >= 40,
1969            "OP_PARAMS_VARIANTS looks truncated ({} entries) — the rest of this \
1970             test would pass vacuously",
1971            OP_PARAMS_VARIANTS.len()
1972        );
1973        assert!(
1974            FUSED_OP_PARAMS_VARIANTS.len() >= 20,
1975            "FUSED_OP_PARAMS_VARIANTS looks truncated ({} entries)",
1976            FUSED_OP_PARAMS_VARIANTS.len()
1977        );
1978
1979        for (name, excluded) in OP_PARAMS_VARIANTS {
1980            assert_eq!(
1981                is_op_params_variant(name),
1982                excluded.is_none(),
1983                "OpParams::{name} — excluded={excluded:?} but the predicate says \
1984                 {}",
1985                is_op_params_variant(name)
1986            );
1987        }
1988        for (name, excluded) in FUSED_OP_PARAMS_VARIANTS {
1989            assert_eq!(
1990                is_fused_op_params_variant(name),
1991                excluded.is_none(),
1992                "FusedOpParams::{name} — excluded={excluded:?} but the predicate \
1993                 says {}",
1994                is_fused_op_params_variant(name)
1995            );
1996        }
1997
1998        // An exclusion with no stated reason is the silent omission this whole
1999        // change exists to eliminate.
2000        for (name, excluded) in OP_PARAMS_VARIANTS.iter().chain(FUSED_OP_PARAMS_VARIANTS) {
2001            if let Some(reason) = excluded {
2002                assert!(
2003                    !reason.trim().is_empty(),
2004                    "{name} is excluded with an empty reason"
2005                );
2006            }
2007        }
2008    }
2009
2010    /// The two deliberate exclusions, pinned so a later reader doesn't "fix"
2011    /// them back in: both name kernels that are synthesized or registered at
2012    /// RUN time, so there is no checked-in kernel for a `.fkc.md` contract to
2013    /// describe. This is the one case where rejection is correct — and the
2014    /// reason is now recorded at the table entry instead of being an
2015    /// unexplained gap.
2016    #[test]
2017    fn runtime_only_op_param_variants_stay_out_of_contract_reach() {
2018        assert!(!is_op_params_variant("JitScalars"));
2019        assert!(!is_fused_op_params_variant("Runtime"));
2020
2021        let src = valid_bundle().replace(
2022            "op_params: { variant: None }",
2023            "op_params: { variant: JitScalars }",
2024        );
2025        let err = validate_str(&src).expect_err("JitScalars is runtime-JIT-only");
2026        assert!(
2027            matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
2028            "got {err:?}"
2029        );
2030    }
2031
2032    // ===== V-FKC-10 — FKC's quant tokens ⊆ FDX's normative code table =====
2033
2034    /// FDX's `codes` module doc names it "the normative owner of the shared
2035    /// dtype/quant/granularity/... codes (the FKC kernel-contract format
2036    /// references them by symbol)". FKC's [`is_fdx_quant_family`] /
2037    /// [`is_fdx_granularity`] hand-copy that table as string literals, and
2038    /// nothing checked the two still agreed — in either direction.
2039    ///
2040    /// The guard reads `codes.rs` as TEXT rather than by symbol, deliberately.
2041    /// FDX's consts live behind fuel-ir's non-default `dlpack` feature, so a
2042    /// symbol-level test would compile only under `--features dlpack` and
2043    /// would report `test result: ok` while running nothing under the default
2044    /// gate — the exact failure mode that let this drift survive. Reading the
2045    /// source text needs no feature, so this runs in a plain
2046    /// `cargo test -p fuel-dispatch`.
2047    ///
2048    /// Read at run time, not `include_str!`d: `fuel-ir` is a sibling crate, so the
2049    /// file is outside this crate's tarball, and an `include_str!` of it would make
2050    /// the published crate unbuildable (the packaged-crate check fails any `src/`
2051    /// include that leaves the crate). From a tarball the test fails loudly rather
2052    /// than skipping: it is a workspace consistency check.
2053    fn fdx_codes_src() -> String {
2054        let path =
2055            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../fuel-ir/src/dlpack/codes.rs");
2056        std::fs::read_to_string(&path).unwrap_or_else(|e| {
2057            panic!(
2058                "the FDX code table must be readable from the workspace (this test checks \
2059                 fuel-dispatch against fuel-ir's source text); read {path:?}: {e}"
2060            )
2061        })
2062    }
2063
2064    /// `(FDX const name, the token an FKC contract writes)`. Every const named
2065    /// here must exist in `codes.rs` and every `FDX_QUANT_*` const there must
2066    /// appear here — so an FDX addition, removal, or rename all fail loudly.
2067    const FDX_FAMILY_TOKENS: &[(&str, &str)] = &[
2068        ("FDX_QUANT_NONE", "NONE"),
2069        ("FDX_QUANT_GGML_BLOCK", "GGML_BLOCK"),
2070        ("FDX_QUANT_MX", "MX"),
2071        ("FDX_QUANT_AFFINE_INT", "AFFINE_INT"),
2072        ("FDX_QUANT_AFFINE_FLOAT", "AFFINE_FLOAT"),
2073        ("FDX_QUANT_AFFINE_BLOCK", "AFFINE_BLOCK"),
2074    ];
2075
2076    /// As [`FDX_FAMILY_TOKENS`], for `FDXScaleGranularity`.
2077    const FDX_GRAN_TOKENS: &[(&str, &str)] = &[
2078        ("FDX_SCALE_GRAN_PER_TENSOR", "PerTensor"),
2079        ("FDX_SCALE_GRAN_PER_TOKEN", "PerToken"),
2080        ("FDX_SCALE_GRAN_PER_CHANNEL", "PerChannel"),
2081        ("FDX_SCALE_GRAN_PER_BLOCK", "PerBlock"),
2082    ];
2083
2084    /// Every `pub const <prefix>… ` declared in FDX's code table.
2085    fn fdx_consts_with_prefix(prefix: &str) -> std::collections::BTreeSet<String> {
2086        fdx_codes_src()
2087            .lines()
2088            .filter_map(|l| l.trim().strip_prefix("pub const ")?.split(':').next())
2089            .map(|n| n.trim().to_string())
2090            .filter(|n| n.starts_with(prefix))
2091            .collect()
2092    }
2093
2094    #[test]
2095    fn fkc_quant_tokens_are_a_faithful_subset_of_the_fdx_code_table() {
2096        let families = fdx_consts_with_prefix("FDX_QUANT_");
2097        let grans = fdx_consts_with_prefix("FDX_SCALE_GRAN_");
2098
2099        // Positive control. A parse that matched nothing — `codes.rs` reshaped,
2100        // the consts moved, the `pub const` spelling changed — would make every
2101        // set comparison below trivially satisfiable in the *dangerous*
2102        // direction (FKC accepts tokens FDX no longer defines). Assert the
2103        // instrument works before trusting what it reports.
2104        assert!(
2105            families.contains("FDX_QUANT_GGML_BLOCK") && grans.contains("FDX_SCALE_GRAN_PER_BLOCK"),
2106            "parsed {} family / {} granularity consts out of {} lines of codes.rs — \
2107             the parse found neither known anchor, so it is broken, not the table",
2108            families.len(),
2109            grans.len(),
2110            fdx_codes_src().lines().count()
2111        );
2112
2113        let declared_families: std::collections::BTreeSet<String> = FDX_FAMILY_TOKENS
2114            .iter()
2115            .map(|(c, _)| c.to_string())
2116            .collect();
2117        assert_eq!(
2118            families, declared_families,
2119            "FDX's FDX_QUANT_* table and FKC's mirror of it have diverged — update \
2120             FDX_FAMILY_TOKENS *and* is_fdx_quant_family together"
2121        );
2122
2123        let declared_grans: std::collections::BTreeSet<String> =
2124            FDX_GRAN_TOKENS.iter().map(|(c, _)| c.to_string()).collect();
2125        assert_eq!(
2126            grans, declared_grans,
2127            "FDX's FDX_SCALE_GRAN_* table and FKC's mirror of it have diverged"
2128        );
2129
2130        // …and the tokens themselves round-trip through the FKC predicates.
2131        for (konst, token) in FDX_FAMILY_TOKENS {
2132            assert!(
2133                is_fdx_quant_family(token),
2134                "FKC rejects `{token}`, the token for the live FDX const {konst}"
2135            );
2136        }
2137        for (konst, token) in FDX_GRAN_TOKENS {
2138            assert!(
2139                is_fdx_granularity(token),
2140                "FKC rejects `{token}`, the token for the live FDX const {konst}"
2141            );
2142        }
2143
2144        // The corpus writes `none` lowercase; both spellings are accepted.
2145        assert!(is_fdx_quant_family("none"));
2146
2147        // Plausible-but-not-FDX tokens must NOT be accepted, or the subset
2148        // property above is satisfied by a predicate that accepts everything.
2149        for bogus in ["PerGroup", "AWQ", "GPTQ", "affine_int", "PERTENSOR", ""] {
2150            assert!(!is_fdx_quant_family(bogus), "family accepted `{bogus}`");
2151            assert!(!is_fdx_granularity(bogus), "granularity accepted `{bogus}`");
2152        }
2153    }
2154
2155    // ===== Rule 8 / 8a — cost =====
2156    #[test]
2157    fn missing_provenance_errors() {
2158        let src = valid_bundle().replace("  provenance: declared\n", "");
2159        let err = validate_str(&src).expect_err("no provenance");
2160        assert!(
2161            matches!(err, FkcError::CostProvenanceMissing { .. }),
2162            "got {err:?}"
2163        );
2164    }
2165
2166    #[test]
2167    fn bad_cost_expr_is_cost_expr_parse() {
2168        let src = valid_bundle().replace("flops: \"n\"", "flops: \"2 * * n\"");
2169        let err = validate_str(&src).expect_err("malformed flops");
2170        assert!(matches!(err, FkcError::CostExprParse { .. }), "got {err:?}");
2171    }
2172
2173    #[test]
2174    fn declared_with_no_coeffs_and_no_class_is_placeholder() {
2175        let src = valid_bundle()
2176            .replace("  class: cheap_elementwise\n", "")
2177            .replace("  flops: \"n\"\n", "")
2178            .replace("  bytes_moved: \"3 * n * 4\"\n", "");
2179        let err = validate_str(&src).expect_err("bare declared cost");
2180        assert!(
2181            matches!(err, FkcError::PlaceholderCost { .. }),
2182            "got {err:?}"
2183        );
2184    }
2185
2186    #[test]
2187    fn judge_measured_all_tilde_is_not_placeholder() {
2188        // The corpus convention: judge_measured + all-`~` coefficients is an
2189        // explicit measured cost, NOT a placeholder.
2190        let src = valid_bundle()
2191            .replace("provenance: declared", "provenance: judge_measured")
2192            .replace("  flops: \"n\"\n", "  flops: ~\n")
2193            .replace("  bytes_moved: \"3 * n * 4\"\n", "  bytes_moved: ~\n");
2194        validate_str(&src).expect("judge_measured + ~ is fine");
2195    }
2196
2197    #[test]
2198    fn placeholder_cost_class_field() {
2199        use crate::fkc::cost_compile::{CostClassKind, classify_cost};
2200        assert!(classify_cost("declared", "cheap_elementwise", false, false).is_none());
2201        assert!(matches!(
2202            classify_cost("declared", "free", false, false),
2203            Some(CostClassKind::Free)
2204        ));
2205        assert!(matches!(
2206            classify_cost("declared", "gemm_like", true, false),
2207            Some(CostClassKind::DeclaredFormula)
2208        ));
2209        assert!(matches!(
2210            classify_cost("declared", "gemm_like", false, true),
2211            Some(CostClassKind::VendorSpec)
2212        ));
2213        assert!(matches!(
2214            classify_cost("judge_measured", "gemm_like", false, false),
2215            Some(CostClassKind::JudgeMeasured)
2216        ));
2217        // integration: a declared+non-free class with NO flops/bytes/cost_fn is rejected on import
2218        use crate::fkc::register::import_bundle_str;
2219        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";
2220        let err = import_bundle_str(src, &StubLink::new())
2221            .expect_err("declared + non-free class + no usable cost path must reject");
2222        assert!(
2223            matches!(err, FkcError::PlaceholderCost { .. }),
2224            "got {err:?}"
2225        );
2226    }
2227
2228    // ===== Rule 9 — determinism/precision =====
2229    #[test]
2230    fn nondeterministic_with_bitstable_true_is_incoherent() {
2231        let src = valid_bundle().replace(
2232            "determinism: same_hardware_bitwise",
2233            "determinism: nondeterministic",
2234        );
2235        let err = validate_str(&src).expect_err("nondeterministic + bit_stable true");
2236        assert!(
2237            matches!(err, FkcError::QuantIncoherent { .. }),
2238            "got {err:?}"
2239        );
2240    }
2241
2242    // ===== Rule 15 — extent =====
2243    #[test]
2244    fn unknown_extent_kind_is_typed_error() {
2245        let src = quant_bundle("none", "").replace(
2246            "      fdx:\n        requires_ext: true\n        quant:\n          family: none\n",
2247            "      fdx:\n        extent_kind: wobbly\n        symbolic_extent: required\n",
2248        );
2249        let err = validate_str(&src).expect_err("bad extent_kind");
2250        assert!(
2251            matches!(err, FkcError::UnknownAdmissibilityEnum { .. }),
2252            "got {err:?}"
2253        );
2254    }
2255
2256    // ===== Rule 17 / §3.10 — describe-only (non-registrable) sections =====
2257
2258    /// A describe-only chassis-umbrella bundle whose `op_kind:` is the
2259    /// DESCRIPTIVE token `binary` (NOT a real `OpKind`) — legal because
2260    /// `registrable: false` skips the dispatch-resolution checks (§3.10).
2261    fn describe_only_bundle() -> String {
2262        r#"---
2263fkc_version: 1
2264provider:
2265  name: test-provider
2266  backend: Cpu
2267  kernel_source: "test-cpu"
2268---
2269
2270# describe-only bundle
2271
2272## binary
2273
2274A chassis umbrella.
2275
2276```fkc
2277kernel: binary
2278registrable: false
2279op_kind: binary
2280blurb: "shared binary chassis (documentation umbrella)"
2281entry_point: "x::chassis"
2282accept:
2283  inputs:
2284    - name: lhs
2285      dtypes: [F32, F64, BF16, F16]
2286      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2287    - name: rhs
2288      dtypes: [F32, F64, BF16, F16]
2289      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2290return:
2291  outputs:
2292    - name: out
2293      dtype_rule: passthrough(lhs)
2294cost:
2295  provenance: judge_measured
2296  class: cheap_elementwise
2297precision:
2298  bit_stable_on_same_hardware: true
2299  audited: true
2300determinism: same_hardware_bitwise
2301```
2302"#
2303        .to_string()
2304    }
2305
2306    #[test]
2307    fn describe_only_with_descriptive_op_kind_validates() {
2308        // `op_kind: binary` is NOT a real OpKind, but registrable: false skips
2309        // the resolution check — the section validates as documentation.
2310        validate_str(&describe_only_bundle()).expect("describe-only section validates");
2311    }
2312
2313    #[test]
2314    fn describe_only_with_tilde_op_kind_validates() {
2315        // `op_kind: ~` (absent) is legal for a describe-only section — neither
2316        // op_kind nor fused_op need resolve.
2317        let src = describe_only_bundle().replace("op_kind: binary\n", "op_kind: ~\n");
2318        validate_str(&src).expect("describe-only with op_kind: ~ validates");
2319    }
2320
2321    #[test]
2322    fn non_describe_only_still_requires_real_op_kind_regression() {
2323        // REGRESSION: the SAME section WITHOUT registrable: false must still
2324        // fail — a descriptive `binary` token is not a real OpKind, so a
2325        // registrable section is rejected (no relaxation of the validator).
2326        let src = describe_only_bundle().replace("registrable: false\n", "");
2327        let err = validate_str(&src).expect_err("registrable section needs a real op_kind");
2328        assert!(matches!(err, FkcError::UnknownOpKind { .. }), "got {err:?}");
2329    }
2330
2331    #[test]
2332    fn describe_only_still_validates_descriptive_dtypes() {
2333        // A describe-only section's descriptive checks STILL run: a bogus dtype
2334        // token is rejected by the FDX-subset drift-guard even though dispatch
2335        // resolution is skipped.
2336        let src = describe_only_bundle().replace("dtypes: [F32, F64, BF16, F16]", "dtypes: [F99]");
2337        let err = validate_str(&src).expect_err("bad dtype in describe-only docs");
2338        assert!(
2339            matches!(err, FkcError::FdxTokenNotInTable { .. }),
2340            "got {err:?}"
2341        );
2342    }
2343
2344    #[test]
2345    fn describe_only_still_validates_layout_coherence() {
2346        // Layout coherence (rule 4) still runs for describe-only docs.
2347        let src = describe_only_bundle().replace(
2348            "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2349            "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2350        );
2351        let err = validate_str(&src).expect_err("incoherent layout in describe-only docs");
2352        assert!(
2353            matches!(err, FkcError::LayoutIncoherent { .. }),
2354            "got {err:?}"
2355        );
2356    }
2357
2358    /// A describe-only zero-operand op (e.g. a `rand_uniform`/`rand_normal`
2359    /// random fill whose only "input" is backend-private RNG state, not a graph
2360    /// tensor): `registrable: false` + `accept.inputs: []`. Mirrors the
2361    /// `metal/sort-random.fkc.md` random-fill sections.
2362    fn describe_only_zero_input_bundle() -> String {
2363        r#"---
2364fkc_version: 1
2365provider:
2366  name: test-provider
2367  backend: Cpu
2368  kernel_source: "test-cpu"
2369---
2370
2371# describe-only zero-input bundle
2372
2373## rand_fill
2374
2375A zero-operand random fill.
2376
2377```fkc
2378kernel: rand_fill
2379registrable: false
2380op_kind: RandUniform
2381blurb: "fill a dense buffer with U(min,max); no input tensor (RNG state is backend-private)"
2382entry_point: "x::rand_fill"
2383accept:
2384  inputs: []
2385return:
2386  outputs:
2387    - name: out
2388      dtype_rule: cast(out)
2389cost:
2390  provenance: judge_measured
2391  class: cheap_elementwise
2392precision:
2393  bit_stable_on_same_hardware: false
2394  audited: true
2395determinism: nondeterministic
2396```
2397"#
2398        .to_string()
2399    }
2400
2401    #[test]
2402    fn describe_only_zero_input_fill_validates() {
2403        // §3.10 carve-out: a describe-only section with `accept.inputs: []` is a
2404        // legitimate zero-operand documentation op (random fill) — the ≥1-input
2405        // required-field check is EXEMPTED for describe-only sections.
2406        validate_str(&describe_only_zero_input_bundle())
2407            .expect("describe-only zero-input fill validates");
2408    }
2409
2410    #[test]
2411    fn registrable_zero_input_still_fails_missing_required_field() {
2412        // REGRESSION: the SAME zero-input section WITHOUT registrable: false must
2413        // still fail the ≥1-input rule — the carve-out applies ONLY to
2414        // describe-only sections (a real dispatch target must consume ≥1 graph
2415        // operand). No relaxation for registrable sections.
2416        let src = describe_only_zero_input_bundle()
2417            .replace("registrable: false\n", "")
2418            // Use a real OpKind so the missing-input rule (not the op-resolution
2419            // rule) is the failure under test. The required-field battery runs
2420            // before op resolution, so `accept.inputs (≥1)` fires first regardless.
2421            .replace("op_kind: RandUniform\n", "op_kind: ReluElementwise\n");
2422        let err = validate_str(&src).expect_err("registrable zero-input section is rejected");
2423        assert!(
2424            matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "accept.inputs (≥1)"),
2425            "got {err:?}"
2426        );
2427    }
2428
2429    // =====================================================================
2430    // Rule 14 — gather (paged_blocks) coherence + the IMPORT-SIDE LIFT
2431    // (FDX gather-sidecar arc, slice A).
2432    // =====================================================================
2433
2434    /// A minimal but COHERENT `registrable: true` paged_blocks bundle: the
2435    /// PagedAttn-shaped operand carries `fdx.gather: paged_blocks` naming the
2436    /// separate `block_table` / `context_lens` U32 input roles (the as-built
2437    /// ABI). `__GATHER__` marks the pool operand's `fdx` block so the negative
2438    /// tests can mutate it into an incoherent form.
2439    fn paged_blocks_bundle() -> String {
2440        r#"---
2441fkc_version: 1
2442provider:
2443  name: test-provider
2444  backend: Cpu
2445  kernel_source: "test-cpu"
2446---
2447
2448# paged bundle
2449
2450## paged_demo
2451
2452A paged attention blurb.
2453
2454```fkc
2455kernel: paged_demo
2456op_kind: PagedAttn
2457blurb: "naive paged attention over a blocked KV cache"
2458entry_point: "x::paged_demo"
2459accept:
2460  inputs:
2461    - name: q
2462      dtypes: [F32]
2463      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2464    - name: k_cache
2465      dtypes: [F32]
2466      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2467      fdx:
2468        requires_ext: true
2469        symbolic_extent: required
2470        gather: { kind: paged_blocks, block_table: block_table, context_lens: context_lens }
2471    - name: block_table
2472      dtypes: [U32]
2473      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2474    - name: context_lens
2475      dtypes: [U32]
2476      layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2477  op_params: { variant: PagedAttn }
2478return:
2479  outputs:
2480    - name: out
2481      dtype_rule: passthrough(q)
2482cost:
2483  provenance: declared
2484  class: attention
2485  flops: "4 * b * hq * sq * ctx * d"
2486  bytes_moved: "2 * b * hq * sq * d * 4"
2487precision:
2488  bit_stable_on_same_hardware: true
2489  audited: true
2490determinism: same_hardware_bitwise
2491```
2492"#
2493        .to_string()
2494    }
2495
2496    #[test]
2497    fn coherent_registrable_paged_blocks_validates() {
2498        // BORN-RED → GREEN (slice A). Before the import-side lift, a REGISTRABLE
2499        // section whose operand carries `fdx.gather: paged_blocks` failed import
2500        // with `GatherNotYetSupported` (validate_file re-raises it for a
2501        // registrable section — only describe-only ones are swallowed). After the
2502        // lift, the gather block is validated for COHERENCE (kind / requires_ext /
2503        // symbolic_extent / real block_table+context_lens roles) and then ACCEPTED
2504        // — registration of `OpKind::PagedAttn` does not depend on the FDX gather
2505        // descriptor (the block_table/context_lens ride as ordinary U32 operands).
2506        validate_str(&paged_blocks_bundle())
2507            .expect("a coherent registrable paged_blocks section validates + is registrable");
2508    }
2509
2510    #[test]
2511    fn incoherent_gather_missing_requires_ext_still_errors() {
2512        // GUARD: the lift must NOT be a blanket pass. An INCOHERENT gather (here:
2513        // no `requires_ext: true`) still fails with the typed `GatherIncoherent`,
2514        // never a silent pass (never-panic, no-silent-fixup).
2515        let src = paged_blocks_bundle().replace("        requires_ext: true\n", "");
2516        let err = validate_str(&src).expect_err("gather without requires_ext must error");
2517        assert!(
2518            matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("requires_ext")),
2519            "got {err:?}",
2520        );
2521    }
2522
2523    #[test]
2524    fn incoherent_gather_dangling_block_table_role_still_errors() {
2525        // GUARD: `gather.block_table` naming a role that is not a real
2526        // `accept.inputs` name is `GatherIncoherent` — the coherence cross-check
2527        // (a table is described in exactly one place + a consistency check, FDX
2528        // §6.9.3) is preserved by the lift.
2529        let src = paged_blocks_bundle().replace("block_table: block_table,", "block_table: nope,");
2530        let err = validate_str(&src).expect_err("dangling block_table role must error");
2531        assert!(
2532            matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("block_table")),
2533            "got {err:?}",
2534        );
2535    }
2536
2537    #[test]
2538    fn unknown_gather_kind_is_unknown_admissibility_enum() {
2539        // GUARD: an unknown `gather.kind` (a future ragged/CSR kind FKC cannot yet
2540        // key) is a typed `UnknownAdmissibilityEnum`, never a silent default.
2541        let src = paged_blocks_bundle().replace("kind: paged_blocks,", "kind: ragged_csr,");
2542        let err = validate_str(&src).expect_err("unknown gather kind must error");
2543        assert!(
2544            matches!(err, FkcError::UnknownAdmissibilityEnum { ref value, .. } if value == "ragged_csr"),
2545            "got {err:?}",
2546        );
2547    }
2548
2549    // =====================================================================
2550    // TASK 3 — the CI lint over the whole checked-in contract corpus.
2551    // =====================================================================
2552
2553    /// A permissive stub `LinkRegistry` that resolves any entry_point to a
2554    /// distinct dummy `KernelRef` (so lowering succeeds). The lint checks
2555    /// parse + lower + validate, NOT real registration.
2556    struct StubLink {
2557        seen: std::sync::Mutex<std::collections::HashMap<String, crate::kernel::KernelRef>>,
2558    }
2559    impl StubLink {
2560        fn new() -> Self {
2561            Self {
2562                seen: std::sync::Mutex::new(std::collections::HashMap::new()),
2563            }
2564        }
2565        fn resolve(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
2566            use std::sync::Arc;
2567            use std::sync::RwLock;
2568            // A family of distinct fn items so distinct symbols get distinct
2569            // pointers (avoids spurious duplicate-pointer collapse on lower —
2570            // lowering does not finalize, but keep them distinct anyway).
2571            fn k0(
2572                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2573                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2574                _l: &[fuel_ir::Layout],
2575                _p: &crate::kernel::OpParams,
2576            ) -> fuel_ir::Result<()> {
2577                Ok(())
2578            }
2579            fn k1(
2580                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2581                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2582                _l: &[fuel_ir::Layout],
2583                _p: &crate::kernel::OpParams,
2584            ) -> fuel_ir::Result<()> {
2585                Ok(())
2586            }
2587            fn k2(
2588                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2589                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2590                _l: &[fuel_ir::Layout],
2591                _p: &crate::kernel::OpParams,
2592            ) -> fuel_ir::Result<()> {
2593                Ok(())
2594            }
2595            fn k3(
2596                _i: &[Arc<RwLock<fuel_memory::Storage>>],
2597                _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2598                _l: &[fuel_ir::Layout],
2599                _p: &crate::kernel::OpParams,
2600            ) -> fuel_ir::Result<()> {
2601                Ok(())
2602            }
2603            let table: [crate::kernel::KernelRef; 4] = [k0, k1, k2, k3];
2604            let mut g = self.seen.lock().unwrap();
2605            if let Some(k) = g.get(symbol) {
2606                return Some(*k);
2607            }
2608            let k = table[g.len() % table.len()];
2609            g.insert(symbol.to_string(), k);
2610            Some(k)
2611        }
2612    }
2613    impl crate::fkc::lower::LinkRegistry for StubLink {
2614        fn resolve_primitive(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2615            self.resolve(s)
2616        }
2617        fn resolve_fused(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2618            self.resolve(s)
2619        }
2620        fn resolve_cost_fn(&self, _name: &str) -> Option<crate::kernel::CostFn> {
2621            // Permissive (§4.4 cost-fn trampoline): the corpus lint checks that a
2622            // NAMED `cost.cost_fn` parses + lowers, not that it resolves to a
2623            // specific production fn (that is each provider's real link registry's
2624            // job). Any name resolves to a dummy CostFn so a cost-fn-pinning
2625            // contract lowers cleanly.
2626            fn c(
2627                _s: &[fuel_ir::Shape],
2628                _d: &[fuel_ir::DType],
2629                _p: &crate::kernel::OpParams,
2630                _c: &fuel_ir::backend::BackendCapabilities,
2631            ) -> crate::fused::CostEstimate {
2632                crate::fused::CostEstimate::default()
2633            }
2634            Some(c)
2635        }
2636    }
2637
2638    /// Recursively collect every `*.fkc.md` under `dir`, excluding any path
2639    /// containing an `_inventory` component.
2640    fn collect_fkc_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
2641        // GAP-157: a corpus sweep that can fail must FAIL, not silently truncate.
2642        // A read_dir error here would drop `.fkc.md` files from the CI lint's
2643        // corpus, and the lint would report "all contracts validated" over an
2644        // incomplete set. (Test-only helper — panic is the correct hard failure.)
2645        let rd = std::fs::read_dir(dir).unwrap_or_else(|e| {
2646            panic!(
2647                "collect_fkc_files: read_dir({}) failed: {e} — the FKC file sweep must FAIL, not silently truncate",
2648                dir.display()
2649            )
2650        });
2651        for entry in rd.flatten() {
2652            let path = entry.path();
2653            if path.is_dir() {
2654                if path.file_name().and_then(|n| n.to_str()) == Some("_inventory") {
2655                    continue;
2656                }
2657                collect_fkc_files(&path, out);
2658            } else if path.to_string_lossy().ends_with(".fkc.md") {
2659                out.push(path);
2660            }
2661        }
2662    }
2663
2664    /// THE CI LINT (Task 3). Walk `fuel-dispatch/kernel-contracts/**/*.fkc.md`
2665    /// (excluding `_inventory/`), parse + lower + validate EVERY file with a
2666    /// permissive stub LinkRegistry, and report the file/section counts. A
2667    /// failing contract is CAPTURED and REPORTED (file + kernel + the rule it
2668    /// tripped), never silently relaxed.
2669    ///
2670    /// `MxNotYetRegistrable` / `GatherNotYetSupported` are
2671    /// describable-but-not-yet-registrable per spec §6/§3.9.1 — they are a
2672    /// CORRECT validate outcome (the contract is legal, the consumer is
2673    /// behind), so the lint records them separately as "deferred", not as
2674    /// hard failures. `FanoutDtypeMismatch` (§3.4) is the same posture at
2675    /// lower time: a MULTI-AXIS dtype contract (varying operands with
2676    /// DIFFERENT dtype lists) is legal but not-yet-fannable by the uniform
2677    /// fan-out importer — surfaced (never silently picked), recorded as
2678    /// deferred, and awaiting a cartesian / per-axis fan-out follow-up.
2679    ///
2680    /// This is a **real CI gate**: it runs as a normal unit test (no
2681    /// `#[ignore]`) and FAILS the build if any checked-in contract has a hard
2682    /// parse/lower/validate failure. The whole corpus is currently clean of hard
2683    /// failures; only the `MxNotYetRegistrable` / `GatherNotYetSupported`
2684    /// deferred (consumer-ahead) cases remain, which are a CORRECT outcome and
2685    /// are NOT counted as failures. Run it verbosely with
2686    /// `cargo test -p fuel-dispatch --lib -- \
2687    ///  ci_lint_corpus_parse_lower_validate --nocapture` to see the full
2688    /// file/section/deferred counts. It does NOT relax a validator or edit the
2689    /// corpus to hide defects — a new hard failure must be fixed (or the section
2690    /// marked describe-only per §3.10) to make this gate green again.
2691    #[test]
2692    fn ci_lint_corpus_parse_lower_validate() {
2693        // Locate the corpus relative to this crate (CARGO_MANIFEST_DIR =
2694        // .../fuel-dispatch). The corpus lives at kernel-contracts/ inside this crate.
2695        let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2696        let corpus = manifest.join("kernel-contracts");
2697        assert!(
2698            corpus.is_dir(),
2699            "corpus dir not found at {}",
2700            corpus.display()
2701        );
2702
2703        let mut files = Vec::new();
2704        collect_fkc_files(&corpus, &mut files);
2705        files.sort();
2706        assert!(!files.is_empty(), "no .fkc.md files found");
2707
2708        let link = StubLink::new();
2709        let mut file_count = 0usize;
2710        let mut section_count = 0usize;
2711        let mut hard_failures: Vec<String> = Vec::new();
2712        let mut deferred: Vec<String> = Vec::new();
2713
2714        for path in &files {
2715            file_count += 1;
2716            let rel = path
2717                .strip_prefix(manifest)
2718                .unwrap_or(path)
2719                .display()
2720                .to_string();
2721            let src = std::fs::read_to_string(path).expect("read corpus file");
2722
2723            // 1) parse.
2724            let file = match parse_file(&src) {
2725                Ok(f) => f,
2726                Err(e) => {
2727                    hard_failures.push(format!("{rel}: PARSE: {e}"));
2728                    continue;
2729                }
2730            };
2731            section_count += file.kernels.len();
2732
2733            // 2) validate (whole file) — but to attribute a failure to a
2734            // specific kernel + rule, validate each kernel individually too.
2735            // First the file-level (version) check.
2736            if !fkc_version_is_supported(file.front_matter.fkc_version) {
2737                hard_failures.push(format!(
2738                    "{rel}: fkc_version {} not in supported set {:?}",
2739                    file.front_matter.fkc_version, FKC_SUPPORTED_VERSIONS
2740                ));
2741                continue;
2742            }
2743            for kernel in &file.kernels {
2744                let kname = kernel.kernel.as_str();
2745                match validate_kernel(kernel) {
2746                    Ok(()) => {}
2747                    Err(
2748                        e @ (FkcError::MxNotYetRegistrable { .. }
2749                        | FkcError::GatherNotYetSupported { .. }),
2750                    ) => {
2751                        deferred.push(format!("{rel} :: {kname}: {e}"));
2752                    }
2753                    Err(e) => {
2754                        hard_failures.push(format!("{rel} :: {kname}: {e}"));
2755                        continue;
2756                    }
2757                }
2758                // 3) lower (with the stub link) — only attempt when validate
2759                // passed (or deferred). Lowering catches link/dtype/cost issues
2760                // validate doesn't; surface those too.
2761                if let Err(e) = lower_one_kernel(&file, kernel, &link) {
2762                    // MxNotYetRegistrable is a register-time gate, not lower —
2763                    // lowering an MX/AFFINE_BLOCK contract succeeds; any lower
2764                    // error here is a real finding.
2765                    match e {
2766                        FkcError::MxNotYetRegistrable { .. }
2767                        | FkcError::GatherNotYetSupported { .. } => {}
2768                        // Describable but NOT-YET-FANNABLE (§3.4 multi-dtype
2769                        // fan-out): a MULTI-AXIS dtype contract whose varying
2770                        // operands enumerate DIFFERENT dtype lists (e.g. a
2771                        // mixed-precision matmul, or an indexing op with an
2772                        // independent data-dtype axis and index-dtype axis).
2773                        // The uniform fan-out importer surfaces this as a typed
2774                        // error rather than silently picking one operand's list
2775                        // (never a silent pick); registering it needs a
2776                        // cartesian / per-axis fan-out follow-up. Recorded as
2777                        // deferred (consumer-behind), not a hard failure.
2778                        FkcError::FanoutDtypeMismatch { .. } => {
2779                            deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2780                        }
2781                        // Describable but NOT-YET-FANNABLE (§3.4 optional-operand
2782                        // fan-out): a section with MULTIPLE / chained trailing
2783                        // optional operands (e.g. Metal layernorm's `alpha` AND
2784                        // `beta`, so `alpha` is optional but not last). The
2785                        // key-builder supports a SINGLE optional LAST input
2786                        // (conv's `bias`); a chained-optional ABI needs a nested
2787                        // fan follow-up. The importer surfaces it as a typed error
2788                        // rather than silently mis-keying (before optional support
2789                        // it was silently coerced to an all-present required key).
2790                        // Recorded as deferred (consumer-behind), not a hard fail.
2791                        FkcError::OptionalOperandNotLast { .. } => {
2792                            deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2793                        }
2794                        _ => hard_failures.push(format!("{rel} :: {kname}: LOWER: {e}")),
2795                    }
2796                }
2797            }
2798        }
2799
2800        eprintln!(
2801            "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",
2802            deferred.len(),
2803            hard_failures.len()
2804        );
2805        if !deferred.is_empty() {
2806            eprintln!(
2807                "--- deferred (describable, not-yet-registrable / not-yet-fannable; spec §6/§3.9.1/§3.4) ---"
2808            );
2809            for d in &deferred {
2810                eprintln!("  {d}");
2811            }
2812        }
2813        if !hard_failures.is_empty() {
2814            eprintln!("--- HARD FAILURES (file :: kernel: rule) ---");
2815            for f in &hard_failures {
2816                eprintln!("  {f}");
2817            }
2818            panic!(
2819                "{} corpus contract(s) failed parse/lower/validate — see the list above \
2820                 (REPORT these, do not relax the validators)",
2821                hard_failures.len()
2822            );
2823        }
2824    }
2825
2826    /// Lower a single kernel within a file using the stub link, mirroring what
2827    /// V-FKC-3, corpus arm: **no two registrable contracts anywhere in the
2828    /// checked-in corpus may claim the same `(kernel_source, entry_point)`.**
2829    ///
2830    /// `register_into` already rejects a duplicate `KernelRef` — but only for
2831    /// the providers a given process actually imports, and the corpus lint
2832    /// above stops at *lower*, so cross-file collisions in the checked-in
2833    /// corpus had no safety net. That is the gap this closes.
2834    ///
2835    /// It deliberately does NOT go through `register_into`, and the reason is
2836    /// worth recording: the lint's `StubLink` hands out
2837    /// `table[g.len() % table.len()]` — **four** fn pointers, cycling — so the
2838    /// 5th distinct symbol aliases the 1st. Registering the corpus through it
2839    /// would report a flood of duplicates that are artifacts of the stub, not
2840    /// of the corpus. Synthesizing an unbounded number of distinct `fn`
2841    /// pointers at runtime is not possible, which is why this net could not be
2842    /// built the obvious way.
2843    ///
2844    /// So it asserts the property at its source instead: two contracts naming
2845    /// the same entry point in the same `kernel_source` are exactly the ones a
2846    /// real link registry resolves to one `KernelRef`. A *structural*
2847    /// assertion — it tests the property the duplicate gate is built on, needs
2848    /// no oracle, and diagnoses by NAME rather than by index.
2849    #[test]
2850    fn ci_lint_corpus_has_no_cross_file_entry_point_collisions() {
2851        let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2852        let corpus = manifest.join("kernel-contracts");
2853        assert!(
2854            corpus.is_dir(),
2855            "corpus dir not found at {}",
2856            corpus.display()
2857        );
2858
2859        let mut files = Vec::new();
2860        collect_fkc_files(&corpus, &mut files);
2861        files.sort();
2862        assert!(!files.is_empty(), "no .fkc.md files found");
2863
2864        // (kernel_source, entry_point) -> first "file :: kernel" that claimed it
2865        let mut claimed: std::collections::HashMap<(String, String), String> =
2866            std::collections::HashMap::new();
2867        // (kernel_source, entry_point, human description)
2868        let mut collisions: Vec<(String, String, String)> = Vec::new();
2869        let mut checked = 0usize;
2870
2871        for path in &files {
2872            let rel = path
2873                .strip_prefix(manifest)
2874                .unwrap_or(path)
2875                .display()
2876                .to_string();
2877            let src = std::fs::read_to_string(path).expect("read corpus file");
2878            let Ok(file) = parse_file(&src) else {
2879                // Parse failures are the other lint's business; skip rather
2880                // than double-report them here.
2881                continue;
2882            };
2883            let kernel_source = file.front_matter.provider.kernel_source.clone();
2884            for kernel in &file.kernels {
2885                // Describe-only sections (§3.10) never reach the binding table,
2886                // so they cannot collide.
2887                if !kernel.registrable {
2888                    continue;
2889                }
2890                let Some(entry) = kernel.entry_point.as_ref() else {
2891                    continue;
2892                };
2893                checked += 1;
2894                let key = (kernel_source.clone(), entry.clone());
2895                let who = format!("{rel} :: {}", kernel.kernel);
2896                match claimed.get(&key) {
2897                    Some(first) => collisions.push((
2898                        key.0.clone(),
2899                        key.1.clone(),
2900                        format!("claimed by BOTH {first} AND {who}"),
2901                    )),
2902                    None => {
2903                        claimed.insert(key, who);
2904                    }
2905                }
2906            }
2907        }
2908
2909        // Report what the instrument actually SAW — a loop that silently
2910        // examined nothing passes identically to one that examined everything.
2911        eprintln!(
2912            "FKC corpus entry-point uniqueness: {checked} registrable contracts across {} files, {} distinct (kernel_source, entry_point) keys",
2913            files.len(),
2914            claimed.len()
2915        );
2916        assert!(
2917            checked > 0,
2918            "the lint examined zero registrable contracts — it is not looking where it thinks"
2919        );
2920
2921        // --- Ratchet, not a cliff ---------------------------------------
2922        //
2923        // The corpus has 13 PRE-EXISTING collisions, all of the same shape: an
2924        // IMPORTED file colliding with an UN-IMPORTED orphan. Verified by
2925        // counting `include_str!` sites — `dispatch/reduce.fkc.md` and
2926        // `vulkan/conv-attn-rope.fkc.md` have ZERO, while their partners
2927        // `cpu/reduce{,-to}.fkc.md` and `vulkan/attention.fkc.md` have two
2928        // each. So none of them registers today and none is a live duplicate.
2929        //
2930        // They are still real debt: two contracts claiming one kernel can
2931        // DISAGREE about precision/cost, which is the divergence FKC exists to
2932        // prevent, and wiring either orphan up would fail registration. They
2933        // are listed rather than deleted because removing corpus contracts is a
2934        // content decision for their authors, not a lint's to make.
2935        //
2936        // The value here is the ratchet: a NEW collision fails immediately.
2937        const KNOWN: &[(&str, &str)] = &[
2938            // dispatch/reduce.fkc.md (orphan) vs cpu/reduce{,-to}.fkc.md
2939            (
2940                "portable-cpu",
2941                "fuel_cpu_backend::byte_kernels::sum_reduce_f32",
2942            ),
2943            (
2944                "portable-cpu",
2945                "fuel_cpu_backend::byte_kernels::max_reduce_f32",
2946            ),
2947            (
2948                "portable-cpu",
2949                "fuel_cpu_backend::byte_kernels::min_reduce_f32",
2950            ),
2951            (
2952                "portable-cpu",
2953                "fuel_cpu_backend::byte_kernels::mean_reduce_f32",
2954            ),
2955            (
2956                "portable-cpu",
2957                "fuel_cpu_backend::byte_kernels::reduce_sum_to_f32",
2958            ),
2959            (
2960                "portable-cpu",
2961                "fuel_cpu_backend::byte_kernels::reduce_max_to_f32",
2962            ),
2963            (
2964                "portable-cpu",
2965                "fuel_cpu_backend::byte_kernels::reduce_max_to_backward_f32",
2966            ),
2967            // vulkan/conv-attn-rope.fkc.md (orphan) vs vulkan/rope.fkc.md: rope_f32 was split
2968            // out of rope.fkc.md's fanned `rope` section to carry its own measured precision
2969            // block (board 240); the orphan names the same literal symbol. The other three
2970            // dtypes still fan from rope.fkc.md's base `rope` symbol and do not collide.
2971            ("vulkan-slang", "fuel_vulkan_backend::fkc::rope_f32"),
2972            // vulkan/conv-attn-rope.fkc.md (orphan) vs vulkan/attention.fkc.md
2973            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f32"),
2974            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_bf16"),
2975            ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f16"),
2976            (
2977                "vulkan-slang",
2978                "fuel_vulkan_backend::fkc::flash_attn_backward_q_f32",
2979            ),
2980            (
2981                "vulkan-slang",
2982                "fuel_vulkan_backend::fkc::flash_attn_backward_k_f32",
2983            ),
2984            (
2985                "vulkan-slang",
2986                "fuel_vulkan_backend::fkc::flash_attn_backward_v_f32",
2987            ),
2988        ];
2989        let is_known =
2990            |key: &(String, String)| KNOWN.iter().any(|(ks, ep)| *ks == key.0 && *ep == key.1);
2991
2992        let novel: Vec<&(String, String, String)> = collisions
2993            .iter()
2994            .filter(|c| !is_known(&(c.0.clone(), c.1.clone())))
2995            .collect();
2996
2997        // An allowlist that is never re-checked ROTS: a fixed collision would
2998        // sit here forever, and the next reader would believe the debt is
2999        // larger than it is. Fail on a stale entry too.
3000        let seen: std::collections::HashSet<(String, String)> = collisions
3001            .iter()
3002            .map(|c| (c.0.clone(), c.1.clone()))
3003            .collect();
3004        let stale: Vec<&(&str, &str)> = KNOWN
3005            .iter()
3006            .filter(|(ks, ep)| !seen.contains(&((*ks).to_string(), (*ep).to_string())))
3007            .collect();
3008
3009        if !stale.is_empty() {
3010            for (ks, ep) in &stale {
3011                eprintln!("  STALE ALLOWLIST ENTRY (no longer collides): {ks} :: {ep}");
3012            }
3013            panic!(
3014                "{} allowlisted collision(s) no longer occur — delete them from KNOWN so the list keeps reflecting the real debt",
3015                stale.len()
3016            );
3017        }
3018
3019        if !novel.is_empty() {
3020            for (ks, ep, who) in &novel {
3021                eprintln!("  NEW COLLISION: kernel_source `{ks}` entry_point `{ep}` — {who}");
3022            }
3023            panic!(
3024                "{} 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.",
3025                novel.len()
3026            );
3027        }
3028    }
3029
3030    /// `lower_file` does per-kernel (front-matter defaults applied).
3031    ///
3032    /// (See `ci_lint_corpus_has_no_cross_file_entry_point_collisions` below for
3033    /// the V-FKC-3 corpus arm.)
3034    fn lower_one_kernel(
3035        file: &FkcFile,
3036        kernel: &FkcKernel,
3037        link: &dyn crate::fkc::lower::LinkRegistry,
3038    ) -> Result<(), FkcError> {
3039        // Reuse the public lower_file by lowering the whole file would re-lower
3040        // everything; instead lower just this kernel via a one-kernel clone.
3041        let mut one = file.clone();
3042        one.kernels = vec![kernel.clone()];
3043        crate::fkc::lower::lower_file(&one, link, &mut Vec::new()).map(|_| ())
3044    }
3045}