fluidfix 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fluidfix/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 devkancheti4-design
3
+ """fluidfix — zero-token repair decisions for mechanical single-line bugs.
4
+
5
+ from fluidfix import Oracle, build_packet, MechanicalObserver, repair
6
+
7
+ oracle = Oracle("path/to/project", python="path/to/venv/bin/python")
8
+ packet = build_packet(oracle, "pkg/module.py")
9
+ observations = MechanicalObserver().observe([packet])[0]
10
+ result = repair(oracle, "pkg/module.py", observations)
11
+ print(result.summary())
12
+ """
13
+ from .acts import (ACTS, KINDS, WORKED_EXAMPLE, Observation, act_for, apply,
14
+ register)
15
+ from .guard import (GuardReport, commit_repair, find_candidate_files,
16
+ guard_once, write_refusal)
17
+ from .lanes import ADVANCE, EMIT, HALT, kind_of, mask_of
18
+ from .localize import Packet, build_packet
19
+ from .loop import RepairResult, repair
20
+ from .observers import ClaudeObserver, MechanicalObserver, observer_prompt
21
+ from .oracle import Oracle
22
+ from .router import pack, route, route_packed
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = [
26
+ "route", "route_packed", "pack",
27
+ "EMIT", "ADVANCE", "HALT", "mask_of", "kind_of",
28
+ "Observation", "KINDS", "ACTS", "WORKED_EXAMPLE", "act_for", "apply",
29
+ "register",
30
+ "Oracle", "Packet", "build_packet",
31
+ "MechanicalObserver", "ClaudeObserver", "observer_prompt",
32
+ "repair", "RepairResult",
33
+ "GuardReport", "guard_once", "find_candidate_files", "commit_repair",
34
+ "write_refusal",
35
+ ]
fluidfix/acts.py ADDED
@@ -0,0 +1,160 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 devkancheti4-design
3
+ # Commercial licensing: see COMMERCIAL.md.
4
+ """The act vocabulary: fault kinds, repairs, and the observation contract.
5
+
6
+ One structure defines each kind's meaning; the mechanical observer's regexes,
7
+ the LLM observer's prompt, and the appliers all derive from it. The router
8
+ never sees any of this — it maps kind -> act code from one worked example.
9
+
10
+ Observations may carry "clear data" pointers that sharpen the applier without
11
+ touching the router:
12
+
13
+ literal_value/-occurrence which numeric literal is wrong (kind 1). On the
14
+ benchmark corpus this alone took in-vocabulary exact repairs from 17/27
15
+ (first-literal heuristic) to 26/27.
16
+ op_occurrence which additive operator is flipped (kind 3), counting binary
17
+ " + "/" - " left to right, 1-based. Closes the remaining measured miss:
18
+ a line carrying both a `+` and the faulty `-`.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from dataclasses import dataclass, field
24
+
25
+ __all__ = ["Observation", "KINDS", "ACTS", "WORKED_EXAMPLE", "apply", "act_for"]
26
+
27
+ from .router import route
28
+
29
+ # The single mapping supplied to the router; every other act is inferred.
30
+ WORKED_EXAMPLE = (0, 5)
31
+
32
+
33
+ @dataclass
34
+ class Observation:
35
+ """What an observer reports. Never a fix, never an act code."""
36
+ lineno: int # 1-based line in the defect file
37
+ kinds: list[int] = field(default_factory=list) # most specific first; [] = refuse
38
+ literal_value: str | None = None
39
+ literal_occurrence: int | None = None
40
+ op_occurrence: int | None = None
41
+ note: str = ""
42
+
43
+
44
+ # kind -> (name, description used verbatim in observer prompts, line-signal regex)
45
+ KINDS = {
46
+ 0: ("strictness",
47
+ 'a token containing < or > has one "=" too many or too few '
48
+ '(a ">=" that should be ">", a "<" that should be "<=", an "->" '
49
+ 'corrupted to "->=", including inside strings)',
50
+ re.compile(r"[<>]=?")),
51
+ 1: ("literal-off-by-one",
52
+ "a numeric literal on the line is exactly one greater than correct "
53
+ "(3601 for 3600, [2:] for [1:], group(2) for group(1))",
54
+ re.compile(r"\d")),
55
+ 2: ("swapped-return-operands",
56
+ 'a "return a OP b" whose two operands are in the wrong order',
57
+ re.compile(r"^\s*return\s+.*\s(?://|[-+*])\s")),
58
+ 3: ("flipped-additive",
59
+ 'a binary " + " that should be " - ", or a " - " that should be '
60
+ '" + " (spaces around the operator)',
61
+ re.compile(r"\s[-+]\s")),
62
+ }
63
+
64
+
65
+ def _flip_strictness(line: str, obs: Observation) -> str:
66
+ for a, b in ((">=", ">"), ("<=", "<")):
67
+ if a in line:
68
+ return line.replace(a, b, 1)
69
+ for a, b in ((">", ">="), ("<", "<=")):
70
+ if a in line:
71
+ return line.replace(a, b, 1)
72
+ return line
73
+
74
+
75
+ def _reduce_literal(line: str, obs: Observation) -> str:
76
+ m = None
77
+ if obs.literal_value:
78
+ hits = [h for h in re.finditer(r"\d+", line) if h.group() == str(obs.literal_value)]
79
+ occ = max(1, obs.literal_occurrence or 1)
80
+ if len(hits) >= occ:
81
+ m = hits[occ - 1]
82
+ if m is None:
83
+ m = re.search(r"\d+", line)
84
+ if not m:
85
+ return line
86
+ new_lit = str(int(m.group()) - 1)
87
+ out = line[:m.start()] + new_lit + line[m.end():]
88
+ # Simplify `x + 1` -> decrement -> `x + 0` -> `x`, but ONLY at the
89
+ # decrement site, and only when the zero stands alone: a global
90
+ # unanchored sub was measured to eat an unrelated `-0.5` elsewhere on
91
+ # the line while the suite still passed.
92
+ if new_lit == "0" and not re.match(r"[0-9.eEjJxXbBoO_]", line[m.end():m.end() + 1] or " "):
93
+ pre = re.search(r"\s*[+-]\s*$", line[:m.start()])
94
+ if pre:
95
+ out = line[:pre.start()] + line[m.end():]
96
+ return out
97
+
98
+
99
+ def _swap_return_operands(line: str, obs: Observation) -> str:
100
+ m = re.match(r"^(\s*return\s+)(.*?)(\s(?://|[-+*])\s)(.*)$", line)
101
+ return line if not m else f"{m.group(1)}{m.group(4)}{m.group(3)}{m.group(2)}"
102
+
103
+
104
+ def _flip_additive(line: str, obs: Observation) -> str:
105
+ # Deliberate divergence from kdebug's substring match: \s admits tabs and
106
+ # non-overlapping scanning skips a "+" that shares a space with a
107
+ # preceding "-" (`a - + b`). fluidfix attempts strictly more candidates
108
+ # on those whitespace edge cases; the suite remains the judge.
109
+ ops = list(re.finditer(r"\s([+-])\s", line))
110
+ if not ops:
111
+ return line
112
+ if obs.op_occurrence and 1 <= obs.op_occurrence <= len(ops):
113
+ m = ops[obs.op_occurrence - 1]
114
+ else:
115
+ # legacy first-match heuristic, kept for observers with no pointer
116
+ m = next((o for o in ops if o.group(1) == "+"), ops[0])
117
+ flipped = "-" if m.group(1) == "+" else "+"
118
+ return line[:m.start(1)] + flipped + line[m.end(1):]
119
+
120
+
121
+ # act code -> applier; codes are one translation of the vocabulary. Renumber
122
+ # them freely — the router recovers the offset from WORKED_EXAMPLE each call.
123
+ ACTS = {
124
+ 5: _flip_strictness,
125
+ 6: _reduce_literal,
126
+ 7: _swap_return_operands,
127
+ 8: _flip_additive,
128
+ }
129
+
130
+
131
+ def act_for(kind: int) -> int:
132
+ """The router's decision: kind -> act code, from the one worked example."""
133
+ return route(WORKED_EXAMPLE[0], WORKED_EXAMPLE[1], kind)
134
+
135
+
136
+ def register(kind: int, name: str, description: str, signal,
137
+ applier) -> int:
138
+ """Teach fluidfix a new fault class. One entry, one transform — the router
139
+ is untouched: it infers this kind's act code from the same single worked
140
+ example, so a new class costs exactly one registration, once, and every
141
+ future member of the class is decided for free.
142
+
143
+ kind: 0-15 (the kernel's domain — one dictionary holds 16 classes).
144
+ signal: compiled regex a line must match for the mechanical observer to
145
+ report this kind (LLM observers receive `description` verbatim).
146
+ applier(line, observation) -> candidate line.
147
+ Returns the act code the router assigned.
148
+ """
149
+ if not 0 <= kind <= 15:
150
+ raise ValueError("kind must be 0..15 — the kernel routes mod 16")
151
+ code = act_for(kind)
152
+ KINDS[kind] = (name, description, signal)
153
+ ACTS[code] = applier
154
+ return code
155
+
156
+
157
+ def apply(line: str, act: int, obs: Observation) -> str:
158
+ """Apply an act to a line. Unknown acts are a no-op (NOPROGRESS upstream)."""
159
+ fn = ACTS.get(act)
160
+ return line if fn is None else fn(line, obs)
fluidfix/cli.py ADDED
@@ -0,0 +1,186 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 devkancheti4-design
3
+ # Commercial licensing: see COMMERCIAL.md.
4
+ """fluidfix CLI.
5
+
6
+ fluidfix guard ROOT [--interval 900] [--commit] [--observer mechanical|claude]
7
+ fluidfix repair ROOT --file pkg/mod.py [--python VENV_PY] [--observer mechanical|claude]
8
+ fluidfix packet ROOT --file pkg/mod.py [--python VENV_PY]
9
+ fluidfix selfcheck
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+
17
+
18
+ def _oracle(args):
19
+ from .oracle import Oracle
20
+ return Oracle(args.root, python=args.python,
21
+ timeout=args.suite_timeout,
22
+ per_test_timeout=args.test_timeout)
23
+
24
+
25
+ def cmd_repair(args) -> int:
26
+ from .localize import build_packet
27
+ from .loop import repair
28
+ oracle = _oracle(args)
29
+ packet = build_packet(oracle, args.file, coverage_target=args.cov)
30
+ if packet is None:
31
+ print("suite is green — nothing to repair (refusing to search)")
32
+ return 3
33
+ if args.observer == "claude":
34
+ from .observers import ClaudeObserver
35
+ obs = ClaudeObserver(model=args.model)
36
+ observations = obs.observe([packet])[0]
37
+ if obs.last_usage is not None:
38
+ u = obs.last_usage
39
+ print(f"observer usage: in={u.input_tokens} out={u.output_tokens}",
40
+ file=sys.stderr)
41
+ else:
42
+ from .observers import MechanicalObserver
43
+ observations = MechanicalObserver().observe([packet])[0]
44
+ result = repair(oracle, args.file, observations,
45
+ candidate_timeout=args.candidate_timeout)
46
+ if args.json:
47
+ print(json.dumps(result.__dict__, default=str, indent=1))
48
+ else:
49
+ print(result.summary())
50
+ return 0 if result.repaired else 2
51
+
52
+
53
+ def _observer(args):
54
+ if args.observer == "claude":
55
+ from .observers import ClaudeObserver
56
+ return ClaudeObserver(model=args.model)
57
+ from .observers import MechanicalObserver
58
+ return MechanicalObserver()
59
+
60
+
61
+ def cmd_guard(args) -> int:
62
+ import time as _time
63
+ from .guard import commit_repair, guard_once, write_refusal
64
+ oracle = _oracle(args)
65
+ observer = _observer(args)
66
+ while True:
67
+ report = guard_once(oracle, observer, coverage_target=args.cov,
68
+ candidate_timeout=args.candidate_timeout)
69
+ print(f"[{_time.strftime('%H:%M:%S')}] {report.summary()}")
70
+ if report.status == "repaired" and args.commit:
71
+ print(" committed" if commit_repair(oracle.root, report)
72
+ else " commit failed — repair left in working tree")
73
+ if report.status == "refused":
74
+ print(f" refusal report: {write_refusal(oracle.root, report)}")
75
+ if args.interval is None:
76
+ return 0 if report.status in ("green", "repaired") else 2
77
+ _time.sleep(args.interval)
78
+
79
+
80
+ def cmd_packet(args) -> int:
81
+ from .localize import build_packet
82
+ packet = build_packet(_oracle(args), args.file, coverage_target=args.cov)
83
+ if packet is None:
84
+ print("suite is green — no packet")
85
+ return 3
86
+ print(packet.render())
87
+ return 0
88
+
89
+
90
+ def cmd_selfcheck(args) -> int:
91
+ """Re-derive the shipped laws from scratch. No network, no dependencies."""
92
+ from .lanes import ADVANCE, EMIT, HALT
93
+ from .router import route
94
+
95
+ bad = 0
96
+ for F1 in range(16):
97
+ for A1 in range(16):
98
+ for Fq in range(16):
99
+ want = (Fq + A1 - F1) % 16
100
+ bad += route(F1, A1, Fq) != want
101
+ print(f"router vs reference, all 4096 (F1,A1,Fq): {4096 - bad}/4096")
102
+
103
+ ident = sum(route(F1, A1, F1) != A1 for F1 in range(16) for A1 in range(16))
104
+ print(f"identity route(F1,A1,F1)==A1: {256 - ident}/256")
105
+
106
+ comp = sum(route(0, o2, route(0, o1, q)) != (q + o1 + o2) % 16
107
+ for o1 in range(16) for o2 in range(16) for q in range(16))
108
+ print(f"composition route(o2, route(o1, q)): {4096 - comp}/4096")
109
+
110
+ lane_bad = 0
111
+ for m in range(256):
112
+ if m and EMIT(m) != (m & -m):
113
+ lane_bad += 1
114
+ if m and ADVANCE(m) >= m:
115
+ lane_bad += 1
116
+ if HALT(m) != (1 if m == 0 else 0):
117
+ lane_bad += 1
118
+ print(f"lanes: EMIT/ADVANCE/HALT on 256 states: {'0 wrong' if not lane_bad else f'{lane_bad} WRONG'}")
119
+
120
+ steps = 0
121
+ for m in range(256):
122
+ w = m
123
+ while not HALT(w):
124
+ w = ADVANCE(w)
125
+ steps += 1
126
+ assert steps < 4096, "ADVANCE failed to reduce"
127
+ print(f"termination: every mask drains ({steps} total steps)")
128
+ print("SELFCHECK PASS" if not (bad or ident or comp or lane_bad) else "SELFCHECK FAIL")
129
+ return 0 if not (bad or ident or comp or lane_bad) else 1
130
+
131
+
132
+ def main(argv=None) -> int:
133
+ p = argparse.ArgumentParser(prog="fluidfix", description=__doc__)
134
+ sub = p.add_subparsers(dest="cmd", required=True)
135
+
136
+ def common(sp, need_file=True):
137
+ sp.add_argument("root", help="target project root (where pytest runs)")
138
+ if need_file:
139
+ sp.add_argument("--file", required=True,
140
+ help="defect file, relative to root")
141
+ sp.add_argument("--python", default=None,
142
+ help="target project's python (default: this one)")
143
+ sp.add_argument("--cov", default=None,
144
+ help="coverage target package (default: inferred)")
145
+ sp.add_argument("--suite-timeout", type=int, default=300,
146
+ help="full-suite budget in seconds (default 300)")
147
+ sp.add_argument("--test-timeout", type=int, default=60,
148
+ help="per-test pytest-timeout in seconds (default 60)")
149
+
150
+ sp = sub.add_parser("guard", help="commit-and-forget maintenance: watch "
151
+ "the suite, restore what breaks, refuse what is novel")
152
+ common(sp, need_file=False)
153
+ sp.add_argument("--observer", choices=["mechanical", "claude"],
154
+ default="mechanical")
155
+ sp.add_argument("--model", default="claude-opus-5")
156
+ sp.add_argument("--candidate-timeout", type=int, default=None,
157
+ help="per-candidate suite budget (default: --suite-timeout)")
158
+ sp.add_argument("--interval", type=int, default=None,
159
+ help="seconds between checks; omit for one pass (CI mode)")
160
+ sp.add_argument("--commit", action="store_true",
161
+ help="git-commit each restoration (only the repaired file)")
162
+ sp.set_defaults(fn=cmd_guard)
163
+
164
+ sp = sub.add_parser("repair", help="localise, observe, and repair one defect")
165
+ common(sp)
166
+ sp.add_argument("--observer", choices=["mechanical", "claude"],
167
+ default="mechanical")
168
+ sp.add_argument("--model", default="claude-opus-5")
169
+ sp.add_argument("--candidate-timeout", type=int, default=None,
170
+ help="per-candidate suite budget (default: --suite-timeout)")
171
+ sp.add_argument("--json", action="store_true")
172
+ sp.set_defaults(fn=cmd_repair)
173
+
174
+ sp = sub.add_parser("packet", help="print the lean observation packet")
175
+ common(sp)
176
+ sp.set_defaults(fn=cmd_packet)
177
+
178
+ sp = sub.add_parser("selfcheck", help="re-verify the shipped laws exhaustively")
179
+ sp.set_defaults(fn=cmd_selfcheck)
180
+
181
+ args = p.parse_args(argv)
182
+ return args.fn(args)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ raise SystemExit(main())
fluidfix/guard.py ADDED
@@ -0,0 +1,157 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 devkancheti4-design
3
+ # Commercial licensing: see COMMERCIAL.md.
4
+ """The guard: commit-and-forget maintenance.
5
+
6
+ green -> touch nothing, sleep
7
+ red -> find the fault file mechanically, localise, observe, route,
8
+ repair — the repo goes back to what it was meant to be
9
+ novel -> refuse LOUDLY (a machine-readable refusal report), tree
10
+ untouched; teaching the class once (register(), or a compiled
11
+ dictionary) makes its whole family free from then on
12
+
13
+ Fault-file discovery is mechanical: source files quoted in the failing
14
+ traceback (deepest frame preferred, test files excluded), falling back to the
15
+ files the failing test executed most, ranked by coverage. No model is needed
16
+ to find the file — only, optionally, to name the fault kind.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import re
23
+ import subprocess
24
+ import time
25
+ from dataclasses import dataclass, field
26
+
27
+ from .localize import build_packet
28
+ from .loop import RepairResult, repair
29
+ from .oracle import Oracle
30
+
31
+ __all__ = ["GuardReport", "find_candidate_files", "guard_once"]
32
+
33
+ _ANSI = re.compile(r"\x1b\[[0-9;]*m")
34
+
35
+
36
+ @dataclass
37
+ class GuardReport:
38
+ status: str # "green" | "repaired" | "refused"
39
+ file: str | None = None
40
+ result: RepairResult | None = None
41
+ candidates: list[str] = field(default_factory=list)
42
+ seconds: float = 0.0
43
+
44
+ def summary(self) -> str:
45
+ if self.status == "green":
46
+ return "suite green — nothing to do"
47
+ if self.status == "repaired":
48
+ return f"{self.file}: {self.result.summary()}"
49
+ return ("REFUSED: fault is outside the taught vocabulary "
50
+ f"(candidate files tried: {', '.join(self.candidates) or 'none found'}). "
51
+ "Teach the class once — register() an observation + transform — "
52
+ "and its whole family becomes free.")
53
+
54
+
55
+ def _is_test_path(rel: str) -> bool:
56
+ parts = rel.replace("\\", "/").split("/")
57
+ base = parts[-1]
58
+ return (base.startswith("test_") or base.endswith("_test.py")
59
+ or "tests" in parts[:-1] or base == "conftest.py")
60
+
61
+
62
+ def find_candidate_files(oracle: Oracle, failing_output: str,
63
+ limit: int = 3) -> list[str]:
64
+ """Project source files implicated by the failure, most suspect first."""
65
+ clean = _ANSI.sub("", failing_output)
66
+ ordered: list[str] = []
67
+ for m in re.finditer(r"([\w./\\-]+\.py)[\":,]", clean):
68
+ p = m.group(1).replace("\\", "/")
69
+ full = p if os.path.isabs(p) else os.path.join(oracle.root, p)
70
+ full = os.path.normpath(full)
71
+ if not full.startswith(oracle.root + os.sep) or not os.path.isfile(full):
72
+ continue
73
+ rel = os.path.relpath(full, oracle.root).replace("\\", "/")
74
+ if _is_test_path(rel):
75
+ continue
76
+ # deepest (latest) traceback frame is closest to the fault
77
+ if rel in ordered:
78
+ ordered.remove(rel)
79
+ ordered.append(rel)
80
+ ordered.reverse()
81
+ if ordered:
82
+ return ordered[:limit]
83
+ # no source frames (pure assertion failure): rank by the failing test's
84
+ # own coverage — run recorded by the caller's failing_output(), so --lf
85
+ # re-runs exactly it. No -x: exit-first suppresses the JSON report.
86
+ cov_json = os.path.join(oracle.root, "_fluidfix_guard_cov.json")
87
+ oracle.run(["--lf", "--tb=no", "--cov=.",
88
+ f"--cov-report=json:{cov_json}"], cache=True)
89
+ ranked: list[tuple[int, str]] = []
90
+ if os.path.exists(cov_json):
91
+ try:
92
+ cov = json.load(open(cov_json))
93
+ for f, data in cov.get("files", {}).items():
94
+ rel = f.replace("\\", "/")
95
+ if _is_test_path(rel) or not rel.endswith(".py"):
96
+ continue
97
+ ranked.append((len(data.get("executed_lines", [])), rel))
98
+ finally:
99
+ os.remove(cov_json)
100
+ ranked.sort(reverse=True)
101
+ return [rel for _, rel in ranked[:limit]]
102
+
103
+
104
+ def guard_once(oracle: Oracle, observer, files: list[str] | None = None,
105
+ coverage_target: str | None = None,
106
+ candidate_timeout: int | None = None) -> GuardReport:
107
+ t0 = time.time()
108
+ fails, out = oracle.failing_output()
109
+ if not fails:
110
+ return GuardReport(status="green", seconds=time.time() - t0)
111
+ candidates = files or find_candidate_files(oracle, out)
112
+ for rel in candidates:
113
+ packet = build_packet(oracle, rel, coverage_target=coverage_target)
114
+ if packet is None:
115
+ continue
116
+ observations = observer.observe([packet])[0]
117
+ result = repair(oracle, rel, observations,
118
+ candidate_timeout=candidate_timeout)
119
+ if result.repaired:
120
+ return GuardReport(status="repaired", file=rel, result=result,
121
+ candidates=candidates,
122
+ seconds=time.time() - t0)
123
+ return GuardReport(status="refused", candidates=candidates,
124
+ seconds=time.time() - t0)
125
+
126
+
127
+ def commit_repair(root: str, report: GuardReport) -> bool:
128
+ """Opt-in: commit a successful restoration. Only the repaired file."""
129
+ if report.status != "repaired":
130
+ return False
131
+ r = report.result
132
+ msg = (f"fluidfix: restore {report.file}:{r.lineno}\n\n"
133
+ f"- {r.old_line.strip()}\n+ {r.new_line.strip()}\n\n"
134
+ f"Routed by the fluidfix kernel ({r.reason}); accepted by the "
135
+ f"project's own suite in {r.suite_runs} runs.")
136
+ try:
137
+ subprocess.run(["git", "-C", root, "add", "--", report.file],
138
+ check=True, capture_output=True, timeout=30)
139
+ subprocess.run(["git", "-C", root, "commit", "-m", msg],
140
+ check=True, capture_output=True, timeout=30)
141
+ return True
142
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
143
+ FileNotFoundError):
144
+ return False
145
+
146
+
147
+ def write_refusal(root: str, report: GuardReport) -> str:
148
+ """A machine-readable teach-me signal, written where CI can pick it up."""
149
+ d = os.path.join(root, ".fluidfix")
150
+ os.makedirs(d, exist_ok=True)
151
+ path = os.path.join(d, "last_refusal.json")
152
+ json.dump({"status": report.status, "candidates": report.candidates,
153
+ "seconds": report.seconds,
154
+ "hint": "fault class is outside the taught vocabulary; "
155
+ "register() it once and its family becomes free"},
156
+ open(path, "w"), indent=1)
157
+ return path
fluidfix/lanes.py ADDED
@@ -0,0 +1,45 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 devkancheti4-design
3
+ # Commercial licensing: see COMMERCIAL.md.
4
+ """Loop discipline — vendored unchanged from fluid-router2
5
+ (https://github.com/devkancheti4-design/fluid-router2).
6
+
7
+ Three machine-authored expressions drive every repair loop. `ADVANCE` strictly
8
+ reduces a live mask, which is what makes the loop a solver rather than a
9
+ classifier: it terminates on all 256 mask states (verified exhaustively in
10
+ fluid-router2's `verify.c`, re-verified by this package's test suite).
11
+
12
+ EMIT which fault to handle next (the lowest live bit)
13
+ ADVANCE clear it and keep going
14
+ HALT nothing left — 1 iff the mask is empty
15
+ """
16
+
17
+ __all__ = ["EMIT", "ADVANCE", "HALT", "mask_of", "kind_of"]
18
+
19
+
20
+ def EMIT(m: int) -> int:
21
+ return m & (-m)
22
+
23
+
24
+ def ADVANCE(m: int) -> int:
25
+ return m - (m & (-m))
26
+
27
+
28
+ def HALT(m: int) -> int:
29
+ # 32-bit semantics of ((m - (m - 1)) + ((-m) >> 31)): 1 iff m == 0.
30
+ if m == 0:
31
+ return 1
32
+ return (m - (m - 1)) + (-1 if -m < 0 else 0)
33
+
34
+
35
+ def mask_of(kinds) -> int:
36
+ """A live mask with one bit per fault kind."""
37
+ m = 0
38
+ for k in kinds:
39
+ m |= 1 << k
40
+ return m
41
+
42
+
43
+ def kind_of(emitted: int) -> int:
44
+ """The kind number of an EMITted bit."""
45
+ return emitted.bit_length() - 1