Skip to content

feat(b1): repoint the upload route to /ingest (PR B of 3, register #51) - #78

Merged
tornidomaroc-web merged 7 commits into
mainfrom
feat/b1-repoint-route-to-ingest
Aug 10, 2026
Merged

feat(b1): repoint the upload route to /ingest (PR B of 3, register #51)#78
tornidomaroc-web merged 7 commits into
mainfrom
feat/b1-repoint-route-to-ingest

Conversation

@tornidomaroc-web

Copy link
Copy Markdown
Owner

(b1) PR B of 3, register #51. src/app/api/ingest/route.ts stops calling the shim and calls /ingest, which has been live since PR A (ee45958). One file under src/. ZERO files under services/. No migration, no SQL, no dashboard change.

DO NOT MERGE. §10 rows V6, V7, V8, V9, V10, V12 are blank, and V12 forbids merging PR B while any PR-A/PR-B row is blank. They are filled by a second commit on this branch after the throwaway run against this PR's preview deployment — not PR A's, per §5.1, because PR A's preview still calls the shim and would green all five while exercising /ingest zero times.


1. Re-authored on current main, not rebased — and that is the point

The pre-split branch feat/b1-railway-owns-chunk-persistence is based on 140bc31, before PR #74. Main has since gained two things that live exactly where this change edits:

  • getServiceUrl() (src/lib/ingestion.ts), which throws in production instead of silently rewriting an unset INGESTION_SERVICE_URL to http://localhost:8000. Preserved here, unchanged.
  • fix(observability): make ingestion failures visible at all four layers (register #54) #74's upstream-cause capture on the non-2xx path — the ${status}: ${detail} string that ended nine days of a 401 outage being indistinguishable from every other failure. Preserved here, folded inside the new guarded write (§2).

Rebasing the old branch would have taken its side of both hunks and silently reverted register #54's fix at the exact moment production starts depending on a new path. That is why this is a re-author.

2. The guarded error write, and why the asymmetry is deliberate

Every post-forward failure writes error through failIfStillProcessing(), which carries .eq('status', 'processing').

Once the request leaves for the ingestion service, that service owns the terminal status — it writes ready before it acks us. So a failure on our side after the forward must never blind-write error. The service may have succeeded and had its ack lost to a timeout, a socket reset, or a platform request cutoff. Stomping a correctly-finished ready row would destroy a document that is fine: the row would report failure to the user while its chunks sat orphaned in the Ask path. The .eq('status','processing') filter makes the write a no-op in exactly that case — the service has already moved the row off processing — while still rescuing the row when the service never got far enough to write anything. PostgREST reports a filtered-out UPDATE as success with zero rows, so "did nothing" and "worked" return identically here, which is correct: both mean the row is already in the state it should be in.

Pre-forward failures keep their unconditional writes, and a reviewer should not read that as an oversight. Before the forward, nothing else can have touched the row — this route inserted it moments ago and no other writer exists. There is no correct state to protect, so an unconditional write is the honest one. The asymmetry tracks exactly one question: can something other than this function have written this row yet?

#54's capture is not weakened by moving inside the guard, and the case split is the reason this is safe:

Service failed… Row state when we write Result
after it could reach the DB (_mark_error ran) already error, with the service's own specific message guard declines — a better diagnosis is not overwritten by a worse one
before it could (401 at its auth gate, 404 from version skew, 503, crash) still processing the ${status}: ${detail} string lands exactly as it did before

And the console.error above it is unconditional in both cases. The status code reaches the logs always, and the database whenever nothing better is already there.

3. What else changes in the route

  • Request shape: document_id and kb_id travel in the form body; the end user's Supabase access token travels in X-Supabase-Token, because Authorization already carries INGESTION_TOKEN. Two credentials, two jobs — service-to-service identity, and the user's database authority. The service is not given a service-role key (register docs: advance the three stale header lines to reality (main @ 63d498c, PR #44) #45: that service was publicly duplicable once, and an RLS-bypassing credential in it turns any exposure into a full-database breach). RLS identity is unchanged by this PR — the writes move process, not principal.
  • Deleted: the local chunk batch-insert loop, the local ready write, and the materialization of every 1024-float embedding plus the full markdown in this function's memory. The service persists all of it.
  • Added: a try/catch around the forward. The in-flight death class previously fell to the outer catch, which logged and returned 500 without touching the row — leaving it at processing forever, with no reaper and no UI path out.
  • Ack validation: a 200 is not taken as success on its own. status === 'ready' and a numeric chunk_count are required before the material_uploaded emit, so a version-skewed service answering 200 with an unrecognised body cannot report a document ready to a user while nothing was persisted.
  • The P5.2 emit stays gated on a confirmed success. What changed is which process did the confirming: the service only acks ready after both its chunk inserts and its documents update landed, and it verifies the update matched a row (an RLS-filtered UPDATE returns 200 with zero rows, not an error).

4. THE TWO-CAUSE HAZARD — accepted, not fixed

Merging this PR swaps the production ingestion image at the same moment production first depends on /ingest. Railway's Watch Paths are empty (register #55), so a merge touching only src/ rebuilds and redeploys services/ingestion from a mutable base tag with unpinned transitive dependencies. The Railway rollback is dead (§8.1.1, register #56): every deployment image is REMOVED, so "roll back" rebuilds from source and restores source, not state. N10's digest pin and hash-locked lockfile are not due until PR C.

This is accepted, not fixed. Stated plainly so nobody discovers it during an incident: if production uploads break after this merge, there are two candidate causes — the route change, and an image rebuilt from different bytes — and no artifact survives to tell them apart.

The detector is the monitor's tick, and its real cost is now measured rather than assumed. production-monitor re-probes /health and the unauthenticated shim on a schedule. Requested cadence is */30; delivered cadence, measured over 18.8 hours, is a mean of ~68 minutes daytime and ~113 minutes full-cycle, with overnight intervals past 3.5 hours (PR #77, docs/b9-queue.md B9-2/B9-3). Not 15 minutes. That is the window in which an ingestion-service regression introduced by this merge stays invisible if it does not surface as a failed upload first.

What is NOT at risk: this PR's own abort path. route.ts is a Vercel deployment and Vercel retains its deployments — abort is Instant Rollback (seconds, no rebuild, exact bundle) or git revert, and the shim is still live because PR C has not run. Neither touches a Railway image. Prefer Instant Rollback: a revert push is itself another main push, and therefore another #55 rebuild.

5. §10 rows

Row Check State
V6 Preview upload via /ingest satisfies Q3 blank — this PR fills, pre-merge
V7 Q4, all six conditions, on the preview upload blank — the production Q4 recorded under V4 does not count
V8 Preview Ask contains QORVANTHIL; Q5 returns rows blank
V9 corrupt.pdf reaches error, 0 chunks; Q6b = 0 blank
V10 material_uploaded = Q7-BEFORE + 1 blank — Q7-BEFORE must be read before the first upload
V12 table committed with no blanks above blank — the merge gate itself

All PR-A rows (V1, V2, V3, V4, V5, V11, N4, N9) are filled and PASS as of PR #76. N5/N6/N8/N10 belong to PR C or after it.

Preconditions before the throwaway run, owner-side: INGESTION_SERVICE_URL and INGESTION_TOKEN must be present and current in Vercel's Preview scope. Preview deployments build with NODE_ENV=production, so getServiceUrl() throws if the URL is unset — and a stale preview token produces a 401 that reads exactly like a code defect. This PR's Vercel check passing proves nothing about either: getServiceUrl() is a function read at request time, not a module-scope const read at build time.

6. Accepted / untested

  • /ingest has still never processed a request anywhere (§7.1). The preview run in §5 is its first-ever exercise, on any environment.
  • _persist's idempotency guard remains unexercised (§7.2) — no flow re-ingests the same document_id.
  • The forwarded access token is short-lived. A conversion outliving it would fail the service's RLS writes and produce an error row with the service's own message, not a silent partial write.
  • tsc, db-types and ingestion-image are vacuous on this PR in the sense that none of them exercises the new call path; tsc does check the ack shape and the removed chunk-insert types. Local: tsc --noEmit clean, eslint clean, and grep -rn "/convert" src/ returns zero hits (N5's future check is not tripped by a comment — see the note at route.ts:11).

**Re-authored on current `main`, not rebased off the pre-split branch.** That branch is based on `140bc31`, before PR #74, and a rebase would have taken its side of two hunks that main has since changed — silently reverting register #54's upstream-cause capture and #74's `getServiceUrl()` production guard, at the exact moment production starts depending on a new path. Both are preserved here.

**The post-forward error write is conditional on `status='processing'`, and the asymmetry is deliberate.** Once the request leaves for the ingestion service, that service owns the terminal status — it writes `ready` before it acks. A failure on our side after the forward must therefore never blind-write `error`: the service may have succeeded and had its ack lost to a timeout, and stomping a correctly-finished `ready` row would destroy a document that is fine, reporting failure to the user while its chunks sit orphaned in the Ask path. The `.eq('status','processing')` filter makes the write a no-op in exactly that case and still rescues the row when the service never got far enough to write anything. **Pre-forward failures keep their unconditional writes** because nothing else can have touched the row yet — there is no correct state to protect.

**#54's capture survives the move.** When the service failed after it could reach the database it has already written `error` with its own, more specific message, and the guard correctly declines to overwrite a better diagnosis with a worse one. When it could not — 401 at its auth gate, 404 from version skew, 503, crash — the row is still `processing` and the `${status}: ${detail}` string lands exactly as before. The `console.error` is unconditional either way.

Also: `document_id`/`kb_id` in the form body and the end user's token in `X-Supabase-Token` (the service is not given a service-role key — register #45); the local chunk batch-insert, the local `ready` write and the in-memory materialization of every 1024-float embedding plus the full markdown are deleted; the forward is wrapped in try/catch, killing the orphan-stuck-at-`processing` class that previously fell to the outer catch untouched; and a 200 is not taken as success until the ack narrows to `status === 'ready'` with a numeric `chunk_count`, so a version-skewed service cannot report a document ready while nothing was persisted. RLS identity is unchanged — the writes move process, not principal.

**NOT MERGEABLE YET.** §10 rows V6, V7, V8, V9, V10 and V12 are blank and V12 forbids merging while any PR-A/PR-B row is. They are filled by a second commit on this branch, from THIS PR's preview deployment (§5.1 — PR A's preview still calls the shim and would green all five while exercising `/ingest` zero times), and only after the Vercel Preview scope is confirmed to carry a current `INGESTION_SERVICE_URL` and `INGESTION_TOKEN`.

**Accepted, not fixed:** merging swaps the production ingestion image (register #55, empty Watch Paths) at the same moment production first depends on `/ingest`, the Railway rollback is dead (#56), and N10's digest pin is not due until PR C — so a post-merge upload failure has two candidate causes and no surviving artifact to separate them. The detector is the monitor's tick, at a MEASURED mean of ~68 min daytime and ~113 min full-cycle, not 15. This PR's own abort path is unaffected: `route.ts` is a Vercel deployment, Vercel retains its deployments, and the shim is still live.

Scope: one file under `src/`. ZERO under `services/`. `tsc --noEmit` clean, `eslint` clean, `grep -rn "/convert" src/` returns zero hits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
knowflow Ready Ready Preview Aug 9, 2026 6:05pm

Written BEFORE the V6-V10 run so these read as conditions, not as post-hoc
justification. §5.1 is not edited; corrections go in §2, per this file.

- §2.6.1 "Confirm email" is now ENABLED, established by evidence: a
  confirmation mail was delivered and its link consumed. §5.1 step 1's
  parenthetical justification is therefore FALSE. Cause recorded: Supabase
  SMTP pointed at a deleted Resend account, so the stored key was dead and no
  confirmation mail was leaving the system at all. DNS was never implicated.
- §2.6.2 The consequent /login claim, CORRECTED. Its earlier unscoped form was
  wrong as stated. It applies to the form-submit step only; the confirmation
  link is observed to land on the dashboard with a live session. The scoped
  version is labelled an UNOBSERVED CODE READING, because it is one.
- §2.6.3 The throwaway account was created on production, not on PR B's
  preview. The claim stated precisely: the only file differing between main and
  PR B that Next compiles or serves is src/app/api/ingest/route.ts, and this
  commit adds no runtime file -- which is what makes signup/page.tsx
  byte-identical on both. Corroborated independently of production's SHA:
  signup/page.tsx unchanged since 48ac550, 2026-07-03.
- §2.6.4 THE UPLOAD ORDER IS LOAD-BEARING and is now written next to the step.
  V9 is satisfied by nearly every post-insert failure path in route.ts, not
  only by a conversion failure: :218-221, :255 and :292 each write a non-null
  error_message with zero chunks WITHOUT /ingest processing anything. So
  corrupt.pdf first against a misconfigured preview yields a false V9 PASS.
  QORVANTHIL first is the discriminator. The :196-200 unset-INGESTION_TOKEN
  branch is recorded as the exception -- it writes no error_message and so
  FAILS V9 rather than faking it; naming it as the example would have been
  wrong.
- §2.6.5 The profiles precondition is kept per register #23, and became LESS
  redundant on 2026-08-08: confirmation-enabled signup makes the client-side
  upsert at signup/page.tsx:42 run with no session, so RLS rejects it with
  42501 and the :48 guard, which swallows 23505 only, routes it to
  console.error. The row now rests entirely on the on_auth_user_created
  trigger.
- §2.6.6 V6-V10 will be owner-attested, not machine-verified: no agent here
  holds a database credential, so §4 runs in the SQL editor and is pasted back
  for adjudication. Same class as V3 and V5. Recorded now so §10's Provenance
  column is not decided at write-up time.

A pointer to §2.6 is added at §5.1 step 1; the step's own text is unedited.

Docs only. No runtime file touched. §10 remains blank; PR B still DO NOT MERGE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…red (register #51)

V9 did not fail. It did not occur. The 9-byte "not a pdf" file mandated by §2.1
was accepted, converted, chunked and embedded, reaching status='ready' with 1
chunk holding exactly those 9 characters. §2.1's clause "reaches MarkItDown,
which raises" is false, and every link after it was conditional on it.

The agent running this verification repeated that claim as the expected code
path and told the owner which exception to watch for, having never run
MarkItDown against that input. §2.7.1 records that, because the defect is not
a wrong document -- it is an unverified assertion passed along a second time
inside the run whose whole purpose is to stop exactly that.

§2.7.2 records the METHOD ahead of the result, because the method is the
transferable part. A local markitdown[all]==0.1.5 harness proves nothing on its
own; it is a different machine with a different dependency resolution, which is
§8.1.1's own finding about this package. What makes it evidence is a CONTROL:
candidate 1 was the 9-byte payload whose production result was already known,
and the discard rule -- if the harness misses the control, throw everything out
-- was fixed before the probe ran. It reproduced production exactly. Six other
payloads then RAISED, every one of them through the extension-driven converter,
which is what identifies the mechanism: MarkItDown sniffs content, and the
plain-text converter is a universal accept for text-like bytes. §2.1's fixture
was defeated by its own readability.

Limit stated rather than left to be found: a calibrated local harness is not the
Railway container, and the control agreeing is evidence about the harness, not
proof about Railway.

§2.7.3 replaces the fixture with 2048 null bytes, md5
c99a74c555371a433d121f551d6c6398, reproducible by `head -c 2048 /dev/zero`.
Random bytes raised identically and were rejected as the fixture precisely
because a future reader cannot regenerate them. Filename deviates to
corrupt2.pdf, because corrupt.pdf now names the junk ready row the invalid
fixture left behind; Q6a's target changes accordingly, per §4's standing
warning about retries putting more than one candidate in range.

§2.7.4 is a BINDING STOP RULE: if the replacement also converts, the run stops.
No third fixture. Two have now been proposed on reasoning and one was wrong.

§2.7.5 records a real product finding -- extension+MIME-only gating at
route.ts:70-88 against a content-sniffing converter, pre-existing and identical
on main, data integrity rather than security -- and corrects the owner's framing
of it BY MEASUREMENT rather than softening it: a structurally destroyed PDF
RAISES as designed, so the hole is type-mismatch, not damage.

Docs only. No runtime file touched. V9 remains unfilled, so §10 still has a
blank PR-B row and V12 cannot be checked. PR B still DO NOT MERGE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tornidomaroc-web and others added 2 commits August 8, 2026 22:21
…cover (register #51)

V9 passes. The replacement fixture from §2.7 -- 2048 null bytes as corrupt2.pdf
-- raised exactly as the calibrated harness predicted, including the
PDFSyntaxError text, and the row landed status=error / embedding_status=error /
chunk_rows=0 with Q6b stuck_processing=0.

THE ROW WORTH READING IS V9's, and not for its four conditions. The
error_message in the database is _mark_error's own string (main.py:257-275) and
is NONE of route.ts's. An unconditional post-forward write would have replaced
it with "ingestion service returned 500: ...". It did not. So the guarded
.eq('status','processing') write at route.ts:179-189 stood down as designed --
the premise of this entire PR observed on the wire rather than read from the
code. The cell is careful about what that means: the guard's EFFECT is observed;
the zero-row UPDATE itself is not directly observable and is not claimed.

V8 came out stronger than its bar. The Ask reproduced the ingested document's
distinguishing account of the batch size -- chunk rows per insert call, chosen
to stay under the REST request-size ceiling rather than for throughput -- which
CONTRADICTS the substitute document authored for this run and never ingested.
The answer could only have come from the artifact actually in the database.

V10 keeps its absolute form: before_count was read, not assumed, and n=1 with
first_at = last_at. It carries an emit-timing corroboration (2.124s after the
document's created_at, consistent with the emit firing on the service's ack at
route.ts:329) and explicitly declines to claim anything about failed uploads,
which were never re-read.

Two artifacts in the throwaway account are documented against misreading. The
QORVANTHIL file is the one authored in an EARLIER session, not the one written
at the start of this run; §2.3 mandates properties rather than bytes, the
substitution was caught before any row was recorded, and V8 was adjudicated
against documents.markdown_content and nothing else. And there is a corrupt.pdf
row sitting at ready with nine bytes of content: that is §2.1's mandated fixture
FAILING TO FAIL, retained deliberately so the finding is inspectable, and it is
why the working fixture had to be named corrupt2.pdf.

§7.4 is the counterweight to a green §10 and is the reason this commit is not
just good news. V9 exercised ONE failure branch. The RESCUE half of the same
guard -- the orphan-stuck-at-processing killer this whole change exists for --
has never executed anywhere, nor have :255, :310, :196-200 or :218-221.
Accepted as untested; PR B is not held on it; recorded so V1-V12 cannot be read
as more than they are.

Also: §2.1's line references drifted (:59-76 to :59-88, :71 to :83), no
substantive change.

All five rows are owner-attested per §2.6.6. Two Railway deployment IDs remain
⟨FILL-IN: dashboard⟩ and §10 forbids PR B moving until they are resolved.

Docs only. No runtime file touched. V12's row is filled by the follow-up commit,
since it cannot precede itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ister #51)

V12 cannot precede itself, so its cell names 1fed6e5 as the commit that filled V6-V10. Recorded as CONDITIONAL, not PASS: V12's stated condition is that the table is committed to main, and this is a branch. Two Railway serving-deployment IDs are still (FILL-IN: dashboard), which §10 says must be resolved before PR B moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gister #51)

Every other transition in this sequence carries a check: PR A's image going live has V1/V2/V4/V5/N4/N9, PR C's shim deletion has N5/N6. PR B's has none - and PR B is the merge at which production changes which endpoint it calls. The next production upload check is N6, at PR C post-merge, so the window between them is verified only by inference from a preview. production-monitor does not close it: it probes /health and /convert's auth posture and never uploads. The absence is asymmetric with the rest of this file, which is what makes it a gap rather than an addition.

N11 IS NOT A MERGE GATE, and §3.1 says so in its first paragraph. §8's binary stays V1-V12 unchanged. N11 is a post-merge observation in the class of N1/N4/N9, and it cannot gate a merge it is defined to run after.

Its remedy is named concretely rather than delegated to TRIGGER 2, whose remedy is Railway-shaped and does not apply: PR B changes a Vercel deployment, Vercel RETAINS its deployments, so the rollback is to the immediately preceding production deployment - a genuine return to a known-good artifact, unlike the Railway rebuild §8.1.1 documented. Followed by a git revert PR and a §7 entry, per register #39.

Docs only. No runtime file touched. Nothing merged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… ALIVE (register #51)

V6 and V9 both name Railway deployment 6e03e8bd (service knowflow, project
athletic-miracle, environment production), built from 365ac24, created
2026-08-03T13:17Z. V7, V8 and V10 read "Same as V6" and resolve with it. The §10
preamble's tally is corrected from two FILL-IN markers to four: two were resolved
2026-08-02 by B8, these two today. No FILL-IN remains, which is the condition §10
states must hold before PR B moves.

THE MANDATORY MARKER IS WRITTEN IN THE FUTURE TENSE, BECAUSE THE PAST TENSE WOULD
HAVE BEEN A LIE. Every other row in §10 carries ARTIFACT REAPED as an accomplished
fact. This deployment read ACTIVE today -- "Deployment successful", 1 replica, US
East, every deployment below it in HISTORY marked REMOVED -- so the cell records
ARTIFACT REAPED - SCHEDULED, NOT YET TRUE AT TIME OF WRITING, and names PR #78's
own merge as the thing that reaps it (empty Watch Paths, register #55). The point
of saying so in the cell is that a later reader will find 6e03e8bd marked REMOVED
and must be able to tell a PREDICTED end state from a falsified record.

THE IDENTIFICATION IS REPO-ENTAILED, NOT MERELY ATTESTED, AND THAT IS THE PART
WORTH KEEPING. 365ac24's commit timestamp is 2026-08-03T13:17:54Z -- the same
minute as the deployment's creation -- and 365ac24 is STILL main's head, so no
commit landed on main between the build and the 2026-08-08 run. With Watch Paths
empty every push rebuilds; there was no push, so no supersession was possible and
exactly one deployment could have served those rows. That argument is checkable
from a clone. The dashboard read agrees with it rather than carrying it.

LIMIT STATED RATHER THAN LEFT TO BE FOUND. The monitor's paired probes (GET
/health 200, POST /convert 401) repeat in this deployment's live log and
corroborate CONTINUOUS SERVING across the window. The log was NOT read for the
V6-V10 upload events themselves. So this cell deliberately claims less than the
16ea983d cell, whose deploy log carried the very events its row asserts.

NOT CAPTURED, AND RECORDED AS NOT CAPTURED: build duration and trigger label. §10
names the deployment ID as the only handle on both. After this PR merges they are
no longer readable from a live deployment.

ONE EDIT BEYOND THE BRIEF, NAMED RATHER THAN SLIPPED IN. V12's row enumerated TWO
blockers, one of which was the FILL-IN cells this commit just removed. Its result
stays CONDITIONAL and V12 is NOT resolved -- its condition is "committed to main"
and this is a branch. Only the count of what remains is corrected, from two
blockers to one (the merge). Leaving it would have shipped a row asserting a
blocker the same commit eliminated, which is the defect this table exists to catch.

Docs only. One file. No runtime file touched, zero files under services/.
PROGRESS.md is deliberately untouched -- the #55/#56 register rows, #54's stale
monitoring claim and the header refresh are all step 4, after N11, so that exactly
one unaudited ingestion rebuild sits between the last known-good state and the
first real production upload through the new path. V12 is deliberately NOT
resolved: its condition is "committed to main", and this is a branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tornidomaroc-web tornidomaroc-web changed the title feat(b1): repoint the upload route to /ingest (PR B of 3, register #51) — DO NOT MERGE, §10 blank feat(b1): repoint the upload route to /ingest (PR B of 3, register #51) Aug 10, 2026
@tornidomaroc-web
tornidomaroc-web merged commit 3cd91bd into main Aug 10, 2026
5 checks passed
@tornidomaroc-web
tornidomaroc-web deleted the feat/b1-repoint-route-to-ingest branch August 10, 2026 14:49
tornidomaroc-web added a commit that referenced this pull request Aug 10, 2026
…B9-2 (register #51) (#79)

**(b1) register #51 — DOCS-ONLY. ZERO files under `src/`. ZERO under `services/`.** No migration, no SQL, no dashboard change, no workflow change, no Watch Paths change, no base-image pin.

**PR B (#78) merged at `3cd91bd` and N11 PASSES.** This PR is the record of that, plus the two findings the run itself produced.

---

## 1. N11 — PASS, and it is an observation, not an eleventh gate

Document `bfd6e9d5-9e95-4739-8579-7fca784f81dc` (`xilvaroth-n11-20260810.md`, 1177 bytes), subject `43387669-8189-4970-96aa-9499464291df`, **created empty with its id recorded before any file existed in it**. All five Q3 conditions: `status='ready'`, `embedding_status='ready'`, `chunk_count=1` = `actual_chunk_rows=1`, `has_markdown=true`, `error_message` null. **Adjudicated by `document_id`, not by position.**

**V12 was discharged by the merge itself**, exactly as its CONDITIONAL cell predicted. **N11 is a post-merge observation with a rollback condition (§3.1)** — PR B merged on V1–V12 and nothing else.

**Corroboration, not a condition:** `markdown_len` 1175 from 1177 source bytes is the **same 2-character delta V6 saw** (1169 from 1171). The converter behaved identically in production and preview.

**The drift check passed and is recorded as a pass, not as an absence.** `/health` read immediately before the upload was byte-identical to the pre-merge capture on all four fields, including `embed_provider` and `embed_model`, which the monitor does not assert.

## 2. Two findings from the run — the reason this PR is not just a filled cell

**Register #57 opened — "the real account" is not a defined term.** N11's first Q3 ran against `72ebd7b5-…`, derived from V4's document `6c0ff12c` exactly as this protocol suggests, and **returned zero rows**. The N11 subject and document are owned by `d6fff4eb-…`. It was our record that was wrong, not the product, and **the upload was not repeated** — the finding was reached by widening the search.

**The hazard is a false rollback arrived at by obeying the document.** A zero-row Q3 is indistinguishable from a failed upload, and §3.1's remedy for a failed N11 is a **Vercel production rollback**. The next reader who runs a production check against the wrong UID rolls back production on a query artefact. **Which account is canonical is unknown and is not guessed at; both UUIDs are recorded and neither is blessed.** The row opens with a binding rule: **every production check must record the `user_id` it ran against.**

**The UI said `ready` with a chunk count before any query ran.** Adjudicating on the screen would have passed N11 on `chunk_count` — a column the pipeline **writes** — without ever comparing it to `actual_chunk_rows`, which is what the chunk table **contains**. A green screen is the pipeline reporting on itself, the same shape as V9's nine-byte `not a pdf` reaching `ready`.

## 3. Register #55 confirmed a second time, and `6e03e8bd` reaped on schedule

PR #78 touched `src/` and `docs/` with **zero files under `services/`** and still reaped `6e03e8bd` in favour of **`35cbf150`**. **This is corroboration, not a first observation** — #55's mechanism was already confirmed on 2026-08-02 via PR #74. What is new is the **before/after deployment pair** and a **machine-readable GitHub commit status** on `3cd91bd` carrying the new deployment id, reproducible with `gh api`, rather than a dashboard read alone.

`6e03e8bd`'s `ARTIFACT REAPED — SCHEDULED, NOT YET TRUE` marker **came true on schedule**, which is the convention working as designed. `35cbf150` now carries the same SCHEDULED marker — and **this PR's own merge is the push that discharges it**, since a docs-only merge rebuilds the ingestion image under empty Watch Paths exactly as #78's did. **N11's artifact is therefore reaped by the very PR that records N11.** The §10 cell says "the next push to `main` reaps it" and this is that push; a later reader finding `35cbf150` REMOVED is seeing the predicted end state, not a falsified record.

## 4. Scope beyond N11, and why each item is here rather than deferred

- **Registers #55 and #56 get §4 rows for the first time.** Both were opened in the 2026-08-02 §7 block and **never had a row**, so for eight days they were invisible to anyone reading the register as a register. A register that exists only inside a changelog paragraph is not findable.
- **B9-2 CLOSED.** All four `b1-verification-protocol.md` cadence sites corrected to the measured figure; the fifth, immutable `PROGRESS.md:217`, is **superseded by the new §7 block rather than edited** — the mechanism B9-2 itself specified. **Included here rather than deferred because leaving four known-false sentences in place, inside the very PR whose §7 block records the sixth instance of false claims surviving in records, was not defensible.** Their line numbers had drifted by PR #78's +360 lines and were re-located by content, not by number.
- **B9-4 OPENED.** `production-monitor` asserts `ok`, `embed_dim` and `supabase_configured` but **never `embed_provider` or `embed_model`**. Under #56's mutable base tag and #55's every-push rebuild, a model swap that kept the dimension at 1024 would write foreign vectors into the same column, pass all three assertions, and silently degrade retrieval against every chunk already stored. Not fixed here — it edits a workflow file, and this is docs-only.
- **B9-3 recorded as OVERDUE**, six days readable and unread, against a row whose own closing sentence forbids exactly that. B9-2's sites were corrected using the **baseline** figures, not the `*/30` result, because that result does not exist yet.

## 5. The PR #78 title incident — the sixth instance, and the count is checkable

PR #78's title carried `— DO NOT MERGE, §10 blank` into the merge window. This repo squash-merges with the PR title as the commit subject, so the string was one command away from being permanent in `main`'s history; the body's opening paragraph and §5 table said the same. **§10 had been filled by `1fed6e5`, `25578be` and `3ee4099` and read "No `⟨FILL-IN⟩` remains" at the merge SHA.** The title was corrected and the body rewritten to **strike the hold and explain it**, so the history does not read as a bypassed gate.

The five priors are enumerated in the §7 block so "sixth" is checkable rather than asserted: the 07-23 #45 rotation claim; §8.1's no-rebuild rollback; §5.1's "Confirm email is disabled"; §2.1's V9 fixture; the `~15 minute` cadence claims. **The common mechanism: every one was true when written, and none had an owner when it stopped being true.**

## 6. Verification honesty

**N11 is `owner-attested`, not machine-verified.** The repository owner ran the SQL and pasted the raw row; **no agent here queried the database**, and none could — no agent holds credentials for it. The merge, the deployment identities, the commit statuses and the fixture bytes **are** machine-verified and reproducible with `gh api`, `git` and a hash. The `/health` comparison is owner-attested.

**`XILVAROTH` was verified absent from the entire repo; it was NOT verified absent from the database**, and that limit was stated before the upload rather than after.

`tsc` and `db-types` **passed vacuously** — confirmed green on `d2e0fc1`, along with `ingestion-image` and both Vercel checks (5/5) — because this PR contains zero TypeScript and zero SQL. Their passing is not evidence for anything in it.

## 7. Merge-order warning, not a request

**`chore/b1c-remove-convert-shim` is 2 ahead and 6 behind `3cd91bd`, and it falls one further behind with this merge and with every merge after it.** It touches `docs/PROGRESS.md` and `docs/b1-verification-protocol.md` — **both files this PR edits.** (Stated against a fixed SHA rather than against `main`, because "6 behind `main`" is false the moment this PR lands, and a merge-order warning that expires at the merge is worth nothing.) Rebasing or merging PR C's branch without re-authoring would take its side of these hunks and silently revert N11's row and this §7 block, which is **exactly the failure PR B avoided by re-authoring rather than rebasing** (#78 §1). PR C should be re-authored on `main` after this lands.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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