Skip to content

feat(cli): add openkos init — create an OKF workspace#3

Merged
jasonssdev merged 26 commits into
mainfrom
feat/init-command
Jul 17, 2026
Merged

feat(cli): add openkos init — create an OKF workspace#3
jasonssdev merged 26 commits into
mainfrom
feat/init-command

Conversation

@jasonssdev

Copy link
Copy Markdown
Owner

What this ships

openkos init — the first vertical slice. It creates a fresh OKF workspace in the current directory (raw/, bundle/index.md, bundle/log.md, openkos.yaml, AGENTS.md), or refuses without writing anything if the directory already looks like a workspace. Every other MVP 1 command depends on a workspace only init can produce.

Design in one line

Check-everything-then-write over four modules: cli/main.py sequences and maps exit codes, config.py and bundle/ own the writes, model/okf.py owns conformance. Phase A is a pure read (refusal is decided before any byte is written); Phase B writes, with the openkos.yaml marker written last so a crash never leaves a directory falsely claiming workspace status.

openkos.yaml is a byte-identical template copy — no name

The generated config carries no name field. The directory is the single source of truth for the workspace's identity; a name copy could only drift out of sync, and nothing in the codebase read it. Removing it means write_config is a plain byte-for-byte template copy, exactly like write_agents.

This closed a real, reproduced data-corruption bug: while name was interpolated, a directory name past ~80 chars containing a run of consecutive spaces was folded by the YAML emitter and the space run silently collapsed on round-trip. Removing the field removed the escaping step, which removed the runtime ruamel.yaml dependency (now dev-only), which removed the whole bug class. A regression test pins byte-identity against that exact pathological name.

The door stays open: a future human-authored display name (e.g. "My thesis" vs thesis-mcd) would be a user-authored field — never derived, never written by init — and adding it then would not reverse this.

Model default unified to qwen3:8b

The model: default was written four inconsistent ways across template, docs, and examples. docs/tech_stack.md owns this decision — it names the Qwen3 family at 7–8B and defers the exact default to the model spike. All four places now agree on qwen3:8b, the value that doesn't pre-empt the spike.

Verification

  • 51 tests, 100% branch coverage (gate 90%), on Python 3.13 and 3.14.
  • ruff check, ruff format --check, mypy clean.
  • uv build + two wheel smoke tests green; the isolated init smoke test diffs the generated openkos.yaml against the packaged template and gets byte-identical.
  • ruamel-yaml is absent from the built wheel's Requires-Dist.

Review

Reviewed under a bounded 4R pass (risk / resilience / readability / reliability) over the final tree: 0 blocking findings. One non-blocking follow-up remains — test_fresh_empty_directory asserts the success message names openkos.yaml but not all five artifacts; a strictly-stronger, test-only assertion, tracked for a follow-up.

SDD trail

Full proposal, spec, design, tasks, and verify report live under openspec/changes/add-init-command/. The change was archived, then reopened when review found the name field was write-only; the reopening history is preserved in those artifacts rather than rewritten.

…derers

Implements the OKF format seam (frontmatter framing, reserved filenames,
§9 rules 1-2 conformance check) and the three bundle modules that render
and write a fresh bundle's index.md and log.md, per D1-D3. Moves
python-frontmatter from dev to runtime dependencies since okf.py now
imports it. No CLI caller yet — init wiring lands in PR 4.
Add config.py with WorkspaceLayout and is_workspace() covering the four
refusal conditions (openkos.yaml, AGENTS.md, non-empty raw/ or bundle/),
plus write_config()/write_agents() generating openkos.yaml and AGENTS.md
from packaged templates via importlib.resources, exclusive-create mode.

Neither is wired into the init command yet (PR 4).
write_config used root.name directly, which is empty for an unresolved
relative path such as Path(".") -- Path(".").name == "". That silently
wrote a nameless workspace instead of failing loudly, and Path(".") is
exactly what init's cwd-based wiring (PR 4) will naturally pass. Resolve
root first so the function is correct for any caller, not dependent on
the caller passing an absolute path.
…ation

Wires config.is_workspace, bundle.create, write_agents, and write_config
together behind `openkos init`: a pure pre-flight check runs before any
write (D1), and openkos.yaml is written last so a crashed init never
falsely claims workspace status (D3).
The unit suite runs init against the editable install, so it would stay
green even if templates/ silently dropped from a future packaging
change. This step runs openkos init against the isolated built wheel in
a scratch directory and asserts all five artifacts land, turning a
lucky fact into an enforced one.
init generates a fixed qwen3.5:9b default rather than an interactive
model picker, and does not pre-create concept folders inside bundle/ --
align docs/cli.md with what init actually does. Also correct the stale
qwen3:8b example in the generated openkos.yaml block and the Ollama
pull prerequisite, and add openkos init to AGENTS.md's MVP 1 vertical
slice ahead of ingest.
datetime.now(UTC).date() filed the initialization entry under the wrong
calendar day for any user west of UTC in the evening, or east of UTC
before local midnight rolls into UTC's next day. log.md is a person's
chronological history, not a machine timestamp, so it belongs in their
own local time -- the same reasoning git applies to commit dates.

DTZ only requires timezone-awareness, not UTC specifically; both
datetime.now(UTC).date() and datetime.now().astimezone().date() satisfy
it, so the rule never actually decided this. Corrected design.md's
rationale to state what was actually decided and why.

Regression test forces two extreme real UTC offsets (Etc/GMT+12 and
Pacific/Kiritimati) via TZ + time.tzset() rather than a single fixed
zone, since ubuntu-latest CI runs in UTC where local == UTC and a
single-zone test could pass alongside this exact bug. Together the two
offsets' "local date differs from UTC" windows cover all 24 hours of a
UTC day, so at least one always disagrees with a UTC-based date.

Sharpened the Bundle Log Shape requirement in spec.md to say whose
"today" -- the machine's local date, not UTC's -- with a dedicated
scenario for when the two differ.
The old docstring claimed the test proves a fresh bundle passes OKF §9
conformance. It cannot fail on any fresh bundle: index.md and log.md
are both reserved, so check_conformance's loop never executes and it
returns [] by construction regardless of what init wrote -- proven by
writing garbage into both reserved files and observing check_conformance
still returns []. Kept the test (a real regression guard for a future
non-reserved .md write), corrected the docstring to say so.
The OKF Conformance requirement claimed rule 3 "passes actively" via the
mechanical check. It does not: model/okf.py implements rules 1-2 only and
defers rule 3 to lint, as its own docstring states. check_conformance skips
reserved filenames, and a fresh bundle holds only index.md and log.md, so
the loop body never runs and it returns [] regardless of file contents.

The behaviour was always correct -- init writes well-formed reserved files,
enforced by the Bundle Index Shape and Bundle Log Shape scenarios. Only the
claim was wrong. Archive merges this delta into openspec/specs/ as the
living contract, so the claim is corrected before it becomes permanent.
`openkos init` was silent on every path: no stderr on refusal, no stdout
on success, and an uncaught FileExistsError if `raw` or `bundle` existed
as a plain file instead of a directory. Add a fifth pre-flight condition
(`config.refusal_reason`, replacing `is_workspace` at the call site) that
names which condition blocked init, print it to stderr on refusal and a
created-artifacts summary to stdout on success, and catch OSError around
Phase B so permission/disk/collision failures exit cleanly instead of
raising a raw traceback.
A 4R review found init silent on both refusal and success, while
design.md's sequence diagram mandates stderr: reason / stdout: created.
The requirement was lost because it lived only in the design: the spec
never required it, so tasks never tasked it, so no test demanded it, so
it was never built. User-visible behaviour belongs in the spec.

Also adds a fifth pre-flight refusal condition -- raw or bundle existing
as a non-directory -- which previously slipped past is_workspace and
surfaced as an uncaught FileExistsError traceback, and requires write
failures to exit non-zero with a clear message rather than a traceback.
config.refusal_reason reimplemented is_workspace's four conditions
instead of calling it, so its "wraps is_workspace" docstring was false
and the two could silently drift apart. Both now read one private
generator (_refusal_conditions) that is the single source of truth for
every reason init may refuse to write.

Also strengthens two tests that only asserted a directory name
appeared in stderr, which passed even if the two refusal messages for
that directory were swapped, and a write-failure test that only
asserted stderr was non-empty, which passed for a bare exception repr.
Both now assert the distinguishing message content.
Merges the workspace-init delta spec into openspec/specs/ as the living
contract (11 requirements, 19 scenarios, byte-identical to the delta --
openspec/specs/ was empty, so this is a pure additive create) and moves
the change folder under openspec/changes/archive/.

No ADRs to accept: the change deliberately created none. Both candidates
failed the hard-to-reverse half of the gate -- the console entry point is
one commit to revert with no wheel published and no publish step in CI,
and removing model selection from init is a deferral, not a decision.

Deferred and not delivered: the interactive model picker docs/cli.md:48
still promises (add-model-selection), git init (add-workspace-git), the
repo-wide model-tag refresh (refresh-model-guidance), and OKF §9 rule 3's
mechanical check (deferred to lint).
`write_config` substituted the workspace directory's basename into the
openkos.yaml template with plain `str.format`. A basename carrying
YAML-significant characters (`:`, quotes, a leading `#`) produced a
malformed or silently altered document — a leading `#` commented the
whole line out. `_yaml_scalar` now delegates the escaping decision to
ruamel.yaml's safe dumper and extracts only the scalar, so the template
stays hand-written and keeps its comments (D5).

This moves ruamel.yaml to a runtime dependency, narrowly: it escapes one
value, it does not emit the document.

The four exclusive-create writes also opened in default text mode, so on
any platform where os.linesep is not LF the template's bytes would be
translated to CRLF, breaking write_agents' documented byte-identical
contract. All four now pass newline="". CI is ubuntu-only and cannot
observe this; the added tests are regression guards, and say so.
The docstring claimed that without the fifth pre-flight condition
`Path.mkdir` would raise an uncaught FileExistsError. It would not:
Phase B is wrapped in `try/except OSError`, which catches it. The
condition's real value is a precise refusal before any write, instead
of a generic caught error partway through Phase B.
Three claims in the archived artifacts were false:

- D2 said exclusive-create "closes the Phase-A->B TOCTOU window". It
  closes it for the four file writes only. The two
  mkdir(parents=True, exist_ok=True) calls that gate the non-empty
  raw/bundle refusal remain racy; that limitation is now named.
- The archive report's traceability table cited thirteen test names
  that do not exist. The verify report's table was already correct.
  Scenarios with no dedicated test are now marked as such rather than
  given an invented one.
- The design's module map described `write_config(root, name, model)`;
  the shipped signature takes root alone.

D5's runtime-dependency consequence is also corrected: ruamel.yaml does
move to runtime, to escape the interpolated scalar.
README described an `init` that creates concept folders, helps you pick
a local model, and initializes a git repository. It does none of those.
Worse, the merged spec explicitly forbids the first: concept-type
folders MUST NOT be pre-created inside bundle/. The README promised the
user exactly what the contract prohibits.

AGENTS.md still called the console entry point a pre-MVP stub that
"moves to openkos.cli.main:app when the cli package lands" — a
migration this same change already performed.

bundle.create()'s docstring claimed exclusive-create closes the
Phase-A -> B TOCTOU window. It closes it for the two leaf files the
function writes, not for the mkdir above it. Scoped to match D2.
… near them

The newline="" regression tests asserted no CR bytes in the output —
true on POSIX whether or not the fix exists, so removing newline=""
would have kept the suite green. They now spy on Path.open and assert
the argument is really passed, which fails on Linux if it is dropped.

_yaml_scalar's docstring promised defence against colons, quotes, a
leading hash and control characters; the tests covered three of those
four. The parametrization now also carries an embedded newline, a tab,
unicode, a 100-character name, the empty string and a form feed. All
round-trip exactly — the escaping was already sound, and is now proved
rather than argued.
refusal_reason() ran outside init's try/except, which guarded only the
Phase B writes. An unreadable pre-existing raw/ or bundle/ made
_non_empty_dir's iterdir() raise PermissionError during pre-flight, so
the command died with a raw traceback -- the one filesystem failure it
did not report cleanly.

Phase A now has its own guard. The message says "failed while checking
the workspace": not "refusing", because no workspace was found, and not
"failed while creating", because nothing was created.
The change was archived on this branch and the branch never merged, so
the archive claimed a completion that was not real. A review then found
that `name` in openkos.yaml is written by init and read by nobody, and
that generating it is what dragged ruamel.yaml into runtime and, with
it, a silent data-corruption bug on directory names holding a run of
spaces past ruamel's fold column.

Removing `name` inverts the spec's Generated Workspace Config
requirement rather than trimming it, so the change goes back in flight:
spec delta, design D5 and tasks are reopened, and the archive report is
dropped because it asserted a completion that no longer holds. The
promoted spec goes with it -- this change created it, and re-archiving
regenerates it from the delta.

Pure move plus deletions; no artifact content is edited here.
The spec's Generated Workspace Config requirement inverts rather than
trims: with `name` gone, `openkos.yaml` is a byte-identical copy of the
packaged template, like AGENTS.md already is. Renamed to Static
openkos.yaml Template; its scenario now asserts byte-identity and that
no directory-derived field survives, at any directory name or length.

Design D5 returns to its original position -- ruamel.yaml does not move
to runtime -- with a round-trip note recording why the intermediate
correction happened and why undoing it is not a flip-flop. Proposal Q7.9
is annotated as superseded: the example file it cited as evidence had
never been examined, and a normative example silently became a
requirement. Model pinned qwen3:8b across the board.
Nothing in the codebase read openkos.yaml's name; init wrote it and no
one consumed it. Generating that one field is what forced a YAML
escaping step, which pulled ruamel.yaml into runtime, which carried a
silent data-corruption bug: a directory name past ruamel's fold column
with a run of two or more spaces round-tripped with the run collapsed.

Removing name removes the whole chain. write_config now reads the
template and writes it byte-for-byte under exclusive-create, the same
shape as write_agents -- no substitution, no dumper, no escaping.
_yaml_scalar and the ruamel import are gone, its tests replaced by one
byte-identity test, and ruamel-yaml returns to a dev-only dependency.
The model default was written four different ways across the docs;
docs/cli.md alone named qwen3.5:9b in three places while examples used
qwen3:8b. tech_stack.md owns this decision and names the Qwen3 family at
7-8B, deferring the exact default to the model spike -- so all of it
unifies on qwen3:8b, the value that does not argue the criterion.

The openkos.yaml example blocks in docs/cli.md and examples/ still
showed a name field the command no longer writes. An example is
normative by inertia (this change's own Q7.9 is the cautionary tale), so
those name lines are removed. user-journey.md's "helps you pick a local
model" claim is dropped too -- init picks nothing; selection is deferred.
…name

The spec scenario "no directory-derived field, regardless of directory
name" had no test. It is the scenario the reopening exists for, so it
should not rest on "by construction" alone -- that is the same footing
the original bug stood on when it survived full branch coverage.

The name is the exact shape that once corrupted the file: 40 chars, a
double space, 40 more chars, which folded past ruamel's column and lost
the space run on round-trip. Verified RED by reintroducing root.name
interpolation into write_config (fails b'a' != b'm'), GREEN on the
shipped code.
The verify report flagged the "no directory-derived field" scenario as
having no dedicated test. It now does, so the report is updated to cite
it in the traceability table and mark the warning resolved rather than
leaving a stale "recommend a follow-up".
@jasonssdev
jasonssdev merged commit d1b52ea into main Jul 17, 2026
5 checks passed
jasonssdev added a commit that referenced this pull request Jul 17, 2026
test_fresh_empty_directory checked only that "openkos.yaml" appeared in
stdout. Because it is the trailing token in the message, that assertion
could not catch a regression dropping or garbling any of the other four
names. It now requires raw/, index.md, log.md, AGENTS.md, and
openkos.yaml each to appear -- RED-confirmed by dropping log.md from the
success message.

Follow-up to PR #3 (WARNING-1 from the bounded review): a strictly
stronger, test-only assertion, deferred there to keep the tree matching
the approved receipt.
jasonssdev added a commit that referenced this pull request Jul 17, 2026
Merge the workspace-init delta spec into openspec/specs/ as the canonical
spec, move the change folder to openspec/changes/archive/, and record the
archive report. Closes out the change reopened for the D5 name-field
revert and reshipped via PRs #3/#4. Doc-only move of prior-reviewed
artifacts; no executable code.
jasonssdev added a commit that referenced this pull request Jul 19, 2026
Close the SDD cycle for add-query-answer (MVP-1 query chain #3, merged
in PR #27). Merge the query-answer delta into a new living capability
spec at openspec/specs/query-answer/spec.md, move the change folder to
openspec/changes/archive/2026-07-18-add-query-answer/, and add the
verify report and archive report.

Verify: PASS (7/7 requirements, 9/9 scenarios). Bounded review
review-bd53402eecbf6307 (HIGH, full 4R): approved, 0 blockers.
@jasonssdev
jasonssdev deleted the feat/init-command branch July 19, 2026 22:36
jasonssdev added a commit that referenced this pull request Jul 21, 2026
…-side + follow-ups (closes slice) (#103)

* fix: catch sqlite3.Error from the graph reindex write path

sqlite_graph.reindex_graph() sat inside reindex()'s try block after
vectors.db/fts.db committed, but the exception ladder (VecUnavailable,
FtsUnavailable, OllamaError) never covered a bare sqlite3.Error from the
graph write (permission/IO/corrupt graph.db) -- a graph-write failure
after vectors/fts already succeeded crashed with a raw traceback instead
of the documented clean exit 1, with no indication which store failed.

Wraps the graph write in its own try/except sqlite3.Error, separate from
the vectors/FTS ladder, printing a message that identifies the graph
index specifically. Deliberately narrow: does not touch the still-open,
separately-tracked follow-up of generic lock-contention errors from the
vectors/FTS ladder.

Carry-over fix from PR2 review (Engram bug #1470).

* feat: add FtsSearchHandle protocol and open-time validation to fts.py

FtsSearchHandle is the structural seam retrieval/answer.py injects as
fts_index -- any object with a search(query, limit) method and a skipped
list satisfies it, mirroring VectorStore/GraphStore's own Protocol-seam
pattern. The real FtsIndex (in-memory or on-disk) already satisfies this
shape without change.

open_fts_index_readonly now runs one cheap validating read (SELECT 1
FROM docs LIMIT 1) immediately after connecting, so an EXISTING file that
is not a valid SQLite database (or lacks the docs table) raises a
sqlite3.Error at open time rather than only failing later on the first
real search() call -- giving the CLI's open-or-degrade layer a single,
well-defined call site to catch corruption.

* feat: add open-time validation to open_graph_store_readonly

Mirrors state/fts.py::open_fts_index_readonly's identical fix: runs one
cheap validating read (SELECT 1 FROM nodes LIMIT 1) immediately after
connecting, so an EXISTING graph.db that is not a valid SQLite database
raises a sqlite3.Error at open time instead of only failing later on the
first real query call.

* refactor: dedupe graph_retrieve's pool floor via retrieval/pool.py

graph_rank's own inline pool_limit = max(limit, 10) is replaced with
pool.pool_limit(limit) -- follow-up #2, the last residual duplication of
the max(limit, 10) floor now that retrieval/pool.py exists.

* feat: rewire answer() to inject read-only fts_index/graph_index handles

answer() no longer builds its own FTS index or graph projection
internally -- it accepts injected, already-open, read-only fts_index/
graph_index handles (mirroring vector_store's existing contract exactly)
and degrades to empty lists when either is None. answer() never computes
or compares a bundle manifest hash (D2 binding contract): staleness
detection is reindex's exclusive job, and a properly-reindexed handle is
always treated as fresh at query time.

The exception-vs-degrade boundary lives entirely at the caller's
store-open call site: fts_index.search() is never wrapped in a
try/except here, so a typed exception raised outside the open path still
propagates unswallowed. graph_rank raising any exception still degrades
gracefully, same as before.

Also wires in retrieval.pool.pool_limit (replacing the inline
max(limit, 10)) and drops the now-unused sqlite_graph/fts.build_index
imports.

Migrated every existing test in test_answer.py from monkeypatching
fts.build_index/sqlite_graph.build_graph to direct fts_index=/
graph_index= injection, and added new coverage for: absent fts_index/
graph_index degrading cleanly, a typed exception from fts_index.search()
propagating unswallowed, and a single consolidated empty-question spy
test proving all four injected handles (fts_index, embedder,
vector_store, graph_index) record zero calls (follow-up #1).

* feat: wire query to inject read-only fts_index/graph_index

Adds _open_fts_or_degrade/_open_graph_or_degrade (mirroring
_open_vector_store_or_degrade exactly) and wires both into query(),
alongside the existing vector_store injection. The reindex hint now
fires on ANY of dense/FTS/graph being absent or unavailable/corrupt, not
just dense. Updates query()'s docstring to describe all three derived
stores as persisted, reindex-written, read-only indexes instead of
claiming FTS/graph carry no persisted state. Also surfaces
ReindexReport.prune_skipped as a distinct summary line in the reindex
command.

* feat: add ReindexReport.prune_skipped for prune-pass observability

Distinguishes 'prune ran and genuinely found nothing to prune'
(pruned == 0, prune_skipped == False) from 'prune was suppressed by a
walk error' (pruned == 0, prune_skipped == True) -- previously
indistinguishable from pruned alone. Surfaced as a distinct stdout line
in the reindex CLI summary (follow-up #3).

* docs(sdd): mark performance-caching Phase 3 tasks complete

Also documents the carry-over fix from PR2 review and flags a
discrepancy: follow-up #4 (WAL/busy_timeout + single-commit) was
reported as already done in PR1, but code inspection shows
vectorstore.py/vectors.db never received it -- left undone per explicit
'do NOT redo' instruction this batch, flagged for a follow-up slice.

* feat: add WAL/busy_timeout PRAGMAs and batch writes to vectors.db

open_vector_store now sets PRAGMA journal_mode=WAL and a busy_timeout of
5000ms (matching state/derived.py's fts.db/graph.db posture), completing
follow-up #4 for the pre-existing (Slice 2b) vectors.db store that PR1/PR2
never touched.

Adds upsert_many/prune_many/commit to the VectorStore Protocol and
VectorStoreDB (additive, mirrors the Slice 2b Protocol-growth precedent):
upsert_many/prune_many write many rows in one call without committing,
so a caller can batch an entire run's writes into ONE commit via
commit(). The existing single-item upsert()/prune() keep their own
per-call-commit contract unchanged for any other caller.

Empirically verified against the real sqlite-vec 0.1.9 extension that
both WAL mode and multi-item uncommitted batches (including a re-upsert
of the same concept_id) work correctly before this change was made.

* feat: batch reindex()'s vector writes into one commit per run

reindex() now calls db.upsert_many/db.prune_many (neither commits) for
the embed/prune batches, then db.commit() ONCE at the end of the run --
covering both batches together -- instead of once per document via the
old per-item upsert()/prune() calls. A run with nothing to embed and
nothing to prune commits zero times. Completes follow-up #4's
single-commit-per-run contract for vectors.db, matching fts.db/graph.db.

* docs: describe FTS/graph as persisted, reindex-written derived stores

docs/cli.md's query/reindex sections no longer describe FTS/graph as
built fresh in-process per call with no persisted state -- they now
describe all three on-disk derived stores (vectors.db, fts.db,
graph.db) as reindex-written, read-only at query time, gated by the
bundle-manifest-hash cache key, atomically rebuilt, and batched into
one commit per store per run.

docs/architecture.md's workspace-structure diagram gains a footnote
reconciling the aspirational consolidated-openkos.db tree with the
actual shipped three-file layout.

docs/adr/0002's stale 'graph is ephemeral (rebuilt per run)' line gets
an update note rather than a rewrite -- ADRs are historical decision
records; the graph remains a rebuildable derived cache regardless, so
the merge/unmerge reversibility reasoning is unaffected.

* docs(sdd): mark performance-caching Phase 3 (29/29) and Phase 4 complete

Follow-up #4 (WAL/busy_timeout + single-commit for vectors.db) is now
genuinely done, closing the discrepancy flagged in the prior commit.
Phase 4's full-gate re-verification and doc updates are also complete --
the performance-caching slice's tasks.md is now 60/60.

* fix: surface reindex summary before the graph-write failure path

The vectors.db/fts.db summary (embedded/cache-hit/pruned/skipped counts
and the prune_skipped follow-up notice) now prints BEFORE the graph
write attempt, not after it. Previously, a sqlite3.Error from
sqlite_graph.reindex_graph() raised typer.Exit(1) before the summary
block ran, so a graph-write failure silently swallowed the summary and
prune_skipped notice even though vectors.db/fts.db had already
committed durably. The clean exit-1 for the graph error is unchanged.

Also corrects the _open_fts_or_degrade/_open_graph_or_degrade
docstrings, which claimed to mirror _open_vector_store_or_degrade
'exactly' -- they are related in intent and return shape, but
structurally different: no explicit path.exists() guard (the callees
are already existence-gated internally) and a narrower except clause
(sqlite3.Error only, since neither FTS nor the graph store has a typed
'unavailable' exception like VecUnavailable). No behavior change.

Review findings R4 (WARNING) and R2 (SUGGESTION).

* test: restore raw graph_rank spy assertion in determinism test

test_graph_retrieval_is_deterministic_across_repeated_calls was
rewritten during the DI migration to assert only the final,
limit-truncated citation order across two calls -- dropping the
stronger spy assertion on graph_retrieve.graph_rank's raw,
pre-truncation output that the pre-migration version had. Restores it:
spies on graph_rank and asserts its raw output is byte-for-byte
identical across both calls, in addition to the citation-order check,
re-establishing score-level determinism coverage a low final limit
could otherwise mask.

Review finding R3 (WARNING).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant