#!/usr/bin/env python3
"""Generate the v0.a slice of the Deductive panelist training corpus.

Per design/FINETUNE_DEDUCTIVE.md Section 2:
  - Train the cite-then-derive MOVE, not the facts.  Target: invariant
    citation + 2-4 step derivation.
  - <= 2 pairs per source document (forces breadth).
  - ~half the pairs invert the obvious answer (forces verb-reading on
    MUST / SHOULD / MAY rather than topic pattern-match).
  - Every target cites a section number.  Targets without citation are
    rejected by impl/qc_corpus_d.py.

v0.a = 19 pairs.  Production target = 1,500-3,000 pairs (multi-session,
likely agent-batch with human verification of citation accuracy).

Source families covered: C11, POSIX.1-2017, RFCs (8259, 7540, 8446, 1034
+ 2308), Python (data model, language reference, stdtypes), Rust (reference,
stdlib), Rail (CLAUDE.md), IEEE 754, bash(1) man.

Discipline note: I (the curator) hand-verified each citation in this slice.
The risk of teaching the model to FABRICATE plausible-looking citations is
the highest failure mode for D; QC must enforce verification before scale.
"""

import json
from pathlib import Path

# Each record:
#   id          - stable D-NNN identifier
#   source      - document name (used for <= 2/doc enforcement)
#   section     - section reference within source (free text)
#   polarity    - "keep" if obvious answer matches target; "invert" if target
#                 contradicts the obvious lay answer
#   prompt      - the question
#   target      - the cite-then-derive answer; MUST contain `section` substring

pairs = [
    # ── C11 (2) ───────────────────────────────────────────────────
    {
        "id": "D-001",
        "source": "ISO/IEC 9899:2011 (C11)",
        "section": "5.2.4.2.1",
        "polarity": "keep",
        "prompt": "Does the C11 standard guarantee that sizeof(int) >= 2?",
        "target": (
            "Yes. C11 section 5.2.4.2.1 defines INT_MIN <= -32767 and "
            "INT_MAX >= 32767, requiring int to represent values in "
            "[-32767, 32767]. A 16-bit two's-complement integer covers "
            "this range exactly, so any conforming implementation has "
            "sizeof(int) * CHAR_BIT >= 16. Since CHAR_BIT >= 8 (5.2.4.2.1), "
            "sizeof(int) >= 2 follows. Cite: ISO/IEC 9899:2011 5.2.4.2.1."
        ),
    },
    {
        "id": "D-006",
        "source": "ISO/IEC 9899:2011 (C11)",
        "section": "6.5p5; 6.2.5p9",
        "polarity": "invert",
        "prompt": "If a signed int multiplication overflows in C11, is the result well-defined (typically wraparound)?",
        "target": (
            "No. C11 section 6.5p5 specifies that 'if an exceptional condition "
            "occurs during the evaluation of an expression, that is, if the "
            "result is not mathematically defined or not in the range of "
            "representable values for its type, the behavior is undefined.' "
            "Signed integer overflow falls under this clause. The wraparound "
            "behavior people assume applies only to unsigned types per "
            "6.2.5p9 ('A computation involving unsigned operands can never "
            "overflow ... reduced modulo the number that is one greater "
            "than the largest value that can be represented'). Cite: "
            "ISO/IEC 9899:2011 6.5p5, 6.2.5p9."
        ),
    },
    # ── POSIX.1-2017 (2) ──────────────────────────────────────────
    {
        "id": "D-002",
        "source": "POSIX.1-2017",
        "section": "System Interfaces / close",
        "polarity": "keep",
        "prompt": "In POSIX, does close(2) on a file descriptor guarantee that all buffered data has been flushed to the underlying storage device?",
        "target": (
            "No. POSIX.1-2017 System Interfaces / close: 'The close() function "
            "shall deallocate the file descriptor indicated by fildes.' It "
            "does not specify a flush to durable storage. fsync(2) is the "
            "operation defined to request transfer of the file's contents "
            "to the storage device. Therefore close alone does not provide "
            "the durability guarantee; the program must call fsync (or "
            "equivalent) before close to obtain it. Cite: POSIX.1-2017, "
            "System Interfaces / close; System Interfaces / fsync."
        ),
    },
    {
        "id": "D-007",
        "source": "POSIX.1-2017",
        "section": "System Interfaces / fork",
        "polarity": "keep",
        "prompt": "When a POSIX process calls fork(), are the parent's open file descriptors duplicated into the child?",
        "target": (
            "Yes. POSIX.1-2017 System Interfaces / fork: 'The child process "
            "shall have its own copy of the parent's file descriptors. Each "
            "of the child's file descriptors shall refer to the same open "
            "file description with the corresponding file descriptor of the "
            "parent.' So the descriptor numbers are duplicated in the child, "
            "but parent and child share the underlying open file description "
            "(meaning the file offset and status flags are shared, not "
            "independent). Cite: POSIX.1-2017, System Interfaces / fork."
        ),
    },
    # ── RFC 8259 (2) ──────────────────────────────────────────────
    {
        "id": "D-004",
        "source": "RFC 8259 (JSON)",
        "section": "4",
        "polarity": "invert",
        "prompt": "Does the JSON specification (RFC 8259) require keys in objects to be unique?",
        "target": (
            "No. RFC 8259 section 4 states: 'The names within an object "
            "SHOULD be unique.' SHOULD, not MUST. Parsers that encounter "
            "duplicate keys are therefore permitted (though not required) "
            "to accept the input; the resulting behavior is "
            "implementation-defined (last-wins is the common choice but is "
            "not mandated by the spec). Therefore the claim 'the JSON spec "
            "requires unique keys' is false; uniqueness is recommended. "
            "Cite: RFC 8259 section 4."
        ),
    },
    {
        "id": "D-011",
        "source": "RFC 8259 (JSON)",
        "section": "6",
        "polarity": "invert",
        "prompt": "Can a JSON number portably represent integers larger than 2^53 exactly?",
        "target": (
            "Not portably. RFC 8259 section 6: 'This specification allows "
            "implementations to set limits on the range and precision of "
            "numbers accepted ... good interoperability can be achieved by "
            "implementations that expect no more precision or range than "
            "these provide, in the sense that implementations will approximate "
            "JSON numbers within the expected precision.' The cited "
            "interoperability range derives from IEEE 754 binary64, "
            "i.e. integers in [-(2^53), 2^53]. A parser MAY accept a larger "
            "integer, but receivers MAY truncate or convert to float. "
            "Therefore exact representation above 2^53 is not portable. "
            "Cite: RFC 8259 section 6."
        ),
    },
    # ── RFC 7540 (1) ──────────────────────────────────────────────
    {
        "id": "D-008",
        "source": "RFC 7540 (HTTP/2)",
        "section": "5.1; 6.4",
        "polarity": "keep",
        "prompt": "In HTTP/2, does a RST_STREAM frame immediately terminate the affected stream?",
        "target": (
            "Yes. RFC 7540 section 6.4: 'The RST_STREAM frame ... allows for "
            "immediate termination of a stream.' Section 5.1 (Stream States) "
            "specifies that receipt of RST_STREAM in any non-'idle' state "
            "transitions the stream to 'closed' immediately; further frames "
            "for that stream MUST be treated as a stream error or ignored. "
            "Cite: RFC 7540 sections 5.1, 6.4."
        ),
    },
    # ── RFC 8446 (1) ──────────────────────────────────────────────
    {
        "id": "D-012",
        "source": "RFC 8446 (TLS 1.3)",
        "section": "1.3",
        "polarity": "invert",
        "prompt": "Does TLS 1.3 still support RSA key exchange the way TLS 1.2 did?",
        "target": (
            "No. RFC 8446 section 1.3 ('Major Differences from TLS 1.2') "
            "states: 'Static RSA and Diffie-Hellman cipher suites have been "
            "removed; all public-key based key exchange mechanisms now "
            "provide forward secrecy.' TLS 1.3 negotiates only (EC)DHE key "
            "exchange or PSK-based modes (RFC 8446 sections 4.1.1, 4.2.7). "
            "RSA certificates remain valid for use in CertificateVerify "
            "signatures, but RSA key transport is gone. Cite: RFC 8446 "
            "sections 1.3, 4.2.7."
        ),
    },
    # ── RFC 1034 / 2308 (1) ───────────────────────────────────────
    {
        "id": "D-013",
        "source": "RFC 1034 / RFC 2308 (DNS)",
        "section": "RFC 1034 section 4.3.4; RFC 2308",
        "polarity": "invert",
        "prompt": "If a DNS query returns NXDOMAIN, does that mean the name is permanently absent and will never exist?",
        "target": (
            "No. RFC 1034 section 4.3.4 ('Negative response caching') and "
            "RFC 2308 ('Negative Caching of DNS Queries') specify that "
            "NXDOMAIN responses are cached for a finite TTL (derived from "
            "the authoritative zone's SOA MINIMUM field, capped by the "
            "resolver). The authoritative meaning at the time of the query "
            "is 'this name does not exist now,' valid for that TTL only. "
            "The name can come into existence later; subsequent queries "
            "after cache expiry will then resolve. Cite: RFC 1034 section "
            "4.3.4; RFC 2308 section 5."
        ),
    },
    # ── Python data model (1) ────────────────────────────────────
    {
        "id": "D-005",
        "source": "Python data model",
        "section": "Objects, values, and types",
        "polarity": "invert",
        "prompt": "In Python, does the operator `is` test value equality?",
        "target": (
            "No. The Python data model "
            "(docs.python.org/3/reference/datamodel.html, section "
            "'Objects, values, and types') specifies that `is` tests "
            "object identity, not value equality. `==` is the operator "
            "that invokes __eq__ for value equality. The distinction "
            "matters when two equal-valued objects are separately "
            "allocated (two list literals, for example): `==` returns "
            "True while `is` returns False. Cite: Python data model, "
            "'Objects, values, and types'."
        ),
    },
    # ── Python language reference (1) ─────────────────────────────
    {
        "id": "D-009",
        "source": "Python language reference",
        "section": "Mappings; Python 3.7 release notes",
        "polarity": "keep",
        "prompt": "Is dict insertion order guaranteed in Python?",
        "target": (
            "Yes, as of Python 3.7. The Python language reference (Mappings "
            "section, docs.python.org/3/reference/datamodel.html) and the "
            "3.7 release notes ('Other Language Changes') promote dict "
            "insertion-order preservation from a CPython 3.6 implementation "
            "detail to an official language guarantee. Therefore relying "
            "on dict insertion order is portable across Python implementations "
            "from 3.7 onward. Cite: Python 3.7 'What's New' / Other Language "
            "Changes; language reference, Mappings."
        ),
    },
    # ── Python stdtypes (1) ───────────────────────────────────────
    {
        "id": "D-014",
        "source": "Python stdtypes",
        "section": "Truth Value Testing",
        "polarity": "invert",
        "prompt": "In Python 3, does bool('False') return False?",
        "target": (
            "No. The Python stdtypes documentation "
            "(docs.python.org/3/library/stdtypes.html, section 'Truth Value "
            "Testing') specifies that any non-empty string is truthy. "
            "'False' is a five-character non-empty string, so bool('False') "
            "returns True, not False. The False-y strings are limited to "
            "the empty string ''. To convert the textual word 'False' to "
            "the boolean False, parse it explicitly (e.g., "
            "`s.lower() == 'true'`). Cite: Python stdtypes, 'Truth Value "
            "Testing'."
        ),
    },
    # ── Rust reference (2) ────────────────────────────────────────
    {
        "id": "D-003",
        "source": "Rust reference",
        "section": "References and Borrowing",
        "polarity": "keep",
        "prompt": "In safe Rust, can a value of type `&mut T` and a value of type `&T` coexist pointing to the same memory at the same time?",
        "target": (
            "No. The Rust reference ('References and Borrowing' / 'Aliasing') "
            "enforces the aliasing rule: at any given time you can have either "
            "one mutable reference or any number of immutable references to "
            "the same data, but never both simultaneously. A safe program that "
            "constructed both forms would fail to compile under the borrow "
            "checker. Unsafe code can violate this rule, but the result is "
            "undefined behavior, not a counterexample to the rule. Cite: "
            "Rust reference, 'References and Borrowing'."
        ),
    },
    {
        "id": "D-010",
        "source": "Rust reference",
        "section": "Dynamically Sized Types",
        "polarity": "invert",
        "prompt": "In Rust, do trait objects (`dyn Trait`) implement the Sized trait?",
        "target": (
            "No. The Rust reference section 'Dynamically Sized Types' "
            "specifies that trait objects (`dyn Trait`) are dynamically "
            "sized types (DSTs) and do not implement Sized. They can only "
            "be used behind a pointer (Box<dyn Trait>, &dyn Trait, "
            "Rc<dyn Trait>, etc.). This is why a function can return "
            "Box<dyn Trait> but not a bare `dyn Trait`: the latter has no "
            "known size at compile time and cannot be returned by value. "
            "Cite: Rust reference, 'Dynamically Sized Types'."
        ),
    },
    # ── Rust stdlib Box (1) ───────────────────────────────────────
    {
        "id": "D-015",
        "source": "Rust stdlib std::boxed::Box",
        "section": "impl Clone",
        "polarity": "keep",
        "prompt": "If T does not implement Clone, can Box<T> still implement Clone?",
        "target": (
            "No. The Rust stdlib documentation "
            "(doc.rust-lang.org/std/boxed/struct.Box.html) defines "
            "`impl<T: Clone + ?Sized> Clone for Box<T>`. The trait bound "
            "T: Clone is required; Box::clone delegates to T::clone and "
            "wraps the result in a new Box. If T does not implement Clone, "
            "the impl is not selected and Box<T> does not implement Clone "
            "either. Cite: Rust stdlib std::boxed::Box, impl Clone."
        ),
    },
    # ── Rail CLAUDE.md (2) ────────────────────────────────────────
    {
        "id": "D-016",
        "source": "Rail CLAUDE.md",
        "section": "Rail Compiler Known Limitations",
        "polarity": "invert",
        "prompt": "Does Rail's `split` function split a string on a multi-character delimiter?",
        "target": (
            "No. Per ~/projects/rail/CLAUDE.md, section 'Rail Compiler "
            "Known Limitations': '`split` is single-character only. "
            "split \"abc\" s splits on a, b, and c individually, NOT on "
            "the substring abc.' For multi-character delimiters the "
            "correct primitive is `str_split` (e.g., str_split \", \" s). "
            "Cite: ~/projects/rail/CLAUDE.md, 'Rail Compiler Known "
            "Limitations'."
        ),
    },
    {
        "id": "D-017",
        "source": "Rail CLAUDE.md",
        "section": "Rail Compiler Known Limitations",
        "polarity": "invert",
        "prompt": "Do Rail's `&&` and `||` operators short-circuit like in C or Python?",
        "target": (
            "No. Per ~/projects/rail/CLAUDE.md, section 'Rail Compiler "
            "Known Limitations': '`&&`/`||` do NOT short-circuit; both "
            "sides always evaluate. Guard with nested if/then/else "
            "instead.' This matters when one operand has side effects or "
            "would error if the other operand's value should have "
            "skipped it. Cite: ~/projects/rail/CLAUDE.md, 'Rail Compiler "
            "Known Limitations'."
        ),
    },
    # ── IEEE 754 (1) ──────────────────────────────────────────────
    {
        "id": "D-018",
        "source": "IEEE 754-2019",
        "section": "5.7.2",
        "polarity": "invert",
        "prompt": "Under IEEE 754, does NaN == NaN evaluate to true?",
        "target": (
            "No. IEEE 754-2019 section 5.7.2 ('General operations / "
            "Comparison predicates') specifies that all ordered comparison "
            "predicates involving NaN return false, with the single "
            "exception of the inequality predicate (!=), which returns "
            "true. Therefore NaN == NaN is false and NaN != NaN is true. "
            "The idiomatic NaN test in most languages is `x != x`. Cite: "
            "IEEE 754-2019 section 5.7.2."
        ),
    },
    # ── bash(1) man (1) ───────────────────────────────────────────
    {
        "id": "D-020",
        "source": "bash(1) man page",
        "section": "Special Parameters; SHELL BUILTIN COMMANDS / set",
        "polarity": "keep",
        "prompt": "In bash, does `$?` expand to the exit status of the last command in a pipeline, or the exit status of the whole pipeline?",
        "target": (
            "The exit status of the last (rightmost) command in the "
            "pipeline. Per bash(1), 'Special Parameters': '$? expands to "
            "the exit status of the most recently executed foreground "
            "pipeline.' The exit status of a pipeline is the exit status "
            "of the last command unless the `pipefail` option is set (see "
            "'SHELL BUILTIN COMMANDS' / set), in which case it is the exit "
            "status of the last (rightmost) command that exited with a "
            "non-zero status, or zero if all commands exited successfully. "
            "Cite: bash(1) man page, 'Special Parameters'; "
            "'SHELL BUILTIN COMMANDS' / set."
        ),
    },
]

# ── Discipline assertions (sanity at gen time; QC script enforces fully) ──

assert len(pairs) == 19, f"expected 19 pairs in v0.a slice, got {len(pairs)}"

# Every target must mention its section reference somewhere.  Use a
# loose check (substring of the FIRST chunk of the section field, since
# the section field may carry multi-section references separated by ';').
for p in pairs:
    first_chunk = p["section"].split(";")[0].strip()
    # Tolerate the section appearing inside Cite: line OR in the prose.
    assert first_chunk in p["target"], (
        f"{p['id']}: target does not contain section ref '{first_chunk}'"
    )

# <= 2 pairs per source document.
from collections import Counter
src_counts = Counter(p["source"] for p in pairs)
violations = {s: c for s, c in src_counts.items() if c > 2}
assert not violations, f"source-document over-quota: {violations}"

# Polarity balance: at least 40% inverted (curator-discipline rule "half
# inverted" is the target; relax to 40% as the floor for v0.a).
inverted = sum(1 for p in pairs if p["polarity"] == "invert")
assert inverted / len(pairs) >= 0.40, f"only {inverted}/{len(pairs)} inverted (< 40% floor)"

out_path = Path("tools/dnra/sets/corpus_d_v0a.jsonl")
with open(out_path, "w") as f:
    for p in pairs:
        f.write(json.dumps(p, separators=(",", ":")) + "\n")

print(f"wrote {len(pairs)} pairs to {out_path}")
print(f"  inverted: {inverted}/{len(pairs)} ({100*inverted/len(pairs):.0f}%)")
print(f"  unique sources: {len(src_counts)}")
print(f"  source distribution:")
for s, c in sorted(src_counts.items(), key=lambda kv: (-kv[1], kv[0])):
    print(f"    {c}  {s}")
