Commit graph

7 commits

Author SHA1 Message Date
570496470b docs(project): the server is the standard way in, the skill the supplement
`okf project`'s closing text and the README's first screen now say it in
that order: register `okf mcp --root` once, and it answers from every
project and reaches subagents; the skill beside the bundle is for someone
who would rather register nothing; neither is made again when a bundle is
rebuilt. Two tests hold the order in both places. The README's stale
`<id>-consume` skill path is corrected to `okf-consume-any`.

v1.1 order F, part F3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:36:52 +02:00
e3408435d0 feat(mcp): the server carries the working method, and describe fans out
**A subagent inherits its session's MCP tools; it does not inherit its skills.**
So the method A3 put in the skill reaches the main thread and no arm running
below it, and the one place every caller sees is the server's own
`instructions` and its tool descriptions. Both are truncated by Claude Code at
2 KB, and truncation is worse than rejection here -- a reader gets the first
half of a method and no sign the rest existed -- so what travels is the SHORT
form and the long one stays in the skill, which has no such cap. A test holds
it under the limit WITH a control, so the assertion is a measurement and not a
tautology.

`okf_describe` without `bundle_id` now describes every served bundle, where it
refused and `okf_ask` in the same position fanned out. The tool a caller is
told to read FIRST was the one requiring a name it did not have yet, and a
tool that refuses the call its sibling accepts is a shape a client must be
told out of band -- the configuration this server exists to remove. The named
call's shape is byte-unchanged, and so is every one-to-one server's: the
fan-out replaces an ERROR, so no caller's bytes move.

`okf project`'s closing lines and the README's first screen carry the one line
the USER runs to register the server on user scope, verified against Claude
Code's own MCP documentation (`claude mcp add [options] <name> -- <command>`).
Nothing here starts Claude Code, and the line says whose it is. Measured:
a project bundle at `<root>/<project>/.okf/<id>` is depth 3, inside
`MAX_DISCOVERY_DEPTH`, so a `--root` server finds what `okf project` wrote --
a test builds one and discovers it rather than reasoning about the walk.

A5, and it was free: `okf skill` without `--out` now refuses in the same
`refused (<code>)` form as every other refusal in this chain. The exit code
does not move -- 2 was already right, "the run did not happen" -- what was
wrong was that a caller parsing our form got argparse's line on the one flag
everybody forgets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 00:03:59 +02:00
30edd3f5d8 feat(skill): one generic skill by default, carrying a working method and an answer form
The operator built a 2313-concept bundle from one project's own documentation,
asked it a question in his own words, and judged the result unusable. The
generated skill was an audit contract: all its discipline sat on the accounting
-- markings, denominators, budget lines, source pointers -- and none of it on
understanding the question, searching again, or writing one coherent answer.
Two sentences actively forbade the second of those.

**The two forbidding sentences are gone and their replacements are tested from
both sides.** "Do not go looking for context the pre-pass deliberately
withheld" read as "one run per question", and no wording of the operator's
question put the right document inside a single run's cut -- so a rule against
a second run was a rule against finding it at all. "Not something to retry with
a narrower question" generalised a budget-refusal case into the same ban.
SS 2.2 of the contract said the first of them, so the contract moved with the
skill rather than being left to disagree with it: a second pre-pass run with
other terms, and a fetch of a concept the payload NAMED, are reachable; SS 9's
two real boundaries -- directory enumeration, the verdict layer -- are not.

**Two new sections, and the checker requires them.** `## Working method`: read
the bundle's map, put the question into the bundle's own words, split a broad
question into 2-4 sub-questions, search per sub-question, read what lay just
outside the cut and search again with its words, same method across several
bundles, then assemble ONE answer ordered by sub-question, saying which source
holds and what is not covered. `## Answer form`: the questioner's language,
plain prose, no `below_k`, no digests, no budget lines, no denominators; short
textbook-style references (document + section, plus bundle where several were
read); and the audit trail written only when the questioner asks for it or
into a document that travels without the skill. `REQUIRED_SECTIONS` follows the
template and the contract's new SS 2.5 and SS 2.6 -- never the other way round.

**The generic skill becomes what `okf skill` and `okf project` write.** A
per-bundle skill's numbers go stale the moment its bundle is rebuilt, one copy
per consuming project, and a project with two bundles installs two
near-identical skills; the generic form carries no bundle's numbers and names
`okf card` for them. `--for-bundle` is the opt-in for the instantiated copy,
which still refuses out loud on a stale pairing -- safe to keep, not enough to
keep default. `rule_bundle_identity` learned to tell a generic skill from an
unfilled template by the frontmatter name the generator writes, so the template
still fails for the opposite reason: it declares no identity because it is
unfinished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:53:34 +02:00
1c97e57212 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>
2026-09-20 23:08:50 +02:00
bf697bfcad
fix(consume): every read path into a bundle is contained, not just okf_fetch
`okf_fetch` resolved a concept through `connectors.safe_resolve` from the day
the server was written. The other three ways into the same bytes did not.
`okf consume` and `okf_ask` reach `consume.build_payload`, `okf_describe`
reaches `mcp_server.card`, and both built the concept path by joining the
index's own name onto the bundle root. `consume._join` refuses a `..` segment
and an absolute target, but it is a STRING rule over the index text, and a
symlink is a fact about the filesystem that reading that text cannot see: the
index could name `lekkasje.md`, that name could be a link to a file outside the
bundle, and the file came back in the answer.

Measured before the fix, on a bundle carrying one honest concept and one
escaping link: 8 of 11 new rows red, the 3 green ones being `okf_fetch` on the
same two links and the known-positive that the clean bundle still answers. So
the suite was not red for an unrelated reason, and the fix is not "refuse every
bundle holding a link".

One place, not three copies: `consume.resolve_in_bundle` makes the check and
`consume.read_path_in_bundle` adds the file's presence. Every reader here goes
through them -- the index walk, the ref, the document prior, the payload, the
card, `okf_fetch`, and the three outside `consume` (`skill`, `quality`,
`project`) that joined the same way.

Two more failure modes in the same check, because they are the same question:

* A NAMED PIPE is not a regular file. `read_text` on one blocks for as long as
  nobody writes to it, which on a server is the whole process; the red row for
  it ran 60 s to a subprocess deadline and now returns in under a second.
* A DEAD INDEX LINK raised `FileNotFoundError`, and the broad handler in
  `handle` wrote `{error}` into the refusal -- the SERVER's absolute path,
  handed to whoever asked, over one index entry naming a file nobody wrote.
  It is `concept_unreadable` now, naming the concept and not the machine.

The returned path is the JOINED one, never the resolved one: `read_concept`
derives a concept id by taking the read path relative to the bundle root, and
once containment holds the two are the same bytes.

2334 passed, 2 skipped (was 2323 + 2). `mypy --strict src/` clean over 25 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:20:38 +02:00
864570b320 feat(accounting): okf build accounts for every source element
okf build --accounting PATH inventories every source document before
extraction, in the gate's per-format vocabulary, and after the run gives
each element one fate (carried / pointer / coded rejection), written as
JSON and summarised in log.md. "carried" is checked against the written
concept bodies, so a gate that drops a line is found (test). Exit 1 on
anything unaccounted or double-booked. Opt-in: +744 s (+19 %) on the
43-document reference corpus, and that corpus fails the check on 24 real
losses (22 images on text-less PDF pages, 2 docx Title paragraphs).

Changed without the flag:
- okf build exits 1 when it extracted documents and persisted none.
  Door B and corpus.measure are unchanged. One test relied on exit 0.
- An image file carried through a persisted document is its own K1b
  column, no longer also extractor_unknown. The set is what the resolver
  actually carried (ExtractedDocument.files), never a byte match.

tools/okf_accounting_gate.py (checks untouched) is green on all six rows,
R761 110 of 110 under both gates.

Report: docs/2026-09-17-innholdsregnskapet-bygget.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:35:20 +02:00
f6fea13299 feat(cli): okf project/consume/check/skill, and a generated skill with no path into a checkout
The reading direction existed only for someone standing in a clone. `consume`,
`contract_check` and `skill` moved from `tools/` into the package and are
reachable as `okf consume`, `okf check` and `okf skill`; `okf project` is new
and does the whole thing in one command.

The red measurement: a consumption skill generated from a checkout carried 4
lines naming that checkout by absolute path, 2 of them the commands the skill
tells a reader to run. It now names `okf consume` and `okf check`, and a test
asserts this repository appears in it nowhere, with a known-positive so the
zero is a measurement rather than a search that could not find.

The `tools/` files stay as ALIASES, not re-exports: a re-export binds copies of
the names into a second module object, so a caller patching one patches a
binding the implementation never reads. Two tests that monkeypatch okf_consume
went green again only under the alias. Every published reproduction block runs
unchanged.

The template and docs/consumption-contract.md (the section 7.4 known-positive)
are force-included into the wheel from the file they are authored in, so both
travel with the commands that cannot run without them and there is still one
authored copy of each.

Step 0, before any of it: okf build's default gained Arm E (--table-grid),
with --no-table-grid as its opt-out. The default moved to D plus F earlier the
same day on Arm F's published 5 of 12 -- a figure measured with Arm E ON.
Without it the fold has no joined table to fold, and the shipped default scored
2 of 12 with docx 0 of 3. Measured on the operator's folder: 30 md / 15
concepts on the new default against 43 / 28 without Arm E.

Install measurement from a fresh uv tool install, empty folder, this repository
nowhere on PYTHONPATH: 5 documents in, 15 concepts out, 0 references to tools/
in the generated skill, okf check conformant (15 rules, 0 findings).

Deviation stated rather than hidden: the order asked that
tests/test_okf_consume.py be left untouched. Two assertions in it read a PATH,
which is the one thing this work changes. Both were moved and the second made
stronger -- it now asserts every command the README recipe names is a
subcommand the CLI registers, which a file existing on disk never proved.

Suite 1414 -> 1427. ruff clean, mypy --strict clean over 21 files.
Record: docs/2026-09-08-o5-okf-project.md

Co-Authored-By: Claude <claude-opus-5>
2026-09-08 21:47:38 +02:00