feat(readme,skill,cli): the first screen an agent reads, three modes, and one flag that made two builds
`okf project` built a bundle two rules behind `okf build`. `cli.build`'s signature defaulted `keep_table_heading` and `sheet_section_rows` to `False` while argparse defaulted both to `True`, and `project.create` calls `build()` as a function. Measured on a five-document folder: 15 concepts / 30 files where `okf build` wrote 26 / 52, the whole difference in the priced sheet -- the document a question about price has to reach. The invariant test could not see it: it compared `project.create` against the same function, and its two fixture documents had neither a table nor a sheet. Both gaps are tests now, and the two paths are byte-equal on that folder (`diff -rq`, 0 differences). README opens with what / one install line / two commands / the three shapes of request; the phase-status paragraph moved down, nothing deleted. One tag is pinned everywhere: README pinned v0.4.0 on its install lines and v0.6.0 below, llms.txt pinned v0.4.0, so an agent reading from the top installed a tag without `okf project`. The skill states three modes -- question, hypothesis (per premise, `confirmed` / `refuted` / `undecidable-from-bundle`), and a task producing a document (source per claim in the artefact, an ungrounded paragraph written and marked rather than dropped, the cut declared inside the document). The five markings are untouched. Generated skills state relative paths in the project layout: `okf consume .okf/<id>` and `okf check --skill .claude/skills/<id>-consume/SKILL.md`, runnable from where `okf project` tells the reader to start `claude`. Two absolute paths to zero, measured with a query shown capable of finding first -- O5's published "4 -> 0" used `grep -c "^/"` against paths indented by two spaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
956714594d
commit
1260fac154
12 changed files with 532 additions and 43 deletions
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.7.0] — 2026-09-09
|
||||||
|
|
||||||
|
The first screen an agent reads, the three shapes of request the skill answers,
|
||||||
|
and one defect that made the command the first screen recommends build a worse
|
||||||
|
bundle than the command it claims to be.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`okf project` built a bundle two rules behind `okf build`.** `cli.build`'s
|
||||||
|
Python signature defaulted `keep_table_heading` and `sheet_section_rows` to
|
||||||
|
`False` while argparse defaulted both to `True`; `project.create` calls
|
||||||
|
`build()` as a function and passes no flag list, so it read the signature.
|
||||||
|
Measured on a five-document folder: `okf project` wrote **15 concepts / 30
|
||||||
|
files** where `okf build` on the same folder wrote **26 / 52**, and the whole
|
||||||
|
difference was in the priced spreadsheet — the document a question about price
|
||||||
|
has to reach. The invariant test that was supposed to catch this could not:
|
||||||
|
it compared `project.create` against the same `build()` function, so both
|
||||||
|
sides carried the same wrong value, and its two fixture documents had neither
|
||||||
|
a table nor a sheet. Both gaps are now tests: one compares the signature's
|
||||||
|
defaults against argparse's, the other builds a document whose concept count
|
||||||
|
actually moves with the two flags. After the fix the two paths are byte-equal
|
||||||
|
on that folder (`diff -rq`, 0 differences).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **A first screen for a reader who has not used this before**, agent or human:
|
||||||
|
what it is in one sentence, one install line, two commands, and the three
|
||||||
|
shapes of request. The phase-status paragraph that used to open the README
|
||||||
|
moved down to `## What this library is`; nothing was deleted.
|
||||||
|
- **Three modes in the consumption skill**, stated in the template, the
|
||||||
|
instantiated skill and the generator:
|
||||||
|
- **Question** — as before, the default.
|
||||||
|
- **Hypothesis** — decomposed into premises and answered **per premise** as
|
||||||
|
`confirmed` / `refuted` / `undecidable-from-bundle`, three literals with no
|
||||||
|
fourth value. A premise whose excerpt is real but does not carry the
|
||||||
|
conclusion is `[sourced-not-sufficient]` on **that premise**, not on the
|
||||||
|
whole answer: four premises and one weak source is three answers and one
|
||||||
|
gap, and reporting it as one refusal throws the three away.
|
||||||
|
- **Task that produces a document or a paragraph** — every claim in the
|
||||||
|
written artefact carries `(bundle_id, concept_id)`, the excerpt's `sha256`,
|
||||||
|
its `title` and whichever `source_*` keys it has; an ungrounded paragraph is
|
||||||
|
**written and marked**, never dropped; and the cut (`considered`,
|
||||||
|
`withheld`, `delivered`) is declared inside the document, because the
|
||||||
|
document travels without the chat.
|
||||||
|
The five markings are untouched — the modes add no sixth.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The generated skill states relative paths where it can.** In the layout
|
||||||
|
`okf project` writes, the commands are now `okf consume .okf/<id>` and
|
||||||
|
`okf check --skill .claude/skills/<id>-consume/SKILL.md`, runnable from the
|
||||||
|
project root — which is where `okf project`'s own closing line tells the
|
||||||
|
reader to start `claude`. A path outside the project root stays absolute on
|
||||||
|
purpose: `../../..` is not more portable, only harder to read. The two
|
||||||
|
absolute paths a generated skill carried are now zero, measured with a query
|
||||||
|
shown capable of finding first — O5's published "4 → 0" used `grep -c "^/"`
|
||||||
|
against paths indented by two spaces, which could not have matched either way.
|
||||||
|
- **One tag is pinned everywhere.** `README.md` pinned `v0.4.0` on its install
|
||||||
|
lines and `v0.6.0` further down, and `llms.txt` pinned `v0.4.0`; an agent
|
||||||
|
reading from the top installed a tag without `okf project`. All install lines
|
||||||
|
now name `v0.7.0`, and the earlier tags are kept as a labelled history
|
||||||
|
section rather than as commands. `llms.txt` gained the `okf project` form and
|
||||||
|
a pointer to the Claude Code section.
|
||||||
|
|
||||||
## [0.6.0] — 2026-09-08
|
## [0.6.0] — 2026-09-08
|
||||||
|
|
||||||
The first tag since `v0.5.0a2`, so everything that had accumulated as
|
The first tag since `v0.5.0a2`, so everything that had accumulated as
|
||||||
|
|
|
||||||
35
CLAUDE.md
35
CLAUDE.md
|
|
@ -263,7 +263,40 @@ and fixtures, never code.
|
||||||
`<id>` to the folder name reduced to `[a-z0-9-]`. It owns NO flag that moves
|
`<id>` to the folder name reduced to `[a-z0-9-]`. It owns NO flag that moves
|
||||||
a bundle's bytes and a test holds it byte-equal to `okf build`; two build
|
a bundle's bytes and a test holds it byte-equal to `okf build`; two build
|
||||||
paths would leave every measurement report pinned to a bundle nobody
|
paths would leave every measurement report pinned to a bundle nobody
|
||||||
produces. `skills/okf-prosjekt/` is the Claude Code skill over it.
|
produces. **That invariant was FALSE from the day those two
|
||||||
|
flags became defaults until O6 measured it, and the test could not see it:** `cli.build`'s Python SIGNATURE defaulted
|
||||||
|
`keep_table_heading` and `sheet_section_rows` to `False` while argparse
|
||||||
|
defaulted both to `True`, and `project.create` calls `build()` as a function,
|
||||||
|
so it read the signature. Measured on a five-document folder, `okf project`
|
||||||
|
wrote **15 concepts / 30 files** against `okf build`'s **26 / 52**, the whole
|
||||||
|
difference in the priced spreadsheet -- the document a question about price
|
||||||
|
has to reach. The byte-equality test compared `project.create` against the
|
||||||
|
same `build()`, so both sides carried the same wrong value, and its two
|
||||||
|
fixture documents had neither a table nor a sheet: **a test and the code
|
||||||
|
agreeing over a set where the difference cannot appear.** Two tests now hold
|
||||||
|
it -- one comparing the signature's defaults against argparse's for every
|
||||||
|
same-typed parameter, one building a document whose concept count actually
|
||||||
|
moves with the two flags. `skills/okf-prosjekt/` is the Claude Code skill
|
||||||
|
over it.
|
||||||
|
- **The generated consumption skill states THREE modes and RELATIVE paths**
|
||||||
|
(O6, 2026-09-09). Question (the default), hypothesis (decomposed into
|
||||||
|
premises and answered PER PREMISE as `confirmed` / `refuted` /
|
||||||
|
`undecidable-from-bundle` -- three literals, no fourth; a weak source is
|
||||||
|
`[sourced-not-sufficient]` on that PREMISE, because four premises and one
|
||||||
|
weak source is three answers and one gap), and a task producing a document
|
||||||
|
(source per claim IN the artefact, an ungrounded paragraph written and marked
|
||||||
|
rather than dropped, the cut declared inside the document because the
|
||||||
|
document travels without the chat). The five markings are untouched -- the
|
||||||
|
modes add no sixth. In the layout `okf project` writes, the commands are
|
||||||
|
`okf consume .okf/<id>` and `okf check --skill
|
||||||
|
.claude/skills/<id>-consume/SKILL.md`, runnable from the project root, which
|
||||||
|
is where `okf project`'s own closing line tells the reader to start `claude`;
|
||||||
|
a path OUTSIDE that root stays absolute on purpose, since `../../..` is not
|
||||||
|
more portable, only harder to read. Two absolute paths to zero -- and
|
||||||
|
**O5's published "4 absolute paths -> 0" was measured with `grep -c "^/"`
|
||||||
|
against paths indented by two spaces**, a query that could not have found one
|
||||||
|
either way, so the zero was never a measurement. Every path assertion here
|
||||||
|
runs its pattern against a known-positive first.
|
||||||
- Build a bundle: `okf build <folder> --bundle <dir> --bundle-id <id>
|
- Build a bundle: `okf build <folder> --bundle <dir> --bundle-id <id>
|
||||||
--okf-version <v>` — the installed console script (`[project.scripts]`),
|
--okf-version <v>` — the installed console script (`[project.scripts]`),
|
||||||
the packaged form of what used to be a shell loop over two `tools/`
|
the packaged form of what used to be a shell loop over two `tools/`
|
||||||
|
|
|
||||||
98
README.md
98
README.md
|
|
@ -1,6 +1,41 @@
|
||||||
# llm-ingestion-okf
|
# llm-ingestion-okf
|
||||||
|
|
||||||
Shared OKF (Open Knowledge Format) ingestion library: spec-based connectors, bundle inbox, and external-bundle import. Security delegated to llm-ingestion-guard.
|
Turn a folder of documents (PDF, DOCX, XLSX, PPTX, MD) into a bundle a model can
|
||||||
|
answer from **with a source on every claim** — offline, deterministic, no model
|
||||||
|
call anywhere in the run path.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Python 3.10+ and [uv](https://docs.astral.sh/uv/). One line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.7.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use it
|
||||||
|
|
||||||
|
```sh
|
||||||
|
okf project ~/my-documents # folder in: bundle + Claude Code skill, in this directory
|
||||||
|
claude # start Claude Code here
|
||||||
|
```
|
||||||
|
|
||||||
|
Then ask in plain language. Three shapes of request work, and the skill states
|
||||||
|
the rules for each:
|
||||||
|
|
||||||
|
- **a question** — "hva er kravene til pris?"
|
||||||
|
- **a hypothesis** — "stemmer det at leverandoeren baerer risikoen for grunnforhold?"
|
||||||
|
Answered per premise as `confirmed` / `refuted` / `undecidable-from-bundle`.
|
||||||
|
- **a task whose answer is a document** — "lag `krav-pris.md` med alle krav til
|
||||||
|
pris, ett avsnitt per krav, med dokument og kravnummer." Every claim in the
|
||||||
|
written file carries its source; a paragraph with no ground is written
|
||||||
|
and marked, never dropped.
|
||||||
|
|
||||||
|
Everything below is detail: [Consume in Claude
|
||||||
|
Code](#consume-in-claude-code) for the same thing in steps and with several
|
||||||
|
bundles at once, [Build](#build) for the flags, [Requirements](#requirements)
|
||||||
|
for the pip fallback and the guard pairing.
|
||||||
|
|
||||||
|
## What this library is
|
||||||
|
|
||||||
Status: phases 1–3 are implemented. Phase 1 (spec-based ingestion) covers
|
Status: phases 1–3 are implemented. Phase 1 (spec-based ingestion) covers
|
||||||
manifest validation, the `file`/`sql`/`http` connectors, deterministic
|
manifest validation, the `file`/`sql`/`http` connectors, deterministic
|
||||||
|
|
@ -17,46 +52,53 @@ formats through a vendored document converter. Three of those five office
|
||||||
rows are **unmeasured** — see [Binary extraction](#binary-extraction). Phase 4
|
rows are **unmeasured** — see [Binary extraction](#binary-extraction). Phase 4
|
||||||
(the Node half) is planned (see `docs/plan/`).
|
(the Node half) is planned (see `docs/plan/`).
|
||||||
|
|
||||||
## Install
|
## Install in detail
|
||||||
|
|
||||||
Python 3.10+. Neither this package nor the guard it depends on is on a package
|
Neither this package nor the guard it depends on is on a package index yet, so
|
||||||
index yet. With uv, one command is enough:
|
both install by direct reference. With uv, one command resolves both:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
uv pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.4.0"
|
uv pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.7.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
uv resolves the guard on its own, because it reads the `[tool.uv.sources]`
|
uv resolves the guard on its own, because it reads the `[tool.uv.sources]`
|
||||||
entry in the `pyproject.toml` **of the tag it is installing**, and `v0.4.0`
|
entry in the `pyproject.toml` **of the tag it is installing**, and `v0.7.0`
|
||||||
points that entry at the guard tag below. Measured 2026-07-25 and re-measured
|
points that entry at `llm-ingestion-guard` `v1.3.0`. Use `uv tool install`
|
||||||
2026-08-20 with an empty `uv` cache; both runs installed
|
instead of `uv pip install` when you want the `okf` command on `PATH` without an
|
||||||
`llm-ingestion-guard==0.2.0` + `llm-ingestion-okf==0.4.0` and imported clean.
|
active virtualenv — that is the form the first screen shows.
|
||||||
|
|
||||||
With plain pip, the transitive git dependency does not resolve on its own —
|
With plain pip, the transitive git dependency does not resolve on its own —
|
||||||
**install the guard first**, or installing this package fails with
|
**install the guard first**, or installing this package fails with
|
||||||
`No matching distribution found for llm-ingestion-guard`:
|
`No matching distribution found for llm-ingestion-guard`:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v0.2.0"
|
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.3.0"
|
||||||
pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.4.0"
|
pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.7.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
The guard tag is paired to the okf tag, not to this branch: `v0.4.0` declares
|
The guard tag is paired to the okf tag, not to this branch. `v0.7.0` declares
|
||||||
`llm-ingestion-guard>=0.2,<0.3`, which `v0.2.0` satisfies and later guard tags
|
`llm-ingestion-guard>=1.2,<2.0`, which `v1.3.0` satisfies; the pairing above is
|
||||||
do not. `main` has since moved its own pin to `>=1.2,<2.0` (see
|
read off that tag's own `[tool.uv.sources]`, not off this branch. Reading a pin
|
||||||
[Requirements](#requirements)); that pin reaches you in the next stable tag,
|
off `main` and installing it against an older okf tag is the one combination
|
||||||
not in the commands above. Reading a pin off this branch and installing it
|
that fails.
|
||||||
against `v0.4.0` is the one combination that fails.
|
|
||||||
|
|
||||||
`v0.6.0` is the current tag and the one the three-line form under [Consume in
|
### Earlier tags, as history
|
||||||
Claude Code](#consume-in-claude-code) installs: it is the first tag carrying the
|
|
||||||
`okf project`, `okf consume`, `okf check` and `okf skill` subcommands, without
|
|
||||||
which that form does not exist. `v0.4.0` is the last tag before the OKF v0.2
|
|
||||||
work. `v0.5.0a2` is a pre-release for the named OKF v0.2 pilot set only.
|
|
||||||
|
|
||||||
**`v0.6.0` does not make OKF v0.2 generally available.** `OKF_LATEST` is
|
These are not install lines. They record what each earlier tag was, so a reader
|
||||||
unchanged and still points at `DEFAULT`; flipping that alias is the GA event and
|
who meets one in an older document knows what they are looking at.
|
||||||
this tag is not it (see [Upstream OKF versions](#upstream-okf-versions)).
|
|
||||||
|
- `v0.7.0` — the current tag: `okf project` builds the bundle `okf build`
|
||||||
|
builds (they were one flag apart before it), and the generated skill states
|
||||||
|
the question, hypothesis and document-task modes with relative paths.
|
||||||
|
- `v0.6.0` — the first tag carrying the `okf project`, `okf consume`,
|
||||||
|
`okf check` and `okf skill` subcommands.
|
||||||
|
- `v0.5.0a2` — a pre-release for the named OKF v0.2 pilot set only.
|
||||||
|
- `v0.4.0` — the last tag before the OKF v0.2 work; it declares
|
||||||
|
`llm-ingestion-guard>=0.2,<0.3`, which only guard `v0.2.0` satisfies.
|
||||||
|
|
||||||
|
**No tag yet makes OKF v0.2 generally available.** `OKF_LATEST` is unchanged and
|
||||||
|
still points at `DEFAULT`; flipping that alias is the GA event and none of the
|
||||||
|
tags above is it (see [Upstream OKF versions](#upstream-okf-versions)).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|
@ -317,7 +359,7 @@ not need this repository — the first line installs the command, the second
|
||||||
builds the bundle and writes a skill beside it, the third asks.
|
builds the bundle and writes a skill beside it, the third asks.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.6.0"
|
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.7.0"
|
||||||
okf project ~/my-documents
|
okf project ~/my-documents
|
||||||
claude
|
claude
|
||||||
```
|
```
|
||||||
|
|
|
||||||
33
llms.txt
33
llms.txt
|
|
@ -1,15 +1,34 @@
|
||||||
# llm-ingestion-okf
|
# llm-ingestion-okf
|
||||||
|
|
||||||
> Shared OKF (Open Knowledge Format) ingestion library: spec-based connectors, bundle inbox, and external-bundle import. Security delegated to llm-ingestion-guard.
|
> Turn a folder of documents (PDF, DOCX, XLSX, PPTX, MD) into a bundle a model can answer from with a source on every claim. Offline and deterministic: no model call anywhere in the run path. Security is delegated to llm-ingestion-guard.
|
||||||
|
|
||||||
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/). Neither this package
|
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/). Neither this package
|
||||||
nor the guard it depends on is on a package index yet; with uv, one command
|
nor the guard it depends on is on a package index yet; one command resolves both:
|
||||||
resolves both:
|
|
||||||
|
|
||||||
```
|
```sh
|
||||||
uv pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.4.0"
|
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.7.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional
|
Then, in the folder you want to work from:
|
||||||
|
|
||||||
- [README](README.md): the pip-only fallback if uv is unavailable, phase status, requirements, and the upstream OKF version policy.
|
```sh
|
||||||
|
okf project ~/my-documents
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
`okf project` writes the bundle to `.okf/<id>/` and a Claude Code skill to
|
||||||
|
`.claude/skills/<id>-consume/` in the current directory. Start `claude` there
|
||||||
|
and ask in plain language. Three shapes of request are supported and the
|
||||||
|
generated skill states the rules for each: a **question**, a **hypothesis**
|
||||||
|
(answered per premise as `confirmed` / `refuted` / `undecidable-from-bundle`),
|
||||||
|
and a **task whose answer is a document** (every claim in the written file
|
||||||
|
carries its source; an ungrounded paragraph is written and marked, never
|
||||||
|
dropped).
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [Consume in Claude Code](README.md#consume-in-claude-code): the same thing in
|
||||||
|
steps, several bundles in one project, and what was measured.
|
||||||
|
- [README](README.md): the pip-only fallback if uv is unavailable, the guard
|
||||||
|
pairing per tag, the build flags, phase status, and the upstream OKF version
|
||||||
|
policy.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "llm-ingestion-okf"
|
name = "llm-ingestion-okf"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
description = "Shared OKF (Open Knowledge Format) ingestion library: spec-based connectors, bundle inbox, and external-bundle import, with security delegated to llm-ingestion-guard."
|
description = "Shared OKF (Open Knowledge Format) ingestion library: spec-based connectors, bundle inbox, and external-bundle import, with security delegated to llm-ingestion-guard."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,59 @@ and the cut; it decides nothing about the question.
|
||||||
when you were handed a bounded window, is the denominator failure below with
|
when you were handed a bounded window, is the denominator failure below with
|
||||||
extra steps.
|
extra steps.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
Three shapes of request, one discipline. Which one you are in is decided by what
|
||||||
|
was asked, never by what the payload happened to contain.
|
||||||
|
|
||||||
|
### Question
|
||||||
|
|
||||||
|
Answer it from the delivered excerpts, mark every claim, and stop. The default.
|
||||||
|
|
||||||
|
### Hypothesis
|
||||||
|
|
||||||
|
A hypothesis is a claim someone wants tested, not a question. **Decompose it
|
||||||
|
into its premises first and answer PER PREMISE** — a single verdict over the
|
||||||
|
whole hypothesis hides which part the bundle actually covered.
|
||||||
|
|
||||||
|
Each premise gets exactly one of three literals:
|
||||||
|
|
||||||
|
| Verdict | Use when |
|
||||||
|
|---|---|
|
||||||
|
| `confirmed` | the delivered excerpts carry the premise |
|
||||||
|
| `refuted` | the delivered excerpts carry its contradiction |
|
||||||
|
| `undecidable-from-bundle` | neither, within what was delivered |
|
||||||
|
|
||||||
|
These three are literals, like the five markings: no fourth value, no
|
||||||
|
"partly confirmed", no translation. A premise whose excerpt is real but does not
|
||||||
|
carry the conclusion is marked `[sourced-not-sufficient]` **on that premise**,
|
||||||
|
not on the whole answer — a hypothesis with four premises and one weak source
|
||||||
|
has three answers and one gap, and reporting it as one refusal throws the three
|
||||||
|
away.
|
||||||
|
|
||||||
|
The hypothesis-level verdict is then stated as a consequence of the per-premise
|
||||||
|
ones, with its reasoning shown. It is `derived`, never `extracted`.
|
||||||
|
|
||||||
|
### Task that produces a document or a paragraph
|
||||||
|
|
||||||
|
Some requests want a written artefact — a note, a section, a table of
|
||||||
|
requirements — rather than an answer in chat. The artefact is held to the same
|
||||||
|
rule as an answer, in the artefact itself:
|
||||||
|
|
||||||
|
- **Every claim carries its source in the document**: `(bundle_id, concept_id)`,
|
||||||
|
the excerpt's `sha256`, its `title`, and whichever `source_*` keys that
|
||||||
|
excerpt has. A footnote, a parenthesis or a trailing line all work; leaving it
|
||||||
|
out because "the chat already said it" does not — the document is what gets
|
||||||
|
read, forwarded and quoted, and it travels without the chat.
|
||||||
|
- **A paragraph with no ground is written, not dropped.** Mark it
|
||||||
|
`[sourced-not-sufficient]` and leave it standing where it belongs, saying what
|
||||||
|
was asked for and what the bundle did not carry. A silently omitted section is
|
||||||
|
the denominator failure with a nicer surface: the reader cannot see the hole,
|
||||||
|
so they read a complete document.
|
||||||
|
- **Declare the cut inside the document**, not only in chat: `considered`,
|
||||||
|
`withheld` and `delivered`, plus the bundle ref. The three counts and the ref
|
||||||
|
are what let a later reader tell whether the document is still current.
|
||||||
|
|
||||||
## Markings
|
## Markings
|
||||||
|
|
||||||
Every claim carries exactly one of these five literals, plus a pointer to the
|
Every claim carries exactly one of these five literals, plus a pointer to the
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,59 @@ library's line-oriented parser can read, so no tier could be derived honestly),
|
||||||
the delivered cap), `over_budget_after_knapsack` (it fitted alone but not
|
the delivered cap), `over_budget_after_knapsack` (it fitted alone but not
|
||||||
alongside the set that was chosen).
|
alongside the set that was chosen).
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
Three shapes of request, one discipline. Which one you are in is decided by what
|
||||||
|
was asked, never by what the payload happened to contain.
|
||||||
|
|
||||||
|
### Question
|
||||||
|
|
||||||
|
Answer it from the delivered excerpts, mark every claim, and stop. The default.
|
||||||
|
|
||||||
|
### Hypothesis
|
||||||
|
|
||||||
|
A hypothesis is a claim someone wants tested, not a question. **Decompose it
|
||||||
|
into its premises first and answer PER PREMISE** — a single verdict over the
|
||||||
|
whole hypothesis hides which part the bundle actually covered.
|
||||||
|
|
||||||
|
Each premise gets exactly one of three literals:
|
||||||
|
|
||||||
|
| Verdict | Use when |
|
||||||
|
|---|---|
|
||||||
|
| `confirmed` | the delivered excerpts carry the premise |
|
||||||
|
| `refuted` | the delivered excerpts carry its contradiction |
|
||||||
|
| `undecidable-from-bundle` | neither, within what was delivered |
|
||||||
|
|
||||||
|
These three are literals, like the five markings: no fourth value, no
|
||||||
|
"partly confirmed", no translation. A premise whose excerpt is real but does not
|
||||||
|
carry the conclusion is marked `[sourced-not-sufficient]` **on that premise**,
|
||||||
|
not on the whole answer — a hypothesis with four premises and one weak source
|
||||||
|
has three answers and one gap, and reporting it as one refusal throws the three
|
||||||
|
away.
|
||||||
|
|
||||||
|
The hypothesis-level verdict is then stated as a consequence of the per-premise
|
||||||
|
ones, with its reasoning shown. It is `derived`, never `extracted`.
|
||||||
|
|
||||||
|
### Task that produces a document or a paragraph
|
||||||
|
|
||||||
|
Some requests want a written artefact — a note, a section, a table of
|
||||||
|
requirements — rather than an answer in chat. The artefact is held to the same
|
||||||
|
rule as an answer, in the artefact itself:
|
||||||
|
|
||||||
|
- **Every claim carries its source in the document**: `(bundle_id, concept_id)`,
|
||||||
|
the excerpt's `sha256`, its `title`, and whichever `source_*` keys that
|
||||||
|
excerpt has. A footnote, a parenthesis or a trailing line all work; leaving it
|
||||||
|
out because "the chat already said it" does not — the document is what gets
|
||||||
|
read, forwarded and quoted, and it travels without the chat.
|
||||||
|
- **A paragraph with no ground is written, not dropped.** Mark it
|
||||||
|
`[sourced-not-sufficient]` and leave it standing where it belongs, saying what
|
||||||
|
was asked for and what the bundle did not carry. A silently omitted section is
|
||||||
|
the denominator failure with a nicer surface: the reader cannot see the hole,
|
||||||
|
so they read a complete document.
|
||||||
|
- **Declare the cut inside the document**, not only in chat: `considered`,
|
||||||
|
`withheld` and `delivered`, plus the bundle ref. The three counts and the ref
|
||||||
|
are what let a later reader tell whether the document is still current.
|
||||||
|
|
||||||
## Markings
|
## Markings
|
||||||
|
|
||||||
Every claim carries exactly one of these five literals, plus a pointer to the
|
Every claim carries exactly one of these five literals, plus a pointer to the
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ from .manifest import (
|
||||||
)
|
)
|
||||||
from .materialize import IngestResult, materialize_bundle
|
from .materialize import IngestResult, materialize_bundle
|
||||||
|
|
||||||
__version__ = "0.6.0"
|
__version__ = "0.7.0"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BlockedFile",
|
"BlockedFile",
|
||||||
|
|
|
||||||
|
|
@ -230,8 +230,8 @@ def _propose_plans(
|
||||||
outline_run: int = 0,
|
outline_run: int = 0,
|
||||||
table_grid: bool = False,
|
table_grid: bool = False,
|
||||||
unit_fold: bool = False,
|
unit_fold: bool = False,
|
||||||
keep_table_heading: bool = False,
|
keep_table_heading: bool = DEFAULT_KEEP_TABLE_HEADING,
|
||||||
sheet_section_rows: bool = False,
|
sheet_section_rows: bool = DEFAULT_SHEET_SECTION_ROWS,
|
||||||
drop_wrapped_outline: bool = False,
|
drop_wrapped_outline: bool = False,
|
||||||
outline_gate: bool = False,
|
outline_gate: bool = False,
|
||||||
first_span_from_zero: bool = False,
|
first_span_from_zero: bool = False,
|
||||||
|
|
@ -301,8 +301,8 @@ def build(
|
||||||
outline_run: int = DEFAULT_OUTLINE_RUN,
|
outline_run: int = DEFAULT_OUTLINE_RUN,
|
||||||
table_grid: bool = DEFAULT_TABLE_GRID,
|
table_grid: bool = DEFAULT_TABLE_GRID,
|
||||||
unit_fold: bool = DEFAULT_UNIT_FOLD,
|
unit_fold: bool = DEFAULT_UNIT_FOLD,
|
||||||
keep_table_heading: bool = False,
|
keep_table_heading: bool = DEFAULT_KEEP_TABLE_HEADING,
|
||||||
sheet_section_rows: bool = False,
|
sheet_section_rows: bool = DEFAULT_SHEET_SECTION_ROWS,
|
||||||
drop_wrapped_outline: bool = DEFAULT_DROP_WRAPPED_OUTLINE,
|
drop_wrapped_outline: bool = DEFAULT_DROP_WRAPPED_OUTLINE,
|
||||||
outline_gate: bool = DEFAULT_OUTLINE_GATE,
|
outline_gate: bool = DEFAULT_OUTLINE_GATE,
|
||||||
first_span_from_zero: bool = DEFAULT_FIRST_SPAN_FROM_ZERO,
|
first_span_from_zero: bool = DEFAULT_FIRST_SPAN_FROM_ZERO,
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,45 @@ def directory_control(bundle_root: Path, *, profile: BundleProfile) -> tuple[int
|
||||||
return walked, on_disk
|
return walked, on_disk
|
||||||
|
|
||||||
|
|
||||||
|
# --- Where the reader runs the commands (O6) -----------------------------------
|
||||||
|
|
||||||
|
#: `okf project` writes the skill to `<root>/.claude/skills/<id>-consume` and
|
||||||
|
#: the bundle to `<root>/.okf/<id>`, then tells the reader to start `claude` in
|
||||||
|
#: `<root>`. So `<root>` is where the skill's commands are read from, and a path
|
||||||
|
#: written relative to it is the one a reader can paste.
|
||||||
|
SKILLS_LAYOUT = (".claude", "skills")
|
||||||
|
|
||||||
|
|
||||||
|
def project_root_of(out: Path) -> Path | None:
|
||||||
|
"""`<root>/.claude/skills/<name>` -> `<root>`; anything else -> None.
|
||||||
|
|
||||||
|
Read off the path rather than passed in, so `okf skill` and `okf project`
|
||||||
|
reach the same answer without a second parameter that could disagree with
|
||||||
|
the layout on disk.
|
||||||
|
"""
|
||||||
|
resolved = out.resolve()
|
||||||
|
if resolved.parts[-3:-1] == SKILLS_LAYOUT:
|
||||||
|
return resolved.parents[2]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def as_written(path: Path, *, base: Path | None) -> str:
|
||||||
|
"""The path as the skill states it: relative to `base` when it is under it.
|
||||||
|
|
||||||
|
A path outside `base` stays absolute on purpose. `../../..` is not more
|
||||||
|
portable than `/Users/...`, it is only harder to read, and a skill that
|
||||||
|
states a path its reader cannot resolve is worse than one that states an
|
||||||
|
honest absolute.
|
||||||
|
"""
|
||||||
|
resolved = path.resolve()
|
||||||
|
if base is None:
|
||||||
|
return str(resolved)
|
||||||
|
try:
|
||||||
|
return resolved.relative_to(base).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return str(resolved)
|
||||||
|
|
||||||
|
|
||||||
# --- The instantiation --------------------------------------------------------
|
# --- The instantiation --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -321,10 +360,11 @@ def render(
|
||||||
name = f"{slug(bundle_id)}-consume"
|
name = f"{slug(bundle_id)}-consume"
|
||||||
text = template_path().read_text(encoding="utf-8")
|
text = template_path().read_text(encoding="utf-8")
|
||||||
text = text.split("---\n", 2)[2]
|
text = text.split("---\n", 2)[2]
|
||||||
|
base = project_root_of(out)
|
||||||
text = _rewrite(
|
text = _rewrite(
|
||||||
text,
|
text,
|
||||||
bundle_root=bundle_root,
|
bundle_root=Path(as_written(bundle_root, base=base)),
|
||||||
skill_path=out.resolve() / "SKILL.md",
|
skill_path=Path(as_written(out / "SKILL.md", base=base)),
|
||||||
bundle_id=bundle_id,
|
bundle_id=bundle_id,
|
||||||
ref=ref,
|
ref=ref,
|
||||||
name=name,
|
name=name,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ build paths and the reports are pinned to one of them.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import inspect
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -20,6 +21,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||||
|
|
||||||
from llm_ingestion_okf import project # noqa: E402
|
from llm_ingestion_okf import project # noqa: E402
|
||||||
from llm_ingestion_okf.cli import build # noqa: E402
|
from llm_ingestion_okf.cli import build # noqa: E402
|
||||||
|
from llm_ingestion_okf.cli import parse_args # noqa: E402
|
||||||
from llm_ingestion_okf.cli import main as okf_main # noqa: E402
|
from llm_ingestion_okf.cli import main as okf_main # noqa: E402
|
||||||
from llm_ingestion_okf.errors import IngestError # noqa: E402
|
from llm_ingestion_okf.errors import IngestError # noqa: E402
|
||||||
|
|
||||||
|
|
@ -185,3 +187,81 @@ def test_the_installed_command_reaches_every_subcommand() -> None:
|
||||||
assert listed.returncode == 0
|
assert listed.returncode == 0
|
||||||
for command in ("build", "consume", "check", "skill", "project"):
|
for command in ("build", "consume", "check", "skill", "project"):
|
||||||
assert command in listed.stdout, command
|
assert command in listed.stdout, command
|
||||||
|
|
||||||
|
|
||||||
|
# --- The two defaults that were one flag apart (O6) ----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_build_signature_defaults_are_the_build_command_defaults() -> None:
|
||||||
|
"""One flag, one default. `okf project` reads the SIGNATURE, not argparse.
|
||||||
|
|
||||||
|
`project.create` calls `build()` as a Python function and passes no flag
|
||||||
|
list, so every segmentation value it gets is the signature's default. When
|
||||||
|
a flag moves to ON in argparse and is left OFF in the signature, there are
|
||||||
|
two defaults for one flag and `okf project` builds a bundle a rule behind
|
||||||
|
the command of the same name.
|
||||||
|
|
||||||
|
The byte-equality test above cannot see this: it calls the same function
|
||||||
|
with the same signature, so both sides carry the same wrong value. That is
|
||||||
|
the mechanism -- a test and the code agreeing over a set where the
|
||||||
|
difference cannot appear.
|
||||||
|
"""
|
||||||
|
parsed = parse_args(["build", "folder", "--bundle", "b", "--okf-version", "0.2"])
|
||||||
|
signature = inspect.signature(build)
|
||||||
|
disagreeing = {
|
||||||
|
name: (parameter.default, getattr(parsed, name))
|
||||||
|
for name, parameter in signature.parameters.items()
|
||||||
|
if hasattr(parsed, name)
|
||||||
|
and parameter.default is not inspect.Parameter.empty
|
||||||
|
and type(getattr(parsed, name)) is type(parameter.default)
|
||||||
|
}
|
||||||
|
disagreeing = {name: pair for name, pair in disagreeing.items() if pair[0] != pair[1]}
|
||||||
|
assert disagreeing == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_sheet_reaches_the_project_bundle_as_it_reaches_the_build_command(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""The same folder through both entry doors, on a document that separates them.
|
||||||
|
|
||||||
|
`--sheet-section-rows` and `--keep-table-heading` are ON in `okf build`.
|
||||||
|
A markdown table under a heading is the smallest document whose concept
|
||||||
|
count moves with them, so this test's set is not empty by construction --
|
||||||
|
which is what let the invariant above pass while it was false.
|
||||||
|
"""
|
||||||
|
folder = tmp_path / "Ark"
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / "krav.md").write_text(
|
||||||
|
"# Prisskjema\n\n"
|
||||||
|
"## 1 Poster\n\n"
|
||||||
|
"| Post | Beskrivelse | Pris |\n|---|---|---|\n"
|
||||||
|
"| 01 | Rigg og drift | 100 |\n"
|
||||||
|
"| 02 | Grunnarbeid | 200 |\n"
|
||||||
|
"| 03 | Betong | 300 |\n"
|
||||||
|
"| 04 | Staal | 400 |\n"
|
||||||
|
"| 05 | Tak | 500 |\n\n"
|
||||||
|
"## 2 Vilkaar\n\nBetaling skjer etter levering.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="",
|
||||||
|
)
|
||||||
|
|
||||||
|
out = tmp_path / "project"
|
||||||
|
bundle, _, _ = project.create(folder, out=out)
|
||||||
|
|
||||||
|
reference = tmp_path / "reference"
|
||||||
|
assert (
|
||||||
|
okf_main(
|
||||||
|
[
|
||||||
|
"build",
|
||||||
|
str(folder),
|
||||||
|
"--bundle",
|
||||||
|
str(reference),
|
||||||
|
"--bundle-id",
|
||||||
|
"ark",
|
||||||
|
"--okf-version",
|
||||||
|
"0.2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
assert tree(bundle) == tree(reference)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -252,3 +253,107 @@ def test_the_generated_skill_carries_a_payload_its_own_bundle_produced(tmp_path:
|
||||||
assert payload["bundle"]["bundle_id"] == okf_consume.root_bundle_id_of(PROVENANCE)
|
assert payload["bundle"]["bundle_id"] == okf_consume.root_bundle_id_of(PROVENANCE)
|
||||||
assert payload["bundle"]["ref"] == okf_consume.bundle_ref(PROVENANCE)
|
assert payload["bundle"]["ref"] == okf_consume.bundle_ref(PROVENANCE)
|
||||||
assert okf_contract_check.check(written.read_text(encoding="utf-8"), payload).findings == ()
|
assert okf_contract_check.check(written.read_text(encoding="utf-8"), payload).findings == ()
|
||||||
|
|
||||||
|
|
||||||
|
# --- The three modes, and the paths a reader can actually run (O6) -------------
|
||||||
|
|
||||||
|
#: A path is absolute here if it starts a line or follows whitespace. The
|
||||||
|
#: measurement O5 published used `grep -c "^/"`, which cannot match a path
|
||||||
|
#: indented by two spaces -- which is the form the generator writes. Every
|
||||||
|
#: assertion below runs this pattern against a known-positive first.
|
||||||
|
ABSOLUTE = re.compile(r"(?:^|[ \t])(/[A-Za-z])", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_layout(tmp_path: Path, bundle: Path) -> tuple[Path, Path, str]:
|
||||||
|
"""Generate into the layout `okf project` writes, and return the pieces."""
|
||||||
|
root = tmp_path / "prosjekt"
|
||||||
|
identity = okf_consume.root_bundle_id_of(bundle)
|
||||||
|
inside = root / ".okf" / identity
|
||||||
|
inside.parent.mkdir(parents=True)
|
||||||
|
shutil.copytree(bundle, inside)
|
||||||
|
out = root / ".claude" / "skills" / f"{identity}-consume"
|
||||||
|
written = _generate(inside, out)
|
||||||
|
return root, written, identity
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_pattern_that_looks_for_absolute_paths_can_find_one(tmp_path: Path) -> None:
|
||||||
|
"""Face 4 first: a query is shown capable of finding before a zero is read.
|
||||||
|
|
||||||
|
O5's own published figure ("4 absolute paths -> 0") was measured with
|
||||||
|
`grep -c "^/"` against a file whose paths are indented by two spaces, so the
|
||||||
|
query could not have found one either way. This is that control.
|
||||||
|
"""
|
||||||
|
known_positive = "prose with no path\n /Users/x/bundle\nokf consume /Users/x/other\n"
|
||||||
|
assert len(ABSOLUTE.findall(known_positive)) == 2
|
||||||
|
assert len(re.findall(r"^/", known_positive, re.MULTILINE)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
||||||
|
def test_a_project_skill_names_its_bundle_and_itself_relative_to_the_project_root(
|
||||||
|
bundle: Path, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""The commands have to be runnable where `claude` is started: the project root.
|
||||||
|
|
||||||
|
`okf project`'s own closing line tells the reader to start `claude` in the
|
||||||
|
project root, so the skill's commands are read from there. An absolute path
|
||||||
|
makes the skill unmovable, unshareable, and wrong for anyone whose clone
|
||||||
|
lives elsewhere.
|
||||||
|
"""
|
||||||
|
root, written, identity = _project_layout(tmp_path, bundle)
|
||||||
|
text = written.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert f".okf/{identity}" in text
|
||||||
|
assert f".claude/skills/{identity}-consume/SKILL.md" in text
|
||||||
|
assert str(root) not in text
|
||||||
|
# Every absolute path left is a scratch write target, not a path into the
|
||||||
|
# machine the skill was generated on. `/tmp/payload.json` is where the
|
||||||
|
# pre-pass puts its payload; naming it relative would litter the project.
|
||||||
|
leftover = [line for line in text.splitlines() if ABSOLUTE.search(line) and "/tmp/" not in line]
|
||||||
|
assert leftover == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
||||||
|
def test_the_generated_skill_declares_the_hypothesis_mode(bundle: Path, tmp_path: Path) -> None:
|
||||||
|
"""A hypothesis is answered per premise, not as one verdict over the whole.
|
||||||
|
|
||||||
|
The three verdicts are literals, like the five markings: a reader that
|
||||||
|
invents a fourth ("partly confirmed") has left the contract.
|
||||||
|
"""
|
||||||
|
_, written, _ = _project_layout(tmp_path, bundle)
|
||||||
|
text = written.read_text(encoding="utf-8")
|
||||||
|
assert "## Modes" in text
|
||||||
|
for literal in ("`confirmed`", "`refuted`", "`undecidable-from-bundle`"):
|
||||||
|
assert literal in text
|
||||||
|
assert "per premise" in text.lower()
|
||||||
|
assert "[sourced-not-sufficient]" in text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
||||||
|
def test_the_generated_skill_declares_the_document_task_mode(bundle: Path, tmp_path: Path) -> None:
|
||||||
|
"""A task whose answer is a document carries the cut INTO the document.
|
||||||
|
|
||||||
|
Dropping an ungrounded paragraph silently is the denominator failure with
|
||||||
|
a nicer surface: the reader cannot see what the bundle did not cover.
|
||||||
|
"""
|
||||||
|
_, written, _ = _project_layout(tmp_path, bundle)
|
||||||
|
text = written.read_text(encoding="utf-8")
|
||||||
|
assert "## Modes" in text
|
||||||
|
assert "Task" in text
|
||||||
|
for token in ("considered", "withheld", "delivered"):
|
||||||
|
assert token in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_five_markings_are_untouched_by_the_modes(tmp_path: Path) -> None:
|
||||||
|
"""The modes add no sixth marking. § 4.3 makes an undeclared extension the defect."""
|
||||||
|
_, written, _ = _project_layout(tmp_path, GOLDEN)
|
||||||
|
text = written.read_text(encoding="utf-8")
|
||||||
|
for literal in okf_contract_check.REQUIRED_MARKINGS:
|
||||||
|
assert literal in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_project_skill_still_passes_the_contract_checker(tmp_path: Path) -> None:
|
||||||
|
"""Relative paths and a new section must not cost conformance."""
|
||||||
|
_, written, _ = _project_layout(tmp_path, GOLDEN)
|
||||||
|
payload = okf_consume.build_payload(GOLDEN, question="hva er kravet til pris?")
|
||||||
|
report = okf_contract_check.check(written.read_text(encoding="utf-8"), payload)
|
||||||
|
assert report.findings == ()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue