feat(ingestion): clips land in web-clip by rule; shared concepts/entities; approve parked notes; remove the classifier (#208, #209) #211

Merged
mathias merged 16 commits from wt/web-clip into main 2026-10-20 19:36:51 +00:00
Owner

Sprint #209, PR-B. Refs #208 #209 #205. Risk: medium (changes where ingestion files notes and what it parks; off by default behind INGEST_WING_ROUTING=1; removes ~1,900 lines of the measured-failed classifier path). The Router is a refuse/park control (client-marker text, unknown wing), so it needs the adversarial pass.

Goal as behaviour (acceptance tests, real pipeline + file layout, model stubbed)

Drop a clip in raw/, do nothing else -> a page in wiki/web-clip/sources/, its entity/concept updated once in the SHARED wiki/entities/wiki/concepts, raw/ holds nothing unprocessed, nothing waits for a human. Client-marked text and a typo'd wing folder are parked instead.

What

  • wing.Router (no model): a deliberate raw/<wing>/ folder or wing: frontmatter must be a plain name of an EXISTING wing (typo never creates one) and then places, whatever the wing; disagreeing signals park; with no signal, text matching the client pattern parks, otherwise the default wing web-clip (the one wing created on first use). Wings are read from disk on every decision. NewRouter fails at startup on a bad default or pattern. Policy change vs #207: a deliberate folder into a client wing places (extraction runs on the local model; it is the owner's act). Flag it if you disagree.
  • Pipeline in wing mode: source -> wiki/<wing>/sources/ (wing/hall frontmatter); concepts/entities stay in the shared dirs, so an entity cited from two wings is ONE page. A source slug already used anywhere but its own wing's sources/ is refused (wiki.FindSlug).
  • brain_promote/brain_reject act on notes parked in raw/needs-review/ (they could not; the documented approval path did not exist); promote drops the proposal keys.
  • Server: INGEST_WING_ROUTING=1, INGEST_DEFAULT_WING (web-clip), INGEST_CLIENT_BLOCK (else CLAUDE_INGEST_CLIENT_BLOCK); bad config is fatal.
  • Removed: LLM classifier, the substring "BM25" scorer, the agreement gate, backtest, wingbacktest, and the old migrate/wingmigrate (built for the per-wing facts/ layout this replaces; migration is rebuilt in PR-C).

Evidence

  • Tests first at every step; go vet ./..., go test -race ./... exit 0, 0 FAIL (grep-checked, &&-chained), lint 0.
  • Mutation-checked: Router client block / known-wing / conflict / plain-name / default; collision guard; shared inventory; per-wing source path; needs-review fallback; proposal-key drop.
  • A test of mine that passed under its mutant was strengthened in this PR's ancestry (#210); here the "same entity, two sources" acceptance test initially failed because my stub gave both clips one title, which the pipeline deliberately merges: test fixed, behaviour unchanged.

Not verified / known

  • Shared pages cite sources by title/slug from every wing. A deliberate folder into a sensitive wing therefore puts that source's title into a shared entity page. The brain's own rule is that client material does not belong in it at all; this PR does not enforce more than the content pattern for unsignalled notes.
  • The client pattern is regex over note text; it catches what it names and nothing else.
  • Legacy .processed markers on the live brain are now handled by #210 (stale -> ignored), but the 12 old originals and legacy pages are untouched until PR-C.
  • brain_promote on a parked note files it as a plain note; it does not run the extraction pipeline on it. Approving a parked clip into extraction is moving it into raw/<wing>/.

Review ask

By execution: (1) any input that makes the watcher write into a wing that does not exist, or outside wiki//sources + shared dirs (folder names, frontmatter values, unicode, CRLF, symlinked folders); (2) the client pattern bypasses (case, unicode, text in frontmatter only, split across chunks); (3) shared-page corruption: two wings ingesting the same entity concurrently/sequentially, title collisions between a source and an entity (FindSlug guard), a source slug equal to an entity slug; (4) flag off: behaviour identical to main for raw/ notes; (5) promote/reject on needs-review with name clashes, symlinked dir, and a parked note whose frontmatter lies; (6) anything lost or leaked by deleting the classifier path (grep for dangling references, env vars, docs).

🤖 Generated with Claude Code


Created via git-mcp on behalf of @mathias

Sprint #209, PR-B. Refs #208 #209 #205. Risk: **medium** (changes where ingestion files notes and what it parks; **off by default** behind `INGEST_WING_ROUTING=1`; removes ~1,900 lines of the measured-failed classifier path). The Router is a refuse/park control (client-marker text, unknown wing), so it needs the adversarial pass. ## Goal as behaviour (acceptance tests, real pipeline + file layout, model stubbed) Drop a clip in `raw/`, do nothing else -> a page in `wiki/web-clip/sources/`, its entity/concept updated once in the SHARED `wiki/entities`/`wiki/concepts`, `raw/` holds nothing unprocessed, nothing waits for a human. Client-marked text and a typo'd wing folder are parked instead. ## What - `wing.Router` (no model): a deliberate `raw/<wing>/` folder or `wing:` frontmatter must be a plain name of an EXISTING wing (typo never creates one) and then places, whatever the wing; disagreeing signals park; with no signal, text matching the client pattern parks, otherwise the default wing `web-clip` (the one wing created on first use). Wings are read from disk on every decision. `NewRouter` fails at startup on a bad default or pattern. **Policy change vs #207: a deliberate folder into a client wing places** (extraction runs on the local model; it is the owner's act). Flag it if you disagree. - Pipeline in wing mode: source -> `wiki/<wing>/sources/` (wing/hall frontmatter); concepts/entities stay in the shared dirs, so an entity cited from two wings is ONE page. A source slug already used anywhere but its own wing's `sources/` is refused (`wiki.FindSlug`). - `brain_promote`/`brain_reject` act on notes parked in `raw/needs-review/` (they could not; the documented approval path did not exist); promote drops the proposal keys. - Server: `INGEST_WING_ROUTING=1`, `INGEST_DEFAULT_WING` (web-clip), `INGEST_CLIENT_BLOCK` (else `CLAUDE_INGEST_CLIENT_BLOCK`); bad config is fatal. - Removed: LLM classifier, the substring "BM25" scorer, the agreement gate, backtest, `wingbacktest`, and the old `migrate`/`wingmigrate` (built for the per-wing `facts/` layout this replaces; migration is rebuilt in PR-C). ## Evidence - Tests first at every step; `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL (grep-checked, `&&`-chained), lint 0. - Mutation-checked: Router client block / known-wing / conflict / plain-name / default; collision guard; shared inventory; per-wing source path; needs-review fallback; proposal-key drop. - A test of mine that passed under its mutant was strengthened in this PR's ancestry (#210); here the "same entity, two sources" acceptance test initially failed because my stub gave both clips one title, which the pipeline deliberately merges: test fixed, behaviour unchanged. ## Not verified / known - **Shared pages cite sources by title/slug from every wing.** A deliberate folder into a sensitive wing therefore puts that source's title into a shared entity page. The brain's own rule is that client material does not belong in it at all; this PR does not enforce more than the content pattern for unsignalled notes. - The client pattern is regex over note text; it catches what it names and nothing else. - Legacy `.processed` markers on the live brain are now handled by #210 (stale -> ignored), but the 12 old originals and legacy pages are untouched until PR-C. - `brain_promote` on a parked note files it as a plain note; it does not run the extraction pipeline on it. Approving a parked clip *into extraction* is moving it into `raw/<wing>/`. ## Review ask By execution: (1) any input that makes the watcher write into a wing that does not exist, or outside wiki/<wing>/sources + shared dirs (folder names, frontmatter values, unicode, CRLF, symlinked folders); (2) the client pattern bypasses (case, unicode, text in frontmatter only, split across chunks); (3) shared-page corruption: two wings ingesting the same entity concurrently/sequentially, title collisions between a source and an entity (`FindSlug` guard), a source slug equal to an entity slug; (4) flag off: behaviour identical to main for raw/ notes; (5) promote/reject on `needs-review` with name clashes, symlinked dir, and a parked note whose frontmatter lies; (6) anything lost or leaked by deleting the classifier path (grep for dangling references, env vars, docs). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Created via git-mcp on behalf of @mathias_
mathias added 7 commits 2026-10-20 18:20:18 +00:00
No model. A subfolder/frontmatter wing must be a plain name of an existing wing
and then places; conflicting signals and client-marker text park; everything
else goes to the default wing. Wings are read from disk per decision.
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With Config.Wing a source goes to wiki/<wing>/sources/ (wing/hall frontmatter);
concepts and entities stay in the shared wiki/concepts and wiki/entities, so an
entity cited from two wings is ONE page with both sources (the per-wing facts/
layout duplicated it). A source slug already used anywhere but its own wing's
sources/ is refused (slug is the graph identity). wiki.FindSlug added.
Mutation-checked: collision guard, shared inventory, per-wing source path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A clip dropped in raw/ with no other action lands in web-clip, its entity and
concept are the shared pages, raw/ is empty and nothing waits for a human;
client-marked text and a typo'd wing name park; a deliberate subfolder places.
Real pipeline and file layout, model stubbed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brain_pending lists a parked note under its bare filename, but promote/reject
only read raw/<name>, so the documented approval path did not exist. They now
fall back to raw/needs-review/ (top level wins on a name clash; a symlinked
directory is refused) and promote drops the resolver's proposal keys.
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Removes the LLM classifier, the substring 'BM25' scorer, the agreement gate,
the backtest and the old migrate/wingmigrate tooling: measured at 2 of 55
pages placed (#208), and the migration was built for the per-wing facts layout
this branch replaces. wing now holds the decision types, the Router and
KnownWings. INGEST_WING_ROUTING=1 builds the Router (default wing
INGEST_DEFAULT_WING=web-clip, client pattern from INGEST_CLIENT_BLOCK or
CLAUDE_INGEST_CLIENT_BLOCK); a bad default or pattern is fatal at startup.
Watcher tests rewritten for the Router, including a deliberate subfolder into
any existing wing, typo'd folders parked, forged-key and edit-while-placing
cases.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into wt/web-clip
CI / Mirror to GitHub (pull_request) Skipped
CI / Lint / Test / Vet (pull_request) Successful in 1m13s
3ccf6b46ac
mathias added 1 commit 2026-10-20 18:23:03 +00:00
fix(pipeline): a slug used in two places is refused even when one is the wing's own (#208)
CI / Lint / Test / Vet (pull_request) Successful in 1m8s
CI / Mirror to GitHub (pull_request) Skipped
8e488b6e4b
FindSlug returned the first hit, so a wing's own page of a name masked another
page of the same name (the retired dirs are searched before the wings, and wings
alphabetically). FindSlugs returns every match and callers refuse any that is
not their own. Found by the legacy-move planner's own test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
mathias added 3 commits 2026-10-20 18:34:54 +00:00
- a source whose slug is taken by anything but this wing's own page gets a
  wing-qualified, stable slug (talk-homelab) instead of failing the ingest
- an extracted entity/concept whose slug belongs to a different page is skipped
  with a warning, so one page can no longer lock a source out of re-ingest
- in a wing only source/concept/entity are valid page types; the model (and so
  the clip) can no longer choose wiki/<type>/ directories
Mutation-checked; stable-slug choice pinned directly because title resolution
masks it at the Run level.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- client pattern is matched against the text and against an NFKC-folded,
  format-character-free, dash-normalised, whitespace-free form
  (non-breaking hyphen, zero-width characters, soft hyphen, line splits,
  fullwidth letters). x/text/unicode/norm was already in the module graph as an
  indirect dependency; this makes it direct, no new download.
- folder/frontmatter wing names must be ASCII (strings.ToLower folds the Kelvin
  sign to 'k') and must not be sources/concepts/entities/wiki; such names are
  never proposed as a wing, and a reserved default is a startup error
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- brain_promote/brain_reject take an optional queue (inbox | needs-review); a
  name present in both is refused instead of silently acting on the top-level
  note; brain_pending labels every entry with its queue
- parked pdf/txt files are listed (they were invisible); only markdown can be
  promoted, the others are moved or rejected
- promote refuses sources/concepts/entities/wiki as a wing
- frontmatterWing reads only a top-level key inside a properly fenced block
- docs/raw-inbox.md: the contract, the three env vars, the known limits
Each behaviour has a test that fails without it (mutation-checked).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Independent review pass 1 (pr-gatekeeper agent, by execution; agent, not four-eyes): BLOCK. Resolution at 87c40fe (every fix has a test that failed first and a mutant that fails it):

  1. HIGH entity/concept whose slug equals another wing's source locks that source out of re-ingest -> such a page is now skipped with a warning (dropSlugClashes); the ingest succeeds; the first wing can still re-ingest.
  2. HIGH same filename in two wings hard-failed the second note -> the second source gets a stable wing-qualified slug (notes-homelab, -2...), pinned directly by TestResolveSourceSlug (title resolution masks the choice at the Run level).
  3. MEDIUM a model-chosen type wrote outside the layout (sources -> retired dir, <wing> -> loose file in another wing) -> in a wing only source|concept|entity are valid types.
  4. MEDIUM parked pdf/txt invisible to brain_pending -> listed (with a pointer to log.md); only markdown can be promoted, the rest moved or rejected.
  5. MEDIUM same-named top-level and parked note: brain_promote silently acted on the top-level one -> refused when ambiguous; new optional queue (inbox|needs-review) on brain_promote/brain_reject/REST; brain_pending labels every entry with its queue.
  6. MEDIUM proposed_wing: sources|concepts|entities|wiki -> reserved names are never proposed or accepted (router, default, promote).
  7. LOW client filter bypassable (non-breaking hyphen, zero-width chars, soft hyphen, line split, fullwidth letters) -> matched against an NFKC-folded, format-char-free, dash-normalised, whitespace-free form. Dependency note: golang.org/x/text was already in the module graph as indirect; now direct, nothing new downloaded.
  8. LOW Kelvin-sign folder resolved to a real wing -> non-ASCII wing names are refused (strings.ToLower folds U+212A to k).
  9. LOW indented/unfenced wing: routed -> only a top-level key inside a properly fenced block counts.
  10. LOW shared-page merge refreshes origin/last_updated to the latest ingest and the first wing's attribution is lost -> not changed, documented in docs/raw-inbox.md. Also not changed: a deliberate folder (wing: web-clip + client text) places by design (explicit wins); explicit-wing notes bypass the client filter.

Also: promote into a brand-new wing still creates it (existing promote rule, unchanged). New docs/raw-inbox.md documents the contract, the env vars (INGEST_WING_ROUTING, INGEST_DEFAULT_WING, INGEST_CLIENT_BLOCK) and the limits; no manifest sets the flag, so it is off in production. Gate: go vet ./..., go test -race ./... exit 0, 0 FAIL, lint 0.

Merge: I am NOT merging this under the sprint's standing approval. It is control code (refuse/park decisions) and I misapplied the risk lane on #210; this one is left for you to classify and merge.


Created via git-mcp on behalf of @mathias

**Independent review pass 1 (pr-gatekeeper agent, by execution; agent, not four-eyes): BLOCK. Resolution at 87c40fe** (every fix has a test that failed first and a mutant that fails it): 1. HIGH entity/concept whose slug equals another wing's source locks that source out of re-ingest -> such a page is now **skipped with a warning** (`dropSlugClashes`); the ingest succeeds; the first wing can still re-ingest. 2. HIGH same filename in two wings hard-failed the second note -> the second source gets a **stable wing-qualified slug** (`notes-homelab`, `-2`...), pinned directly by `TestResolveSourceSlug` (title resolution masks the choice at the Run level). 3. MEDIUM a model-chosen `type` wrote outside the layout (`sources` -> retired dir, `<wing>` -> loose file in another wing) -> in a wing only `source|concept|entity` are valid types. 4. MEDIUM parked pdf/txt invisible to `brain_pending` -> listed (with a pointer to log.md); only markdown can be promoted, the rest moved or rejected. 5. MEDIUM same-named top-level and parked note: `brain_promote` silently acted on the top-level one -> **refused when ambiguous**; new optional `queue` (`inbox`|`needs-review`) on brain_promote/brain_reject/REST; `brain_pending` labels every entry with its queue. 6. MEDIUM `proposed_wing: sources|concepts|entities|wiki` -> reserved names are never proposed or accepted (router, default, promote). 7. LOW client filter bypassable (non-breaking hyphen, zero-width chars, soft hyphen, line split, fullwidth letters) -> matched against an NFKC-folded, format-char-free, dash-normalised, whitespace-free form. Dependency note: `golang.org/x/text` was already in the module graph as indirect; now direct, nothing new downloaded. 8. LOW Kelvin-sign folder resolved to a real wing -> non-ASCII wing names are refused (`strings.ToLower` folds U+212A to `k`). 9. LOW indented/unfenced `wing:` routed -> only a top-level key inside a properly fenced block counts. 10. LOW shared-page merge refreshes `origin`/`last_updated` to the latest ingest and the first wing's attribution is lost -> **not changed**, documented in docs/raw-inbox.md. Also not changed: a deliberate folder (`wing: web-clip` + client text) places by design (explicit wins); explicit-wing notes bypass the client filter. Also: promote into a brand-new wing still creates it (existing promote rule, unchanged). New `docs/raw-inbox.md` documents the contract, the env vars (`INGEST_WING_ROUTING`, `INGEST_DEFAULT_WING`, `INGEST_CLIENT_BLOCK`) and the limits; no manifest sets the flag, so it is off in production. Gate: `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL, lint 0. **Merge:** I am NOT merging this under the sprint's standing approval. It is control code (refuse/park decisions) and I misapplied the risk lane on #210; this one is left for you to classify and merge. --- _Created via git-mcp on behalf of @mathias_
mathias added 3 commits 2026-10-20 18:49:12 +00:00
dropSlugClashes only compared against what was on disk, so a clip named like an
entity the model extracted wrote both pages and neither could be updated again.
In-batch winners: source over entity over concept. Against the brain, only
entities/concepts are skipped; the source's slug was already chosen free or its
own by resolveSourceSlug, so an existing bad pair no longer locks the source out.
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Views of each chunk: as is; NFKD + marks/format/fillers dropped + Cyrillic/Greek
lookalikes mapped + dashes normalised, with whitespace collapsed (acme corp,
\s) or removed (acme-\nclient); and a soft hyphen read as a dash. Chunks of 1 MiB
with a 4 KiB overlap bound the folding's allocation (it was ~14x the input: 919
MiB for a 64 MiB note). Documented limits: leetspeak, punctuation splits,
encodings and homoglyphs outside a small table. Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
fix(api,watcher,server): reject moves non-markdown untouched; read caps; BOM; strict routing flag (review of #211, pass 2)
CI / Lint / Test / Vet (pull_request) Successful in 1m2s
CI / Mirror to GitHub (pull_request) Skipped
49286ba8b8
- brain_reject on a parked pdf/txt moves the bytes; it no longer prepends YAML
- raw/ notes are read through a cap (readCapped); an endless reader test fails
  instead of exhausting memory
- a byte-order mark no longer hides a note's wing: key
- INGEST_WING_ROUTING is parsed strictly; a typo stops the server rather than
  leaving routing silently off
- tests for the reserved-wing check in every disguise (Sources, sources., ...)
Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Independent review pass 2 (pr-gatekeeper agent, by execution): BLOCK at 87c40fe. Resolution at the head above (tests first, mutation-checked):

  1. HIGH in-batch slug clash (a clip named notes.md whose model output also has an entity "Notes" wrote two pages with one slug, and neither could be updated again) -> dropSlugClashes now resolves clashes inside the batch (source over entity over concept) and, against the brain, never skips the SOURCE page (its slug was already chosen free or its own); an existing bad pair no longer locks the source out. Also covers concept+entity with one title in a batch.
  2. MEDIUM client filter vs realistic patterns (acme corp with NBSP/double space/newline, \s+, \b) and soft hyphen as the dash, homoglyphs (Cyrillic a/e/o/c), combining marks, Hangul/braille fillers, grapheme joiner -> folded in several views (NFKD, marks/format/fillers dropped, lookalikes mapped, dashes normalised; whitespace collapsed in one view, removed in another; soft hyphen read as a dash in a third). Still NOT caught, stated in the docs: leetspeak, punctuation splits (a.c.m.e), encodings, homoglyphs outside a small table.
  3. MEDIUM-LOW memory (folding allocated ~14x the input: 919 MiB for 64 MiB) -> checked in 1 MiB chunks with a 4 KiB overlap (a marker on a boundary is found; tested at 9 offsets and over 2-byte runes). Not measured as RSS.
  4. LOW BOM hid wing: -> stripped. 5. LOW brain_reject on parked pdf/txt prepended YAML -> non-markdown moves byte for byte; readRawNote is now capped (64 MiB, endless-reader test that fails instead of draining memory). 7. LOW test gap (reserved-wing check without Sanitise passed all tests) -> tests for Sources, sources., SOURCES , Entities/, concepts\, wiki. INGEST_WING_ROUTING=true silently left routing off -> parsed strictly, a typo is a startup error.
  5. LOW determinism of the qualified slug: accepted as the reviewer described (stable while the page exists; bounded at 100 candidates).

Gate: go vet ./..., go test -race ./... exit 0, 0 FAIL, lint 0. Next: a third independent pass; merge stays with you (control code).


Created via git-mcp on behalf of @mathias

**Independent review pass 2 (pr-gatekeeper agent, by execution): BLOCK at 87c40fe. Resolution at the head above** (tests first, mutation-checked): 1. **HIGH in-batch slug clash** (a clip named `notes.md` whose model output also has an entity "Notes" wrote two pages with one slug, and neither could be updated again) -> `dropSlugClashes` now resolves clashes **inside the batch** (source over entity over concept) and, against the brain, never skips the SOURCE page (its slug was already chosen free or its own); an existing bad pair no longer locks the source out. Also covers concept+entity with one title in a batch. 2. **MEDIUM client filter** vs realistic patterns (`acme corp` with NBSP/double space/newline, `\s+`, `\b`) and soft hyphen as the dash, homoglyphs (Cyrillic a/e/o/c), combining marks, Hangul/braille fillers, grapheme joiner -> folded in several views (NFKD, marks/format/fillers dropped, lookalikes mapped, dashes normalised; whitespace collapsed in one view, removed in another; soft hyphen read as a dash in a third). Still NOT caught, stated in the docs: leetspeak, punctuation splits (`a.c.m.e`), encodings, homoglyphs outside a small table. 3. **MEDIUM-LOW memory** (folding allocated ~14x the input: 919 MiB for 64 MiB) -> checked in 1 MiB chunks with a 4 KiB overlap (a marker on a boundary is found; tested at 9 offsets and over 2-byte runes). Not measured as RSS. 4. **LOW BOM hid `wing:`** -> stripped. 5. **LOW `brain_reject` on parked pdf/txt prepended YAML** -> non-markdown moves byte for byte; `readRawNote` is now capped (64 MiB, endless-reader test that fails instead of draining memory). 7. **LOW test gap** (reserved-wing check without Sanitise passed all tests) -> tests for `Sources`, `sources.`, `SOURCES `, `Entities/`, `concepts\`, ` wiki `. `INGEST_WING_ROUTING=true` silently left routing off -> parsed strictly, a typo is a startup error. 6. LOW determinism of the qualified slug: accepted as the reviewer described (stable while the page exists; bounded at 100 candidates). Gate: `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL, lint 0. Next: a third independent pass; merge stays with you (control code). --- _Created via git-mcp on behalf of @mathias_
mathias added 1 commit 2026-10-20 18:56:04 +00:00
- in a wing, an ingest that writes no source page is an error: the note goes to
  raw/failed with the warnings in log.md instead of leaving the inbox as
  'processed' (model returned [], only invalid types, only clashing entities)
- the wing's source path is deterministic, so Resolve no longer redirects a
  source onto another note's page because the model gave both the same title
  ('Home'); that overwrote the hash and the text
- a batch tie between concept and entity prefers the page already on disk, so
  its update is not lost; the wing's own source page keeps its slug even when a
  foreign page shares it
- brain_pending lists an unreadable (over-cap) note with the reason instead of
  dropping it; the client filter's 4 KiB match-span limit is documented
Each has a test that fails without it (mutation-checked).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Independent review pass 3 (pr-gatekeeper agent, by execution): BLOCK on one finding at 49286ba. Resolution at 27b1579 (tests first, mutation-checked):

  • F1 HIGH: an ingest that wrote nothing was archived as "processed" (model returns []; only pages of a type the layout has no place for; only entities whose slug is taken). Pre-existing for [], widened by this PR. In a wing, an ingest that writes no source page is now an error: the watcher keeps the note in raw/failed/ and the reason is in log.md; it is never archived as processed. Skips (unchanged source) are still skips. Flag-off behaviour unchanged (the legacy [] case is ticketed separately).
  • F2 MEDIUM: two different notes the model titled alike ("Home") shared one source page and overwrote each other's hash (Resolve matched sources by title). In a wing the source path is already deterministic (the filename), so the title no longer redirects it.
  • F3: concept+entity tie lost the update of an existing page -> a batch tie prefers the page already on disk; the wing's own source page keeps its slug even if a foreign page shares it (found while fixing F2).
  • F4: an over-cap .md vanished from brain_pending -> listed with the reason.
  • F5 documented limit: a client-filter match that can span more than 4 KiB across a 1 MiB chunk edge is not guaranteed (crafted padding, notes over 1 MiB only); stated in the code and docs/raw-inbox.md. F6 (a pattern matching the empty string parks everything): fail-closed, operator-only, documented. F7: ~10 s to check a 64 MiB note (memory bounded: 192-338 MiB TotalAlloc, 135-199 MiB HeapSys for 63 MiB); tolerable, documented.

Gate: go vet ./..., go test -race ./... exit 0, 0 FAIL, lint 0. Pass 4 (narrow) follows; merge stays with Mathias.


Created via git-mcp on behalf of @mathias

**Independent review pass 3 (pr-gatekeeper agent, by execution): BLOCK on one finding at 49286ba. Resolution at 27b1579** (tests first, mutation-checked): - **F1 HIGH: an ingest that wrote nothing was archived as "processed"** (model returns `[]`; only pages of a type the layout has no place for; only entities whose slug is taken). Pre-existing for `[]`, widened by this PR. In a wing, an ingest that writes no source page is now an **error**: the watcher keeps the note in `raw/failed/` and the reason is in `log.md`; it is never archived as processed. Skips (unchanged source) are still skips. Flag-off behaviour unchanged (the legacy `[]` case is ticketed separately). - **F2 MEDIUM: two different notes the model titled alike ("Home") shared one source page and overwrote each other's hash** (`Resolve` matched sources by title). In a wing the source path is already deterministic (the filename), so the title no longer redirects it. - **F3: concept+entity tie lost the update of an existing page** -> a batch tie prefers the page already on disk; the wing's own source page keeps its slug even if a foreign page shares it (found while fixing F2). - **F4: an over-cap `.md` vanished from `brain_pending`** -> listed with the reason. - **F5 documented limit:** a client-filter match that can span more than 4 KiB across a 1 MiB chunk edge is not guaranteed (crafted padding, notes over 1 MiB only); stated in the code and docs/raw-inbox.md. **F6** (a pattern matching the empty string parks everything): fail-closed, operator-only, documented. **F7:** ~10 s to check a 64 MiB note (memory bounded: 192-338 MiB TotalAlloc, 135-199 MiB HeapSys for 63 MiB); tolerable, documented. Gate: `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL, lint 0. Pass 4 (narrow) follows; merge stays with Mathias. --- _Created via git-mcp on behalf of @mathias_
mathias added 1 commit 2026-10-20 19:00:20 +00:00
fix(pipeline,api): write the source page last; say how to clear an unreadable note (review of #211, pass 4)
CI / Lint / Test / Vet (pull_request) Successful in 1m3s
CI / Mirror to GitHub (pull_request) Skipped
90752f2ac9
The source page carries source_hash, which the drift check trusts. Written first,
a batch that failed midway left a hash behind, so re-ingesting the same content
was skipped and the rest of the batch was lost for good. In a wing it is now
written last. An over-cap note in brain_pending says promote/reject cannot act
on it and to delete or move it by hand. Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Independent review pass 4 (pr-gatekeeper agent, narrow, by execution): nothing blocking at 27b1579. It confirmed F1-F4 hold and do not break each other: the matrix of model outputs ([], invalid JSON, invalid types only, clashing entities only, two source pages, Skipped, dry-run) ends in an error or a skip in wing mode, never in a silent "processed"; legacy flag-off behaviour is byte-identical to main on those inputs. Two non-blocking findings, both handled at 90752f2:

  • A (medium): mid-write failure + re-ingest lost the rest of the batch (the source page, carrying source_hash, was written first; the next ingest of identical content was a hash skip). In a wing the source page is now written LAST; test fails without it (a failed batch leaves no source page, and the retry is a real ingest).
  • B (low-medium): an over-cap note is listed but cannot be cleared via promote/reject. The excerpt now says so and to delete or move it by hand; renaming it out via reject is ticketed.

Everything else the reviewers found and I chose not to fix this sprint is collected in #212 with sources. Gate: go vet ./..., go test -race ./... exit 0, 0 FAIL, lint 0.

Review history for the record: four independent agent passes (not four-eyes) at 49286ba-era heads: pass 1 BLOCK (10 findings), pass 2 BLOCK (in-batch slug clash, filter patterns, memory), pass 3 BLOCK (nothing-ingested archived as processed; title remap merging notes), pass 4 clear. Every fix has a failing-first test and was mutation-checked; several of my own tests passed under their mutant and were rebuilt. Merge is yours: control code, and I misapplied the risk lane once already in this sprint (#210).


Created via git-mcp on behalf of @mathias

**Independent review pass 4 (pr-gatekeeper agent, narrow, by execution): nothing blocking at 27b1579.** It confirmed F1-F4 hold and do not break each other: the matrix of model outputs (`[]`, invalid JSON, invalid types only, clashing entities only, two source pages, Skipped, dry-run) ends in an error or a skip in wing mode, never in a silent "processed"; legacy flag-off behaviour is byte-identical to main on those inputs. Two non-blocking findings, both handled at 90752f2: - **A (medium): mid-write failure + re-ingest lost the rest of the batch** (the source page, carrying `source_hash`, was written first; the next ingest of identical content was a hash skip). In a wing the source page is now written LAST; test fails without it (a failed batch leaves no source page, and the retry is a real ingest). - **B (low-medium): an over-cap note is listed but cannot be cleared via promote/reject.** The excerpt now says so and to delete or move it by hand; renaming it out via reject is ticketed. Everything else the reviewers found and I chose not to fix this sprint is collected in **#212** with sources. Gate: `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL, lint 0. **Review history for the record:** four independent agent passes (not four-eyes) at 49286ba-era heads: pass 1 BLOCK (10 findings), pass 2 BLOCK (in-batch slug clash, filter patterns, memory), pass 3 BLOCK (nothing-ingested archived as processed; title remap merging notes), pass 4 clear. Every fix has a failing-first test and was mutation-checked; several of my own tests passed under their mutant and were rebuilt. **Merge is yours:** control code, and I misapplied the risk lane once already in this sprint (#210). --- _Created via git-mcp on behalf of @mathias_
mathias merged commit 4c2276b46b into main 2026-10-20 19:36:51 +00:00
Author
Owner

Merged by me via the Gitea API with the owner token, on Mathias's explicit yes for this PR (chat, 2026-10-20: "Du mergar åt mig via API:t"). The MCP merge tool refuses above risk/low by design; this bypass is recorded here and in #212 item 15. Head 90752f2, CI green on that commit. The routing is OFF until INGEST_WING_ROUTING is set on the ingestion deployment (nothing in infra sets it). Next, in order: deploy, legacymove dry run shown to Mathias before -apply, then the infra change to enable routing (prepared as an issue/draft, not applied).


Created via git-mcp on behalf of @mathias

Merged by me via the Gitea API with the owner token, **on Mathias's explicit yes for this PR** (chat, 2026-10-20: "Du mergar åt mig via API:t"). The MCP merge tool refuses above risk/low by design; this bypass is recorded here and in #212 item 15. Head 90752f2, CI green on that commit. The routing is OFF until `INGEST_WING_ROUTING` is set on the ingestion deployment (nothing in infra sets it). Next, in order: deploy, `legacymove` dry run shown to Mathias before `-apply`, then the infra change to enable routing (prepared as an issue/draft, not applied). --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.