Compare commits

...

2 Commits

Author SHA1 Message Date
sulthan f568fb5e8c fix: an asleep browser Lane is not a stalled one on the admin page (#110)
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-08-16 22:04:18 +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 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 the series page) runs in the background so a slow CDN can't consume a
Lane's gap. 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 Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth
against fingerprint-based blocking; any failure log and skip. kagane, comix against fingerprint-based blocking; any failure log and skip. kagane, comix
and novelfull sit behind Cloudflare JavaScript challenges the TLS client 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 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 all. `Checked` next to `Due` is what separates a stopped Lane from a quiet
one, so neither figure may be dropped from the row. 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 const challengePollInterval = 2 * time.Second
// isInterstitial reports whether html is Cloudflare's challenge page rather // 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 // than the site's own. Matched on the challenge orchestration path
// stable across the interstitial's wording and locale — the visible "Just a // (/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. // 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 { 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 // 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) { func TestClassifyBrowserInterruption(t *testing.T) {
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) { if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err) 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 // 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 // per request. The Lane still paces at the default gap, which is what
// the owner's page must show rather than a zero. // the owner's page must show rather than a zero.
st.Gap = defaultGap st.Gap, st.Asleep = defaultGap, true
return defaultGap return defaultGap
} }
if s.Browser != nil { if s.Browser != nil {
+21
View File
@@ -1388,6 +1388,11 @@ func TestBrowserLaneWakeThresholds(t *testing.T) {
if got := browser.callCount(); got != 0 { if got := browser.callCount(); got != 0 {
t.Fatalf("browser fetches with 3 freshly-due series = %d, want 0 (Chrome stays asleep)", got) 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. // 5 due crosses the count threshold.
for i := 3; i < 5; i++ { for i := 3; i < 5; i++ {
seed(i) seed(i)
@@ -1396,6 +1401,9 @@ func TestBrowserLaneWakeThresholds(t *testing.T) {
if got := browser.callCount(); got != 5 { if got := browser.callCount(); got != 5 {
t.Fatalf("browser fetches with 5 due series = %d, want 5", got) 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. // A single long-neglected series wakes the browser by age alone.
seedForCheck(t, s, "kagane:ancient", "https://kagane.to/series/ancient", 0) seedForCheck(t, s, "kagane:ancient", "https://kagane.to/series/ancient", 0)
p.runOnce(context.Background()) 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 // 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 // Lanes are skipped: every fetch would fail anyway, and their Series must not
// burn their stamps on a dead Chrome (issue #100). // burn their stamps on a dead Chrome (issue #100).
+5
View File
@@ -19,6 +19,11 @@ type LaneState struct {
Clamped bool Clamped bool
Refusing bool Refusing bool
Browser 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). // 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. // is configured, which stops those Series just as completely.
BrowserLost bool BrowserLost bool
// Stalled marks a Lane with Series waiting that its last pass did not read // 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 Stalled bool
Asleep bool
// Attention is the one flag the template colours on, so an unhealthy Lane // Attention is the one flag the template colours on, so an unhealthy Lane
// is found at a glance rather than read for. // is found at a glance rather than read for.
Attention bool Attention bool
@@ -152,8 +155,9 @@ func (h *Handler) lanesView() lanesView {
for _, l := range snap.Lanes { for _, l := range snap.Lanes {
lost := l.Browser && !snap.BrowserReachable lost := l.Browser && !snap.BrowserReachable
// Series waiting and none read is the shape of a Lane that has stopped // 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. // working, as distinct from one that is quiet for want of work — or one
stalled := l.Due > 0 && l.Checked == 0 // deliberately leaving Chrome asleep until its group gathers.
stalled := l.Due > 0 && l.Checked == 0 && !l.Asleep
gap := "" gap := ""
if l.Gap > 0 { if l.Gap > 0 {
gap = l.Gap.Truncate(time.Second).String() gap = l.Gap.Truncate(time.Second).String()
@@ -168,6 +172,7 @@ func (h *Handler) lanesView() lanesView {
Refusing: l.Refusing, Refusing: l.Refusing,
BrowserLost: lost, BrowserLost: lost,
Stalled: stalled, Stalled: stalled,
Asleep: l.Asleep,
Attention: l.Clamped || l.Refusing || lost || stalled, Attention: l.Clamped || l.Refusing || lost || stalled,
}) })
} }
@@ -23,6 +23,7 @@
{{if .Refusing}}<span class="lane-mark">refusing</span>{{end}} {{if .Refusing}}<span class="lane-mark">refusing</span>{{end}}
{{if .BrowserLost}}<span class="lane-mark">no browser</span>{{end}} {{if .BrowserLost}}<span class="lane-mark">no browser</span>{{end}}
{{if .Stalled}}<span class="lane-mark">not checking</span>{{end}} {{if .Stalled}}<span class="lane-mark">not checking</span>{{end}}
{{if .Asleep}}<span class="lane-mark">browser asleep</span>{{end}}
</li> </li>
{{end}} {{end}}
</ul> </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 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 // 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. // 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", "10": "Go Test Helpers",
"11": "Store Tests", "11": "Store Tests",
"12": "Bookmarks API Handler", "12": "Bookmarks API Handler",
"13": "Web UI Handlers", "13": "displayChapter",
"14": "Go Error Handling", "14": "Go Error Handling",
"15": "CDP Browser Client",
"16": "Cloudflare bot scoring and poll cadence — what is actually documented", "16": "Cloudflare bot scoring and poll cadence — what is actually documented",
"17": "Go Code Style Guide", "17": "Go Code Style Guide",
"18": "Agent Skills", "18": "Agent Skills",
@@ -30,7 +29,6 @@
"28": "Allocation Patterns", "28": "Allocation Patterns",
"29": "Observability & Alerting", "29": "Observability & Alerting",
"30": "AGENTS.md", "30": "AGENTS.md",
"31": "Store",
"32": "Open", "32": "Open",
"33": "Go Testing Guide", "33": "Go Testing Guide",
"34": "Session Store", "34": "Session Store",
@@ -41,7 +39,6 @@
"39": "UI Critique 2026-07-26A", "39": "UI Critique 2026-07-26A",
"40": "UI Critique 2026-07-26B", "40": "UI Critique 2026-07-26B",
"41": "Handler", "41": "Handler",
"42": "pgtest.go",
"43": "Issue Tracker & Triage", "43": "Issue Tracker & Triage",
"44": "Ticket Workflow", "44": "Ticket Workflow",
"45": "Go Perf Alert Rules", "45": "Go Perf Alert Rules",
@@ -124,7 +121,6 @@
"122": "Identity comes from Discord OAuth; we store no passwords and send no email", "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", "123": "The wire format stays flat and deliberately does not mirror the schema",
"124": "ADR-0005: On-demand browser sidecar", "124": "ADR-0005: On-demand browser sidecar",
"125": "sessions_test.go",
"126": "Bookmark Manager", "126": "Bookmark Manager",
"127": "triage-labels.md", "127": "triage-labels.md",
"128": "Cross-Ticket Contract", "128": "Cross-Ticket Contract",
+28 -44
View File
@@ -1,16 +1,16 @@
# Graph Report - mangaBookmark (2026-08-16) # Graph Report - mangaBookmark (2026-08-16)
## Corpus Check ## Corpus Check
- 119 files · ~290,953 words - 119 files · ~291,777 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 1736 nodes · 3597 edges · 221 communities (70 shown, 151 thin omitted) - 1739 nodes · 3610 edges · 217 communities (67 shown, 150 thin omitted)
- Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 361 edges (avg confidence: 0.78) - Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 363 edges (avg confidence: 0.78)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## 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 `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@@ -28,9 +28,8 @@
- [[_COMMUNITY_Go Test Helpers|Go Test Helpers]] - [[_COMMUNITY_Go Test Helpers|Go Test Helpers]]
- [[_COMMUNITY_Store Tests|Store Tests]] - [[_COMMUNITY_Store Tests|Store Tests]]
- [[_COMMUNITY_Bookmarks API Handler|Bookmarks API Handler]] - [[_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_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_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_Go Code Style Guide|Go Code Style Guide]]
- [[_COMMUNITY_Agent Skills|Agent Skills]] - [[_COMMUNITY_Agent Skills|Agent Skills]]
@@ -46,7 +45,6 @@
- [[_COMMUNITY_Allocation Patterns|Allocation Patterns]] - [[_COMMUNITY_Allocation Patterns|Allocation Patterns]]
- [[_COMMUNITY_Observability & Alerting|Observability & Alerting]] - [[_COMMUNITY_Observability & Alerting|Observability & Alerting]]
- [[_COMMUNITY_AGENTS|AGENTS.md]] - [[_COMMUNITY_AGENTS|AGENTS.md]]
- [[_COMMUNITY_Store|Store]]
- [[_COMMUNITY_Go Testing Guide|Go Testing Guide]] - [[_COMMUNITY_Go Testing Guide|Go Testing Guide]]
- [[_COMMUNITY_Session Store|Session Store]] - [[_COMMUNITY_Session Store|Session Store]]
- [[_COMMUNITY_Web UI Filter Logic|Web UI Filter Logic]] - [[_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-26A|UI Critique 2026-07-26A]]
- [[_COMMUNITY_UI Critique 2026-07-26B|UI Critique 2026-07-26B]] - [[_COMMUNITY_UI Critique 2026-07-26B|UI Critique 2026-07-26B]]
- [[_COMMUNITY_Handler|Handler]] - [[_COMMUNITY_Handler|Handler]]
- [[_COMMUNITY_pgtest.go|pgtest.go]]
- [[_COMMUNITY_Issue Tracker & Triage|Issue Tracker & Triage]] - [[_COMMUNITY_Issue Tracker & Triage|Issue Tracker & Triage]]
- [[_COMMUNITY_Ticket Workflow|Ticket Workflow]] - [[_COMMUNITY_Ticket Workflow|Ticket Workflow]]
- [[_COMMUNITY_Go Perf Alert Rules|Go Perf Alert Rules]] - [[_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_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_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_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_Bookmark Manager|Bookmark Manager]]
- [[_COMMUNITY_triage-labels|triage-labels.md]] - [[_COMMUNITY_triage-labels|triage-labels.md]]
- [[_COMMUNITY_Cross-Ticket Contract|Cross-Ticket Contract]] - [[_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] - **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] - **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" ### Community 0 - "HTMX Library Internals"
Cohesion: 0.08 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" ### Community 1 - "Cover Fetch Test Helpers"
Cohesion: 0.09 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" ### Community 2 - "Manga Userscript Adapters"
Cohesion: 0.06 Cohesion: 0.06
@@ -293,12 +289,12 @@ Cohesion: 0.07
Nodes (103): bookmarkNewKaganeSeries(), bookmarkNewNovelfullSeries(), bookmarkNewSeries(), Context, Store, T, newAcquirer(), readBookmark() (+95 more) Nodes (103): bookmarkNewKaganeSeries(), bookmarkNewNovelfullSeries(), bookmarkNewSeries(), Context, Store, T, newAcquirer(), readBookmark() (+95 more)
### Community 5 - "Bookmarks API Tests" ### Community 5 - "Bookmarks API Tests"
Cohesion: 0.07 Cohesion: 0.11
Nodes (71): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+63 more) Nodes (54): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+46 more)
### Community 7 - "Cover & Acquire Internals" ### Community 7 - "Cover & Acquire Internals"
Cohesion: 0.06 Cohesion: 0.08
Nodes (43): Addr, fakeLanes, Context, Store, WaitGroup, isInterstitial(), defaultCoverResolver(), fetchCoverBytes() (+35 more) Nodes (31): fakeLanes, Context, Store, WaitGroup, isInterstitial(), fetchCoverBytes(), browserWakeDue(), fetcherFor() (+23 more)
### Community 8 - "System Architecture Concepts" ### Community 8 - "System Architecture Concepts"
Cohesion: 0.15 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) 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" ### Community 11 - "Store Tests"
Cohesion: 0.14 Cohesion: 0.07
Nodes (45): Store, T, newTestStore(), readLatestCheckedAt(), readSeries(), secondReader(), seedForCheck(), seedSecondReader() (+37 more) Nodes (81): M, TestMain(), M, TestMain(), M, Main(), start(), URL() (+73 more)
### Community 12 - "Bookmarks API Handler" ### Community 12 - "Bookmarks API Handler"
Cohesion: 0.08 Cohesion: 0.08
Nodes (32): Handler, Request, ResponseWriter, Store, Healthz(), writeJSON(), Auth(), compressible() (+24 more) 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" ### Community 14 - "Go Error Handling"
Cohesion: 0.06 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) 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" ### Community 16 - "Cloudflare bot scoring and poll cadence — what is actually documented"
Cohesion: 0.06 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) 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) Nodes (12): coverResponse(), Request, T, TestCoverFetcherCanonicalisesJpgAlias(), TestCoverFetcherFetchesPublicHTTPSImage(), TestCoverFetcherRefusesUnsafeDestinationsBeforeRequest(), TestCoverFetcherRejectsNonImage(), TestCoverFetcherRejectsOversizedBody() (+4 more)
### Community 26 - "Open" ### Community 26 - "Open"
Cohesion: 0.05 Cohesion: 0.06
Nodes (61): awaitPromise(), browserConnectionLost(), classifyBrowserError(), comixRead(), comixSeriesPageURL(), Action, Context, Mutex (+53 more) Nodes (57): awaitPromise(), browserConnectionLost(), classifyBrowserError(), comixRead(), comixSeriesPageURL(), Action, Context, Mutex (+49 more)
### Community 27 - "Find Skills Guide" ### Community 27 - "Find Skills Guide"
Cohesion: 0.14 Cohesion: 0.14
@@ -384,10 +380,6 @@ Nodes (9): Alerting rules (examples), CPU saturation, GC pressure, Goroutine lea
Cohesion: 0.40 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 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" ### Community 33 - "Go Testing Guide"
Cohesion: 0.20 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) 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 Nodes (6): Design Health Score, Design Specificity Verdict, Minor Observations, Persona Red Flags, Priority Issues, Questions to Consider
### Community 41 - "Handler" ### Community 41 - "Handler"
Cohesion: 0.14 Cohesion: 0.05
Nodes (18): currentLib(), currentTab(), filterBookmarks(), Client, HandlerFunc, Request, ResponseWriter, Store (+10 more) Nodes (25): coverRelativePath(), coverSourceAddress(), displayChapter(), Store, scanSeries(), currentLib(), currentTab(), filterBookmarks() (+17 more)
### Community 42 - "pgtest.go"
Cohesion: 0.18
Nodes (12): M, TestMain(), M, TestMain(), M, Main(), start(), URL() (+4 more)
### Community 45 - "Go Perf Alert Rules" ### Community 45 - "Go Perf Alert Rules"
Cohesion: 0.50 Cohesion: 0.50
@@ -532,10 +520,6 @@ Nodes (3): Consequence, The wire format stays flat and deliberately does not mir
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): ADR-0005: On-demand browser sidecar, Constraints, Decision 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" ### Community 154 - "ADR-0011: Sightings — a Reader report defers a Poll where being wrong hurts only them"
Cohesion: 0.33 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 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 ## Knowledge Gaps
- **524 isolated node(s):** `bookmarkmanager/backend`, `ctxKey`, `loginView`, `ctxKey`, `test` (+519 more) - **524 isolated node(s):** `bookmarkmanager/backend`, `ctxKey`, `loginView`, `ctxKey`, `test` (+519 more)
These have ≤1 connection - possible missing edges or undocumented components. 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 ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _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`?** - **Why does `Open()` connect `Store Tests` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Tests`, `Handler`, `displayChapter`?**
_High betweenness centrality (0.060) - this node is a cross-community bridge._ _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`, `CDP Browser Client`, `ResponseWriter`?** - **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.050) - this node is a cross-community bridge._ _High betweenness centrality (0.051) - 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`?** - **Why does `newRouter()` connect `Bookmarks API Tests` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Handler`, `displayChapter`, `ResponseWriter`?**
_High betweenness centrality (0.030) - this node is a cross-community bridge._ _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?** - **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._ _`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?** - **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" "semantic_hash": "dac242903b0e98c3e4395159d609e08e"
}, },
"backend/main.go": { "backend/main.go": {
"mtime": 1786863963.3450089, "mtime": 1786886056.023822,
"ast_hash": "d5a50b03438d7c120079d40cc578462b", "ast_hash": "d5a50b03438d7c120079d40cc578462b",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -35,7 +35,7 @@
"semantic_hash": "4a94ac85bad6bce330d085bcc0ae3ffd" "semantic_hash": "4a94ac85bad6bce330d085bcc0ae3ffd"
}, },
"userscript/manga-bookmark.user.js": { "userscript/manga-bookmark.user.js": {
"mtime": 1786872888.3482232, "mtime": 1786886056.0389624,
"ast_hash": "4e79684942234b26f4a1fb03fccd6d35", "ast_hash": "4e79684942234b26f4a1fb03fccd6d35",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -170,8 +170,8 @@
"semantic_hash": "e51077b6a7f1f67afc748f1a32a1557d" "semantic_hash": "e51077b6a7f1f67afc748f1a32a1557d"
}, },
"backend/web_test.go": { "backend/web_test.go": {
"mtime": 1786865248.7717106, "mtime": 1786891437.925184,
"ast_hash": "7c298e39c63e4502cf6272d1e206e9ef", "ast_hash": "74aed7f5a5b196d2b860cd6a34266ec6",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/main_test.go": { "backend/main_test.go": {
@@ -220,22 +220,22 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/design-system.md": { "docs/design-system.md": {
"mtime": 1786865139.9022946, "mtime": 1786886056.0313923,
"ast_hash": "3bd39932997c8e245508874194db4e49", "ast_hash": "3bd39932997c8e245508874194db4e49",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/api_test.go": { "backend/api_test.go": {
"mtime": 1786868080.2317674, "mtime": 1786886056.016252,
"ast_hash": "b64372df5429fb14e48b37b3bab10402", "ast_hash": "b64372df5429fb14e48b37b3bab10402",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/cover_test.go": { "backend/cover_test.go": {
"mtime": 1786863966.7084978, "mtime": 1786886056.016252,
"ast_hash": "fd6b3f6ef46bd17b0241d104b6e5bdf6", "ast_hash": "fd6b3f6ef46bd17b0241d104b6e5bdf6",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/api/handlers.go": { "backend/internal/api/handlers.go": {
"mtime": 1786867745.788687, "mtime": 1786886056.016252,
"ast_hash": "ae4f3b961450f06a06e0632eb273a5b2", "ast_hash": "ae4f3b961450f06a06e0632eb273a5b2",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -245,13 +245,13 @@
"semantic_hash": "385b36f58488b7e6d93eb6d6034e9ee3" "semantic_hash": "385b36f58488b7e6d93eb6d6034e9ee3"
}, },
"backend/internal/latest/browser.go": { "backend/internal/latest/browser.go": {
"mtime": 1786861012.026504, "mtime": 1786889854.5578685,
"ast_hash": "75f34a974568d122681939dd297944a2", "ast_hash": "b65f3693db77c69ef0a6247fd75118bc",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/browser_test.go": { "backend/internal/latest/browser_test.go": {
"mtime": 1786857336.6414917, "mtime": 1786889854.5578685,
"ast_hash": "800fa6aa471ada054a3c48943ad17ab7", "ast_hash": "118aba3d794b9faf893c45e58298e11a",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/fetch.go": { "backend/internal/latest/fetch.go": {
@@ -260,17 +260,17 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/poller.go": { "backend/internal/latest/poller.go": {
"mtime": 1786872774.641225, "mtime": 1786891405.415882,
"ast_hash": "5e55e2ac5cb7d347807754f92425d29b", "ast_hash": "54f6eec9d2090c7713f502c8553063a2",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/poller_test.go": { "backend/internal/latest/poller_test.go": {
"mtime": 1786865291.3758123, "mtime": 1786891462.5665002,
"ast_hash": "1da669ac748a8d1a0289e557392fa3fd", "ast_hash": "512806a9c6e15036a75f01bf5577c20b",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/sites.go": { "backend/internal/latest/sites.go": {
"mtime": 1786868771.0663204, "mtime": 1786886056.0200372,
"ast_hash": "5786000dd0dfd5b4b3bd2b87b1fc100b", "ast_hash": "5786000dd0dfd5b4b3bd2b87b1fc100b",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -340,12 +340,12 @@
"semantic_hash": "0b6764a0ee20f5cb7748eecd31a1d220" "semantic_hash": "0b6764a0ee20f5cb7748eecd31a1d220"
}, },
"backend/internal/store/store.go": { "backend/internal/store/store.go": {
"mtime": 1786868763.646761, "mtime": 1786886056.0200372,
"ast_hash": "34f55617b7409f03f02e925bec92fac0", "ast_hash": "34f55617b7409f03f02e925bec92fac0",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/store/store_test.go": { "backend/internal/store/store_test.go": {
"mtime": 1786867781.301985, "mtime": 1786886056.0200372,
"ast_hash": "16f69d97eb63bcec7409294466642411", "ast_hash": "16f69d97eb63bcec7409294466642411",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -380,12 +380,12 @@
"semantic_hash": "19a573773be4ca22570ca2f8543120c5" "semantic_hash": "19a573773be4ca22570ca2f8543120c5"
}, },
"backend/internal/web/web.go": { "backend/internal/web/web.go": {
"mtime": 1786863744.1453943, "mtime": 1786886056.023822,
"ast_hash": "b051f2de214c4b253ee09fbbdb8ebb60", "ast_hash": "b051f2de214c4b253ee09fbbdb8ebb60",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/reader_credential_test.go": { "backend/reader_credential_test.go": {
"mtime": 1786863966.7092986, "mtime": 1786886056.023822,
"ast_hash": "0c93de387af595901c43b6bdb3bed8cc", "ast_hash": "0c93de387af595901c43b6bdb3bed8cc",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -395,7 +395,7 @@
"semantic_hash": "8008a187690764436540fab47ba0cfcc" "semantic_hash": "8008a187690764436540fab47ba0cfcc"
}, },
"userscript/novel-bookmark.user.js": { "userscript/novel-bookmark.user.js": {
"mtime": 1786872902.809149, "mtime": 1786886056.0389624,
"ast_hash": "36c81462919719f0e053ca5d633a243a", "ast_hash": "36c81462919719f0e053ca5d633a243a",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -425,17 +425,17 @@
"semantic_hash": "6c6f3e4c4c2f57867894280bce728c50" "semantic_hash": "6c6f3e4c4c2f57867894280bce728c50"
}, },
"backend/AGENTS.md": { "backend/AGENTS.md": {
"mtime": 1786872951.3127444, "mtime": 1786891513.6486945,
"ast_hash": "68bc86af9593adad1e37fa05188fe63e", "ast_hash": "b4233ebc5ff905b701ded9427277ad8c",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/CLAUDE.md": { "backend/CLAUDE.md": {
"mtime": 1786872951.3127444, "mtime": 1786891513.6486945,
"ast_hash": "68bc86af9593adad1e37fa05188fe63e", "ast_hash": "b4233ebc5ff905b701ded9427277ad8c",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/web/templates/app.html": { "backend/internal/web/templates/app.html": {
"mtime": 1786864445.1584811, "mtime": 1786886056.023822,
"ast_hash": "99d3e1139042d0eb61627155dddb3843", "ast_hash": "99d3e1139042d0eb61627155dddb3843",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -465,7 +465,7 @@
"semantic_hash": "bcc3101498a66cf8b79f9d97c6c9cd6b" "semantic_hash": "bcc3101498a66cf8b79f9d97c6c9cd6b"
}, },
"backend/internal/web/templates/readers.html": { "backend/internal/web/templates/readers.html": {
"mtime": 1786864296.770521, "mtime": 1786886056.023822,
"ast_hash": "2b1cbb24d6185e48561966db1af04ba4", "ast_hash": "2b1cbb24d6185e48561966db1af04ba4",
"semantic_hash": "" "semantic_hash": ""
}, },
@@ -635,42 +635,42 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/status.go": { "backend/internal/latest/status.go": {
"mtime": 1786865033.5648637, "mtime": 1786891396.3216066,
"ast_hash": "8141514efa5a7a66e77b9a62d4982d52", "ast_hash": "5893c77b117a5bcec3a4583aa51e7f42",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/store/migrations/0010_reader_sightings.sql": { "backend/internal/store/migrations/0010_reader_sightings.sql": {
"mtime": 1786863695.6957178, "mtime": 1786886056.0200372,
"ast_hash": "a72c08c63fad530df3c793d16edfa385", "ast_hash": "a72c08c63fad530df3c793d16edfa385",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/web/admin.go": { "backend/internal/web/admin.go": {
"mtime": 1786865087.473256, "mtime": 1786891411.0618205,
"ast_hash": "b1c0f23a102a9bc7f84adf306d743f56", "ast_hash": "e02249496b57096cf1929f9cd0e35c3e",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/web/templates/admin.html": { "backend/internal/web/templates/admin.html": {
"mtime": 1786865108.4095602, "mtime": 1786886056.023822,
"ast_hash": "f04ea1cb01369d53053eac489e796faa", "ast_hash": "f04ea1cb01369d53053eac489e796faa",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/web/templates/lanes.html": { "backend/internal/web/templates/lanes.html": {
"mtime": 1786865113.5362902, "mtime": 1786891416.6118941,
"ast_hash": "8b9f9c7a56e0ec3bf950aa70acd95e92", "ast_hash": "0e96f66560bac29713a71ec31139d25e",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/latest/sighting_test.go": { "backend/internal/latest/sighting_test.go": {
"mtime": 1786868815.9760568, "mtime": 1786886056.0200372,
"ast_hash": "3b1372bbeeb538b73f86f57da210b4cc", "ast_hash": "3b1372bbeeb538b73f86f57da210b4cc",
"semantic_hash": "" "semantic_hash": ""
}, },
"backend/internal/store/migrations/0011_series_sightings.sql": { "backend/internal/store/migrations/0011_series_sightings.sql": {
"mtime": 1786867605.3728056, "mtime": 1786886056.0200372,
"ast_hash": "d0ade928a6e951eb179e4d0255cca526", "ast_hash": "d0ade928a6e951eb179e4d0255cca526",
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/adr/0011-sighting-deferral-trust-model.md": { "docs/adr/0011-sighting-deferral-trust-model.md": {
"mtime": 1786872970.473592, "mtime": 1786886056.0313923,
"ast_hash": "0b6ca704f62185c9d629320f2d8475b5", "ast_hash": "0b6ca704f62185c9d629320f2d8475b5",
"semantic_hash": "" "semantic_hash": ""
} }