feat(propose): --outline-run selects Arm D, default 0 is off
This commit is contained in:
parent
5080240366
commit
6e2238454c
2 changed files with 146 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue