feat(project): --gate reaches the build, and the bundle says which one
`project.create` called `build()` with five keyword arguments and no `gate=`, so `okf project` screened by the package default and nothing anywhere said that was a choice rather than the only option. The gate is not a segmentation rule -- it is a screen about whether a document may be persisted at all -- so it is the one flag this command owns that may move a bundle's bytes, and the module docstring, the README paragraph and the test file's own claim are corrected rather than left standing beside the new flag. The default is `okf build`'s default, so an unflagged `okf project` is the bytes it always was; the byte-equality invariant against `okf build` holds unchanged. An unknown gate name still raises `gate_invalid` rather than falling back -- a fallback reproduces the defect with an extra step. Tests first, all three red: the gate name read back out of the bundle's own `log.md` (not out of the call), the flag parsed by this command's parser, and the unknown name refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
404bed26e2
commit
1c97e57212
3 changed files with 66 additions and 11 deletions
|
|
@ -1409,7 +1409,7 @@ No security functionality is reimplemented here.
|
||||||
`--gate` takes `guard-trusted-source` (the default), `guard-user-upload` or
|
`--gate` takes `guard-trusted-source` (the default), `guard-user-upload` or
|
||||||
`none`, and the name is written into the bundle's `log.md` either way, so a
|
`none`, and the name is written into the bundle's `log.md` either way, so a
|
||||||
consumer holding a bundle can tell a screened one from an unscreened one
|
consumer holding a bundle can tell a screened one from an unscreened one
|
||||||
without asking. `okf project` has no such flag and takes the default.
|
without asking. `okf project` takes the same `--gate` with the same default: it is the one flag there that may move a bundle's bytes, and it is there because a command that cannot reach the gate screens by a default nothing said was a choice.
|
||||||
|
|
||||||
That paragraph is new, and the sentence above it was true of our own command
|
That paragraph is new, and the sentence above it was true of our own command
|
||||||
until 2026-09-15: `okf build` injected a permissive stub and no argument
|
until 2026-09-15: `okf build` injected a permissive stub and no argument
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
"""One folder of documents in, one questionable project out, in one command.
|
"""One folder of documents in, one questionable project out, in one command.
|
||||||
|
|
||||||
`okf project <folder>` is `okf build` followed by `okf skill`, plus the summary
|
`okf project <folder>` is `okf build` followed by `okf skill`, plus the summary
|
||||||
a person needs in order to know what they just got. It adds no rule of its own
|
a person needs in order to know what they just got. It adds no rule of its
|
||||||
and owns no flag that changes a bundle's bytes: the build runs on THIS
|
own: the build runs on THIS package's defaults, so a project bundle and an
|
||||||
package's default, so a project bundle and an `okf build` bundle of the same
|
`okf build` bundle of the same folder at the same stamp are the same bytes.
|
||||||
folder at the same stamp are the same bytes.
|
|
||||||
|
**One flag here DOES move a bundle's bytes, and it is stated rather than
|
||||||
|
implied: `--gate`.** Every other flag `okf build` owns is deliberately absent,
|
||||||
|
for the reason above -- two build paths would leave every measurement report
|
||||||
|
pinned to a bundle nobody produces. The gate is different in kind: it is not a
|
||||||
|
rule about how a document is cut but a screen about whether a document may be
|
||||||
|
persisted at all, and a command that cannot reach it screens by the package
|
||||||
|
default while saying nothing about it. The default is `okf build`'s default,
|
||||||
|
so an unflagged `okf project` is the bytes it always was.
|
||||||
|
|
||||||
**Why a third command rather than a documented three-step.** The three-step
|
**Why a third command rather than a documented three-step.** The three-step
|
||||||
existed and was measured on a reader: set `PYTHONPATH`, take a snapshot of a
|
existed and was measured on a reader: set `PYTHONPATH`, take a snapshot of a
|
||||||
|
|
@ -34,8 +42,8 @@ import unicodedata
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import consume, skill
|
from . import consume, skill
|
||||||
from .cli import DEFAULT_STAMP, build
|
from .cli import DEFAULT_GATE, DEFAULT_STAMP, build
|
||||||
from .corpus import CorpusReport
|
from .corpus import GATE_NAMES, CorpusReport
|
||||||
from .errors import IngestError
|
from .errors import IngestError
|
||||||
from .inbox import walk_inbox
|
from .inbox import walk_inbox
|
||||||
from .profiles import SEGMENTED_OKF_V0_2
|
from .profiles import SEGMENTED_OKF_V0_2
|
||||||
|
|
@ -176,6 +184,7 @@ def create(
|
||||||
out: Path,
|
out: Path,
|
||||||
bundle_id: str | None = None,
|
bundle_id: str | None = None,
|
||||||
ingested_at: str = DEFAULT_STAMP,
|
ingested_at: str = DEFAULT_STAMP,
|
||||||
|
gate: str = DEFAULT_GATE,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
) -> tuple[Path, Path, str]:
|
) -> tuple[Path, Path, str]:
|
||||||
"""Build the bundle, generate the skill, return both paths and the summary.
|
"""Build the bundle, generate the skill, return both paths and the summary.
|
||||||
|
|
@ -191,6 +200,7 @@ def create(
|
||||||
ingested_at=ingested_at,
|
ingested_at=ingested_at,
|
||||||
bundle_id=identity,
|
bundle_id=identity,
|
||||||
okf_version=PROJECT_OKF_VERSION,
|
okf_version=PROJECT_OKF_VERSION,
|
||||||
|
gate=gate,
|
||||||
)
|
)
|
||||||
if report.conservation_failed:
|
if report.conservation_failed:
|
||||||
raise IngestError(
|
raise IngestError(
|
||||||
|
|
@ -230,6 +240,16 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
||||||
default=DEFAULT_STAMP,
|
default=DEFAULT_STAMP,
|
||||||
help=f"stamped verbatim. Default {DEFAULT_STAMP}: deterministic, never the clock",
|
help=f"stamped verbatim. Default {DEFAULT_STAMP}: deterministic, never the clock",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--gate",
|
||||||
|
choices=GATE_NAMES,
|
||||||
|
default=DEFAULT_GATE,
|
||||||
|
help=(
|
||||||
|
"the persist gate every concept body passes before it is written, "
|
||||||
|
f"as `okf build` takes it. Default {DEFAULT_GATE}. `none` screens "
|
||||||
|
"NOTHING; the name is written into the bundle's log.md either way"
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--force", action="store_true", help="replace an existing SKILL.md at the destination"
|
"--force", action="store_true", help="replace an existing SKILL.md at the destination"
|
||||||
)
|
)
|
||||||
|
|
@ -248,6 +268,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
out=out,
|
out=out,
|
||||||
bundle_id=args.bundle_id,
|
bundle_id=args.bundle_id,
|
||||||
ingested_at=args.ingested_at,
|
ingested_at=args.ingested_at,
|
||||||
|
gate=args.gate,
|
||||||
force=args.force,
|
force=args.force,
|
||||||
)
|
)
|
||||||
except (IngestError, consume.ConsumeError, skill.SkillError) as exc:
|
except (IngestError, consume.ConsumeError, skill.SkillError) as exc:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"""`okf project`: one folder in, one bundle plus one skill out.
|
"""`okf project`: one folder in, one bundle plus one skill out.
|
||||||
|
|
||||||
The command adds no rule and owns no flag that changes a bundle's bytes, so
|
The command adds no rule, and owns exactly ONE flag that changes a bundle's
|
||||||
these tests are mostly about that: the project bundle must be the SAME bytes
|
bytes -- `--gate`, which is a screen and not a segmentation rule. These tests
|
||||||
`okf build` writes for the same folder at the same stamp, or there are two
|
are mostly about the rest: the project bundle must be the SAME bytes `okf
|
||||||
build paths and the reports are pinned to one of them.
|
build` writes for the same folder at the same stamp, or there are two build
|
||||||
|
paths and the reports are pinned to one of them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -267,3 +268,36 @@ def test_a_sheet_reaches_the_project_bundle_as_it_reaches_the_build_command(
|
||||||
== 0
|
== 0
|
||||||
)
|
)
|
||||||
assert tree(bundle) == tree(reference)
|
assert tree(bundle) == tree(reference)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_gate_reaches_the_build_and_the_bundle_says_which_one(
|
||||||
|
folder: Path, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""`okf project --gate` is the one flag here that MAY move a bundle's bytes.
|
||||||
|
|
||||||
|
`project.create` called `build()` with five keyword arguments and no
|
||||||
|
`gate=`, so the gate name was unreachable from this command: every project
|
||||||
|
bundle was screened by the package default and nothing said so was a
|
||||||
|
choice. The gate's name is written into the bundle's own `log.md`, so the
|
||||||
|
check is the bundle's, not the call's.
|
||||||
|
"""
|
||||||
|
out = tmp_path / "project"
|
||||||
|
bundle, _, _ = project.create(folder, out=out, gate="none")
|
||||||
|
log = (bundle / "log.md").read_text(encoding="utf-8")
|
||||||
|
assert "NOTHING WAS SCREENED" in log
|
||||||
|
|
||||||
|
default = tmp_path / "default"
|
||||||
|
other, _, _ = project.create(folder, out=default)
|
||||||
|
assert "NOTHING WAS SCREENED" not in (other / "log.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_gate_flag_is_parsed_by_the_project_command(folder: Path, tmp_path: Path) -> None:
|
||||||
|
args = project.parse_args([str(folder), "--out", str(tmp_path), "--gate", "none"])
|
||||||
|
assert args.gate == "none"
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unknown_gate_name_does_not_start_the_run(folder: Path, tmp_path: Path) -> None:
|
||||||
|
"""A fallback would reproduce the defect the gate was added to close."""
|
||||||
|
with pytest.raises(IngestError) as caught:
|
||||||
|
project.create(folder, out=tmp_path / "project", gate="guard-nonesuch")
|
||||||
|
assert caught.value.code == "gate_invalid"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue