generated from coulomb/repo-seed
IB-WP-0016-T06: OpenRouter live-run guardrails
Add --chapter / --from-chapter / --to-chapter / --chunk selection flags to generate init and generate from-source, plumb them into init_generation_infospace via a new _filter_chunks_by_chapter helper, and refuse to create an infospace when the filters reject every chunk (InfospaceError "empty_chapter_selection"). The flags use the same T03/T02 plumbing (chapter labels, roman numerals, chunk ids) so a single-chapter selection is a one-flag command. OpenRouter run-record metadata (model, request_id, usage tokens, retry_count, duration_seconds) already lands in output/workflows/runs/*.yaml; this task just adds the smoke test that proves it stays there, plus the parallel guarantee that the same provider metadata reaches generated artifact provenance via provenance.provider_metadata. tests/test_openrouter_live.py covers: - chapter-filter, from/to-chapter range, and empty-selection failure on init (non-live, deterministic) - CLI smoke through generate from-source with --chapter - a pytest-skipped live OpenRouter one-chapter end-to-end gated by OPENROUTER_API_KEY + INFOSPACE_BENCH_ENABLE_LIVE_OPENROUTER, with INFOSPACE_BENCH_LIVE_MODEL override (default openai/gpt-4o-mini) docs/generic-source-generator.md gains a "Live OpenRouter runs (handle with care)" section that walks plan-before-run, single-chapter live run, the budget/usage artifacts, and the checks a reviewer should run before scaling to the full book. 129 tests pass, 1 skipped (the live smoke, correctly gated). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,30 @@ def init_generation_infospace(
|
||||
name: str,
|
||||
profile: str = DEFAULT_PROFILE,
|
||||
max_chunks: int | None = None,
|
||||
chapter_filter: list[str] | None = None,
|
||||
chunk_filter: list[str] | None = None,
|
||||
from_chapter: int | None = None,
|
||||
to_chapter: int | None = None,
|
||||
) -> Any:
|
||||
chunks = normalize_source(source, max_chunks=max_chunks)
|
||||
chunks = _filter_chunks_by_chapter(
|
||||
chunks,
|
||||
chapter_filter=chapter_filter,
|
||||
chunk_filter=chunk_filter,
|
||||
from_chapter=from_chapter,
|
||||
to_chapter=to_chapter,
|
||||
)
|
||||
if not chunks:
|
||||
raise InfospaceError(
|
||||
"empty_chapter_selection",
|
||||
"Selection filters excluded every chunk; nothing to register",
|
||||
{
|
||||
"chapter_filter": chapter_filter,
|
||||
"chunk_filter": chunk_filter,
|
||||
"from_chapter": from_chapter,
|
||||
"to_chapter": to_chapter,
|
||||
},
|
||||
)
|
||||
infospace = create_infospace(Path(workspace), slug, name=name)
|
||||
_install_profile(infospace.root, profile)
|
||||
_write_workflows(infospace.root, profile)
|
||||
@@ -256,6 +278,42 @@ def plan_generation_summary(
|
||||
}
|
||||
|
||||
|
||||
def _filter_chunks_by_chapter(
|
||||
chunks: list[SourceChunk],
|
||||
*,
|
||||
chapter_filter: list[str] | None,
|
||||
chunk_filter: list[str] | None,
|
||||
from_chapter: int | None,
|
||||
to_chapter: int | None,
|
||||
) -> list[SourceChunk]:
|
||||
chunk_set = {value.strip() for value in (chunk_filter or []) if value.strip()}
|
||||
label_set = {value.strip().lower() for value in (chapter_filter or []) if value.strip()}
|
||||
if not chunk_set and not label_set and from_chapter is None and to_chapter is None:
|
||||
return list(chunks)
|
||||
out: list[SourceChunk] = []
|
||||
for chunk in chunks:
|
||||
if chunk_set and chunk.chunk_id not in chunk_set:
|
||||
continue
|
||||
if label_set:
|
||||
label = (chunk.chapter_label or "").strip().lower()
|
||||
number_match = (
|
||||
chunk.chapter_number is not None
|
||||
and str(chunk.chapter_number) in label_set
|
||||
)
|
||||
label_match = label in label_set if label else False
|
||||
if not (number_match or label_match):
|
||||
continue
|
||||
if from_chapter is not None or to_chapter is not None:
|
||||
if chunk.chapter_number is None:
|
||||
continue
|
||||
if from_chapter is not None and chunk.chapter_number < from_chapter:
|
||||
continue
|
||||
if to_chapter is not None and chunk.chapter_number > to_chapter:
|
||||
continue
|
||||
out.append(chunk)
|
||||
return out
|
||||
|
||||
|
||||
def _select_source_chunks(
|
||||
sources: list[Any],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user