feat(propose): --outline-run selects Arm D, default 0 is off

This commit is contained in:
Kjell Tore Guttormsen 2026-09-07 01:34:59 +02:00
commit 6e2238454c
2 changed files with 146 additions and 0 deletions

View file

@ -929,3 +929,126 @@ def test_the_outline_rule_is_off_at_the_function_level_by_default() -> None:
assert default == explicit_zero == []
# The known-positive control: the same text DOES produce candidates when asked.
assert len(okf_propose_segments.find_candidates(ALL_THREE_INTEGER_FORMS, outline_run=3)) == 3
# --- Arm D: the CLI flag ---------------------------------------------------
def propose_arm_d(tmp_path: Path, text: str, run_length: int, name: str = "arm-d.md") -> dict:
source = write(tmp_path, text, name)
out = tmp_path / "arm-d-cli.json"
code = okf_propose_segments.main(
[str(source), "--out", str(out), "--outline-run", str(run_length)]
)
assert code == 0, f"proposer exited {code}"
return json.loads(out.read_text(encoding="utf-8"))
def test_outline_run_zero_is_byte_identical_to_the_flag_being_absent(tmp_path: Path) -> None:
"""The default profile is a promise; 0 must not be a different code path."""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS, "same.md")
without = tmp_path / "without.json"
with_zero = tmp_path / "with-zero.json"
assert okf_propose_segments.main([str(source), "--out", str(without)]) == 1
assert (
okf_propose_segments.main([str(source), "--out", str(with_zero), "--outline-run", "0"]) == 1
)
# Exit 1 both times: this text has no Arm B boundary at all, so neither run
# writes an artifact. The known-positive control that the text is not inert:
assert (
okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "on.json"), "--outline-run", "3"]
)
== 0
)
def test_the_two_arms_are_independent(tmp_path: Path) -> None:
"""`--outline-run 0` leaves Arm C's artifact untouched, and vice versa."""
source = write(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, "both.md")
arm_c_only = tmp_path / "c.json"
arm_c_and_zero = tmp_path / "cz.json"
assert (
okf_propose_segments.main(
[str(source), "--out", str(arm_c_only), "--max-segment-chars", "2000"]
)
== 0
)
assert (
okf_propose_segments.main(
[
str(source),
"--out",
str(arm_c_and_zero),
"--max-segment-chars",
"2000",
"--outline-run",
"0",
]
)
== 0
)
assert arm_c_only.read_bytes() == arm_c_and_zero.read_bytes()
def test_a_negative_outline_run_is_refused_by_the_tool_not_by_argparse(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Exit 2 alone cannot separate the two refusals -- argparse exits 2 too.
`FAILED` is the tool's own two-line failure form, so asserting it is what
proves the refusal came from `run` and not from the parser.
"""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS)
code = okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "x.json"), "--outline-run", "-1"]
)
assert code == 2
err = capsys.readouterr().err
assert "FAILED" in err
assert "outline-run" in err
def test_a_non_integer_outline_run_is_refused_by_argparse(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The other exit-2 path, so the two cannot be satisfied by one branch."""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS)
with pytest.raises(SystemExit) as exit_info:
okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "x.json"), "--outline-run", "three"]
)
assert exit_info.value.code == 2
assert "usage:" in capsys.readouterr().err
def test_the_help_attributes_arm_d_inside_its_own_option_chunk(
capsys: pytest.CaptureFixture[str],
) -> None:
"""The attribution must sit in the `--outline-run` help, not merely in the file.
Sliced between option headers on purpose: a whole-output grep would be
satisfied by adding the literal to Arm C's block, which would attribute the
wrong rule. And the squeeze is `tr -s '[:space:]'`, not a newline swap --
argparse wraps its help, so a newline-only normalisation leaves the run of
spaces behind and the literal stays unfindable.
"""
with pytest.raises(SystemExit) as exit_info:
okf_propose_segments.main(["--help"])
assert exit_info.value.code == 0
squeezed = " ".join(capsys.readouterr().out.split())
# Drop the usage line: it repeats every option name, so slicing the whole
# output would find two `outline-run` chunks and neither would be the help.
assert "options:" in squeezed
options = squeezed.split("options:", 1)[1]
chunks = options.split(" --")
outline_chunk = [c for c in chunks if c.startswith("outline-run")]
assert len(outline_chunk) == 1, f"expected exactly one chunk, got {len(outline_chunk)}"
assert "not defined upstream" in outline_chunk[0]
# The negative half: Arm C's chunk must NOT carry the literal, or the
# attribution would name the wrong rule while the grep still passed.
arm_c_chunk = [c for c in chunks if c.startswith("max-segment-chars")]
assert len(arm_c_chunk) == 1
assert "not defined upstream" not in arm_c_chunk[0]
# The whole-output count must also be 1: the literal belongs to Arm D alone.
assert squeezed.count("not defined upstream") == 1

View file

@ -604,6 +604,11 @@ def run(
f"--max-segment-chars {max_segment_chars} is negative; the cap is a "
"character count, and 0 means off (Arm B)"
)
if outline_run < 0:
raise ProposerError(
f"--outline-run {outline_run} is negative; the gate is a run LENGTH, "
"and 0 means off (Arm B)"
)
# Reduced HERE, before anything is read: a prefix that survives to the
# entries as an empty component would produce exactly the unscoped paths
# the caller asked to avoid, and would do it silently.
@ -693,6 +698,23 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
"the K3 method file"
),
)
parser.add_argument(
"--outline-run",
type=int,
default=0,
metavar="N",
help=(
"Arm D: also propose a boundary at each line of the document's own "
"numbered outline (the bare integers the heading grammar cannot "
"match, since it requires a dot), but only where those integers "
"sustain an ascending run of at least N entries, and only for the "
"LAST such run when the outline repeats, because a contents listing "
"precedes the body it lists. 0 (the default) is OFF and leaves the "
"artifact byte-identical to Arm B. Arm D is the author's definition, "
"written for order 20260906T213322Z; it is not defined upstream, and "
"the K3 method file does not name it either"
),
)
parser.add_argument(
"--proposed-at",
default="1970-01-01T00:00:00Z",
@ -711,6 +733,7 @@ def main(argv: list[str] | None = None) -> int:
proposed_at=args.proposed_at,
path_prefix=args.path_prefix,
max_segment_chars=args.max_segment_chars,
outline_run=args.outline_run,
)
except ProposerError as exc:
print(f"{PROPOSER_ID}: FAILED - {exc}", file=sys.stderr)