P13 measured this lift and REFUSED it, because okf >=0.8.5 emits the ownership
stamp as the V1 flow mapping `generated: { by: process:okf-ingest, at: ... }`
where 0.3.2 emitted `true`, and `_carries_complete_ingest_stamp` read the new
form as NOT a stamp -- write_concept_file's forgery refusal would have shipped
DISARMED with the whole fail-closed suite green. That blocker is closed first,
red-first, and then the pin moves.
ROW 1, THE SECURITY HALF. `_claims_ingest_ownership` widens the predicate from
"reads as boolean True" to "claims ingest ownership", of which the boolean is
the pre-V1 spelling. The recogniser for the new half is `decode_flow_value` --
the module's ONE flow decoder, the same argument write_concept_file already
makes for `verified`: the writer refuses exactly what the reader can read. A
value the decoder REFUSES is therefore not an ownership claim and writes
through, which is what keeps this from collapsing into "any non-empty
generated". Two arms red before the fix; no YAML library introduced.
THE PIN. okf v0.3.2 -> v0.8.5, guard v0.3.4 -> v1.4.0 spelled `tag =`, not
`rev =`, and not the declared floor 1.2.0 -- both P13 premises hold and the
reason now lives next to the pin in pyproject.toml. The ":40" comment is
corrected: okf has ONE runtime dependency, the guard, and that is what binds
the two lines together. 27/27 imported names resolve across five modules.
THE GOLDENS, REGENERATED AS A DECISION. Seven concept files across four
examples/ingest-golden-* bundles, one line each. Two were regenerated by the
REAL materializer; the other five are derived (http/sql/mcp cannot materialize
outside the tests' stubs) and then MEASURED -- all four golden suites compare
byte for byte against what the stubs produce, and all four are green. The four
`generated == "true"` asserts now read ONE source, conftest.
expected_generated_stamp: four literals for one emitter fact are four places a
later release can leave half-corrected, which is exactly how the pre-V1 form
survived until P13 measured it. tests/test_okf.py keeps its literal on purpose
-- that one round-trips a CURATED half-stamp through our own writer.
THE BLOCK READER. Measured with the full denominator: all four delivered
knowledge bases write `sources` as a BLOCK sequence and none in flow form
(n100 446/446, n200 1133/1133, n500 270/270, r761 2756/2756 = 4605/4605), and
`evidence_for` reported `unreadable` on 4605 of 4605 -- the falsification layer
had no address for any document in any base. `okf.decode_block_mappings` is the
second CARRIER of one grammar, never a second grammar: colon-SPACE separator,
unquote_scalar, duplicate keys refused, SPEC 5.2's actor rule applied. okf's
consume.read_sources was READ for the form and not called; po calls no okf
reader, which is measured and deliberate. After: 4605 present / 4605 entries.
Reading is not a licence to WRITE -- the emitter is untouched and both writers
still refuse what decode_flow_value refuses.
THREE FINDINGS. (1) The first block reader INVENTED data on `- { k: v }` items
-- SPEC-canonical, and the shape tests/golden/block-form-provenance writes for
`verified` -- decoding it as `{'{ id': '...'}`. No arm caught it: the 5.2 actor
rule shielded the fixture by accident. Closed with a flow-decoder branch and
four new arms. (2) One of my own arms was VACUOUS, found by my own mutation M5:
it claimed to prove the colon-SPACE rule and stayed green under first-colon,
because the two rules agree on every delivered value. Renamed, labelled, and
the claim moved to the arm that actually witnesses it. (3) OPEN, and it needs
the operator: the commons-owned worked example declares its second concept
`unreadable`/`block-sequence`, which is now false for po. `shared/` is
pull-only, so closing it needs a commons amendment; the test asserts the
divergence instead of skipping it, keeping the discriminating half (the example
says two entries were seen and the reader returns exactly two).
NINE EXISTING ARMS REWRITTEN, NONE WEAKENED. All nine pinned "the block form is
unreadable" -- the behaviour this order changes. Each keeps its claim on a
specimen that is still unreadable for a reason of its own (5.2: an entry naming
no actor), or pins the REVERSED direction where the old arm stood so the change
cannot be silent. Two got STRONGER: multi-verified.md was authored for "a reader
keeping the last entry reports machine-confirmed for a concept a human signed",
and that could not be tested while the form was unreadable. Three node ids were
renamed; nothing was removed in substance.
Suite 1582 -> 1606 passed / 5 skipped. Both demo goldens byte-unchanged
(ea8c534... / ede3e2f..., shasum -a 1 of the CONTENT, never the git blob id).
ruff check / ruff format / mypy green. shared/ untouched.
Six mutations, all red against the WHOLE suite, each with its own signature:
row 1 detached (2) / block reader detached (17) / flow-item branch detached (7)
/ a stray indented line folds into an INVENTED entry (4) / separator becomes the
first colon (1 -- and that is finding 2) / the stamp expectation reverts to
"true" (4).
Order: 20260912T195112Z-995611104-from-.claude
Record: docs/2026-09-12-p13b-okf-bump.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
7.2 KiB
Python
200 lines
7.2 KiB
Python
"""I4 Step 2/3 — the ``sql`` connector (``read_sql``) + ``materialize`` dispatch.
|
|
|
|
Mirrors tests/test_ingest_materialize.py for the second source type. Covers the §4 connector
|
|
rules for ``sql`` (env-resolved ``connection_ref`` → sqlite path, read-only enforcement,
|
|
single-statement, streaming ``max_rows`` cap §8) and the §5 TYPED cell rendering the ``file``
|
|
path never exercised (all-strings): INTEGER→plain decimal, REAL→shortest round-trip, TEXT
|
|
verbatim, SQL NULL→empty string, BLOB/other→fail (never silent coercion). Offline: sqlite
|
|
fixtures under tmp_path, zero network, zero credentials, zero model calls.
|
|
|
|
Pinned SQL §5 decisions (spec-silent, delegated to I4 by the session plan; see brief):
|
|
``connection_ref`` names an env var whose value is a filesystem path to a sqlite database
|
|
opened read-only; ``float`` renders via ``repr`` (Python's shortest round-trip form).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from conftest import expected_generated_stamp
|
|
from portfolio_optimiser import okf
|
|
from portfolio_optimiser.ingest import (
|
|
IngestError,
|
|
_sql_value_to_text,
|
|
materialize,
|
|
read_sql,
|
|
)
|
|
|
|
_INGESTED_AT = "2026-07-03T12:00:00Z"
|
|
|
|
|
|
def _make_db(path: Path, script: str) -> Path:
|
|
"""Build a sqlite fixture (read-write) from a DDL/DML script, then close it."""
|
|
conn = sqlite3.connect(path)
|
|
try:
|
|
conn.executescript(script)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return path
|
|
|
|
|
|
def _sql_manifest(
|
|
tmp_path: Path, extractions: list[dict[str, Any]], *, connection_ref: str = "PROJ_DB"
|
|
) -> Path:
|
|
manifest = {
|
|
"manifest_version": 1,
|
|
"source": {"type": "sql", "id": "db", "connection_ref": connection_ref},
|
|
"bundle_summary": "SQL extracts from the project database.",
|
|
"extractions": extractions,
|
|
}
|
|
path = tmp_path / "manifest.json"
|
|
path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
# --- connector: env resolution + fail-fast (§4) --------------------------------------------------
|
|
|
|
|
|
def test_read_sql_unset_connection_ref_fails(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("PROJ_DB", raising=False)
|
|
with pytest.raises(IngestError):
|
|
read_sql("PROJ_DB", "SELECT 1", max_rows=10)
|
|
|
|
|
|
def test_read_sql_missing_db_file_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("PROJ_DB", str(tmp_path / "does-not-exist.sqlite"))
|
|
with pytest.raises(IngestError):
|
|
read_sql("PROJ_DB", "SELECT 1", max_rows=10)
|
|
|
|
|
|
# --- connector: §5 typed cell rendering ----------------------------------------------------------
|
|
|
|
|
|
def test_read_sql_renders_each_type_per_section5(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""INTEGER→plain decimal, REAL→shortest round-trip (``repr``, integral 4200.0 preserved),
|
|
TEXT verbatim, SQL NULL→empty string — all in one ordered extract."""
|
|
db = _make_db(
|
|
tmp_path / "s.sqlite",
|
|
"""
|
|
CREATE TABLE t (i INTEGER, r REAL, s TEXT, n TEXT);
|
|
INSERT INTO t VALUES (7, 1.5, 'hei', NULL);
|
|
INSERT INTO t VALUES (3, 4200.0, 'verden', 'x');
|
|
""",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
header, rows = read_sql("PROJ_DB", "SELECT i, r, s, n FROM t ORDER BY i", max_rows=100)
|
|
assert header == ["i", "r", "s", "n"]
|
|
assert rows == [
|
|
["3", "4200.0", "verden", "x"],
|
|
["7", "1.5", "hei", ""],
|
|
]
|
|
|
|
|
|
def test_read_sql_null_is_empty_string(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
db = _make_db(
|
|
tmp_path / "n.sqlite",
|
|
"CREATE TABLE t (v TEXT); INSERT INTO t VALUES (NULL);",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
_, rows = read_sql("PROJ_DB", "SELECT v FROM t", max_rows=10)
|
|
assert rows == [[""]] # SQL NULL → empty string, never "None"
|
|
|
|
|
|
def test_read_sql_blob_value_fails_never_silent(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
db = _make_db(
|
|
tmp_path / "b.sqlite",
|
|
"CREATE TABLE t (b BLOB); INSERT INTO t VALUES (x'00ff');",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
with pytest.raises(IngestError): # §5: any other value type MUST fail
|
|
read_sql("PROJ_DB", "SELECT b FROM t", max_rows=10)
|
|
|
|
|
|
def test_sql_value_to_text_rejects_bool_defensively() -> None:
|
|
# sqlite3 never returns Python bool; the guard prevents str(True)->"True" silent coercion
|
|
# (bool is an int subclass, so it would otherwise slip through the integer branch).
|
|
with pytest.raises(IngestError):
|
|
_sql_value_to_text(True)
|
|
|
|
|
|
# --- connector: §8 cap + §4 read-only / single-statement -----------------------------------------
|
|
|
|
|
|
def test_read_sql_max_rows_exceeded_is_error(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
db = _make_db(
|
|
tmp_path / "m.sqlite",
|
|
"CREATE TABLE t (i INTEGER); INSERT INTO t VALUES (1),(2),(3);",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
with pytest.raises(IngestError): # §8: error, never silent truncation
|
|
read_sql("PROJ_DB", "SELECT i FROM t ORDER BY i", max_rows=2)
|
|
|
|
|
|
def test_read_sql_is_read_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
db = _make_db(
|
|
tmp_path / "ro.sqlite",
|
|
"CREATE TABLE t (i INTEGER); INSERT INTO t VALUES (1);",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
with pytest.raises(IngestError): # §4: read-only enforced at the DB (mode=ro)
|
|
read_sql("PROJ_DB", "INSERT INTO t VALUES (2)", max_rows=10)
|
|
_, rows = read_sql("PROJ_DB", "SELECT count(*) FROM t", max_rows=10)
|
|
assert rows == [["1"]] # the refused write never happened
|
|
|
|
|
|
def test_read_sql_rejects_multiple_statements(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
db = _make_db(
|
|
tmp_path / "ms.sqlite",
|
|
"CREATE TABLE t (i INTEGER); INSERT INTO t VALUES (1);",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
with pytest.raises(IngestError): # §4: one statement
|
|
read_sql("PROJ_DB", "SELECT i FROM t; SELECT i FROM t", max_rows=10)
|
|
|
|
|
|
# --- materialize dispatch: sql executes (was refused in I2) ---------------------------------------
|
|
|
|
|
|
def test_sql_manifest_materializes_with_provenance(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
db = _make_db(
|
|
tmp_path / "s.sqlite",
|
|
"CREATE TABLE costs (item TEXT, amount REAL); INSERT INTO costs VALUES ('led', 120.0);",
|
|
)
|
|
monkeypatch.setenv("PROJ_DB", str(db))
|
|
manifest = _sql_manifest(
|
|
tmp_path,
|
|
[
|
|
{
|
|
"id": "costs",
|
|
"title": "Project costs",
|
|
"query": "SELECT item, amount FROM costs ORDER BY item",
|
|
"okf_type": "dataset",
|
|
"max_rows": 100,
|
|
}
|
|
],
|
|
)
|
|
written = materialize(manifest, tmp_path / "bundle", ingested_at=_INGESTED_AT)
|
|
assert len(written) == 1
|
|
fm = okf.parse_frontmatter(written[0])
|
|
assert fm["source_system"] == "db"
|
|
assert fm["generated"] == expected_generated_stamp(_INGESTED_AT)
|
|
assert fm["source_query"] == "SELECT item, amount FROM costs ORDER BY item"
|
|
body = written[0].read_text(encoding="utf-8")
|
|
assert "| item | amount |" in body
|
|
assert "| led | 120.0 |" in body
|