phase: 90_upload_no_scan

All verification is complete and green. Final report:

**Phase 90 (upload_no_scan) — final verification pass: all criteria verified, no fixes needed**

- Verified (not re-implemented): all 3 tasks already complete; working tree carries the phase's uncommitted changes
- Upload pipeline: `_run_upload` stops after row upsert; success = `{"message": "uploaded"}`, null/0/0 progress; `UploadOut` removed from `app/schemas.py`; gates/unpack/swap/failed states intact
- Copy: button "Upload", bare "Processing…", result line "Uploaded \<name\> — press Sync sources to import it."; hint + caption re-pointed at Sync; no "Upload &" remnants in `frontend/`
- Tests: `pytest tests/unit/test_git_sources.py tests/integration/test_git_sources_upload.py tests/unit/test_frontend_sync_upload.py` → 56 passed; E2E phase suite `tests/e2e/test_upload_no_scan.py` → 3 passed (zero docs after upload; ignore edit honored by Sync — 2 added, `notes/skipme.md` excluded; re-upload in-place, still zero docs); affected suites `test_archive_upload_sources.py` + `test_sync_upload_progress.py` + `test_source_removal_cleanup.py` → 15 passed
- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **1808 passed, 99% coverage** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors

**Completion criteria:**
1. Upload settles "ready for sync", zero docs, row + ignore editor — ✅ (E2E test 1)
2. Button "Upload", no "Upload &" copy — ✅ (`rg` empty in frontend/, app/, tests/)
3. RAG-page "Sync sources" imports upload honoring edited ignores — ✅ (isolated E2E test 2)
4. Suite green, >90% coverage, lint clean — ✅ (99%, ruff+pyright clean)
5. Atomic commit — ✅ left to harness per executor rules (no git add/commit run; tree staged-ready)

**Deviations:** `regenerate_overview` still appears once in `git_sources.py` — inside `delete_git_source` (phase-69 locked total-removal behavior, not the upload leg; upload leg is scan-free). `.agents/PLAN.md` does not exist in this repo; locked decisions A1–A4 were enforced from the phase files.

**Next pending phase:** `91_admin_theme_tab` (in `todo/`).
This commit is contained in:
2026-09-09 14:08:18 -04:00
parent 0c8a7b9974
commit 3095c4c577
27 changed files with 2478 additions and 1224 deletions
+31 -21
View File
@@ -79,15 +79,18 @@ export async function mount(root) {
* tree, in order (startSyncPolling):
* 1. sync running → "Syncing… <file> (n/m)" — bare "Syncing…" until
* the import's first file (clone/pull, A4);
* 2. upload running → "Importing <file> (n/m)" — the background
* archive scan (the "clicked upload, then opened
* 2. upload running → BARE "Importing…" — the background upload
* RUN (phase 90: unpack + register only, no
* scan — its status never carries a file or
* counts; the "clicked upload, then opened
* sources" contract, A3);
* 3. sync success → the phase-32 settle (counts + catalog refresh);
* 4. sync failed → the phase-32 failure (banner + modal);
* 5. upload success → settle "Sync sources" + catalog refresh
* (loadDocs — the new documents must appear); the
* upload's counts live on the Sources page, never
* in #sync-result (A3);
* (loadDocs — phase 90: an upload no longer
* changes the KB, the re-read is a no-op safety
* net); the upload's result line lives on the
* Sources page, never in #sync-result (A3);
* 6. upload failed → settle "Sync sources" — the failure is the
* Sources page's error banner, never this page's (A3);
* 7. both idle → retry-ready idle.
@@ -134,14 +137,16 @@ export async function mount(root) {
}
/* Phase 64 (task 04): the live-file label. `kind` picks the prefix —
* "sync" → "Syncing…", "upload" → "Importing" (the background scan's
* "sync" → "Syncing…", "upload" → "Importing" (the background run's
* word, A3). The current file — the status endpoint's full
* source/relative/path (A4) — is appended while one is being processed;
* the BARE prefix shows during the clone/pull (sync) or unpack (upload)
* phase, before any file is indexed. The counts appear only once the
* import has started (total > 0). CSS ellipsizes the button label; the
* same untruncated text goes to the button title + #sync-result (the
* aria-live announcer). */
* the BARE prefix shows during the clone/pull (sync), before any file
* is indexed. Phase 90: the upload run is unpack + register only (no
* scan), so its status never carries a file or counts — the
* "Importing" label is always the bare one. The counts appear only
* once the import has started (total > 0). CSS ellipsizes the button
* label; the same untruncated text goes to the button title +
* #sync-result (the aria-live announcer). */
function fmtSyncLabel(kind, currentFile, done, total) {
const prefix = kind === "upload" ? "Importing" : "Syncing…";
let label = currentFile ? `${prefix} ${currentFile}` : prefix;
@@ -283,11 +288,12 @@ export async function mount(root) {
}
/* The 2 s poll (phase 64 task 04): each tick fetches BOTH jobs — the
* sync AND the background upload scan — and applies the two-job
* decision tree in order (see the section header). The 403 on the SYNC
* fetch hides the button (the whoami backstop); a 403 on the UPLOAD
* fetch is simply "no upload" (never a hide), and a network blip on
* either fetch retries next tick. */
* sync AND the background upload run (phase 90: unpack + register,
* no scan) — and applies the two-job decision tree in order (see the
* section header). The 403 on the SYNC fetch hides the button (the
* whoami backstop); a 403 on the UPLOAD fetch is simply "no upload"
* (never a hide), and a network blip on either fetch retries next
* tick. */
function startSyncPolling() {
if (syncPollTimer !== null) return;
const tick = async () => {
@@ -305,7 +311,8 @@ export async function mount(root) {
applySyncIdle();
return;
}
// The SECOND job: the background upload scan (admin-only surface).
// The SECOND job: the background upload run (phase 90: unpack +
// register only — no scan; admin-only surface).
try {
const ur = await fetch("/api/git-sources/upload/status");
if (ur.ok) uploadStatus = await ur.json();
@@ -406,9 +413,10 @@ export async function mount(root) {
/* Load-time re-attach (ADMIN ONLY): a running run re-enters running
* state, a terminal run renders its last result. Phase 64 (A3): with
* the sync IDLE, an in-flight background upload scan adopts the button
* the same way — the "user clicked upload, then opened sources" case;
* a terminal upload is a no-op (the boot-time loadDocs() already shows
* the sync IDLE, an in-flight background upload RUN (phase 90: unpack
* + register — the bare "Importing…" label) adopts the button the
* same way — the "user clicked upload, then opened sources" case; a
* terminal upload is a no-op (the boot-time loadDocs() already shows
* the current catalog). */
async function initSyncButton() {
if (!syncBtn) return;
@@ -436,7 +444,9 @@ export async function mount(root) {
applySyncFailure(status);
return;
}
// Sync idle: check the SECOND job — an in-flight upload scan re-attaches.
// Sync idle: check the SECOND job — an in-flight upload run
// re-attaches (the bare "Importing…" label — phase 90: unpack +
// register only).
let upload;
try {
const ur = await fetch("/api/git-sources/upload/status");