phase: 145_git_source_links
Build and Push Containers / build-and-push-app (push) Successful in 2m23s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**Phase 145 final verification pass — all green, no code changes needed**

- **Verified (no defects found):** resolver `origin_for_source` + `_url_basename` (scp/`.git`/trailing-slash, sanitize, ambiguity WARNING); `origin:`/`branch:` lines in all four read shapes via shared `_origin_lines`; HIGH `<tools>` origin rule (verbatim + only-byte-change pins); `DocContent.origin_url/origin_branch` (absent-never-null serializer); `renderDocument` "Original" anchor (copyable verbatim URL, `_blank`/`noopener`, branch in title); mock scripted origin-link flow (disjoint trigger — grep-verified vs all other E2E files)
- **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → 3402 passed, `app/` coverage **99%** (>90% gate); `uv run pytest tests/e2e/test_git_source_links.py -v --no-cov` → **4 passed** in isolation; `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings
- **Completion criteria:** (1) git read results carry origin/branch, local byte-identical — ✅ (unit pins + E2E control); (2) HIGH `<tools>` rule pinned, LOW byte-identical — ✅; (3) modal AND /document.html show Original link, branch in label when pinned, nothing for local — ✅; (4) full suite green + coverage + E2E + lint/types — ✅; (5) no completed-phase behavior change (read header line 1, mock prefix, done frame/SourceRef untouched, LOW sha) — ✅
- **Extra regression check:** re-ran shared-mock E2E suites `test_read_line_ranges.py` (2 passed) and `test_document_viewer.py` (8 passed) — no cross-suite regressions from the `mock_llm.py` addition
- **Documentation:** README.md reviewed in full (env table ⊇ `.env.example`, pages/routes, config defaults, scripts, 17-color palette, 300 s guard, 2 s sync poll, phase-145 provenance section matches implementation) and AGENTS.md (72rem container, no-CDN test, smoke test, gitignore, gates) — **verified consistent, no changes needed**
- **Notable:** phase-116 screenshot PNG was regenerated during this phase's E2E runs (byte re-encode only, visually identical, correct no-anchor local-source meta row) — left in tree for the harness commit
- **Next pending phase:** `146_search_tool_scope` (then 147, 148, 149)
This commit is contained in:
2026-10-01 11:13:43 -04:00
parent 486ceb4019
commit 32a3ec4f5e
37 changed files with 2856 additions and 22 deletions
@@ -0,0 +1,38 @@
# Phase 145 — the answer points at the original git source: the model links it, the modal shows it
**Source:** `TODO.md` L7 — "there always needs to be a link to the original git source if possible. I've asked a few questions like 'how does the phased execution process work for pi.dev?' And the correct answer is an explanation and then point to the skills repo in the answer so I can go download the skills for my personal use. It's important to provide the full source so users get the complete answer at once rather than extracting it bit by bit with lots of questions."
**Story:** n/a (TODO-derived phase, Protocol B append).
**Context:** `app/rag/agent.py` — the read branch's result shapes (~L1630–1680: `Document {source}/{path}:\n` + `date: …\n` + image marker + content; the `E2E mock`'s `_READ_RESULT_PREFIX` = the FIRST line only — `tests/e2e/mock_llm.py` L1412 — so a new line BELOW the first line keeps the mock contract); `app/models.py` — `GitSource` (`url` L289, `kind` L292, `token` L316 — the PAT, never served, `branch` L333 — phase 135, NULL = default branch); `app/rag/git_sources.py` — `sanitize_url` (L~150: the phase-121 credential-free output mask — strips userinfo, byte-identical for clean URLs) and `clone_url_for` (the single credential-injection site); `app/schemas.py` — `DocContent` (the `GET /api/documents/content` payload — the phase-106 `created_at` field is the recent optional-metadata precedent) and `SourceRef` (UNCHANGED this phase — the done frame stays byte-identical, the origin rides the content payload instead); `app/api/docs.py` — the content endpoint (the `(source, path)` lookup, percent-encoding); `frontend/assets/document.js` — `renderDocument` (L172+: the shared meta-badge row — `metaBadge`/`pathCode` idioms — used by BOTH the doc modal and the full page); `app/rag/prompts.py` — the `<tools>` section (L162+: the `ls`/`read`/`grep` teaching, HIGH prompt only); `app/rag/retriever.py` — `source_ref_with_image` (untouched).
**Locked (owner-confirmed 2026-09-30, chat):** the model learns the origin from a new `origin:`/`branch:` line in read results (git sources only, credentials stripped) and is taught to include the original-source link in its answer — it builds the browse URL itself (no fragile server-side GitHub/Gitea URL construction); the doc modal + document page get the "Original" external link (one `renderDocument` site, fed by the content payload); local (non-git) sources get no link.
## Objective
An answer grounded in a git-sourced document comes with the way to the complete original in ONE step: the read result tells the model the repository origin (and pinned branch), the prompt teaches it to include the original-source link in its answer text (so it works in the UI AND through the API tokens), and the document modal + full page show the same origin as a visible "Original" external link.
## Dependencies
- `144_modal_back_button` (todo) — pipeline order only; code touch (the modal's meta row) is the same `renderDocument` surface, no ordering risk.
## Design (shared by all tasks — the executor reads this, not the chat)
- **The origin resolver (task 01):** `app/rag/git_sources.py` (the GitSource-domain home) — `origin_for_source(db, source_name) -> tuple[str, str | None] | None`: the source's directory basename (the importer's `source = root.name`) mapped to a `kind = "git"` row by the row's URL basename — the URL's path component, trailing `/` stripped, `.git` suffix stripped, last path segment (scp-style `git@host:owner/repo.git` → the part after `:` first; `sanitize_url` applied to the result so a legacy embedded-credential row never leaks; `branch` returned as-is — NULL means the default branch). EXACTLY ONE match → `(sanitized_url, branch)`; zero or TWO-OR-MORE matches (two repos with the same basename) → `None` (no line — never guess; the ambiguity is logged at WARNING with the sanitized names). No caching (the table is a handful of rows; a read's extra query is trivial).
- **The read result (task 01):** a git-sourced document's read result gains, IMMEDIATELY AFTER the `date:` line (before the image marker — the marker keeps its phase-122 position relative to the content): `origin: {sanitized_url}` and, when a branch is pinned, `branch: {branch}` — one line each, no URL for local sources, no lines at all for a source with no unambiguous git row (the result is byte-identical to pre-phase for every non-git doc — the existing pins pass unchanged; the mock's `_READ_RESULT_PREFIX` first-line contract is untouched — the new lines sit BELOW the header). The plain-read, truncated-plain, range-read, and truncated-range shapes all carry the lines at the same position (one insertion helper — the four result builders share it).
- **The teaching (task 02):** the `<tools>` section gains the citation-source rule (the `read` teaching's natural home — the model answers from what it READ): when an answer uses a document whose read result carries an `origin:` line, the answer INCLUDES the original source — a link to the repository (the `origin` line) with the document's path (from the `Document …` header) named, and the pinned branch when one is shown — formatted as the repository's file-view URL where the model can tell it (GitHub/Gitea/GitLab `…/blob/<branch>/<path>`), else the repo URL + path as text. The point (the owner's words, in the model's language): the user should get the complete source at once — one link, not a second question. Local-sourced documents carry no `origin` line — nothing to add. The `SUGGEST_INTRO` citation discipline (cite only what you read) is UNCHANGED — this rule extends the cited documents' answers, it does not widen the cite set.
- **The UI (task 03):** `DocContent` (the content payload) gains `origin_url: str | None = None` + `origin_branch: str | None = None` (the resolver's output for the document's source — NULL/absent for local sources, the `image_url`-style optional-metadata shape; a null field in the content payload is fine — the omission rule binds the SSE frames, not this payload). `renderDocument` (document.js L172+) — when `doc.origin_url` is present, the meta row gains, after the path code: an "Original" anchor (`<a>` — `createElement`, the never-build-HTML rule; `href = doc.origin_url`, `target="_blank" rel="noopener"` — the modal's "Full page" escape-hatch idiom; `textContent = doc.origin_url` VERBATIM — the copyable-link contract — in the `doc-path` ellipsis style, the full URL in a `title` attribute that also carries ` — branch: <name>` when `origin_branch` is present) — ONE site: the modal AND the full page show it (both call `renderDocument`).
- **The E2E (task 04):** a git row seeded over the fixture source's name (the resolver is name-based — a `kind="git"` row whose URL basename matches the fixture source name makes the fixture docs "git-sourced" for this phase's purposes), a scripted mock flow whose final answer carries the repo URL, and the modal's meta anchor asserted.
## Tasks
1. `01_origin_resolution.md` — the resolver + the read-result `origin:`/`branch:` lines (the four read shapes).
2. `02_prompt_teaching.md` — the `<tools>` original-source rule + the prompt pins.
3. `03_content_payload_render_link.md` — `DocContent` fields + the content endpoint + the `renderDocument` "Original" anchor (modal + full page).
4. `04_e2e_source_link.md` — the dedicated E2E (the answer carries the repo link; the modal shows the Original anchor; non-git docs stay byte-identical).
## Testing & Quality
- Unit: the resolver (the mapping, the sanitization, the ambiguity, the scp-form URL, the `.git` suffix, the no-row case); the read-result insertion (four shapes × {git, git+branch, local, ambiguous} — non-git byte-identical pins); the prompt copy pin; the payload/endpoint; the renderer (the anchor present/absent, the branch label, the ellipsis title).
- Integration: the content endpoint returns `origin_url` for a seeded git row (and null for a plain local source).
- E2E: one dedicated Playwright file, run in isolation, mock-only.
- Coverage: **>90%** on new/modified code (`app/`).
## Completion Criteria
- [ ] a git-sourced read result carries `origin: <sanitized url>` (+ `branch:` when pinned); every local-source read result is byte-identical to pre-phase
- [ ] the HIGH prompt's `<tools>` section teaches the original-source link rule (pinned verbatim); the LOW prompt is byte-identical
- [ ] the doc modal AND /document.html show the "Original" external link for git-sourced docs (the branch in the label when pinned); nothing new for local docs
- [ ] `uv run pytest --cov=app --cov-report=term-missing` green, `app/` coverage >90%; `uv run pytest tests/e2e/test_git_source_links.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean
- [ ] no behavior change in completed phases (the read header's first line, the mock's parse, the done frame, the LOW prompt, the non-git read results)