diff --git a/backend/internal/latest/fetch.go b/backend/internal/latest/fetch.go index 866bed4..72d965e 100644 --- a/backend/internal/latest/fetch.go +++ b/backend/internal/latest/fetch.go @@ -11,8 +11,9 @@ import ( ) // maxBodyBytes caps what a single series page can cost in memory. Real pages -// measured 100-400 KB on 2026-07-26, so this is roughly 10x headroom and mostly -// guards against a proxy handing back something enormous. +// measured 100-400 KB on 2026-07-26; lightnovelworld runs larger — 685 KB and +// 1.18 MB measured 2026-08-11 — so the headroom there is roughly 3.5x, and the +// cap mostly guards against a proxy handing back something enormous. const maxBodyBytes = 4 << 20 // chromeUA matches the client profile below. A Chrome fingerprint paired with a diff --git a/backend/internal/latest/poller_test.go b/backend/internal/latest/poller_test.go index ae496b2..59b3cf8 100644 --- a/backend/internal/latest/poller_test.go +++ b/backend/internal/latest/poller_test.go @@ -1010,10 +1010,10 @@ func TestRunOnceFillsBlankCoverFromSeriesPage(t *testing.T) { }, { name: "lightnovelworld novel", - key: "lightnovelworld:a-will-eternal", site: "lightnovelworld", - seriesID: "a-will-eternal", seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/", + key: "lightnovelworld:all-jobs-and-classes-i-just-wanted-one-skill-not-them-all", site: "lightnovelworld", + seriesID: "all-jobs-and-classes-i-just-wanted-one-skill-not-them-all", seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/", kind: store.KindNovel, body: lnwSeriesFixture + lnwCoverFixture, - wantCover: "https://lightnovelworld.net/wp-content/uploads/2026/03/a-will-eternal-1.webp", + wantCover: "https://i1.wp.com/lightnovelworld.net/wp-content/uploads/2025/10/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all.jpg", }, { name: "kagane manga", diff --git a/backend/internal/latest/sites.go b/backend/internal/latest/sites.go index a4b1225..0c354ec 100644 --- a/backend/internal/latest/sites.go +++ b/backend/internal/latest/sites.go @@ -3,6 +3,7 @@ package latest import ( "encoding/json" "html" + "log" "net/url" "regexp" "strconv" @@ -58,11 +59,26 @@ var kaganeChapterRe = regexp.MustCompile(`"chapter_no":"([0-9.]+)"`) // "//chapter-[-].html". Verified live 2026-08-05. var novelfullSlugRe = regexp.MustCompile(`^/([^/?#]+)\.html$`) -// lnwSlugRe does the same for lightnovelworld, whose series pages live under -// /novel// while its chapter URLs are flat at the site root: -// "/-chapter-/", absolute in the page's own anchors. Verified live -// 2026-08-05. -var lnwSlugRe = regexp.MustCompile(`^/novel/([^/?#]+)/?$`) +// lnwChapterRe matches any chapter-shaped address on lightnovelworld. Unlike +// asura, novelfull and comix — which scope to their stored series slug so a +// foreign chapter link cannot contribute — this Site's chapter addresses carry +// the Chapter Slug, which is not the Series identity: one Series may publish +// under several Chapter Slugs (measured 2026-08-11: a sampled novel serves +// 1-99 under one slug and 100-423 under another), so no stored-slug pattern can +// cover a Series' whole list. An unscoped match is safe because +// latestChapterFrom truncates the body at the comment thread before scanning +// (lnwCommentMarker); without that, a visitor's comment could set the Latest +// Chapter on the shared Series row. +var lnwChapterRe = regexp.MustCompile(`lightnovelworld\.net/[a-z0-9-]+-chapter-([0-9.]+)/`) + +// lnwCommentMarker is the boundary of lightnovelworld's server-rendered +// wpdiscuz comment thread. It occurs exactly once per page and follows every +// chapter anchor (measured 2026-08-11, +// docs/research/lightnovelworld-chapter-vs-series-slug.md §6), so cutting the +// body at its first occurrence keeps the whole chapter list while excluding a +// region any visitor can write to. Absent means the page shape changed: the +// body is skipped, never scanned whole. +const lnwCommentMarker = "wpd-threads" // latestChapterFrom returns the highest chapter number body advertises for this // series. ok is false when the body yields nothing usable — an unknown site, an @@ -82,6 +98,7 @@ var lnwSlugRe = regexp.MustCompile(`^/novel/([^/?#]+)/?$`) // such guarantee — demonicChapterRe matches any chaptered.php?manga= anchor // with no per-series scoping, because the stored series_id for demonic is a // slug, not the numeric id the URL carries, so it cannot easily be scoped. +// lightnovelworld is unscoped and body-truncated instead — see lnwChapterRe. func latestChapterFrom(site, seriesURL, body string) (latestChapter, bool) { var re *regexp.Regexp switch site { @@ -126,15 +143,19 @@ func latestChapterFrom(site, seriesURL, body string) (latestChapter, bool) { // and neither may contribute to the maximum. re = regexp.MustCompile(`/` + regexp.QuoteMeta(m[1]) + `/chapter-([0-9.]+)`) case "lightnovelworld": - u, err := url.Parse(seriesURL) - if err != nil { + // The comment thread below the chapter list is the one region of the + // page any visitor can write to, so the scan never reads past it (see + // lnwChapterRe). A body without the marker is skipped, never scanned + // whole — a redesign must degrade into staleness, not into a wrong + // shared value; the logged body length tells a markup change from a + // body the size cap cut short. + i := strings.Index(body, lnwCommentMarker) + if i < 0 { + log.Printf("latest poll %q: no %s marker in %d bytes", seriesURL, lnwCommentMarker, len(body)) return latestChapter{}, false } - m := lnwSlugRe.FindStringSubmatch(u.Path) - if m == nil { - return latestChapter{}, false - } - re = regexp.MustCompile(`lightnovelworld\.net/` + regexp.QuoteMeta(m[1]) + `-chapter-([0-9.]+)/`) + body = body[:i] + re = lnwChapterRe default: return latestChapter{}, false } diff --git a/backend/internal/latest/sites_test.go b/backend/internal/latest/sites_test.go index 16cbab0..d665b25 100644 --- a/backend/internal/latest/sites_test.go +++ b/backend/internal/latest/sites_test.go @@ -1,6 +1,9 @@ package latest -import "testing" +import ( + "strings" + "testing" +) // Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af // fetched 2026-07-26. The first anchor is the "First Chapter" shortcut: it is a @@ -69,14 +72,113 @@ const novelfullSeriesFixture = ` Chapter 9999 ` -// Trimmed from https://lightnovelworld.net/novel/a-will-eternal/ fetched -// 2026-08-05. Its chapter anchors are absolute and flat — /-chapter-/ -// at the site root, not under /novel/. The last anchor is another series'. +// Trimmed from +// https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/ +// fetched 2026-08-11, whole document (~307 KB decoded; the wire body is ~32 KB +// zstd-compressed). This +// novel publishes its chapters under two Chapter Slugs: 1–99 at +// …-not-chapter-/ and 100–423 at …-not-them-all-chapter-/, and the page +// lists them newest-first, so the …-not-them-all anchors precede the …-not +// anchors. Every anchor through the comment-thread marker is verbatim page +// text (the site renders this novel's chapter titles as "[ ... words ]"). The +// comment block after the marker is the real wpdiscuz comment #wpd-comm-358_0 +// from https://lightnovelworld.net/novel/the-sword-illuminates-the-great-wilderness/ +// — the pinned page serves zero comments — with its share/link/vote/reply +// boilerplate trimmed. The comment's body carried no link, so the bare to https://lightnovelworld.net/overgeared-chapter-2059/ inside +// wpd-comment-text is the one composed element; that URL is a real chapter of +// a real different novel (overgeared; fetched, HTTP 200). const lnwSeriesFixture = ` -Chapter 1 -Chapter 1317 -Chapter 1298 -Chapter 9999 +
  • + +
    Vol. 1 Ch. 404
    +
    [ ... words ]
    +
    April 12, 2026
    +
    +
  • +
  • + +
    Vol. 1 Ch. 423
    +
    [ ... words ]
    +
    April 7, 2026
    +
    +
  • +
  • + +
    Vol. 1 Ch. 300
    +
    [ ... words ]
    +
    April 4, 2026
    +
    +
  • +
  • + +
    Vol. 1 Ch. 200
    +
    [ ... words ]
    +
    March 29, 2026
    +
    +
  • +
  • + +
    Vol. 1 Ch. 100
    +
    [ ... words ]
    +
    March 22, 2026
    +
    +
  • +
  • + +
    Vol. 1 Ch. 99
    +
    Chapter 99
    +
    November 5, 2025
    +
    +
  • +
  • + +
    Vol. 1 Ch. 50
    +
    Chapter 50
    +
    October 29, 2025
    +
    +
  • +
  • + +
    Vol. 1 Ch. 1
    +
    Chapter 01
    +
    October 11, 2025
    +
    +
  • +
    +
    +
    +
    +
    + hasbi asy +
    +
    + Member +
    + +
    +
    +
    +
    + hasbi asy +
    +
    + + 1 month ago +
    + +
    + + +
    +
    +
    +
    +
    ` // Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af @@ -98,8 +200,10 @@ const kaganeCoverFixture = `{"series_covers":[{"cover_id":"019fe11a-84d1-714b-9c // Trimmed from https://novelfull.com/reverend-insanity.html on 2026-08-10. const novelfullCoverFixture = `` -// Trimmed from https://lightnovelworld.net/novel/a-will-eternal/ on 2026-08-10. -const lnwCoverFixture = `` +// Trimmed from +// https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/ +// on 2026-08-11. +const lnwCoverFixture = `` func TestCoverFrom(t *testing.T) { const comixURL = "https://comix.to/title/n8we-dungeons-and-crayons" @@ -139,7 +243,7 @@ func TestCoverFrom(t *testing.T) { { name: "lightnovelworld reads og image", site: "lightnovelworld", body: lnwCoverFixture, wantOK: true, - wantCover: "https://lightnovelworld.net/wp-content/uploads/2026/03/a-will-eternal-1.webp", + wantCover: "https://i1.wp.com/lightnovelworld.net/wp-content/uploads/2025/10/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all.jpg", }, { name: "later metadata cover survives empty match", @@ -307,24 +411,38 @@ func TestLatestChapterFrom(t *testing.T) { body: novelfullSeriesFixture, wantOK: false, }, + // Stored before the slug split, so the address carries the ...-not + // Chapter Slug; the 100-423 block under the other slug must still win. + // The body is the chapter-list portion of lnwSeriesFixture with the + // comment block omitted; the marker is kept, because a body without it + // is skipped, not scanned. { - name: "lightnovelworld takes the max and ignores another series", + name: "lightnovelworld max spans both chapter slugs", site: "lightnovelworld", - seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/", - body: lnwSeriesFixture, - wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317", + seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not/", + body: strings.SplitN(lnwSeriesFixture, lnwCommentMarker, 2)[0] + lnwCommentMarker, + wantOK: true, wantNum: 423, wantLabel: "Chapter 423", }, { - name: "lightnovelworld tolerates a series url with no trailing slash", + name: "lightnovelworld comment anchor cannot set the latest chapter", site: "lightnovelworld", - seriesURL: "https://lightnovelworld.net/novel/a-will-eternal", + seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/", body: lnwSeriesFixture, - wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317", + wantOK: true, wantNum: 423, wantLabel: "Chapter 423", + }, + // Marker removed from the fixture, comment block still present: a + // redesign must degrade into a skip, never into the comment's number. + { + name: "lightnovelworld body without the comment marker is skipped", + site: "lightnovelworld", + seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/", + body: strings.ReplaceAll(lnwSeriesFixture, lnwCommentMarker, ""), + wantOK: false, }, { name: "lightnovelworld yields nothing on a challenge page", site: "lightnovelworld", - seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/", + seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/", body: challengeFixture, wantOK: false, }, diff --git a/backend/internal/latest/smoke_lnw_test.go b/backend/internal/latest/smoke_lnw_test.go new file mode 100644 index 0000000..cd3e589 --- /dev/null +++ b/backend/internal/latest/smoke_lnw_test.go @@ -0,0 +1,101 @@ +package latest + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" +) + +// lnwSeriesPageFloor is the smallest body that can still be a whole +// lightnovelworld Series page. Whole pages measured 685 KB..1.18 MB on +// 2026-08-11 and carry the marker at ~94% of the document, so a body under +// 100 KB is a challenge, a notice, or a truncated read — asserting on it +// would report the marker missing when it was never fetched. +const lnwSeriesPageFloor = 100 << 10 + +// TestSmokeLnwCommentBoundary is the live proof that the comment-thread marker +// the lightnovelworld chapter scan truncates at (lnwCommentMarker, +// "wpd-threads") still holds on the Site. The scan depends on it: when the +// marker vanishes every Series is skipped and logged — correct, but silent +// until a Reader notices their Latest Chapter has stopped moving. It runs only +// when SMOKE_LNW_SERIES_URL is set — the URL of the live Series page to check. +// The immortality-simulator page measured 2026-08-11 +// (docs/research/lightnovelworld-chapter-vs-series-slug.md) is the default to +// point it at: +// +// SMOKE_LNW_SERIES_URL=https://lightnovelworld.net/novel/immortality-simulator/ go test -v -run TestSmokeLnwCommentBoundary ./internal/latest +// +// A red run means the Site's markup has moved — the marker is gone, occurs +// more than once, or no longer follows the last chapter anchor — and the scan +// in sites.go is now skipping this Site. Revisit sites.go before anything +// else; the test is not flaky. A Cloudflare challenge or a non-200 is +// distinguished from a marker failure by the "not a marker failure" messages +// below, which carry the observed status and body length. +func TestSmokeLnwCommentBoundary(t *testing.T) { + seriesURL := os.Getenv("SMOKE_LNW_SERIES_URL") + if seriesURL == "" { + t.Skip("SMOKE_LNW_SERIES_URL unset") + } + if !fetchableSeriesURL("lightnovelworld", seriesURL) { + t.Fatalf("%q is not a fetchable lightnovelworld series URL", seriesURL) + } + + f, err := NewTLSFetcher() + if err != nil { + t.Fatalf("NewTLSFetcher: %v", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + body, status, err := f.Get(ctx, seriesURL) + if err != nil { + t.Fatalf("Get: %v", err) + } + if status != http.StatusOK { + t.Fatalf("status = %d, body %d bytes — not a marker failure; the Site did not answer this IP with a Series page", status, len(body)) + } + if len(body) < lnwSeriesPageFloor { + t.Fatalf("body %d bytes — not a whole Series page (measured 685 KB..1.18 MB); not a marker failure, likely a Cloudflare challenge or a non-Series response", len(body)) + } + if !lnwChapterRe.MatchString(body) { + t.Fatalf("no chapter anchor in %d bytes — not a lightnovelworld Series page; not a marker failure, likely a Cloudflare challenge or a non-Series response", len(body)) + } + markerIdx := strings.Index(body, lnwCommentMarker) + if failures := checkLnwCommentBoundary(body); len(failures) > 0 { + t.Fatalf("%s (body %d bytes)", strings.Join(failures, "; "), len(body)) + } + t.Logf("ok: %q once at byte %d, body %d bytes", lnwCommentMarker, markerIdx, len(body)) +} + +// checkLnwCommentBoundary verifies the three marker assertions against a +// fetched Series body: the marker occurs exactly once, every chapter anchor +// precedes it, and at least one anchor precedes it at all. It returns one +// human-readable failure per broken assertion — with observed offsets and body +// length — and empty when the page is healthy. +func checkLnwCommentBoundary(body string) []string { + markerIdx := strings.Index(body, lnwCommentMarker) + switch n := strings.Count(body, lnwCommentMarker); { + case n == 0: + return []string{fmt.Sprintf("%q occurs 0 times in %d bytes, want exactly 1", lnwCommentMarker, len(body))} + case n != 1: + return []string{fmt.Sprintf("%q occurs %d times in %d bytes (first at byte %d), want exactly 1", lnwCommentMarker, n, len(body), markerIdx)} + } + lastAnchor, anchorsBefore := -1, 0 + for _, m := range lnwChapterRe.FindAllStringIndex(body, -1) { + if m[0] < markerIdx { + anchorsBefore++ + } + lastAnchor = m[0] + } + var failures []string + if lastAnchor >= markerIdx { + failures = append(failures, fmt.Sprintf("last chapter anchor at byte %d does not precede the marker at byte %d", lastAnchor, markerIdx)) + } + if anchorsBefore == 0 { + failures = append(failures, fmt.Sprintf("no chapter anchor before the marker at byte %d — the truncated prefix the scan sees yields nothing", markerIdx)) + } + return failures +} diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json index 0014f97..6d49d43 100644 --- a/graphify-out/.graphify_labels.json +++ b/graphify-out/.graphify_labels.json @@ -15,10 +15,8 @@ "13": "Web UI Handlers", "14": "Go Error Handling", "15": "CDP Browser Client", - "16": "Cover Storage", "17": "Go Code Style Guide", "18": "Agent Skills", - "19": "Store Migrations", "20": "I/O Performance Patterns", "21": "CPU Optimization", "22": "Caching Patterns", @@ -37,7 +35,6 @@ "35": "Web UI Filter Logic", "36": "Userscript Test Harness", "37": "Product & Security Context", - "38": "Session Store Tests", "39": "UI Critique 2026-07-26A", "40": "UI Critique 2026-07-26B", "41": "DB Cutover Runbook", diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index ec21587..d2ab02d 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,16 +1,16 @@ # Graph Report - mangaBookmark (2026-08-11) ## Corpus Check -- 107 files · ~261,410 words +- 108 files · ~265,471 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 1634 nodes · 3179 edges · 280 communities (65 shown, 215 thin omitted) -- Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 309 edges (avg confidence: 0.77) +- 1644 nodes · 3206 edges · 277 communities (62 shown, 215 thin omitted) +- Extraction: 90% EXTRACTED · 10% INFERRED · 0% AMBIGUOUS · INFERRED: 312 edges (avg confidence: 0.77) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `c400c91a` +- Built from commit: `27dda601` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -31,10 +31,8 @@ - [[_COMMUNITY_Web UI Handlers|Web UI Handlers]] - [[_COMMUNITY_Go Error Handling|Go Error Handling]] - [[_COMMUNITY_CDP Browser Client|CDP Browser Client]] -- [[_COMMUNITY_Cover Storage|Cover Storage]] - [[_COMMUNITY_Go Code Style Guide|Go Code Style Guide]] - [[_COMMUNITY_Agent Skills|Agent Skills]] -- [[_COMMUNITY_Store Migrations|Store Migrations]] - [[_COMMUNITY_IO Performance Patterns|I/O Performance Patterns]] - [[_COMMUNITY_CPU Optimization|CPU Optimization]] - [[_COMMUNITY_Caching Patterns|Caching Patterns]] @@ -53,7 +51,6 @@ - [[_COMMUNITY_Web UI Filter Logic|Web UI Filter Logic]] - [[_COMMUNITY_Userscript Test Harness|Userscript Test Harness]] - [[_COMMUNITY_Product & Security Context|Product & Security Context]] -- [[_COMMUNITY_Session Store Tests|Session Store Tests]] - [[_COMMUNITY_UI Critique 2026-07-26A|UI Critique 2026-07-26A]] - [[_COMMUNITY_UI Critique 2026-07-26B|UI Critique 2026-07-26B]] - [[_COMMUNITY_DB Cutover Runbook|DB Cutover Runbook]] @@ -300,7 +297,7 @@ 2. `newWebTestServer()` - 49 edges 3. `newTestStore()` - 42 edges 4. `newTestStore()` - 41 edges -5. `e()` - 32 edges +5. `e()` - 33 edges 6. `Handler` - 29 edges 7. `ne()` - 28 edges 8. `De()` - 28 edges @@ -333,7 +330,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 (280 total, 215 thin omitted) +## Communities (277 total, 215 thin omitted) ### Community 0 - "HTMX Library Internals" Cohesion: 0.08 @@ -345,19 +342,19 @@ Nodes (84): floatPtr(), testConfig(), getCover(), Cookie, Handler, ResponseRecor ### Community 2 - "Manga Userscript Adapters" Cohesion: 0.06 -Nodes (77): adapterFor(), anchorsFromDocument(), anchorsFromHTML(), apiDelete(), apiGet(), apiPut(), applyFabPos(), applyLatestChapterIfChanged() (+69 more) +Nodes (76): adapterFor(), anchorsFromDocument(), anchorsFromHTML(), apiDelete(), apiGet(), apiPut(), applyFabPos(), applyLatestChapterIfChanged() (+68 more) ### Community 3 - "Novel Userscript Adapters" Cohesion: 0.06 -Nodes (74): adapterFor(), anchorsFromDocument(), anchorsFromHTML(), apiDelete(), apiGet(), apiPut(), applyFabPos(), applyLatestChapterIfChanged() (+66 more) +Nodes (79): adapterFor(), anchorsFromDocument(), anchorsFromHTML(), apiDelete(), apiGet(), apiPut(), applyFabPos(), applyLatestChapterIfChanged() (+71 more) ### Community 4 - "Series Acquisition Tests" Cohesion: 0.10 Nodes (67): bookmarkNewKaganeSeries(), bookmarkNewNovelfullSeries(), bookmarkNewSeries(), Context, Store, T, newAcquirer(), readBookmark() (+59 more) ### Community 5 - "Bookmarks API Tests" -Cohesion: 0.07 -Nodes (71): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+63 more) +Cohesion: 0.08 +Nodes (67): auth(), getBookmarks(), Handler, Request, Store, T, newTestServer(), newTestStore() (+59 more) ### Community 7 - "Cover & Acquire Internals" Cohesion: 0.08 @@ -376,8 +373,8 @@ 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 (44): scanSeries(), Store, T, newTestStore(), readLatestCheckedAt(), readSeries(), secondReader(), seedForCheck() (+36 more) +Cohesion: 0.05 +Nodes (75): T, TestCreateAndGetSession(), TestDeleteSessionIsPerReader(), TestDeleteSessionRevokes(), TestExpiredSessionIsGone(), TestGetSessionUnknownID(), applyMigration(), CoverContentType() (+67 more) ### Community 12 - "Bookmarks API Handler" Cohesion: 0.08 @@ -392,21 +389,13 @@ 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.12 -Nodes (22): Action, awaitPromise(), browserConnectionLost(), classifyBrowserError(), Context, Mutex, isInterstitial(), jsString() (+14 more) - -### Community 16 - "Cover Storage" -Cohesion: 0.09 -Nodes (7): CoverContentType(), coverRelativePath(), coverSourceAddress(), displayChapter(), Store, TestDisplayChapter(), Bookmark +Cohesion: 0.08 +Nodes (29): Action, awaitPromise(), browserConnectionLost(), classifyBrowserError(), Context, Mutex, isInterstitial(), jsString() (+21 more) ### Community 17 - "Go Code Style Guide" Cohesion: 0.08 Nodes (23): Code Style Details, Extract Complex Conditions, Value vs Pointer Arguments, Code Organization Within Files, Complex Conditions & Init Scope, Composite Literals, Control Flow, Cross-References (+15 more) -### Community 19 - "Store Migrations" -Cohesion: 0.19 -Nodes (18): applyMigration(), migrate(), Open(), refreshOwnerToken(), seedOwner(), TestCoverIsContentAddressedOnFilesystem(), TestCoverPersistsAcrossReopen(), TestMigration0002BackfillsExistingBookmarks() (+10 more) - ### Community 20 - "I/O Performance Patterns" Cohesion: 0.11 Nodes (18): Avoid io.ReadAll for large payloads, Batch Operations, Buffered I/O, Cgo Overhead, Channel: batch processing from a stream, Concurrent Multi-Stage Pipelines, Connection pooling, Database: batch inserts over row-by-row (+10 more) @@ -463,10 +452,6 @@ Nodes (5): closeCardPanels(), setActiveTab(), toggleChapterForm(), toggleConfirm Cohesion: 0.17 Nodes (11): Accessibility & Inclusion, Brand Commitments, Capabilities and Constraints, Evidence on Hand, Operating Context, Platform, Positioning, Product (+3 more) -### Community 38 - "Session Store Tests" -Cohesion: 0.48 -Nodes (6): T, TestCreateAndGetSession(), TestDeleteSessionIsPerReader(), TestDeleteSessionRevokes(), TestExpiredSessionIsGone(), TestGetSessionUnknownID() - ### Community 39 - "UI Critique 2026-07-26A" Cohesion: 0.29 Nodes (6): Design Health Score, Design Specificity Verdict, Minor Observations, Persona Red Flags, Priority Issues, Questions to Consider @@ -592,19 +577,19 @@ Cohesion: 0.50 Nodes (3): Live URL shapes (verified 2026-07-26, may drift — re-check against live pages before trust), Second script: `novel-bookmark.user.js`, Userscript structure (single IIFE, `manga-bookmark.user.js`) ## Knowledge Gaps -- **544 isolated node(s):** `bookmarkmanager/backend`, `ctxKey`, `loginView`, `ctxKey`, `test` (+539 more) +- **545 isolated node(s):** `bookmarkmanager/backend`, `ctxKey`, `loginView`, `ctxKey`, `test` (+540 more) These have ≤1 connection - possible missing edges or undocumented components. - **215 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 `New()` connect `Series Acquisition Tests` to `Bookmarks API Tests`, `Cover & Acquire Internals`, `Session Middleware`, `Web UI Handlers`, `Store Migrations`?** - _High betweenness centrality (0.037) - this node is a cross-community bridge._ -- **Why does `Open()` connect `Store Migrations` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Tests`, `Store Tests`, `Cover Storage`, `PG Test Infrastructure`?** - _High betweenness centrality (0.033) - this node is a cross-community bridge._ +- **Why does `New()` connect `Series Acquisition Tests` to `Bookmarks API Tests`, `Cover & Acquire Internals`, `Session Middleware`, `Store Tests`, `Web UI Handlers`?** + _High betweenness centrality (0.041) - this node is a cross-community bridge._ +- **Why does `Open()` connect `Store Tests` to `Cover Fetch Test Helpers`, `Series Acquisition Tests`, `Bookmarks API Tests`, `PG Test Infrastructure`?** + _High betweenness centrality (0.035) - 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`?** - _High betweenness centrality (0.020) - this node is a cross-community bridge._ + _High betweenness centrality (0.022) - this node is a cross-community bridge._ - **Are the 47 inferred relationships involving `testConfig()` (e.g. with `TestListRendersAcquiredCover()` and `TestPublicCoverNeverEchoesNonImage()`) actually correct?** _`testConfig()` has 47 INFERRED edges - model-reasoned connections that need verification._ - **Are the 8 inferred relationships involving `newWebTestServer()` (e.g. with `TestListRendersAcquiredCover()` and `TestPublicCoverRejectsUnknownAddress()`) actually correct?** @@ -612,4 +597,4 @@ _Questions this graph is uniquely positioned to answer:_ - **Are the 6 inferred relationships involving `newTestStore()` (e.g. with `TestCreateAndGetSession()` and `TestDeleteSessionIsPerReader()`) actually correct?** _`newTestStore()` has 6 INFERRED edges - model-reasoned connections that need verification._ - **What connects `bookmarkmanager/backend`, `ctxKey`, `loginView` to the rest of the system?** - _588 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file + _589 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file diff --git a/graphify-out/graph.html b/graphify-out/graph.html index 3cb14fd..8d374fe 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -63,12 +63,12 @@
    -
    1634 nodes · 3179 edges · 280 communities
    +
    1644 nodes · 3206 edges · 277 communities