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:
Kjell Tore Guttormsen 2026-09-20 23:08:50 +02:00
commit 1c97e57212
3 changed files with 66 additions and 11 deletions

View file

@ -1,10 +1,18 @@
"""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
a person needs in order to know what they just got. It adds no rule of its own
and owns no flag that changes a bundle's bytes: the build runs on THIS
package's default, so a project bundle and an `okf build` bundle of the same
folder at the same stamp are the same bytes.
a person needs in order to know what they just got. It adds no rule of its
own: the build runs on THIS package's defaults, so a project bundle and an
`okf build` bundle of the same 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
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 . import consume, skill
from .cli import DEFAULT_STAMP, build
from .corpus import CorpusReport
from .cli import DEFAULT_GATE, DEFAULT_STAMP, build
from .corpus import GATE_NAMES, CorpusReport
from .errors import IngestError
from .inbox import walk_inbox
from .profiles import SEGMENTED_OKF_V0_2
@ -176,6 +184,7 @@ def create(
out: Path,
bundle_id: str | None = None,
ingested_at: str = DEFAULT_STAMP,
gate: str = DEFAULT_GATE,
force: bool = False,
) -> tuple[Path, Path, str]:
"""Build the bundle, generate the skill, return both paths and the summary.
@ -191,6 +200,7 @@ def create(
ingested_at=ingested_at,
bundle_id=identity,
okf_version=PROJECT_OKF_VERSION,
gate=gate,
)
if report.conservation_failed:
raise IngestError(
@ -230,6 +240,16 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
default=DEFAULT_STAMP,
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(
"--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,
bundle_id=args.bundle_id,
ingested_at=args.ingested_at,
gate=args.gate,
force=args.force,
)
except (IngestError, consume.ConsumeError, skill.SkillError) as exc: