Compare commits

...

2 Commits

Author SHA1 Message Date
sulthan 5f9cfbb27e fix: an asleep browser Lane is not a stalled one on the admin page
laneRow.Stalled was Due > 0 && Checked == 0, which is exactly the shape
of the on-demand wake gate declining to start Chrome (ADR-0005): under
five due Series and none overdue by 15m, a browser Lane reads nothing on
purpose. kagane and novelfull each have one bookmarked Series, so that is
their state for most of every cycle and the page marked both "not
checking" — spending the one mark that means "go look" on the commonest
healthy state.

LaneState.Asleep is set at the wake gate, excluded from Stalled, kept out
of Attention, and rendered as "browser asleep". Verified in prod: those
Lanes wake on the age rule 15m past due (kagane and novelfull both polled
at 14:55Z for a 14:40Z due moment, novelfull picking up Chapter 4458).
2026-08-16 22:03:57 +07:00
sulthan 20fff588cc fix: don't read Cloudflare's injected jsd script as a refusal (#109)
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-08-16 21:17:29 +07:00
14 changed files with 870 additions and 618 deletions
+13
View File
@@ -127,6 +127,13 @@ Guidance for OpenCode (and Claude Code) working under `backend/`. See root `AGEN
and cover work (both healing a stored source URL and filling a blank from
the series page) runs in the background so a slow CDN can't consume a
Lane's gap.
A refusal is only ever the challenge *page*: `isInterstitial` matches the
orchestration path `/cdn-cgi/challenge-platform/h/`, never the bare prefix.
Cloudflare injects `/cdn-cgi/challenge-platform/scripts/jsd/main.js` into
ordinary 200 pages once a zone turns JS detections on, which demonic did on
2026-08-16 — the prefix match then read every real demonic page as a refusal
and parked that Lane in 15m backoff while plain TLS was returning the full
series page.
Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth
against fingerprint-based blocking; any failure log and skip. kagane, comix
and novelfull sit behind Cloudflare JavaScript challenges the TLS client
@@ -259,3 +266,9 @@ Guidance for OpenCode (and Claude Code) working under `backend/`. See root `AGEN
than recording zeroes; a Lane that has never reached a pace renders no gap at
all. `Checked` next to `Due` is what separates a stopped Lane from a quiet
one, so neither figure may be dropped from the row.
Due-without-Checked is *not* by itself a stall: a browser Lane under both
wake thresholds sets `LaneState.Asleep` at the on-demand gate and renders
"browser asleep" instead of "not checking", and never counts toward
`Attention`. That is the commonest healthy state for kagane, comix and
novelfull — one due Series, nothing checked — so spending the stall mark on
it would train the owner to ignore the mark that matters.
+10 -3
View File
@@ -250,11 +250,18 @@ func browserConnectionLost(ctx context.Context) bool {
const challengePollInterval = 2 * time.Second
// isInterstitial reports whether html is Cloudflare's challenge page rather
// than the site's own. Matched on the challenge runtime's script path, which is
// stable across the interstitial's wording and locale — the visible "Just a
// than the site's own. Matched on the challenge orchestration path
// (/cdn-cgi/challenge-platform/h/<b|g|x>/orchestrate/...), which is stable
// across the interstitial's wording and locale — the visible "Just a
// moment..." title is neither.
//
// The bare "/cdn-cgi/challenge-platform/" prefix is NOT enough: Cloudflare
// injects /cdn-cgi/challenge-platform/scripts/jsd/main.js into ordinary 200
// pages when JS detections are on, so matching the prefix declared every real
// demonic page a refusal and parked that Lane in 15m backoff (observed
// 2026-08-16, demonic turned detections on).
func isInterstitial(html string) bool {
return strings.Contains(html, "/cdn-cgi/challenge-platform/")
return strings.Contains(html, "/cdn-cgi/challenge-platform/h/")
}
// run navigates to target and re-reads until done reports an answer, bounded by
+19
View File
@@ -117,6 +117,25 @@ func TestBrowserOnlyCoverURL(t *testing.T) {
})
}
}
// The jsd script is injected into ordinary 200 pages when a zone turns JS
// detections on; only the orchestration path means the page itself is the
// challenge. Conflating the two parked the demonic Lane in refusal backoff
// while every fetch was in fact the real series page (observed 2026-08-16).
func TestIsInterstitial(t *testing.T) {
if !isInterstitial(challengeFixture) {
t.Fatal("challenge page not detected as interstitial")
}
const jsdInjected = `<html><head><title>The Possessed Grappler</title>
<script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></head>
<body><a href="/chaptered.php?manga=13721&chapter=22">Chapter 22</a></body></html>`
if isInterstitial(jsdInjected) {
t.Fatal("real page carrying the injected jsd script misread as interstitial")
}
if got, ok := demonicLatestChapter("", jsdInjected); !ok || got.Label != "Chapter 22" {
t.Fatalf("demonicLatestChapter = %+v, ok = %v, want Chapter 22", got, ok)
}
}
func TestClassifyBrowserInterruption(t *testing.T) {
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)
+1 -1
View File
@@ -263,7 +263,7 @@ func (p *Poller) runLanePass(ctx context.Context, name string, paced bool) time.
// browser): waking it for a single Poll would cost a challenge solve
// per request. The Lane still paces at the default gap, which is what
// the owner's page must show rather than a zero.
st.Gap = defaultGap
st.Gap, st.Asleep = defaultGap, true
return defaultGap
}
if s.Browser != nil {
+21
View File
@@ -1388,6 +1388,11 @@ func TestBrowserLaneWakeThresholds(t *testing.T) {
if got := browser.callCount(); got != 0 {
t.Fatalf("browser fetches with 3 freshly-due series = %d, want 0 (Chrome stays asleep)", got)
}
// The owner's page reads this state off the snapshot, and Due-without-
// Checked has to be distinguishable there from a Lane that has stopped.
if lane := laneByName(t, p, "kagane"); !lane.Asleep || lane.Due != 3 || lane.Checked != 0 {
t.Fatalf("asleep kagane lane = %+v, want Asleep with 3 due and 0 checked", lane)
}
// 5 due crosses the count threshold.
for i := 3; i < 5; i++ {
seed(i)
@@ -1396,6 +1401,9 @@ func TestBrowserLaneWakeThresholds(t *testing.T) {
if got := browser.callCount(); got != 5 {
t.Fatalf("browser fetches with 5 due series = %d, want 5", got)
}
if lane := laneByName(t, p, "kagane"); lane.Asleep {
t.Fatalf("woken kagane lane still reports Asleep: %+v", lane)
}
// A single long-neglected series wakes the browser by age alone.
seedForCheck(t, s, "kagane:ancient", "https://kagane.to/series/ancient", 0)
p.runOnce(context.Background())
@@ -1404,6 +1412,19 @@ func TestBrowserLaneWakeThresholds(t *testing.T) {
}
}
// laneByName pulls one Lane out of the poller's snapshot, failing rather than
// returning a zero LaneState a caller would assert against by accident.
func laneByName(t *testing.T, p *Poller, site string) LaneState {
t.Helper()
for _, lane := range p.LaneStatus().Lanes {
if lane.Site == site {
return lane
}
}
t.Fatalf("no %q lane in the snapshot", site)
return LaneState{}
}
// When one browser Lane loses the sidecar, the round's remaining browser
// Lanes are skipped: every fetch would fail anyway, and their Series must not
// burn their stamps on a dead Chrome (issue #100).
+5
View File
@@ -19,6 +19,11 @@ type LaneState struct {
Clamped bool
Refusing bool
Browser bool
// Asleep marks a browser Lane whose last pass declined to wake Chrome
// because it was under both wake thresholds (ADR-0005). Due without
// Checked then means "waiting for the group to gather", not "stopped", and
// the page must not draw it as a stall.
Asleep bool
}
// Status is the owner's page snapshot of the whole poller (issue #102).
+8 -3
View File
@@ -61,8 +61,11 @@ type laneRow struct {
// is configured, which stops those Series just as completely.
BrowserLost bool
// Stalled marks a Lane with Series waiting that its last pass did not read
// — the difference between a stopped Lane and a quiet one (story 13).
// — the difference between a stopped Lane and a quiet one (story 13). A
// browser Lane holding Chrome asleep under the wake thresholds is neither,
// so it carries Asleep instead and never Stalled.
Stalled bool
Asleep bool
// Attention is the one flag the template colours on, so an unhealthy Lane
// is found at a glance rather than read for.
Attention bool
@@ -152,8 +155,9 @@ func (h *Handler) lanesView() lanesView {
for _, l := range snap.Lanes {
lost := l.Browser && !snap.BrowserReachable
// Series waiting and none read is the shape of a Lane that has stopped
// working, as distinct from one that is quiet for want of work.
stalled := l.Due > 0 && l.Checked == 0
// working, as distinct from one that is quiet for want of work — or one
// deliberately leaving Chrome asleep until its group gathers.
stalled := l.Due > 0 && l.Checked == 0 && !l.Asleep
gap := ""
if l.Gap > 0 {
gap = l.Gap.Truncate(time.Second).String()
@@ -168,6 +172,7 @@ func (h *Handler) lanesView() lanesView {
Refusing: l.Refusing,
BrowserLost: lost,
Stalled: stalled,
Asleep: l.Asleep,
Attention: l.Clamped || l.Refusing || lost || stalled,
})
}
@@ -23,6 +23,7 @@
{{if .Refusing}}<span class="lane-mark">refusing</span>{{end}}
{{if .BrowserLost}}<span class="lane-mark">no browser</span>{{end}}
{{if .Stalled}}<span class="lane-mark">not checking</span>{{end}}
{{if .Asleep}}<span class="lane-mark">browser asleep</span>{{end}}
</li>
{{end}}
</ul>
+31
View File
@@ -776,6 +776,37 @@ func TestAdminPageShowsLaneStatus(t *testing.T) {
}
}
// A browser Lane under both wake thresholds holds Chrome asleep (ADR-0005), so
// Series due with none checked is the design working, not a stopped Lane. The
// two must not render the same mark: "not checking" is the owner's cue to go
// looking, and spending it on the commonest healthy browser-Lane state trains
// them to ignore it.
func TestAsleepBrowserLaneIsNotMarkedStalled(t *testing.T) {
lanes := fakeLanes{latest.Status{
Lanes: []latest.LaneState{
{Site: "kagane", Due: 1, LastRun: time.Now(), Gap: 10 * time.Second, Browser: true, Asleep: true},
},
BrowserConfigured: true,
BrowserReachable: true,
}}
router, st, _ := oauthWebTestServer(t, lanes)
req := httptest.NewRequest(http.MethodGet, "/ui/admin/lanes", nil)
req.AddCookie(sessionCookie(t, st))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
body := rr.Body.String()
if strings.Contains(body, "not checking") {
t.Errorf("an asleep browser Lane is marked as stalled:\n%s", body)
}
if !strings.Contains(body, "browser asleep") {
t.Errorf("an asleep browser Lane says nothing about why it read nothing:\n%s", body)
}
if strings.Contains(body, `class="attention"`) {
t.Errorf("an asleep browser Lane is coloured as unhealthy:\n%s", body)
}
}
// A Lane whose pass never reached a figure must not have that figure drawn as
// a zero: a refusing Lane still reports the due count and gap its last real
// pass saw, and a Lane that has never reached one omits it entirely.
+1 -5
View File
@@ -12,9 +12,8 @@
"10": "Go Test Helpers",
"11": "Store Tests",
"12": "Bookmarks API Handler",
"13": "Web UI Handlers",
"13": "displayChapter",
"14": "Go Error Handling",
"15": "CDP Browser Client",
"16": "Cloudflare bot scoring and poll cadence — what is actually documented",
"17": "Go Code Style Guide",
"18": "Agent Skills",
@@ -30,7 +29,6 @@
"28": "Allocation Patterns",
"29": "Observability & Alerting",
"30": "AGENTS.md",
"31": "Store",
"32": "Open",
"33": "Go Testing Guide",
"34": "Session Store",
@@ -41,7 +39,6 @@
"39": "UI Critique 2026-07-26A",
"40": "UI Critique 2026-07-26B",
"41": "Handler",
"42": "pgtest.go",
"43": "Issue Tracker & Triage",
"44": "Ticket Workflow",
"45": "Go Perf Alert Rules",
@@ -124,7 +121,6 @@
"122": "Identity comes from Discord OAuth; we store no passwords and send no email",
"123": "The wire format stays flat and deliberately does not mirror the schema",
"124": "ADR-0005: On-demand browser sidecar",
"125": "sessions_test.go",
"126": "Bookmark Manager",
"127": "triage-labels.md",
"128": "Cross-Ticket Contract",
+28 -44
View File
@@ -1,16 +1,16 @@
# Graph Report - mangaBookmark (2026-08-16)
## Corpus Check
- 119 files · ~290,953 words
- 119 files · ~291,777 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1736 nodes · 3597 edges · 221 communities (70 shown, 151 thin omitted)
- Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 361 edges (avg confidence: 0.78)
- 1739 nodes · 3610 edges · 217 communities (67 shown, 150 thin omitted)
- Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 363 edges (avg confidence: 0.78)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `56afb9f2`
- Built from commit: `20fff588`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -28,9 +28,8 @@
- [[_COMMUNITY_Go Test Helpers|Go Test Helpers]]
- [[_COMMUNITY_Store Tests|Store Tests]]
- [[_COMMUNITY_Bookmarks API Handler|Bookmarks API Handler]]
- [[_COMMUNITY_Web UI Handlers|Web UI Handlers]]
- [[_COMMUNITY_displayChapter|displayChapter]]
- [[_COMMUNITY_Go Error Handling|Go Error Handling]]
- [[_COMMUNITY_CDP Browser Client|CDP Browser Client]]
- [[_COMMUNITY_Cloudflare bot scoring and poll cadence — what is actually documented|Cloudflare bot scoring and poll cadence — what is actually documented]]
- [[_COMMUNITY_Go Code Style Guide|Go Code Style Guide]]
- [[_COMMUNITY_Agent Skills|Agent Skills]]
@@ -46,7 +45,6 @@
- [[_COMMUNITY_Allocation Patterns|Allocation Patterns]]
- [[_COMMUNITY_Observability & Alerting|Observability & Alerting]]
- [[_COMMUNITY_AGENTS|AGENTS.md]]
- [[_COMMUNITY_Store|Store]]
- [[_COMMUNITY_Go Testing Guide|Go Testing Guide]]
- [[_COMMUNITY_Session Store|Session Store]]
- [[_COMMUNITY_Web UI Filter Logic|Web UI Filter Logic]]
@@ -56,7 +54,6 @@
- [[_COMMUNITY_UI Critique 2026-07-26A|UI Critique 2026-07-26A]]
- [[_COMMUNITY_UI Critique 2026-07-26B|UI Critique 2026-07-26B]]
- [[_COMMUNITY_Handler|Handler]]
- [[_COMMUNITY_pgtest.go|pgtest.go]]
- [[_COMMUNITY_Issue Tracker & Triage|Issue Tracker & Triage]]
- [[_COMMUNITY_Ticket Workflow|Ticket Workflow]]
- [[_COMMUNITY_Go Perf Alert Rules|Go Perf Alert Rules]]
@@ -139,7 +136,6 @@
- [[_COMMUNITY_Identity comes from Discord OAuth; we store no passwords and send no email|Identity comes from Discord OAuth; we store no passwords and send no email]]
- [[_COMMUNITY_The wire format stays flat and deliberately does not mirror the schema|The wire format stays flat and deliberately does not mirror the schema]]
- [[_COMMUNITY_ADR-0005 On-demand browser sidecar|ADR-0005: On-demand browser sidecar]]
- [[_COMMUNITY_sessions_test.go|sessions_test.go]]
- [[_COMMUNITY_Bookmark Manager|Bookmark Manager]]
- [[_COMMUNITY_triage-labels|triage-labels.md]]
- [[_COMMUNITY_Cross-Ticket Contract|Cross-Ticket Contract]]
@@ -270,7 +266,7 @@
- **Headless browser infrastructure (sidecar, on-demand, home deployment)** — docs_adr_0005_on_demand_browser_headless_shell, docs_adr_0005_on_demand_browser_cdp, docs_adr_0005_on_demand_browser_on_demand_start, docs_adr_0006_browser_on_the_home_machine_home_machine_rationale [INFERRED 0.85]
- **lightnovelworld series-identity investigation and fix** — docs_research_lightnovelworld_chapter_vs_series_slug_issue_77, docs_research_lightnovelworld_chapter_vs_series_slug_unscoped_regex, docs_adr_0008_series_identity_is_discovered_not_derived_discovered_identity [INFERRED 0.85]
## Communities (221 total, 151 thin omitted)
## Communities (217 total, 150 thin omitted)
### Community 0 - "HTMX Library Internals"
Cohesion: 0.08
@@ -278,7 +274,7 @@ Nodes (101): A(), ae(), an(), at(), B(), be(), bn(), bt() (+93 more)
### Community 1 - "Cover Fetch Test Helpers"
Cohesion: 0.09
Nodes (89): floatPtr(), testConfig(), getCover(), Cookie, Handler, ResponseRecorder, T, TestListRendersAcquiredCover() (+81 more)
Nodes (88): floatPtr(), testConfig(), getCover(), Cookie, Handler, ResponseRecorder, T, TestListRendersAcquiredCover() (+80 more)
### Community 2 - "Manga Userscript Adapters"
Cohesion: 0.06
@@ -293,12 +289,12 @@ Cohesion: 0.07
Nodes (103): bookmarkNewKaganeSeries(), bookmarkNewNovelfullSeries(), bookmarkNewSeries(), Context, Store, T, newAcquirer(), readBookmark() (+95 more)
### Community 5 - "Bookmarks API Tests"
Cohesion: 0.07
Nodes (71): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+63 more)
Cohesion: 0.11
Nodes (54): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+46 more)
### Community 7 - "Cover & Acquire Internals"
Cohesion: 0.06
Nodes (43): Addr, fakeLanes, Context, Store, WaitGroup, isInterstitial(), defaultCoverResolver(), fetchCoverBytes() (+35 more)
Cohesion: 0.08
Nodes (31): fakeLanes, Context, Store, WaitGroup, isInterstitial(), fetchCoverBytes(), browserWakeDue(), fetcherFor() (+23 more)
### Community 8 - "System Architecture Concepts"
Cohesion: 0.15
@@ -313,21 +309,21 @@ Cohesion: 0.05
Nodes (39): Test Helpers, Test Timeout, Basic Handler Test, HTTP Handler Testing, Query Parameters and Headers, Docker Compose Fixture, Integration Testing, SQL Schema Fixture (+31 more)
### Community 11 - "Store Tests"
Cohesion: 0.14
Nodes (45): Store, T, newTestStore(), readLatestCheckedAt(), readSeries(), secondReader(), seedForCheck(), seedSecondReader() (+37 more)
Cohesion: 0.07
Nodes (81): M, TestMain(), M, TestMain(), M, Main(), start(), URL() (+73 more)
### Community 12 - "Bookmarks API Handler"
Cohesion: 0.08
Nodes (32): Handler, Request, ResponseWriter, Store, Healthz(), writeJSON(), Auth(), compressible() (+24 more)
### Community 13 - "displayChapter"
Cohesion: 0.09
Nodes (36): Addr, Config, NewBrowserFetcher(), defaultCoverResolver(), Client, Context, NewCoverFetcher(), NewCoverFetcherWithResolver() (+28 more)
### Community 14 - "Go Error Handling"
Cohesion: 0.06
Nodes (33): Creating Errors, Custom Error Types, Custom types that wrap other errors, Decision table: which error strategy to use, Error Creation, Error String Conventions, Errors as Values, `errors.New` — static error messages (+25 more)
### Community 15 - "CDP Browser Client"
Cohesion: 0.20
Nodes (17): applyMigration(), migrate(), Open(), refreshOwnerToken(), seedOwner(), TestCoverIsContentAddressedOnFilesystem(), TestCoverPersistsAcrossReopen(), TestMigration0002BackfillsExistingBookmarks() (+9 more)
### Community 16 - "Cloudflare bot scoring and poll cadence — what is actually documented"
Cohesion: 0.06
Nodes (33): 1.1 The score itself, 1.2 The detection engines (Enterprise Bot Management), 1.3 Rate limiting is a separate product, 1. What a bot score is and what feeds it, 2.1 What each plan gets, 2.2 Bot Fight Mode specifics (the Free-plan product), 2.3 Does the free tier "score" continuously?, 2. The free-plan reality (+25 more)
@@ -365,8 +361,8 @@ Cohesion: 0.33
Nodes (12): coverResponse(), Request, T, TestCoverFetcherCanonicalisesJpgAlias(), TestCoverFetcherFetchesPublicHTTPSImage(), TestCoverFetcherRefusesUnsafeDestinationsBeforeRequest(), TestCoverFetcherRejectsNonImage(), TestCoverFetcherRejectsOversizedBody() (+4 more)
### Community 26 - "Open"
Cohesion: 0.05
Nodes (61): awaitPromise(), browserConnectionLost(), classifyBrowserError(), comixRead(), comixSeriesPageURL(), Action, Context, Mutex (+53 more)
Cohesion: 0.06
Nodes (57): awaitPromise(), browserConnectionLost(), classifyBrowserError(), comixRead(), comixSeriesPageURL(), Action, Context, Mutex (+49 more)
### Community 27 - "Find Skills Guide"
Cohesion: 0.14
@@ -384,10 +380,6 @@ Nodes (9): Alerting rules (examples), CPU saturation, GC pressure, Goroutine lea
Cohesion: 0.40
Nodes (5): Map of pointers for large, frequently updated structs, Memory Layout, Pointer receivers for large structs, Struct field alignment, Zero-size field at end of struct
### Community 31 - "Store"
Cohesion: 0.09
Nodes (5): coverRelativePath(), coverSourceAddress(), Store, scanSeries(), ReaderSummary
### Community 33 - "Go Testing Guide"
Cohesion: 0.20
Nodes (10): CI Regression Detection, Common Mistakes, Core Philosophy, Cross-References, Decision Tree: Where Is Time Spent?, Deep Dives, Go Performance Optimization, Iterative Optimization Methodology (+2 more)
@@ -417,12 +409,8 @@ Cohesion: 0.29
Nodes (6): Design Health Score, Design Specificity Verdict, Minor Observations, Persona Red Flags, Priority Issues, Questions to Consider
### Community 41 - "Handler"
Cohesion: 0.14
Nodes (18): currentLib(), currentTab(), filterBookmarks(), Client, HandlerFunc, Request, ResponseWriter, Store (+10 more)
### Community 42 - "pgtest.go"
Cohesion: 0.18
Nodes (12): M, TestMain(), M, TestMain(), M, Main(), start(), URL() (+4 more)
Cohesion: 0.05
Nodes (25): coverRelativePath(), coverSourceAddress(), displayChapter(), Store, scanSeries(), currentLib(), currentTab(), filterBookmarks() (+17 more)
### Community 45 - "Go Perf Alert Rules"
Cohesion: 0.50
@@ -532,10 +520,6 @@ Nodes (3): Consequence, The wire format stays flat and deliberately does not mir
Cohesion: 0.50
Nodes (3): ADR-0005: On-demand browser sidecar, Constraints, Decision
### Community 125 - "sessions_test.go"
Cohesion: 0.48
Nodes (6): T, TestCreateAndGetSession(), TestDeleteSessionIsPerReader(), TestDeleteSessionRevokes(), TestExpiredSessionIsGone(), TestGetSessionUnknownID()
### Community 154 - "ADR-0011: Sightings — a Reader report defers a Poll where being wrong hurts only them"
Cohesion: 0.33
Nodes (5): ADR-0011: Sightings — a Reader report defers a Poll where being wrong hurts only them, Constraints preserved, Decision, Tradeoffs and rejections, Why
@@ -551,17 +535,17 @@ Nodes (3): Live URL shapes (verified 2026-07-26, may drift — re-check against
## Knowledge Gaps
- **524 isolated node(s):** `bookmarkmanager/backend`, `ctxKey`, `loginView`, `ctxKey`, `test` (+519 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **151 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **150 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Open()` connect `CDP Browser Client` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Tests`, `pgtest.go`, `Store Tests`, `Store`?**
_High betweenness centrality (0.060) - this node is a cross-community bridge._
- **Why does `New()` connect `Series Acquisition Tests` to `Bookmarks API Tests`, `Cover & Acquire Internals`, `Handler`, `Session Middleware`, `CDP Browser Client`, `ResponseWriter`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._
- **Why does `newRouter()` connect `Bookmarks API Tests` to `Cover Fetch Test Helpers`, `Bookmarks API Handler`, `Series Acquisition Tests`, `ResponseWriter`?**
_High betweenness centrality (0.030) - this node is a cross-community bridge._
- **Why does `Open()` connect `Store Tests` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Tests`, `Handler`, `displayChapter`?**
_High betweenness centrality (0.057) - this node is a cross-community bridge._
- **Why does `New()` connect `Series Acquisition Tests` to `Bookmarks API Tests`, `Cover & Acquire Internals`, `Handler`, `Session Middleware`, `Store Tests`, `displayChapter`, `ResponseWriter`?**
_High betweenness centrality (0.051) - this node is a cross-community bridge._
- **Why does `newRouter()` connect `Bookmarks API Tests` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Handler`, `displayChapter`, `ResponseWriter`?**
_High betweenness centrality (0.031) - this node is a cross-community bridge._
- **Are the 25 inferred relationships involving `newTestStore()` (e.g. with `TestAcquireDoesNotBlockTheWrite()` and `TestAcquireFailureLeavesTheBookmarkIntact()`) actually correct?**
_`newTestStore()` has 25 INFERRED edges - model-reasoned connections that need verification._
- **Are the 48 inferred relationships involving `testConfig()` (e.g. with `TestListRendersAcquiredCover()` and `TestPublicCoverNeverEchoesNonImage()`) actually correct?**
File diff suppressed because one or more lines are too long
+689 -519
View File
File diff suppressed because it is too large Load Diff
+39 -39
View File
@@ -25,7 +25,7 @@
"semantic_hash": "dac242903b0e98c3e4395159d609e08e"
},
"backend/main.go": {
"mtime": 1786863963.3450089,
"mtime": 1786886056.023822,
"ast_hash": "d5a50b03438d7c120079d40cc578462b",
"semantic_hash": ""
},
@@ -35,7 +35,7 @@
"semantic_hash": "4a94ac85bad6bce330d085bcc0ae3ffd"
},
"userscript/manga-bookmark.user.js": {
"mtime": 1786872888.3482232,
"mtime": 1786886056.0389624,
"ast_hash": "4e79684942234b26f4a1fb03fccd6d35",
"semantic_hash": ""
},
@@ -170,8 +170,8 @@
"semantic_hash": "e51077b6a7f1f67afc748f1a32a1557d"
},
"backend/web_test.go": {
"mtime": 1786865248.7717106,
"ast_hash": "7c298e39c63e4502cf6272d1e206e9ef",
"mtime": 1786891437.925184,
"ast_hash": "74aed7f5a5b196d2b860cd6a34266ec6",
"semantic_hash": ""
},
"backend/main_test.go": {
@@ -220,22 +220,22 @@
"semantic_hash": ""
},
"docs/design-system.md": {
"mtime": 1786865139.9022946,
"mtime": 1786886056.0313923,
"ast_hash": "3bd39932997c8e245508874194db4e49",
"semantic_hash": ""
},
"backend/api_test.go": {
"mtime": 1786868080.2317674,
"mtime": 1786886056.016252,
"ast_hash": "b64372df5429fb14e48b37b3bab10402",
"semantic_hash": ""
},
"backend/cover_test.go": {
"mtime": 1786863966.7084978,
"mtime": 1786886056.016252,
"ast_hash": "fd6b3f6ef46bd17b0241d104b6e5bdf6",
"semantic_hash": ""
},
"backend/internal/api/handlers.go": {
"mtime": 1786867745.788687,
"mtime": 1786886056.016252,
"ast_hash": "ae4f3b961450f06a06e0632eb273a5b2",
"semantic_hash": ""
},
@@ -245,13 +245,13 @@
"semantic_hash": "385b36f58488b7e6d93eb6d6034e9ee3"
},
"backend/internal/latest/browser.go": {
"mtime": 1786861012.026504,
"ast_hash": "75f34a974568d122681939dd297944a2",
"mtime": 1786889854.5578685,
"ast_hash": "b65f3693db77c69ef0a6247fd75118bc",
"semantic_hash": ""
},
"backend/internal/latest/browser_test.go": {
"mtime": 1786857336.6414917,
"ast_hash": "800fa6aa471ada054a3c48943ad17ab7",
"mtime": 1786889854.5578685,
"ast_hash": "118aba3d794b9faf893c45e58298e11a",
"semantic_hash": ""
},
"backend/internal/latest/fetch.go": {
@@ -260,17 +260,17 @@
"semantic_hash": ""
},
"backend/internal/latest/poller.go": {
"mtime": 1786872774.641225,
"ast_hash": "5e55e2ac5cb7d347807754f92425d29b",
"mtime": 1786891405.415882,
"ast_hash": "54f6eec9d2090c7713f502c8553063a2",
"semantic_hash": ""
},
"backend/internal/latest/poller_test.go": {
"mtime": 1786865291.3758123,
"ast_hash": "1da669ac748a8d1a0289e557392fa3fd",
"mtime": 1786891462.5665002,
"ast_hash": "512806a9c6e15036a75f01bf5577c20b",
"semantic_hash": ""
},
"backend/internal/latest/sites.go": {
"mtime": 1786868771.0663204,
"mtime": 1786886056.0200372,
"ast_hash": "5786000dd0dfd5b4b3bd2b87b1fc100b",
"semantic_hash": ""
},
@@ -340,12 +340,12 @@
"semantic_hash": "0b6764a0ee20f5cb7748eecd31a1d220"
},
"backend/internal/store/store.go": {
"mtime": 1786868763.646761,
"mtime": 1786886056.0200372,
"ast_hash": "34f55617b7409f03f02e925bec92fac0",
"semantic_hash": ""
},
"backend/internal/store/store_test.go": {
"mtime": 1786867781.301985,
"mtime": 1786886056.0200372,
"ast_hash": "16f69d97eb63bcec7409294466642411",
"semantic_hash": ""
},
@@ -380,12 +380,12 @@
"semantic_hash": "19a573773be4ca22570ca2f8543120c5"
},
"backend/internal/web/web.go": {
"mtime": 1786863744.1453943,
"mtime": 1786886056.023822,
"ast_hash": "b051f2de214c4b253ee09fbbdb8ebb60",
"semantic_hash": ""
},
"backend/reader_credential_test.go": {
"mtime": 1786863966.7092986,
"mtime": 1786886056.023822,
"ast_hash": "0c93de387af595901c43b6bdb3bed8cc",
"semantic_hash": ""
},
@@ -395,7 +395,7 @@
"semantic_hash": "8008a187690764436540fab47ba0cfcc"
},
"userscript/novel-bookmark.user.js": {
"mtime": 1786872902.809149,
"mtime": 1786886056.0389624,
"ast_hash": "36c81462919719f0e053ca5d633a243a",
"semantic_hash": ""
},
@@ -425,17 +425,17 @@
"semantic_hash": "6c6f3e4c4c2f57867894280bce728c50"
},
"backend/AGENTS.md": {
"mtime": 1786872951.3127444,
"ast_hash": "68bc86af9593adad1e37fa05188fe63e",
"mtime": 1786891513.6486945,
"ast_hash": "b4233ebc5ff905b701ded9427277ad8c",
"semantic_hash": ""
},
"backend/CLAUDE.md": {
"mtime": 1786872951.3127444,
"ast_hash": "68bc86af9593adad1e37fa05188fe63e",
"mtime": 1786891513.6486945,
"ast_hash": "b4233ebc5ff905b701ded9427277ad8c",
"semantic_hash": ""
},
"backend/internal/web/templates/app.html": {
"mtime": 1786864445.1584811,
"mtime": 1786886056.023822,
"ast_hash": "99d3e1139042d0eb61627155dddb3843",
"semantic_hash": ""
},
@@ -465,7 +465,7 @@
"semantic_hash": "bcc3101498a66cf8b79f9d97c6c9cd6b"
},
"backend/internal/web/templates/readers.html": {
"mtime": 1786864296.770521,
"mtime": 1786886056.023822,
"ast_hash": "2b1cbb24d6185e48561966db1af04ba4",
"semantic_hash": ""
},
@@ -635,42 +635,42 @@
"semantic_hash": ""
},
"backend/internal/latest/status.go": {
"mtime": 1786865033.5648637,
"ast_hash": "8141514efa5a7a66e77b9a62d4982d52",
"mtime": 1786891396.3216066,
"ast_hash": "5893c77b117a5bcec3a4583aa51e7f42",
"semantic_hash": ""
},
"backend/internal/store/migrations/0010_reader_sightings.sql": {
"mtime": 1786863695.6957178,
"mtime": 1786886056.0200372,
"ast_hash": "a72c08c63fad530df3c793d16edfa385",
"semantic_hash": ""
},
"backend/internal/web/admin.go": {
"mtime": 1786865087.473256,
"ast_hash": "b1c0f23a102a9bc7f84adf306d743f56",
"mtime": 1786891411.0618205,
"ast_hash": "e02249496b57096cf1929f9cd0e35c3e",
"semantic_hash": ""
},
"backend/internal/web/templates/admin.html": {
"mtime": 1786865108.4095602,
"mtime": 1786886056.023822,
"ast_hash": "f04ea1cb01369d53053eac489e796faa",
"semantic_hash": ""
},
"backend/internal/web/templates/lanes.html": {
"mtime": 1786865113.5362902,
"ast_hash": "8b9f9c7a56e0ec3bf950aa70acd95e92",
"mtime": 1786891416.6118941,
"ast_hash": "0e96f66560bac29713a71ec31139d25e",
"semantic_hash": ""
},
"backend/internal/latest/sighting_test.go": {
"mtime": 1786868815.9760568,
"mtime": 1786886056.0200372,
"ast_hash": "3b1372bbeeb538b73f86f57da210b4cc",
"semantic_hash": ""
},
"backend/internal/store/migrations/0011_series_sightings.sql": {
"mtime": 1786867605.3728056,
"mtime": 1786886056.0200372,
"ast_hash": "d0ade928a6e951eb179e4d0255cca526",
"semantic_hash": ""
},
"docs/adr/0011-sighting-deferral-trust-model.md": {
"mtime": 1786872970.473592,
"mtime": 1786886056.0313923,
"ast_hash": "0b6ca704f62185c9d629320f2d8475b5",
"semantic_hash": ""
}