2026-06-10-ingest-command.md
docs/superpowers/plans/2026-06-10-ingest-command.md
tesserae ingest Command Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a first-class tesserae ingest <file|url> command that merges a single document or URL into the knowledge base, correctly (byte-identical to a full compile) and — once proven — without re-extracting the whole corpus.
Architecture: Persist-then-ingest. A local path is used as-is; a URL is fetched, converted to markdown, and written into the tracked corpus (data/ingested/<slug>.md) with provenance front-matter, then the normal pipeline runs on it as a file. The command is registered as a new top-level route (no collision with the namespaced tesserae code ingest). Phase 1 ships a correct command that drives the existing full compile. Phase 2 adds the incremental fast path behind a per-invocation override and a golden-parity test that gates the default flip; the existing provenance-readiness + over-cap fallback in Project.compile is the correctness backstop.
Tech Stack: Python 3.11, argparse CLI, pydantic models, pytest (+ tmp_path, monkeypatch). New optional extra [ingest-url] = httpx + markdownify. Network is always mocked in tests.
Reference spec: docs/superpowers/specs/2026-06-10-ingest-command-design.md
File Structure
Create:
tesserae/ingest/__init__.py— package exports (ingest_sources,is_url,fetch_to_source).tesserae/ingest/fetch.py— URL detection + fetch-to-markdown-file with provenance front-matter. Behind[ingest-url].tesserae/ingest/orchestrator.py— resolve inputs → (URL? fetch) → drive compile → report. The B→C seam.tests/test_ingest_fetch.py— unit tests foris_url/fetch_to_source(HTTP mocked).tests/test_ingest_orchestrator.py— unit tests foringest_sources(file + URL + dry-run + exact).tests/test_ingest_cli.py— CLI arg-parsing + routing tests.tests/test_ingest_parity.py— golden parity:ingest(X)≡ fullcompile(corpus ∪ X).docs/commands/ingest.md— English command doc (+ 7-language i18n in the final task).
Modify:
pyproject.toml— addingest-urloptional-dependency.tesserae/cli.py— add_build_ingest_parser,_route_ingest,_handle_ingest_docs; register"ingest"in_NEW_DISPATCH.tesserae/project.py— addincremental_override: Optional[bool] = Noneseam toingest()/compile()(Phase 2 only).
Phase 1 — Correct, shippable command (full recompile under the hood)
Task 1: Scaffold the tesserae/ingest/ package and the [ingest-url] extra
Files:
- Create:
tesserae/ingest/__init__.py - Modify:
pyproject.toml(the[project.optional-dependencies]block) - [ ] Step 1: Create the package init
# tesserae/ingest/__init__.py
"""Single-source ingest: merge one document or URL into the KB.
See docs/superpowers/specs/2026-06-10-ingest-command-design.md.
"""
from tesserae.ingest.fetch import fetch_to_source, is_url
from tesserae.ingest.orchestrator import ingest_sources
__all__ = ["ingest_sources", "fetch_to_source", "is_url"]
NOTE: this imports
fetchandorchestratorwhich are created in Tasks 2–7. Until then the package import will fail — that is expected; do not run the package import until Task 7.
- [ ] Step 2: Add the optional extra to
pyproject.toml
Find the block (currently):
[project.optional-dependencies]
synthesis-llm = ["anthropic>=0.40"]
raganything = ["raganything>=1.3.0"]
raganything-all = ["raganything[all]>=1.3.0"]
raganything-semantic = ["raganything[all]>=1.3.0", "sentence-transformers>=3.0"]
semantic = ["model2vec>=0.3"]
Add one line after semantic:
ingest-url = ["httpx>=0.24", "markdownify>=0.11"]
- [ ] Step 3: Commit
git add tesserae/ingest/__init__.py pyproject.toml
git commit -m "feat(ingest): scaffold ingest package + [ingest-url] extra"
Task 2: is_url helper
Files:
- Create:
tesserae/ingest/fetch.py - Test:
tests/test_ingest_fetch.py - [ ] Step 1: Write the failing test
# tests/test_ingest_fetch.py
from tesserae.ingest.fetch import is_url
def test_is_url_true_for_http_and_https():
assert is_url("http://example.com/a")
assert is_url("https://arxiv.org/abs/2401.12345")
def test_is_url_false_for_paths():
assert not is_url("notes/a.md")
assert not is_url("/abs/path/to/file.md")
assert not is_url("./relative.md")
assert not is_url("ftp://example.com/x") # only http(s) is a URL for ingest
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_fetch.py -v Expected: FAIL with ModuleNotFoundError: No module named 'tesserae.ingest.fetch'
- [ ] Step 3: Write minimal implementation
# tesserae/ingest/fetch.py
"""Fetch a URL into a tracked markdown source. Behind the [ingest-url] extra."""
from __future__ import annotations
def is_url(value: str) -> bool:
"""True only for http(s) URLs — everything else is treated as a local path."""
return value.startswith("http://") or value.startswith("https://")
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_fetch.py -v Expected: PASS (2 tests)
- [ ] Step 5: Commit
git add tesserae/ingest/fetch.py tests/test_ingest_fetch.py
git commit -m "feat(ingest): add is_url helper"
Task 3: _slugify + provenance front-matter rendering
Files:
- Modify:
tesserae/ingest/fetch.py - Test:
tests/test_ingest_fetch.py - [ ] Step 1: Write the failing tests
# add to tests/test_ingest_fetch.py
from tesserae.ingest.fetch import _slugify, _render_frontmatter
def test_slugify_url_is_filesystem_safe_and_stable():
slug = _slugify("https://arxiv.org/abs/2401.12345")
assert slug == "arxiv-org-abs-2401-12345"
# stable + deterministic
assert _slugify("https://arxiv.org/abs/2401.12345") == slug
def test_slugify_truncates_long_urls_with_hash_suffix():
long = "https://example.com/" + "a" * 200
slug = _slugify(long)
assert len(slug) <= 80
# two different long URLs that share an 80-char prefix must not collide
other = "https://example.com/" + "a" * 199 + "b"
assert _slugify(long) != _slugify(other)
def test_render_frontmatter_emits_sorted_yaml_block():
fm = _render_frontmatter(
{"source_url": "https://x.com", "content_sha256": "abc", "fetched_at": "2026-06-10T00:00:00Z"}
)
assert fm.startswith("---\n")
assert fm.rstrip().endswith("---")
assert "source_url: https://x.com" in fm
# keys sorted for deterministic output
assert fm.index("content_sha256") < fm.index("fetched_at") < fm.index("source_url")
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_fetch.py -v Expected: FAIL with ImportError: cannot import name '_slugify'
- [ ] Step 3: Write minimal implementation
# add to tesserae/ingest/fetch.py (top: add imports)
import hashlib
import re
from typing import Dict
_SLUG_MAX = 80
def _slugify(value: str) -> str:
"""Filesystem-safe, deterministic slug. Long values get a hash suffix to avoid collisions."""
stripped = re.sub(r"^https?://", "", value)
slug = re.sub(r"[^a-zA-Z0-9]+", "-", stripped).strip("-").lower()
if len(slug) <= _SLUG_MAX:
return slug
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:8]
return slug[: _SLUG_MAX - 9].rstrip("-") + "-" + digest
def _render_frontmatter(meta: Dict[str, str]) -> str:
"""Render a deterministic YAML front-matter block (keys sorted)."""
lines = ["---"]
for key in sorted(meta):
lines.append(f"{key}: {meta[key]}")
lines.append("---")
return "\n".join(lines) + "\n"
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_fetch.py -v Expected: PASS (all)
- [ ] Step 5: Commit
git add tesserae/ingest/fetch.py tests/test_ingest_fetch.py
git commit -m "feat(ingest): add slugify + frontmatter rendering"
Task 4: fetch_to_source — fetch URL, convert, persist (HTTP mocked)
Files:
- Modify:
tesserae/ingest/fetch.py - Test:
tests/test_ingest_fetch.py - [ ] Step 1: Write the failing tests
# add to tests/test_ingest_fetch.py
import pytest
from tesserae.ingest.fetch import fetch_to_source
class _FakeResponse:
def __init__(self, *, status_code=200, text="", content_type="text/html"):
self.status_code = status_code
self.text = text
self.headers = {"content-type": content_type}
def raise_for_status(self):
if self.status_code >= 400:
raise RuntimeError(f"HTTP {self.status_code}")
def _fake_get(response):
def _get(url, timeout=None, follow_redirects=True, headers=None):
return response
return _get
def test_fetch_html_writes_markdown_file_with_frontmatter(tmp_path, monkeypatch):
html = "<html><body><h1>Title</h1><p>Hello world</p></body></html>"
monkeypatch.setattr(
"tesserae.ingest.fetch._http_get",
_fake_get(_FakeResponse(text=html, content_type="text/html")),
)
dest = tmp_path / "data" / "ingested"
path = fetch_to_source("https://example.com/post", dest)
assert path.parent == dest
assert path.suffix == ".md"
body = path.read_text(encoding="utf-8")
assert body.startswith("---\n")
assert "source_url: https://example.com/post" in body
assert "content_sha256:" in body
assert "fetched_at:" in body
# markdown conversion happened (heading survives, tags stripped)
assert "Title" in body
assert "<h1>" not in body
def test_fetch_non_2xx_raises_and_writes_nothing(tmp_path, monkeypatch):
monkeypatch.setattr(
"tesserae.ingest.fetch._http_get",
_fake_get(_FakeResponse(status_code=404, content_type="text/html")),
)
dest = tmp_path / "data" / "ingested"
with pytest.raises(RuntimeError):
fetch_to_source("https://example.com/missing", dest)
assert not dest.exists() or not any(dest.iterdir())
def test_fetch_missing_extra_raises_actionable_error(tmp_path, monkeypatch):
# Simulate httpx/markdownify not installed.
monkeypatch.setattr("tesserae.ingest.fetch._http_get", None)
monkeypatch.setattr("tesserae.ingest.fetch._html_to_markdown", None)
with pytest.raises(ImportError, match=r"pip install tesserae\[ingest-url\]"):
fetch_to_source("https://example.com/post", tmp_path)
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_fetch.py -v Expected: FAIL with ImportError: cannot import name 'fetch_to_source'
- [ ] Step 3: Write minimal implementation
# add to tesserae/ingest/fetch.py
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Optional
# Lazy, import-guarded bindings so the base install (no [ingest-url]) still imports
# this module. Tests monkeypatch these two names directly.
_http_get: Optional[Callable] = None
_html_to_markdown: Optional[Callable] = None
def _load_url_deps() -> None:
"""Bind _http_get / _html_to_markdown from the optional extra, or raise."""
global _http_get, _html_to_markdown
if _http_get is not None and _html_to_markdown is not None:
return
try:
import httpx
from markdownify import markdownify as _md
except ImportError as exc: # pragma: no cover - exercised via monkeypatch in tests
raise ImportError(
"URL ingest requires the optional extra: pip install tesserae[ingest-url]"
) from exc
if _http_get is None:
_http_get = lambda url, timeout=None, follow_redirects=True, headers=None: httpx.get(
url, timeout=timeout, follow_redirects=follow_redirects, headers=headers or {}
)
if _html_to_markdown is None:
_html_to_markdown = lambda html: _md(html)
def fetch_to_source(url: str, dest_dir: Path, *, title: Optional[str] = None) -> Path:
"""Fetch ``url``, convert to markdown, persist under ``dest_dir`` with provenance.
Returns the written file path. Raises on non-2xx, on a missing [ingest-url] extra,
and writes nothing on failure.
"""
if _http_get is None or _html_to_markdown is None:
_load_url_deps()
response = _http_get(url, timeout=30.0, follow_redirects=True, headers={"User-Agent": "tesserae-ingest"})
response.raise_for_status()
content_type = response.headers.get("content-type", "")
raw = response.text
if "html" in content_type:
body = _html_to_markdown(raw)
else:
# text/plain, text/markdown, or unknown: keep raw (extractor tolerates plain md)
body = raw
sha = hashlib.sha256(raw.encode("utf-8")).hexdigest()
meta = {
"source_url": url,
"content_sha256": sha,
"fetched_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
if title:
meta["title"] = title
dest_dir.mkdir(parents=True, exist_ok=True)
path = dest_dir / f"{_slugify(url)}.md"
path.write_text(_render_frontmatter(meta) + "\n" + body.strip() + "\n", encoding="utf-8")
return path
NOTE on the
_load_url_depsguard: when tests monkeypatch_http_get/_html_to_markdownto real callables,fetch_to_sourceskips_load_url_deps. When a test sets BOTH toNone,_load_url_depsruns and (with the extra absent in CI's base env) raises the actionableImportError. If CI installs[ingest-url], that specific test must instead monkeypatch_load_url_depsto raise — adjust in Step 1 if the test env has the extra.
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_fetch.py -v Expected: PASS (all). If test_fetch_missing_extra_raises_actionable_error fails because the extra IS installed in the dev env, change that test to monkeypatch.setattr("tesserae.ingest.fetch._load_url_deps", lambda: (_ for _ in ()).throw(ImportError("pip install tesserae[ingest-url]"))).
- [ ] Step 5: Commit
git add tesserae/ingest/fetch.py tests/test_ingest_fetch.py
git commit -m "feat(ingest): fetch_to_source — fetch, convert, persist with provenance"
Task 5: arxiv special-case (add arxiv_id to front-matter)
Files:
- Modify:
tesserae/ingest/fetch.py - Test:
tests/test_ingest_fetch.py - [ ] Step 1: Write the failing test
# add to tests/test_ingest_fetch.py
from tesserae.ingest.fetch import _arxiv_id_from_url
def test_arxiv_id_extracted_from_abs_url():
assert _arxiv_id_from_url("https://arxiv.org/abs/2401.12345") == "2401.12345"
assert _arxiv_id_from_url("https://arxiv.org/abs/2401.12345v2") == "2401.12345"
assert _arxiv_id_from_url("https://example.com/post") is None
def test_fetch_arxiv_url_records_arxiv_id_in_frontmatter(tmp_path, monkeypatch):
monkeypatch.setattr(
"tesserae.ingest.fetch._http_get",
_fake_get(_FakeResponse(text="<p>abstract</p>", content_type="text/html")),
)
path = fetch_to_source("https://arxiv.org/abs/2401.12345", tmp_path / "ing")
body = path.read_text(encoding="utf-8")
assert "arxiv_id: 2401.12345" in body
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_fetch.py -v Expected: FAIL with ImportError: cannot import name '_arxiv_id_from_url'
- [ ] Step 3: Write minimal implementation
# add to tesserae/ingest/fetch.py
_ARXIV_RE = re.compile(r"arxiv\.org/abs/(\d{4}\.\d{4,5})")
def _arxiv_id_from_url(url: str) -> Optional[str]:
m = _ARXIV_RE.search(url)
return m.group(1) if m else None
Then in fetch_to_source, after building meta and before writing, add:
arxiv_id = _arxiv_id_from_url(url)
if arxiv_id:
meta["arxiv_id"] = arxiv_id
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_fetch.py -v Expected: PASS (all)
- [ ] Step 5: Commit
git add tesserae/ingest/fetch.py tests/test_ingest_fetch.py
git commit -m "feat(ingest): record arxiv_id in fetched-source frontmatter"
Task 6: ingest_sources orchestrator — local-file happy path (full recompile)
Files:
- Create:
tesserae/ingest/orchestrator.py - Test:
tests/test_ingest_orchestrator.py - [ ] Step 1: Write the failing test
# tests/test_ingest_orchestrator.py
from pathlib import Path
from tesserae.project import ProjectWiki
from tesserae.ingest.orchestrator import ingest_sources
def _seed_project(root: Path) -> ProjectWiki:
(root / "data").mkdir(parents=True, exist_ok=True)
(root / "data" / "seed.md").write_text(
"---\ntype: paper\n---\n# Seed\n\nGraph neural networks for retrieval.\n",
encoding="utf-8",
)
return ProjectWiki.init(root, name="ingest_test")
def test_ingest_local_file_merges_and_reports(tmp_path):
wiki = _seed_project(tmp_path)
wiki.compile(changed_only=False) # establish baseline graph
new = tmp_path / "data" / "new.md"
new.write_text("---\ntype: paper\n---\n# New\n\nDiffusion models for planning.\n", encoding="utf-8")
result = ingest_sources(wiki, [str(new)], exact=True)
assert result["path_taken"] == "full-recompile"
assert result["node_count"] > 0
assert "graph_path" in result
# the new doc's content is now in the graph
graph_text = Path(result["graph_path"]).read_text(encoding="utf-8")
assert "Diffusion" in graph_text or "diffusion" in graph_text.lower()
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_orchestrator.py -v Expected: FAIL with ModuleNotFoundError: No module named 'tesserae.ingest.orchestrator'
- [ ] Step 3: Write minimal implementation
# tesserae/ingest/orchestrator.py
"""Orchestrate single-source ingest: resolve inputs, (fetch URLs), drive compile, report.
This is the B->C seam: v1 drives compile synchronously. A future v2 (--async) splits the
compile() call into an instant approximate write + an enqueued reconcile.
"""
from __future__ import annotations
from pathlib import Path
from typing import List, Optional, Sequence
from tesserae.ingest.fetch import fetch_to_source, is_url
def ingest_sources(
wiki,
inputs: Sequence[str],
*,
source_kind: Optional[str] = None,
title: Optional[str] = None,
exact: bool = True,
dry_run: bool = False,
) -> dict:
"""Ingest one or more file paths / URLs into ``wiki``'s knowledge base.
``exact=True`` forces a full recompile (correct by construction). ``exact=False`` opts
the run into the incremental fast path (Phase 2). Returns a report dict with keys:
``path_taken``, ``node_count``, ``edge_count``, ``processed_files``, ``skipped_files``,
``graph_path``, ``sources`` (resolved input paths).
"""
resolved: List[str] = []
dest = Path(wiki.project_root) / "data" / "ingested"
for item in inputs:
if is_url(item):
path = fetch_to_source(item, dest, title=title)
resolved.append(str(path))
else:
resolved.append(item)
if dry_run:
return {
"path_taken": "dry-run",
"sources": resolved,
"node_count": 0,
"edge_count": 0,
"processed_files": 0,
"skipped_files": 0,
"graph_path": str(wiki.paths.graph),
}
# Phase 1: always full recompile (correct). Phase 2 flips this on exact=False.
result = wiki.ingest(resolved, source_kind=source_kind, changed_only=False)
return {
"path_taken": "full-recompile",
"sources": resolved,
"node_count": result["node_count"],
"edge_count": result["edge_count"],
"processed_files": result["processed_files"],
"skipped_files": result["skipped_files"],
"graph_path": result["graph_path"],
}
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_orchestrator.py -v Expected: PASS
- [ ] Step 5: Commit
git add tesserae/ingest/orchestrator.py tests/test_ingest_orchestrator.py
git commit -m "feat(ingest): orchestrator local-file happy path (full recompile)"
Task 7: orchestrator — URL input + dry-run
Files:
- Test:
tests/test_ingest_orchestrator.py - (No new impl — verifies Task 6 code paths; add impl only if a test fails.)
- [ ] Step 1: Write the failing tests
# add to tests/test_ingest_orchestrator.py
def test_ingest_url_fetches_persists_and_merges(tmp_path, monkeypatch):
wiki = _seed_project(tmp_path)
wiki.compile(changed_only=False)
class _Resp:
status_code = 200
text = "<h1>Remote</h1><p>Reinforcement learning from human feedback.</p>"
headers = {"content-type": "text/html"}
def raise_for_status(self): pass
monkeypatch.setattr(
"tesserae.ingest.fetch._http_get",
lambda url, timeout=None, follow_redirects=True, headers=None: _Resp(),
)
result = ingest_sources(wiki, ["https://example.com/rlhf"], exact=True)
persisted = tmp_path / "data" / "ingested"
assert any(persisted.glob("*.md"))
assert result["path_taken"] == "full-recompile"
assert result["node_count"] > 0
def test_ingest_dry_run_writes_no_graph_but_fetches(tmp_path, monkeypatch):
wiki = _seed_project(tmp_path)
new = tmp_path / "data" / "dry.md"
new.write_text("---\ntype: paper\n---\n# Dry\n\ncontent\n", encoding="utf-8")
result = ingest_sources(wiki, [str(new)], dry_run=True)
assert result["path_taken"] == "dry-run"
assert result["sources"] == [str(new)]
- [ ] Step 2: Run tests to verify they pass (impl already exists)
Run: pytest tests/test_ingest_orchestrator.py -v Expected: PASS. If test_ingest_url_... fails on data/ingested not being a discovered source root, confirm ProjectWiki.init seeds data as a default source (spec §6 / project.py default_sources). If data/ingested/ is not auto-discovered, fix in orchestrator by passing the persisted path explicitly to wiki.ingest([...]) (already done — resolved carries the exact path), so the parity here is on the graph, not discovery.
- [ ] Step 3: Commit
git add tests/test_ingest_orchestrator.py
git commit -m "test(ingest): URL fetch + dry-run orchestrator paths"
Task 8: CLI wiring — tesserae ingest top-level command
Files:
- Modify:
tesserae/cli.py - Test:
tests/test_ingest_cli.py - [ ] Step 1: Write the failing tests
# tests/test_ingest_cli.py
import tesserae.cli as cli
def test_ingest_is_a_top_level_command():
assert "ingest" in cli._NEW_DISPATCH
def test_ingest_parser_accepts_inputs_and_flags():
parser = cli._build_ingest_parser()
args = parser.parse_args(["a.md", "https://x.com", "--exact", "--project", "/tmp/p"])
assert args.inputs == ["a.md", "https://x.com"]
assert args.exact is True
assert args.project == "/tmp/p"
assert args._handler == "_handle_ingest_docs"
def test_code_ingest_still_exists_unchanged():
# the namespaced code-graph ingest must remain `tesserae code ingest`
code_parser = cli._build_code_parser()
ns = code_parser.parse_args(["ingest", "--project", "."])
assert ns._handler == "_handle_code_ingest"
def test_ingest_dispatch_calls_orchestrator(monkeypatch, tmp_path):
captured = {}
def _fake_ingest_sources(wiki, inputs, **kw):
captured["inputs"] = list(inputs)
captured["kw"] = kw
return {"path_taken": "full-recompile", "node_count": 1, "edge_count": 0,
"processed_files": 1, "skipped_files": 0,
"graph_path": str(tmp_path / "g.json"), "sources": list(inputs)}
monkeypatch.setattr("tesserae.cli.ingest_sources", _fake_ingest_sources)
monkeypatch.setattr("tesserae.cli.ProjectWiki", _StubWiki)
rc = cli.main(["ingest", "x.md", "--project", str(tmp_path), "--exact"])
assert rc == 0
assert captured["inputs"] == ["x.md"]
assert captured["kw"]["exact"] is True
class _StubWiki:
project_root = "."
@staticmethod
def load(project):
return _StubWiki()
- [ ] Step 2: Run tests to verify they fail
Run: pytest tests/test_ingest_cli.py -v Expected: FAIL (_build_ingest_parser missing / "ingest" not in _NEW_DISPATCH).
- [ ] Step 3: Write the implementation in
tesserae/cli.py
Add the import near the other ingest imports at the top of the dispatch section:
from tesserae.ingest.orchestrator import ingest_sources
Add the parser builder (place near _build_code_parser, ~line 2412):
def _build_ingest_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="tesserae ingest",
description="Ingest a single document file or URL into the knowledge base.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("inputs", nargs="+", help="File path(s) or http(s) URL(s) to ingest")
parser.add_argument("--project", default=".", help="Project root directory")
parser.add_argument("--title", default=None, help="Title override (useful for URLs)")
parser.add_argument("--source-kind", default=None, help="Override source classification")
parser.add_argument("--exact", action="store_true", help="Force a full recompile (skip the fast path)")
parser.add_argument("--dry-run", action="store_true", help="Fetch + report, write no graph")
parser.set_defaults(_handler="_handle_ingest_docs")
return parser
Add the route (place near _route_code):
def _route_ingest(rest: List[str]) -> int:
args = _build_ingest_parser().parse_args(rest)
return _resolve_handler(args._handler)(args)
Add the handler (place near _handle_ingest, ~line 940 — DO NOT modify the existing _handle_ingest, which is used by compile <paths>):
def _handle_ingest_docs(args: argparse.Namespace) -> int:
wiki = ProjectWiki.load(args.project)
result = ingest_sources(
wiki,
args.inputs,
source_kind=args.source_kind,
title=args.title,
exact=args.exact,
dry_run=getattr(args, "dry_run", False),
)
print(
f"Ingested ({result['path_taken']}): "
f"processed={result['processed_files']} skipped={result['skipped_files']} "
f"nodes={result['node_count']} edges={result['edge_count']}"
)
print(f"Sources: {', '.join(result['sources'])}")
print(f"Graph: {result['graph_path']}")
return 0
Register in _NEW_DISPATCH (line ~2938), add one entry:
"ingest": _route_ingest,
- [ ] Step 4: Run tests to verify they pass
Run: pytest tests/test_ingest_cli.py -v Expected: PASS (all 4)
- [ ] Step 5: Run the full ingest suite + a smoke run
Run: pytest tests/test_ingest_fetch.py tests/test_ingest_orchestrator.py tests/test_ingest_cli.py -v Expected: PASS. Then a real smoke run:
cd /tmp && rm -rf ingsmoke && mkdir ingsmoke && cd ingsmoke
printf -- '---\ntype: paper\n---\n# A\n\ngraph retrieval\n' > a.md
python -m tesserae.cli init --name smoke 2>/dev/null || tesserae init --name smoke
tesserae ingest a.md --exact
Expected: prints Ingested (full-recompile): ... nodes=N ....
- [ ] Step 6: Commit
git add tesserae/cli.py tests/test_ingest_cli.py
git commit -m "feat(ingest): wire top-level 'tesserae ingest' command"
Task 9: English documentation page
Files:
- Create:
docs/commands/ingest.md - [ ] Step 1: Write the doc
# `tesserae ingest`
Merge a single document file or URL into the knowledge base.
## Usage
tesserae ingest <input>... [--title T] [--source-kind K] [--exact] [--dry-run]
`<input>` is one or more local file paths or `http(s)` URLs. URLs are fetched, converted to
markdown, and persisted under `data/ingested/<slug>.md` with provenance front-matter
(`source_url`, `fetched_at`, `content_sha256`, and `arxiv_id` when detected), then merged.
URL ingest requires the optional extra:
pip install tesserae[ingest-url]
## Flags
- `--exact` — force a full recompile (always correct; default until the incremental fast
path is proven byte-identical).
- `--dry-run` — fetch and report what would be ingested; write no graph.
- `--title` — title override, useful for bare URLs.
- `--source-kind` — override the source classification.
## Relationship to `compile`
`tesserae compile` (no args) re-extracts the whole tracked corpus. `tesserae ingest <x>`
adds one source. It is distinct from `tesserae code ingest`, which mints a code graph from
Python source.
- [ ] Step 2: Verify it renders / links are valid
Run: pytest tests/test_link_integrity.py -v (if the docs are part of the site corpus) or visually confirm. Expected: no broken-link failures introduced.
- [ ] Step 3: Commit
git add docs/commands/ingest.md
git commit -m "docs(ingest): English command reference"
Task 10: i18n — translate the command doc into all 7 languages
Files:
- Create:
docs/commands/ingest.{ko,zh,ja,ru,es,fr,de}.md(or the project's established i18n path layout — confirm before writing).
Project invariant: every doc under
docs/ships in ko/zh/ja/ru/es/fr/de. Before writing, inspect an existing translated doc to copy the EXACT path/naming convention (e.g.docs/i18n/<lang>/commands/ingest.mdvsdocs/commands/ingest.<lang>.md).
- [ ] Step 1: Find the established i18n layout
Run: ls docs && find docs -name '*.ko.md' -o -path '*/ko/*' | head Expected: reveals whether translations are suffix-based or directory-based.
- [ ] Step 2: Create the 7 translations following that exact layout
Translate docs/commands/ingest.md (Task 9) into ko, zh, ja, ru, es, fr, de, preserving the code blocks and flag names verbatim (only prose is translated).
- [ ] Step 3: Verify the i18n completeness check passes
Run: the project's i18n/doc-coverage test if one exists (search: pytest -k i18n -v or pytest tests/test_docs_i18n*.py -v). Expected: PASS / all 7 languages present.
- [ ] Step 4: Commit
git add docs/commands/ingest.*.md docs/i18n 2>/dev/null
git commit -m "docs(ingest): i18n translations (ko/zh/ja/ru/es/fr/de)"
Phase 2 — Incremental fast path, gated by a golden-parity test
Ship gate (spec §7):
ingestkeeps--exact(full recompile) as the DEFAULT until the parity test in Task 13 is green. Only then does Task 14 flip the default.
Task 11: incremental_override seam in Project.ingest / Project.compile
Files:
- Modify:
tesserae/project.py(theincremental_enabledlookup, ~line 537; bothingestandcompilesignatures, lines 448 and 1324) - Test:
tests/test_ingest_incremental_seam.py - [ ] Step 1: Read the current incremental gate before editing
Run: open tesserae/project.py lines 448–470, 534–557, 1324–1380. Confirm compile() delegates to ingest() and that incremental_enabled = bool(cfg.get("incremental_compile", False)) is the single gate.
- [ ] Step 2: Write the failing test
# tests/test_ingest_incremental_seam.py
from pathlib import Path
from tesserae.project import ProjectWiki
def _seed(root: Path) -> ProjectWiki:
(root / "data").mkdir(parents=True, exist_ok=True)
(root / "data" / "s.md").write_text("---\ntype: paper\n---\n# S\n\nretrieval graphs\n", encoding="utf-8")
return ProjectWiki.init(root, name="seam_test")
def test_incremental_override_enables_without_config_flag(tmp_path, monkeypatch):
wiki = _seed(tmp_path)
wiki.compile(changed_only=False)
captured = {}
# Spy on the gate by reading the warning the incremental path logs when enabled.
import logging
records = []
handler = logging.Handler()
handler.emit = lambda r: records.append(r.getMessage())
logging.getLogger("tesserae.project").addHandler(handler)
(tmp_path / "data" / "n.md").write_text("---\ntype: paper\n---\n# N\n\ndiffusion planning\n", encoding="utf-8")
# override=True must take the incremental branch even though config flag is default-False
wiki.ingest([str(tmp_path / "data" / "n.md")], changed_only=True, incremental_override=True)
assert any("incremental_compile is ENABLED" in m for m in records)
- [ ] Step 3: Run test to verify it fails
Run: pytest tests/test_ingest_incremental_seam.py -v Expected: FAIL — ingest() got an unexpected keyword argument 'incremental_override'.
- [ ] Step 4: Implement the seam
In tesserae/project.py, add incremental_override: Optional[bool] = None to BOTH the ingest() signature (line 448) and the compile() signature (line 1324), and thread it from compile() into its internal ingest() call. Then change the gate (line 537) from:
incremental_enabled = bool(cfg.get("incremental_compile", False))
to:
incremental_enabled = (
incremental_override
if incremental_override is not None
else bool(cfg.get("incremental_compile", False))
)
- [ ] Step 5: Run test to verify it passes
Run: pytest tests/test_ingest_incremental_seam.py -v Expected: PASS
- [ ] Step 6: Run the incremental parity suite to confirm no regression
Run: pytest tests/test_incremental_parity.py -v Expected: PASS (the seam is inert when incremental_override is None).
- [ ] Step 7: Commit
git add tesserae/project.py tests/test_ingest_incremental_seam.py
git commit -m "feat(ingest): per-invocation incremental_override seam in compile/ingest"
Task 12: orchestrator fast path (exact=False → incremental override)
Files:
- Modify:
tesserae/ingest/orchestrator.py - Test:
tests/test_ingest_orchestrator.py - [ ] Step 1: Write the failing test
# add to tests/test_ingest_orchestrator.py
def test_ingest_fast_path_uses_incremental_override(tmp_path, monkeypatch):
wiki = _seed_project(tmp_path)
wiki.compile(changed_only=False)
seen = {}
real_ingest = wiki.ingest
def _spy(inputs, **kw):
seen.update(kw)
return real_ingest(inputs, **kw)
monkeypatch.setattr(wiki, "ingest", _spy)
(tmp_path / "data" / "f.md").write_text("---\ntype: paper\n---\n# F\n\ncontent\n", encoding="utf-8")
result = ingest_sources(wiki, [str(tmp_path / "data" / "f.md")], exact=False)
assert seen.get("changed_only") is True
assert seen.get("incremental_override") is True
assert result["path_taken"] == "incremental"
- [ ] Step 2: Run test to verify it fails
Run: pytest tests/test_ingest_orchestrator.py::test_ingest_fast_path_uses_incremental_override -v Expected: FAIL (orchestrator still always full-recompiles; path_taken == "full-recompile").
- [ ] Step 3: Implement the fast path
In tesserae/ingest/orchestrator.py, replace the Phase-1 single-branch compile with:
if exact:
result = wiki.ingest(resolved, source_kind=source_kind, changed_only=False)
path_taken = "full-recompile"
else:
result = wiki.ingest(
resolved, source_kind=source_kind, changed_only=True, incremental_override=True
)
path_taken = "incremental"
return {
"path_taken": path_taken,
"sources": resolved,
"node_count": result["node_count"],
"edge_count": result["edge_count"],
"processed_files": result["processed_files"],
"skipped_files": result["skipped_files"],
"graph_path": result["graph_path"],
}
- [ ] Step 4: Run test to verify it passes
Run: pytest tests/test_ingest_orchestrator.py -v Expected: PASS (all). Existing exact=True tests still report full-recompile.
- [ ] Step 5: Commit
git add tesserae/ingest/orchestrator.py tests/test_ingest_orchestrator.py
git commit -m "feat(ingest): incremental fast path behind exact=False"
Task 13: GOLDEN PARITY — ingest(X) ≡ full compile(corpus ∪ X)
Files:
- Test:
tests/test_ingest_parity.py - [ ] Step 1: Write the parity test (the correctness contract)
# tests/test_ingest_parity.py
"""Golden parity: ingest(X) graph.json == full compile(corpus ∪ X), byte-for-byte."""
from pathlib import Path
from tesserae.project import ProjectWiki
from tesserae.ingest.orchestrator import ingest_sources
def _write(root: Path, name: str, body: str) -> Path:
(root / "data").mkdir(parents=True, exist_ok=True)
p = root / "data" / name
p.write_text(body, encoding="utf-8")
return p
_CORPUS = {
"a.md": "---\ntype: paper\n---\n# A\n\nGraph neural networks for retrieval.\n",
"b.md": "---\ntype: paper\n---\n# B\n\nRetrieval augmented generation.\n",
}
_NEW = ("c.md", "---\ntype: paper\n---\n# C\n\nDiffusion models for planning.\n")
def _full_compile_bytes(root: Path) -> bytes:
wiki = ProjectWiki.init(root, name="parity")
for name, body in _CORPUS.items():
_write(root, name, body)
_write(root, _NEW[0], _NEW[1])
wiki.compile(changed_only=False)
return wiki.paths.graph.read_bytes()
def _ingest_bytes(root: Path, *, exact: bool) -> bytes:
wiki = ProjectWiki.init(root, name="parity")
for name, body in _CORPUS.items():
_write(root, name, body)
wiki.compile(changed_only=False) # baseline WITHOUT the new doc
new = _write(root, _NEW[0], _NEW[1])
ingest_sources(wiki, [str(new)], exact=exact)
return wiki.paths.graph.read_bytes()
def test_ingest_exact_matches_full_compile(tmp_path):
full = _full_compile_bytes(tmp_path / "full")
ing = _ingest_bytes(tmp_path / "ing", exact=True)
assert ing == full, "ingest --exact graph.json must equal a full compile"
def test_ingest_fast_matches_full_compile(tmp_path):
full = _full_compile_bytes(tmp_path / "full2")
ing = _ingest_bytes(tmp_path / "ing2", exact=False)
assert ing == full, (
"INCREMENTAL ingest graph.json diverged from a full compile. If this fails, the "
"additive case has hit a residual gap (multi-owner winner-change / untracked "
"post-pass edges). Fix by extending the full-recompile fallback condition in "
"Project.compile (~line 843) to detect this case — do NOT relax the assertion."
)
- [ ] Step 2: Run the parity tests
Run: pytest tests/test_ingest_parity.py -v Expected: test_ingest_exact_matches_full_compile PASS. test_ingest_fast_matches_full_compile is the gate — it MUST pass before Task 14. If it fails, follow the assertion message: extend the fallback in Project.compile so the offending additive shape degrades to a full recompile, then re-run. Do not weaken the test.
- [ ] Step 3: Commit
git add tests/test_ingest_parity.py
git commit -m "test(ingest): golden parity — ingest == full compile"
Task 14: Flip the default to the fast path (only if Task 13 is green)
Files:
- Modify:
tesserae/cli.py(_build_ingest_parser),tesserae/ingest/orchestrator.py(defaultexact),docs/commands/ingest.md - Test:
tests/test_ingest_cli.py - [ ] Step 1: Confirm the gate
Run: pytest tests/test_ingest_parity.py -v Expected: BOTH parity tests PASS. If test_ingest_fast_matches_full_compile is not green, STOP — do not flip the default.
- [ ] Step 2: Write the failing test for the new default
# add to tests/test_ingest_cli.py
def test_ingest_defaults_to_fast_path():
parser = cli._build_ingest_parser()
args = parser.parse_args(["a.md"])
assert args.exact is False # fast path by default; --exact opts into full recompile
- [ ] Step 3: Run test to verify it fails
Run: pytest tests/test_ingest_cli.py::test_ingest_defaults_to_fast_path -v Expected: FAIL (currently --exact is a store_true defaulting False — this already passes IF the parser default is False). If it already passes, the only real change is making the orchestrator default exact=False and updating docs; proceed to Step 4.
- [ ] Step 4: Make fast the default
In tesserae/ingest/orchestrator.py, change the ingest_sources signature default exact: bool = True → exact: bool = False. In tesserae/cli.py _handle_ingest_docs, the flag already maps --exact→True, so no CLI change needed beyond confirming. Update docs/commands/ingest.md to state the default is now the incremental fast path with automatic full-recompile fallback.
- [ ] Step 5: Run the entire ingest + parity suite
Run: pytest tests/test_ingest_fetch.py tests/test_ingest_orchestrator.py tests/test_ingest_cli.py tests/test_ingest_parity.py tests/test_ingest_incremental_seam.py -v Expected: ALL PASS.
- [ ] Step 6: Run the full project suite (no regressions)
Run: pytest -q Expected: green (matches the project's known-green baseline).
- [ ] Step 7: Commit
git add tesserae/ingest/orchestrator.py tesserae/cli.py docs/commands/ingest.md tests/test_ingest_cli.py
git commit -m "feat(ingest): default to incremental fast path (parity-gated)"
Self-Review (completed during planning)
- Spec coverage: §4 architecture → Tasks 6–8; §5 command surface + flags → Task 8; §6 components (fetch, orchestrator, CLI) → Tasks 2–8; persisted-source convention → Tasks 4–6; §7 correctness guard (override + existing fallback + parity gate) → Tasks 11–14; §8 error handling (non-2xx, missing extra, dry-run) → Tasks 4, 7; §9 testing (golden parity, guard, fetch mocked, CLI, i18n) → Tasks 4–14; §10 phasing (B now, C seam) → orchestrator docstring Task 6; §11 R2 dependency choice → Task 1 (httpx + markdownify). All sections mapped.
- Placeholder scan: every code step contains complete code; the only "find the convention first" step (Task 10 i18n layout) is a concrete inspection instruction, not a placeholder.
- Type consistency:
ingest_sources(wiki, inputs, *, source_kind, title, exact, dry_run),fetch_to_source(url, dest_dir, *, title),is_url(str), report dict keys (path_taken/node_count/edge_count/processed_files/skipped_files/graph_path/sources) and handler name_handle_ingest_docsare consistent across all tasks.
Open items deferred to the implementer (from spec §11)
- R2 resolved to
httpx + markdownify; swap in Task 1 if the project prefers stdliburllib+ a different converter. PDF handling is NOT in v1 (HTML/text only). - Q1 multiple inputs: handled —
ingest_sourcesaccepts a list and passes all resolved paths to a singlewiki.ingest([...])(one compile over all new sources). - Q2 slug collisions: handled by
_slugify's hash suffix for long/colliding URLs.