From e7e22a12a58bc39b32aeb4a2877d882e27e9600c Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Tue, 11 Aug 2026 18:21:50 +0700 Subject: [PATCH] lightnovelworld Series identity is read from the chapter page (#80) (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements spec #80 / ADR-0008 — Gitea issues #86, #87, #88, #89, #90, all closed. A Reader bookmarks a novel on lightnovelworld and it never shows a New Chapter, because the Series identity was derived from the chapter address instead of read from the page. One Series can publish under several Chapter Slugs, so the derived key points at a slug that 404s. - **#89** — the userscript's lnw adapter stops deriving `seriesUrl`/`seriesId` from the path. It reads the page's own pointer (`a[aria-label='All Chapter']`), falling back to the microdata breadcrumb's second crumb, and carries `chapterSlug` on the page object, stored nowhere. - **#87** — the Poll's lnw chapter scan is unscoped (no stored-slug pattern can cover a Series' whole list) and truncated at the `wpd-threads` comment thread, the one region a visitor can write to. Marker absent means skip and log with the body length, never scan whole. Corrects the `maxBodyBytes` headroom comment to the measured 3.5x. - **#86** — the scan fixture is now text trimmed from a real, wholly-fetched Series page instead of a hand-written cross-series anchor that no live page carries. - **#90** — stale stored rows repair themselves on the next chapter visit: a pure transform over cache, queue and last-checked map, silent to the Reader, with progress, favourite and lifecycle bucket preserved when two rows merge. - **#88** — an env-gated live canary (`SMOKE_LNW_SERIES_URL`) proving the marker still occurs exactly once and still follows the last chapter anchor, asserted against the production symbols themselves. Verified on the merged branch: `go test ./...` green, `gofmt -l internal/latest/` silent, both userscripts `node --check` clean, 35/35 + 29/29 logic tests. Live canary green (marker once at byte 612,182 of 651,795). #90 verified on device with Playwright. Open follow-up: **#91** — the userscript's client-side latest-chapter scan is still scoped to the derived slug. Reviewed-on: https://gitea.violetcrown.my.id/sulthan/mangaBookmark/pulls/92 Co-authored-by: Sulthan Zaki Co-committed-by: Sulthan Zaki --- backend/internal/latest/fetch.go | 5 +- backend/internal/latest/poller_test.go | 6 +- backend/internal/latest/sites.go | 45 +- backend/internal/latest/sites_test.go | 156 ++- backend/internal/latest/smoke_lnw_test.go | 101 ++ graphify-out/.graphify_labels.json | 3 - graphify-out/GRAPH_REPORT.md | 57 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 1263 ++++++++++++++------- graphify-out/manifest.json | 57 +- userscript/AGENTS.md | 10 +- userscript/novel-bookmark.user.js | 186 ++- userscript/test/novel-logic.test.js | 385 +++++++ 13 files changed, 1731 insertions(+), 551 deletions(-) create mode 100644 backend/internal/latest/smoke_lnw_test.go 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