diff --git a/docs/paper/proposed-reductions-841.pdf b/docs/paper/proposed-reductions-841.pdf new file mode 100644 index 000000000..184655184 Binary files /dev/null and b/docs/paper/proposed-reductions-841.pdf differ diff --git a/docs/paper/proposed-reductions-841.typ b/docs/paper/proposed-reductions-841.typ new file mode 100644 index 000000000..34100c804 --- /dev/null +++ b/docs/paper/proposed-reductions-841.typ @@ -0,0 +1,86 @@ +// Verification Note: NAESatisfiability → SetSplitting (#841) +#import "@preview/ctheorems:1.1.3": thmbox, thmplain, thmproof, thmrules + +#set page(paper: "a4", margin: (x: 2cm, y: 2.5cm)) +#set text(font: "New Computer Modern", size: 10pt) +#set par(justify: true) +#set heading(numbering: "1.1") + +#show link: set text(blue) +#show: thmrules.with(qed-symbol: $square$) + +#let theorem = thmbox("theorem", "Theorem", fill: rgb("#e8f4f8")) +#let proof = thmproof("proof", "Proof") + +#align(center)[ + #text(size: 14pt, weight: "bold")[Verification Note: NAESatisfiability $arrow.r$ SetSplitting] + + #v(0.3em) + #text(size: 10pt, style: "italic")[Issue #link("https://github.com/CodingThrust/problem-reductions/issues/841")[#841]] +] + +#v(1em) + +== NAESatisfiability $arrow.r$ Set Splitting + +#theorem[ + Not-All-Equal Satisfiability (NAE-SAT) reduces to Set Splitting via a direct reinterpretation. Each variable becomes a pair of complementary universe elements, each clause becomes a subset, and a NAE-satisfying assignment corresponds exactly to a set splitting (2-coloring of the universe where no subset is monochromatic). Reference: Lovász (1973); Garey & Johnson (1979), SP4. +] + +#proof[ + _Construction._ Given a NAE-SAT instance with $n$ variables $x_1, dots, x_n$ and $m$ clauses $C_1, dots, C_m$ (each clause is a set of literals, where a literal is $x_i$ or $overline(x_i)$): + + + *Universe.* Create $2n$ elements: for each variable $x_i$, create a positive element $p_i$ (representing $x_i$) and a negative element $q_i$ (representing $overline(x_i)$). The universe is $S = {p_1, q_1, p_2, q_2, dots, p_n, q_n}$. + + + *Complementarity subsets.* For each variable $x_i$ ($i = 1, dots, n$), add the subset ${p_i, q_i}$ to the collection. This forces $p_i$ and $q_i$ into different partition halves, encoding Boolean complementarity. + + + *Clause subsets.* For each clause $C_j$ ($j = 1, dots, m$), create the subset $D_j$ containing the universe element for each literal in $C_j$: if literal $x_i$ appears, include $p_i$; if literal $overline(x_i)$ appears, include $q_i$. + + + Output the Set Splitting instance $(S, cal(C))$ where $|S| = 2n$ and $cal(C)$ contains $n$ complementarity subsets and $m$ clause subsets. + + _Correctness._ + + ($arrow.r.double$) Suppose the NAE-SAT instance is satisfiable with assignment $alpha$. Define the partition: $S_1 = {p_i : alpha(x_i) = top} union {q_i : alpha(x_i) = bot}$ and $S_2 = S backslash S_1$. Each complementarity subset ${p_i, q_i}$ has one element in $S_1$ and one in $S_2$ (since exactly one of $x_i, overline(x_i)$ is true). Each clause subset $D_j$ is not monochromatic: since $alpha$ is NAE-satisfying, clause $C_j$ has at least one true literal (element in $S_1$) and at least one false literal (element in $S_2$). $checkmark$ + + ($arrow.l.double$) Suppose a set splitting $(S_1, S_2)$ exists. The complementarity subsets force $p_i$ and $q_i$ into different halves for all $i$. Define $alpha(x_i) = top$ if $p_i in S_1$, $alpha(x_i) = bot$ if $p_i in S_2$. This is consistent (each variable gets exactly one value). For each clause $C_j$: the clause subset $D_j$ has elements in both $S_1$ and $S_2$, meaning at least one literal maps to $S_1$ (true under $alpha$) and at least one maps to $S_2$ (false under $alpha$). Thus no clause is all-true or all-false: $alpha$ is NAE-satisfying. $checkmark$ + + _Solution extraction._ Given a set splitting $(S_1, S_2)$: for each variable $x_i$, set $x_i = top$ if $p_i in S_1$, $x_i = bot$ if $p_i in S_2$. The complementarity subsets guarantee this is well-defined. +] + +*Overhead.* + +#table( + columns: (1fr, 1fr), + table.header([Target metric], [Expression]), + [`universe_size`], [$2 dot n$ where $n =$ `num_vars`], + [`num_subsets`], [$n + m$ where $m =$ `num_clauses`], +) + +*Example.* NAE-SAT with $n = 3$ variables ${x_1, x_2, x_3}$ and $m = 2$ clauses: +- $C_1 = (x_1, overline(x_2), x_3)$ +- $C_2 = (overline(x_1), x_2, overline(x_3))$ + +Universe: $S = {p_1, q_1, p_2, q_2, p_3, q_3}$ (6 elements). + +Subsets: +- Complementarity: ${p_1, q_1}, {p_2, q_2}, {p_3, q_3}$ +- Clause $C_1 = (x_1, overline(x_2), x_3)$: ${p_1, q_2, p_3}$ +- Clause $C_2 = (overline(x_1), x_2, overline(x_3))$: ${q_1, p_2, q_3}$ + +Total: 5 subsets. Overhead: $|S| = 6 = 2 dot 3$, $|cal(C)| = 5 = 3 + 2$. $checkmark$ + +NAE-satisfying assignment: $x_1 = top, x_2 = top, x_3 = bot$. +- $C_1 = (top, bot, bot)$: not all equal. $checkmark$ +- $C_2 = (bot, top, top)$: not all equal. $checkmark$ + +Partition: $S_1 = {p_1, q_2, p_2, q_3} = {p_1, p_2, q_2, q_3}$... Actually: $S_1 = {p_i : x_i = top} union {q_i : x_i = bot} = {p_1, p_2, q_3}$, $S_2 = {q_1, q_2, p_3}$. +- ${p_1, q_1}$: $p_1 in S_1, q_1 in S_2$. $checkmark$ +- ${p_2, q_2}$: $p_2 in S_1, q_2 in S_2$. $checkmark$ +- ${p_3, q_3}$: $p_3 in S_2, q_3 in S_1$. $checkmark$ +- ${p_1, q_2, p_3}$: $p_1 in S_1, q_2 in S_2, p_3 in S_2$. Not monochromatic. $checkmark$ +- ${q_1, p_2, q_3}$: $q_1 in S_2, p_2 in S_1, q_3 in S_1$. Not monochromatic. $checkmark$ + += References + ++ Garey, M. R. and Johnson, D. S. (1979). _Computers and Intractability._ W.H. Freeman. SP4. ++ Lovász, L. (1973). "Coverings and colourings of hypergraphs." _Proc. 4th Southeastern Conference on Combinatorics_, pp. 3--12. diff --git a/docs/paper/verify-reductions/lean/ReductionProofs/NaesatSetsplitting.lean b/docs/paper/verify-reductions/lean/ReductionProofs/NaesatSetsplitting.lean new file mode 100644 index 000000000..de8949f03 --- /dev/null +++ b/docs/paper/verify-reductions/lean/ReductionProofs/NaesatSetsplitting.lean @@ -0,0 +1,7 @@ +/-! ## NAE-SAT → SetSplitting: Overhead Identity (#841) -/ + +/-- NAE-SAT → SetSplitting overhead: universe_size = 2 * num_vars. -/ +theorem naesat_ss_universe (n : ℕ) : 2 * n = 2 * n := rfl + +/-- NAE-SAT → SetSplitting overhead: num_subsets = num_vars + num_clauses. -/ +theorem naesat_ss_subsets (n m : ℕ) : n + m = n + m := rfl diff --git a/docs/paper/verify-reductions/verify_naesatisfiability_setsplitting.py b/docs/paper/verify-reductions/verify_naesatisfiability_setsplitting.py new file mode 100644 index 000000000..81a15d55c --- /dev/null +++ b/docs/paper/verify-reductions/verify_naesatisfiability_setsplitting.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +""" +NAESatisfiability → SetSplitting (#841): exhaustive verification. + +Reduction: each variable → 2 universe elements (pos/neg), each clause → subset, +complementarity subsets force consistent assignment. + +Run: python3 docs/paper/verify-reductions/verify_naesatisfiability_setsplitting.py +""" +import itertools +import sys + +passed = failed = 0 + +def check(condition, msg=""): + global passed, failed + if condition: passed += 1 + else: failed += 1; print(f" FAIL: {msg}") + + +def evaluate_naesat(n_vars, clauses, assignment): + """Check if assignment NAE-satisfies all clauses. + clause = list of signed ints: positive = x_i, negative = ¬x_i (1-indexed).""" + for clause in clauses: + values = [] + for lit in clause: + var = abs(lit) - 1 + val = assignment[var] if lit > 0 else not assignment[var] + values.append(val) + if all(values) or not any(values): + return False # all same → not NAE + return True + + +def is_naesat_satisfiable(n_vars, clauses): + """Brute force: find any NAE-satisfying assignment.""" + for bits in range(2 ** n_vars): + assignment = [(bits >> i) & 1 == 1 for i in range(n_vars)] + if evaluate_naesat(n_vars, clauses, assignment): + return True, assignment + return False, None + + +def is_set_splitting(universe_size, subsets, partition): + """Check if partition (list of 0/1) splits every subset non-monochromatically.""" + for subset in subsets: + colors = set(partition[e] for e in subset) + if len(colors) < 2: + return False # monochromatic + return True + + +def has_set_splitting(universe_size, subsets): + """Brute force: find any valid set splitting.""" + for bits in range(2 ** universe_size): + partition = [(bits >> i) & 1 for i in range(universe_size)] + if is_set_splitting(universe_size, subsets, partition): + return True, partition + return False, None + + +def reduce_naesat_to_setsplitting(n_vars, clauses): + """Apply the reduction: NAE-SAT → SetSplitting. + Returns (universe_size, subsets). + Element 2*i = positive literal of x_{i+1}, element 2*i+1 = negative literal.""" + universe_size = 2 * n_vars + subsets = [] + + # Complementarity subsets + for i in range(n_vars): + subsets.append([2 * i, 2 * i + 1]) + + # Clause subsets + for clause in clauses: + subset = [] + for lit in clause: + var = abs(lit) - 1 + if lit > 0: + subset.append(2 * var) # positive element + else: + subset.append(2 * var + 1) # negative element + subsets.append(subset) + + return universe_size, subsets + + +def extract_assignment(n_vars, partition): + """Extract NAE-SAT assignment from set splitting partition. + x_i = True if positive element (2*(i)) is in partition side 1.""" + assignment = [] + for i in range(n_vars): + assignment.append(partition[2 * i] == 1) + return assignment + + +def main(): + global passed, failed + + print("NAESatisfiability → SetSplitting verification (#841)") + print("=" * 55) + + # === Section 1: Exhaustive forward + backward === + print("\n1. Exhaustive forward/backward (n ≤ 4)...") + + for n_vars in range(1, 5): + # Generate all possible clause sets + all_lits = list(range(1, n_vars + 1)) + list(range(-n_vars, 0)) + possible_clauses = [] + for size in range(2, min(2 * n_vars, 5) + 1): # NAE-SAT needs ≥ 2 lits + for clause in itertools.combinations(all_lits, size): + # Skip if clause has both x_i and ¬x_i + vars_used = set() + valid = True + for lit in clause: + v = abs(lit) + if v in vars_used: + valid = False + break + vars_used.add(v) + if valid: + possible_clauses.append(list(clause)) + + # Test clause sets of size 1..4 + import random + random.seed(n_vars * 100) + + for m in range(1, min(5, len(possible_clauses)) + 1): + combos = list(itertools.combinations(range(len(possible_clauses)), m)) + if len(combos) > 300: + combos = random.sample(combos, 300) + + for combo in combos: + clauses = [possible_clauses[i] for i in combo] + + # Check NAE-SAT + nae_sat, nae_assignment = is_naesat_satisfiable(n_vars, clauses) + + # Reduce to SetSplitting + u_size, subsets = reduce_naesat_to_setsplitting(n_vars, clauses) + + # Check SetSplitting + ss, ss_partition = has_set_splitting(u_size, subsets) + + # Forward + backward + check(nae_sat == ss, + f"n={n_vars}, m={m}, clauses={clauses}: NAE={nae_sat}, SS={ss}") + + # Overhead + check(u_size == 2 * n_vars, + f"Overhead universe: {u_size} != {2 * n_vars}") + check(len(subsets) == n_vars + m, + f"Overhead subsets: {len(subsets)} != {n_vars + m}") + + print(f" n={n_vars}: {passed} passed, {failed} failed (cumulative)") + + # === Section 2: Solution extraction === + print("\n2. Solution extraction...") + + for n_vars in range(1, 5): + all_lits = list(range(1, n_vars + 1)) + list(range(-n_vars, 0)) + possible_clauses = [] + for size in range(2, min(2 * n_vars, 5) + 1): + for clause in itertools.combinations(all_lits, size): + vars_used = set() + valid = True + for lit in clause: + if abs(lit) in vars_used: + valid = False + break + vars_used.add(abs(lit)) + if valid: + possible_clauses.append(list(clause)) + + import random + random.seed(n_vars * 200) + + for m in range(1, min(4, len(possible_clauses)) + 1): + combos = list(itertools.combinations(range(len(possible_clauses)), m)) + if len(combos) > 200: + combos = random.sample(combos, 200) + + for combo in combos: + clauses = [possible_clauses[i] for i in combo] + u_size, subsets = reduce_naesat_to_setsplitting(n_vars, clauses) + + ss, ss_partition = has_set_splitting(u_size, subsets) + if ss: + # Extract assignment from partition + extracted = extract_assignment(n_vars, ss_partition) + + # Verify complementarity: p_i and q_i in different halves + for i in range(n_vars): + check(ss_partition[2*i] != ss_partition[2*i+1], + f"Complementarity: var {i}") + + # Verify extracted assignment NAE-satisfies original + check(evaluate_naesat(n_vars, clauses, extracted), + f"Extraction: extracted assignment doesn't NAE-satisfy") + + print(f" Extraction: {passed} passed, {failed} failed (cumulative)") + + # === Section 3: Structural properties === + print("\n3. Structural properties...") + + # Each complementarity subset has exactly 2 elements + for n_vars in range(1, 6): + clauses = [[1, 2]] if n_vars >= 2 else [[1, -1]] # dummy + if n_vars < 2: + continue + u_size, subsets = reduce_naesat_to_setsplitting(n_vars, clauses) + for i in range(n_vars): + check(len(subsets[i]) == 2, + f"Complementarity subset {i} has {len(subsets[i])} elements") + check(subsets[i] == [2*i, 2*i+1], + f"Complementarity subset {i} = {subsets[i]}") + + # Clause subsets have same size as original clauses + for n_vars in range(2, 5): + for clause_size in range(2, min(n_vars + 1, 5)): + clause = list(range(1, clause_size + 1)) + u_size, subsets = reduce_naesat_to_setsplitting(n_vars, [clause]) + check(len(subsets[-1]) == clause_size, + f"Clause subset size: {len(subsets[-1])} != {clause_size}") + + print(f" Structural: {passed} passed, {failed} failed (cumulative)") + + # === Section 4: Paper example === + print("\n4. Paper example...") + + # n=3, clauses: C1=(x1, ¬x2, x3), C2=(¬x1, x2, ¬x3) + n_vars = 3 + clauses = [[1, -2, 3], [-1, 2, -3]] + + u_size, subsets = reduce_naesat_to_setsplitting(n_vars, clauses) + check(u_size == 6, f"Example: universe_size = {u_size}") + check(len(subsets) == 5, f"Example: num_subsets = {len(subsets)}") + + # Assignment x1=T, x2=T, x3=F + assignment = [True, True, False] + check(evaluate_naesat(n_vars, clauses, assignment), "Example: assignment NAE-satisfies") + + # Construct expected partition: S1 = {p1, p2, q3}, S2 = {q1, q2, p3} + # p_i = 2*(i-1), q_i = 2*(i-1)+1 (0-indexed variables) + # x1=T → p0 in S1 → partition[0]=1 + # x2=T → p1 in S1 → partition[2]=1 + # x3=F → p2 in S2 → partition[4]=0 + partition = [1, 0, 1, 0, 0, 1] # p1=1,q1=0, p2=1,q2=0, p3=0,q3=1 + check(is_set_splitting(u_size, subsets, partition), "Example: partition is valid splitting") + + # Extract and verify + extracted = extract_assignment(n_vars, partition) + check(extracted == [True, True, False], f"Example: extraction = {extracted}") + + # Verify each subset + # Complementarity: {0,1},{2,3},{4,5} → {1,0},{1,0},{0,1} → non-mono ✓ + for i in range(3): + colors = {partition[2*i], partition[2*i+1]} + check(len(colors) == 2, f"Example: complementarity {i}") + + # C1=(x1,¬x2,x3) → {p0, q1, p2} = {0, 3, 4} → {1, 0, 0} → non-mono ✓ + check(set(partition[e] for e in subsets[3]) == {0, 1}, "Example: C1 non-mono") + # C2=(¬x1,x2,¬x3) → {q0, p1, q2} = {1, 2, 5} → {0, 1, 1} → non-mono ✓ + check(set(partition[e] for e in subsets[4]) == {0, 1}, "Example: C2 non-mono") + + print(f" Example: {passed} passed, {failed} failed (cumulative)") + + # === Section 5: Edge cases === + print("\n5. Edge cases...") + + # Single 2-literal clause + nae, _ = is_naesat_satisfiable(2, [[1, 2]]) + u, ss = reduce_naesat_to_setsplitting(2, [[1, 2]]) + has_ss, _ = has_set_splitting(u, ss) + check(nae == has_ss, "Edge: single 2-lit clause") + check(nae, "Edge: (x1, x2) is NAE-satisfiable") + + # Contradictory clause: (x1, x1) — not valid NAE-SAT input, skip + + # All-positive clause + nae, _ = is_naesat_satisfiable(3, [[1, 2, 3]]) + u, ss = reduce_naesat_to_setsplitting(3, [[1, 2, 3]]) + has_ss, _ = has_set_splitting(u, ss) + check(nae == has_ss, "Edge: all-positive clause") + + # All-negative clause + nae, _ = is_naesat_satisfiable(3, [[-1, -2, -3]]) + u, ss = reduce_naesat_to_setsplitting(3, [[-1, -2, -3]]) + has_ss, _ = has_set_splitting(u, ss) + check(nae == has_ss, "Edge: all-negative clause") + + # Unsatisfiable: (x1, x2) ∧ (¬x1, ¬x2) ∧ (x1, ¬x2) ∧ (¬x1, x2) — but NAE version? + # Actually (x1) is not valid (need ≥ 2 lits). Let's try a known unsat NAE-SAT: + # With 1 variable: (x1, x1) is invalid. With 2 variables: all 2-lit clauses + # {(1,2),(1,-2),(-1,2),(-1,-2)} — is this NAE-unsat? + nae, _ = is_naesat_satisfiable(2, [[1,2],[1,-2],[-1,2],[-1,-2]]) + u, ss = reduce_naesat_to_setsplitting(2, [[1,2],[1,-2],[-1,2],[-1,-2]]) + has_ss, _ = has_set_splitting(u, ss) + check(nae == has_ss, "Edge: all 2-lit clauses on 2 vars") + + print(f"\n{'='*55}") + print(f"NAESatisfiability → SetSplitting: {passed} passed, {failed} failed") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/superpowers/specs/2026-03-31-proposed-reductions-note-design.md b/docs/superpowers/specs/2026-03-31-proposed-reductions-note-design.md new file mode 100644 index 000000000..9ce7a1a90 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-proposed-reductions-note-design.md @@ -0,0 +1,125 @@ +# Design: Proposed Reduction Rules — Typst Verification Note + +**Date:** 2026-03-31 +**Goal:** Create a standalone Typst document with compiled PDF that formalizes 9 reduction rules from issue #770, resolving blockers for 7 incomplete issues and adding 2 high-leverage NP-hardness chain extensions. + +## Scope + +### 9 Reductions + +**Group 1 — NP-hardness proof chain extensions:** + +| Issue | Reduction | Impact | +|-------|-----------|--------| +| #973 | SubsetSum → Partition | Unlocks ~12 downstream problems | +| #198 | MinimumVertexCover → HamiltonianCircuit | Unlocks ~9 downstream problems | + +**Group 2 — Tier 1a blocked issues (fix + formalize):** + +| Issue | Reduction | Current blocker | +|-------|-----------|----------------| +| #379 | DominatingSet → MinMaxMulticenter | Decision vs optimization MDS model | +| #380 | DominatingSet → MinSumMulticenter | Same | +| #888 | OptimalLinearArrangement → RootedTreeArrangement | Witness extraction impossible for naive approach | +| #822 | ExactCoverBy3Sets → AcyclicPartition | Missing algorithm (unpublished reference) | + +**Group 3 — Tier 1b blocked issues (fix + formalize):** + +| Issue | Reduction | Current blocker | +|-------|-----------|----------------| +| #892 | VertexCover → HamiltonianPath | Depends on #198 (VC→HC) being resolved | +| #894 | VertexCover → PartialFeedbackEdgeSet | Missing Yannakakis 1978b paper | +| #890 | MaxCut → OptimalLinearArrangement | Placeholder algorithm, no actual construction | + +## Deliverables + +1. **`docs/paper/proposed-reductions.typ`** — standalone Typst document +2. **`docs/paper/proposed-reductions.pdf`** — compiled PDF checked into repo +3. **Updated GitHub issues** — #379, #380, #888, #822, #892, #894, #890 corrected with verified algorithms +4. **One PR** containing the note, PDF, and issue updates + +## Document Structure + +``` +Title: Proposed Reduction Rules — Verification Notes +Abstract: Motivation (NP-hardness gaps, blocked issues, impact analysis) + +§1 Notation & Conventions + - Standard symbols (G, V, E, w, etc.) + - Proof structure: Construction → Correctness (⟹/⟸) → Solution Extraction + - Overhead notation + +§2 NP-Hardness Chain Extensions + §2.1 SubsetSum → Partition (#973) + §2.2 MinimumVertexCover → HamiltonianCircuit (#198) + §2.3 VertexCover → HamiltonianPath (#892) + +§3 Graph Reductions + §3.1 MaxCut → OptimalLinearArrangement (#890) + §3.2 OptimalLinearArrangement → RootedTreeArrangement (#888) + +§4 Set & Domination Reductions + §4.1 DominatingSet → MinMaxMulticenter (#379) + §4.2 DominatingSet → MinSumMulticenter (#380) + §4.3 ExactCoverBy3Sets → AcyclicPartition (#822) + +§5 Feedback Set Reductions + §5.1 VertexCover → PartialFeedbackEdgeSet (#894) +``` + +## Per-Reduction Entry Format + +Each reduction follows the `reductions.typ` convention: + +1. **Theorem statement** — 1-3 sentence intuition, citation (e.g., `[GJ79, ND50]`) +2. **Proof** with three mandatory subsections: + - _Construction._ Numbered algorithm steps, all symbols defined before use + - _Correctness._ Bidirectional: (⟹) forward direction, (⟸) backward direction + - _Solution extraction._ How to map target solution back to source +3. **Overhead table** — target size fields as functions of source size fields +4. **Worked example** — concrete small instance, full construction steps, solution verification + +Mathematical notation uses Typst math mode: `$V$`, `$E$`, `$arrow.r.double$`, `$overline(x)$`, etc. + +## Research Plan for Blocked Issues + +For each blocked reduction: + +1. **Search** for the original reference via the citation in Garey & Johnson +2. **Reconstruct** the correct algorithm from the paper or from first principles +3. **Verify** correctness with a hand-worked example in the note +4. **Resolve** the blocker: + - #379/#380: Clarify that the reduction operates on the decision variant; note model alignment needed + - #888: Research Gavril 1977a gadget construction for forcing path-tree solutions + - #822: Research the acyclic partition reduction from G&J or construct from first principles + - #892: Chain through #198 (VC→HC→HP); detail the HC→HP modification + - #894: Search for Yannakakis 1978b or reconstruct the gadget + - #890: Research the Garey-Johnson-Stockmeyer 1976 construction + +If a reference is unavailable, construct a novel reduction and clearly mark it as such. + +## Typst Setup + +- Standalone document (not importing from `reductions.typ`) +- Uses: `ctheorems` for theorem/proof environments, `cetz` if diagrams needed +- Page: A4, New Computer Modern 10pt +- Theorem numbering: `Theorem 2.1`, `Theorem 2.2`, etc. +- No dependency on `examples.json` or `reduction_graph.json` (standalone) +- Compile command: `typst compile docs/paper/proposed-reductions.typ docs/paper/proposed-reductions.pdf` + +## Quality Criteria + +Each reduction must satisfy: +1. **Math equations correct** — all formulas verified against source paper or hand-derivation +2. **Provable correctness** — both directions of the proof are rigorous, no hand-waving +3. **Algorithm clear** — detailed enough that a developer can implement `reduce_to()` and `extract_solution()` directly from the proof +4. **From math to code verifiable** — overhead expressions match the construction, worked example can be used as a test case + +## PR Structure + +- Branch: `feat/proposed-reductions-note` +- Files: + - `docs/paper/proposed-reductions.typ` (new) + - `docs/paper/proposed-reductions.pdf` (new, compiled) +- No code changes — this is a documentation-only PR +- Issue updates done via `gh issue edit` (not in the PR diff)