Skip to content

Unwrap Readplace self-URLs before saving articles#870

Open
FagnerMartinsBrack wants to merge 4 commits into
mainfrom
claude/readplace-self-url-unwrap-nu8u1h
Open

Unwrap Readplace self-URLs before saving articles#870
FagnerMartinsBrack wants to merge 4 commits into
mainfrom
claude/readplace-self-url-unwrap-nu8u1h

Conversation

@FagnerMartinsBrack

Copy link
Copy Markdown
Member

Summary

When users save articles via Readplace's /view/<host>/<path> wrapper URLs, the system now unwraps them to recover the original article URL. This ensures save records, dedup keys, and card links use the canonical original URL rather than self-referential Readplace URLs.

Changes

  • New preparse-readplace-url module: Implements URL unwrapping logic with two exports:

    • initPreparseReadplaceUrl(): Creates a function that recursively collapses /view/<host>/<path> wrappers and ?url= query params, handling edge cases like nested wrappers, percent-encoded query/fragment separators (%3F, %23), explicit schemes, and Readplace's own tracking params
    • withReadplacePreparse(): Decorator that wraps the ValidateSaveableUrl validator to preparse string inputs before validation
  • New originalUrlFromViewPath() export in view-path.ts: Recovers the original article URL from a /view/<tail> path segment by reusing parseViewPath logic, ensuring consistent handling of encoding rules and redirect resolution

  • Integrated at composition roots: Both app.ts and test-app.ts now decorate validateSaveableUrl with withReadplacePreparse, ensuring every save path (queue, import, etc.) unwraps self-URLs before validation

  • Comprehensive test coverage:

    • 20 unit tests for initPreparseReadplaceUrl covering recursive unwrapping, encoding edge cases, port matching, and malformed input handling
    • 3 tests for withReadplacePreparse decorator behavior
    • 6 tests for originalUrlFromViewPath covering scheme handling and decode errors
    • Integration tests in queue.save-articles.route.test.ts and view.route.test.ts verifying end-to-end unwrapping

Implementation Details

  • Unwrapping is recursive and terminating: each accepted step removes one /view/<host> layer, so the path strictly shrinks
  • Handles both path-based (/view/example.com/post) and query-based (/view?url=...) wrapper formats
  • Preserves explicit http:// schemes while defaulting to https:// for scheme-less URLs
  • Gracefully degrades: malformed URLs, non-wrapper paths, and decode errors are returned unchanged
  • Port matching is strict: localhost:3000 and localhost:4000 are treated as different hosts

https://claude.ai/code/session_01EoT1snrsX53BSK7Q5FYubH

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 10m 51s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving)

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts changes (originalUrlFromViewPath)
  • Review composition root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage
  • Post final review

Verdict: 0 high, 1 medium, 2 low. The core design is sound — unwrapping before validation, reusing parseViewPath as the single source of truth, a correct loop-termination argument, and comprehensive composition-root wiring (every save path receives the wrapped validator via deps.validateSaveableUrl; the two extractLinksFromPageUrl raw-validator uses are benign because import commit re-validates at import.page.ts:289). It's also a security improvement: a /view/127.0.0.1/… wrapper now resolves to 127.0.0.1 and is correctly rejected by the SSRF guard instead of being stored as a public readplace.com self-URL.

🟠 Medium — view-route unwrap test is vacuous

view.route.test.ts"unwraps a nested Readplace self-URL…". buildReaderHarness's fake parseArticle (line 60) returns title: "Hello World" for any URL, and the test only asserts that title. If the view route stopped unwrapping (articleUrl left as https://<selfHost>/view/example.com/post), the route would still stub-save, the fake would still return "Hello World", and the test would still pass — so it can't catch a regression in the view-route unwrap path (the only test targeting it). Assert on the unwrapped URL surfaced in the page instead — the existing ctaAction(doc) helper + expect(cta.getAttribute("href")).toContain(encodeURIComponent("https://example.com/post")). Fix this →

🟡 Low — negative assertion in queue test

queue.save-articles.route.test.ts uses expect(urls).not.toContain("http://localhost:3000/view/…"). The test-driven-design skill says Avoid Negative Test Assertions; with saved: 1 and the positive toContain already present, it's largely redundant.

🟡 Low — O(n²) on adversarially deep nesting

preparse-readplace-url.ts re-parses current with new URL once per /view/<self> layer. Termination is correct, but a pathologically deep single URL (reachable via an authenticated import/manifest) is ~O(n²) and could burn CPU until the Lambda times out. Cheap to harden (bound URL length or cap iterations); normal 0–1-layer URLs are unaffected.

The formatted review block has been emitted to the run output for the review automation to post.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. View-route unwrap test is vacuous — it can't detect a regression

In projects/hutch/src/runtime/web/pages/view/view.route.test.ts, the new test "unwraps a nested Readplace self-URL and renders the underlying article" asserts only on the article title:

expect(doc.querySelector("[data-test-reader-title]")?.textContent).toBe("Hello World");

But buildReaderHarness's fake parser returns "Hello World" for every URL:

const parseArticle: ParseArticle = async () => buildParseResult(); // title: "Hello World", ignores the URL

The view route resolves the article URL through deps.validateSaveableUrl(parsed.articleUrl) (view.page.ts:135), which is the wrapped validator — so unwrapping does happen in production. The problem is the test cannot prove it: if the view route stopped unwrapping (articleUrl left as https://<selfHost>/view/example.com/post), the route would still stub-save under that self-URL, the fake parser would still return "Hello World", and the test would still pass with 200. Since this is the only test exercising the view-route unwrap path, that path is effectively unprotected against regression — exactly the kind of gap coverage tooling can't catch (the preparse code is covered by the unit tests regardless).

Assert on something derived from the resolved articleUrl. The reader CTA already carries it — view.page.ts builds saveParams = new URLSearchParams([["url", articleUrl], …]) and the template renders it as data-test-view-cta-action, and the test file already has a ctaAction(doc) helper:

const cta = ctaAction(doc);
expect(cta.getAttribute("href")).toContain(encodeURIComponent("https://example.com/post"));

That fails if unwrapping regresses (the href would encode the self-URL). The new queue test is the right model — it asserts on the stored URL, which reflects the unwrapped value. Fix this →

Low Priority Suggestions

1. Negative test assertion in the queue test

In projects/hutch/src/runtime/web/pages/queue/queue.save-articles.route.test.ts, the new test ends with:

expect(urls).not.toContain("http://localhost:3000/view/fagnerbrack.com/business-success");

The test-driven-design skill explicitly calls out Avoid Negative Test Assertions (.not.toContain() goes stale on refactor). Given the test already asserts saved: 1 and expect(urls).toContain("https://fagnerbrack.com/business-success"), the negative line is largely redundant — consider dropping it or asserting the exact expected URL set. Fix this →

2. Worst-case O(n²) on adversarially deep nesting

In projects/hutch/src/runtime/web/pages/view/preparse-readplace-url.ts, the for (;;) loop re-parses current with new URL on each iteration and runs once per /view/<self> layer. The termination argument in the comment is correct (the pathname strictly shrinks), but a pathologically deep single URL (https://self/view/self/view/self/…, reachable via an authenticated import file or queue manifest where extraction yields one long URL) makes total work ~O(n²) and could burn CPU until the Lambda times out. Impact is limited (authenticated, self-inflicted, timeout-bounded), but it's cheap to harden — bound the input URL length or cap the unwrap-layer count before/within the loop. Normal 0–1-layer URLs are unaffected. Fix this →

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Fix the review issues. Use your Read tool to read the local file .github/workflows/claude-PR-code-review-auto-apply.md for instructions. The file is in the checked-out repository.

Context

Review to Address

See the review comment above this one for the issues to fix.

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 14m 31s —— View job


Auto-fix complete — PR #870

  • Read auto-apply instructions & CLAUDE.md guidelines
  • Examine the review (0 high, 1 medium, 2 low)
  • Trace the view-route unwrap flow to confirm fix approach
  • Medium: Strengthen the vacuous view-route unwrap test
  • Run pnpm check — ✅ 2599 unit + 11 integration + 1 E2E pass; coverage thresholds met (functions 100%)
  • Commit & push (db47f942)

What was fixed

🟠 Medium — view-route unwrap test was vacuous. In view.route.test.ts, the test "unwraps a nested Readplace self-URL and renders the underlying article" asserted only on the article title. Because buildReaderHarness's fake parseArticle returns "Hello World" for every URL, the test passed even if the route stopped unwrapping — leaving the only view-route unwrap test unable to catch a regression.

Approach

Added an assertion on the resolved articleUrl as it surfaces in the page. view.page.ts:237 builds saveParams from the validated (unwrapped) articleUrl and renders it as the Save CTA href, so I parse that href and assert its url param equals the unwrapped original:

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL);

Why this form (over the review's literal toContain(encodeURIComponent(...)) suggestion): it reuses the file's existing ctaAction(doc) helper and the exact parsed.searchParams.get("url") pattern already used at lines 331–333/371–372, so it's consistent and robust to encoding details. If unwrapping regresses, the href carries the self-URL (…/view/example.com/post) and the assertion fails — exactly the regression the review flagged.

Not done (intentional)

Per the auto-apply instructions (fix HIGH/MEDIUM only), the two 🟡 LOW items were left for the author:

  • Negative assertion (.not.toContain) in queue.save-articles.route.test.ts
  • Worst-case O(n²) on adversarially deep nesting in preparse-readplace-url.ts
    · branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

Comment review against the code-comments skill

I reviewed the comment changes in this PR against the new code-comments skill (now the single source of truth for comment policy: a comment may exist only for a why neither the code nor the commit/PR history can carry, or an approved hack — and any added/kept comment needs explicit human approval).

My review found the comment change(s) here compliant with the skill (5 checked, 0 violations).

@claude — please double-check the comment changes against .claude/skills/code-comments/SKILL.md and leave them as-is unless you find a concrete violation. Do not add new comments.

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 6m 51s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving)

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts changes (originalUrlFromViewPath)
  • Review composition root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage (verify prior medium/low issues addressed)
  • Post final review

Verdict: 0 high, 1 medium, 2 low. The design is sound and I re-confirmed it end-to-end:

  • Unwrap-before-validate at the composition root is the right seam — every save path receives the wrapped validator via deps.validateSaveableUrl (queue :553/:657/:779/:803/:913/:972, import commit :289, view :113/:136/:291/:311, server :352). The only raw validateSaveableUrl uses (app.ts:221/:402, link extraction) are benign because import commit re-validates each URL through the wrapped deps.validateSaveableUrl (import.page.ts:289), so the stored URL is unwrapped. SaveableUrlSchema (the raw zod path) is referenced only by test files, so nothing in production bypasses preparse.
  • parseViewPath reuse keeps the %3F?, %23#, %2525%25, and explicit-http:// rules as a single source of truth.
  • Termination holds: each path-unwrap strips one /view/<self> layer and each ?url= extraction returns a strict substring, so current shrinks.
  • Security improvement, no bypass: a /view/127.0.0.1/… wrapper now resolves to 127.0.0.1 and is rejected by the SSRF guard. Preparse only collapses self-host wrappers and feeds the inner target back through validate, so it can't be used to bypass validation.

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. View-route unwrap test is still vacuous — it cannot detect a regression

projects/hutch/src/runtime/web/pages/view/view.route.test.ts:131-144"unwraps a nested Readplace self-URL and renders the underlying article" asserts only on the article title:

expect(doc.querySelector("[data-test-reader-title]")?.textContent).toBe("Hello World");

buildReaderHarness's fake parser returns "Hello World" for every URL (view.route.test.ts:60const parseArticle: ParseArticle = async () => buildParseResult(), ignores the URL). I traced the route: handleViewArticle resolves articleUrl from validation.url (view.page.ts:136-141), which is the preparsed output of the wrapped validator — so unwrapping does happen in production. But the test can't prove it: if the view route stopped unwrapping (articleUrl left as https://<selfHost>/view/example.com/post), the route would still stub-save, the fake would still return "Hello World", and the test would still pass 200. This is the only test exercising the view-route unwrap path, so that path is unprotected against regression.

Note: the previous auto-apply attempt (#28432151461) logged this fix as done but the commit never landed — the assertion is unchanged on the current head (2de6c15d), so the finding persists.

Assert on the resolved articleUrl, which the Save CTA href already carries (saveParams = new URLSearchParams([["url", articleUrl], …])). The cleanest form matches the file's existing convention at view.route.test.ts:331-333 (ctaAction helper already exists at :51):

const href = ctaAction(doc).getAttribute("href");
assert(href, "save action must have an href");
expect(new URL(href, "http://localhost").searchParams.get("url")).toBe("https://example.com/post");

That fails if unwrapping regresses (the href would carry the self-URL). Fix this →

Low Priority Suggestions

1. Negative test assertion in the queue test

projects/hutch/src/runtime/web/pages/queue/queue.save-articles.route.test.ts ends with:

expect(urls).not.toContain("http://localhost:3000/view/fagnerbrack.com/business-success");

The test-driven-design skill calls out Avoid Negative Test Assertions (goes stale on refactor). Given the test already asserts saved: 1 and the positive toContain on the unwrapped URL, the negative line is redundant — and it hard-codes the origin rather than deriving it from TEST_APP_ORIGIN (which view.route.test.ts:133 does correctly). Drop it or assert the exact stored URL set. Fix this →

2. Worst-case O(n²) on adversarially deep nesting

projects/hutch/src/runtime/web/pages/view/preparse-readplace-url.ts — the for (;;) loop re-parses current with new URL once per /view/<self> layer (and originalUrlFromViewPath recurses per stacked scheme prefix). Termination is correct, but a pathologically deep single URL (reachable via an authenticated import file / queue manifest) is ~O(n²) and could burn CPU until the Lambda times out. Impact is limited (authenticated, self-inflicted, timeout-bounded), but it's cheap to harden — bound the input URL length or cap the unwrap-iteration count. Normal 0–1-layer URLs are unaffected. Fix this →

This PR has issues that should be addressed before merging.
· branch claude/readplace-self-url-unwrap-nu8u1h

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 3m 24s —— View job


Comment double-check against code-comments skill — PR #870

  • Read .claude/skills/code-comments/SKILL.md (single source of truth for comment policy)
  • Enumerate every comment change in the diff (git diff origin/main...HEAD)
  • Classify each against the two allowed reasons + the four "Never" rules
  • Confirm verdict and leave compliant comments as-is

Scope. 5 comments, all in 2 files. The three composition-root changes (app.ts, test-app.ts, e2e-server.main.ts) add only imports + wiring — no comments. Matches your count of 5.

# Location Verdict Reason it qualifies as a why the code can't carry
1 preparse-readplace-url.ts:6 (LANDING_PATHS) ✅ Compliant Explains the routing distinction — ?url= query vs path — that drives the branch in unwrapSelfViewUrl. The LANDING_PATHS name alone doesn't carry "these two carry the article in a query param".
2 preparse-readplace-url.ts:10 (initPreparseReadplaceUrl) ✅ Compliant Textbook why: the consequence "saving the wrapper verbatim stores a self-referential record keyed on Readplace's own host" is invisible from the body.
3 preparse-readplace-url.ts:18 (for (;;)) ✅ Compliant Termination argument for an unbounded loop (the path strictly shrinks). Exactly the invariant a comment should hold — it stops a future reader "fixing" the loop with a depth cap.
4 preparse-readplace-url.ts:46 (withReadplacePreparse) ✅ Compliant Architectural why: composed at the root so every save path/client benefits with no per-route code. (Lead sentence leans what, but serves the why.)
5 view-path.ts:50 (originalUrlFromViewPath) ✅ Compliant Single-source-of-truth rationale for delegating to the shared parser, plus why the recursion is safe (a redirect is the same article canonicalised).

One borderline observation (sub-threshold, not a violation). Comment #5 names the sibling symbol parseViewPath in prose. The skill's "never name another file or symbol by path" rule guards against prose references rotting on rename. But here parseViewPath is also called directly two lines below (view-path.ts:62), so the dependency is already expressed in code — the prose reinforces the why, it doesn't substitute for a code link, and an IDE rename touches the adjacent call. Staleness risk is low. I judge this below the bar for a concrete violation and, per your instruction, left it unchanged. (If you ever want zero symbol-names in prose, the minimal reword is "…delegates to the shared /view parser so the … rules aren't reimplemented" — a style preference, not a fix.)

Approval gate. Satisfied — your review in this session explicitly approved these comments.

Verdict: 5 checked, 0 concrete violations. No code changed, no comments added.

· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. View-route unwrap test is still vacuous — it cannot detect a regression

projects/hutch/src/runtime/web/pages/view/view.route.test.ts:131-144 — the test "unwraps a nested Readplace self-URL and renders the underlying article" asserts only on the article title:

expect(doc.querySelector("[data-test-reader-title]")?.textContent).toBe("Hello World");

buildReaderHarness's fake parser returns "Hello World" for every URL (view.route.test.ts:60const parseArticle: ParseArticle = async () => buildParseResult(), ignores the URL). Tracing the route: handleViewArticle resolves articleUrl from validation.url (view.page.ts:136-141), the preparsed output of the wrapped validator — so unwrapping does happen in production. But the test can't prove it: if the view route stopped unwrapping (articleUrl left as https://<selfHost>/view/example.com/post), the route would still stub-save, the fake would still return "Hello World", and the test would still pass with 200. This is the only test exercising the view-route unwrap path, so that path is unprotected against regression.

The previous auto-apply attempt logged this fix as done but the commit never landed — the assertion is unchanged on the current head (2de6c15d), so the finding persists.

Assert on the resolved articleUrl, which the Save CTA href already carries (saveParams = new URLSearchParams([["url", articleUrl], …])). The cleanest form matches the file's existing convention at view.route.test.ts:331-333 (the ctaAction helper already exists at :51):

const href = ctaAction(doc).getAttribute("href");
assert(href, "save action must have an href");
expect(new URL(href, "http://localhost").searchParams.get("url")).toBe("https://example.com/post");

That fails if unwrapping regresses (the href would carry the self-URL). Fix this →

Low Priority Suggestions

1. Negative test assertion in the queue test

projects/hutch/src/runtime/web/pages/queue/queue.save-articles.route.test.ts ends with expect(urls).not.toContain("http://localhost:3000/view/fagnerbrack.com/business-success"). The test-driven-design skill calls out Avoid Negative Test Assertions (goes stale on refactor). Given the test already asserts saved: 1 and the positive toContain on the unwrapped URL, the negative line is redundant — and it hard-codes the origin rather than deriving it from TEST_APP_ORIGIN (which view.route.test.ts:133 does correctly). Drop it or assert the exact stored URL set. Fix this →

2. Worst-case O(n²) on adversarially deep nesting

projects/hutch/src/runtime/web/pages/view/preparse-readplace-url.ts — the for (;;) loop re-parses current with new URL once per /view/<self> layer (and originalUrlFromViewPath recurses per stacked scheme prefix). Termination is correct, but a pathologically deep single URL (reachable via an authenticated import file / queue manifest) is ~O(n²) and could burn CPU until the Lambda times out. Impact is limited (authenticated, self-inflicted, timeout-bounded), but it's cheap to harden — bound the input URL length or cap the unwrap-iteration count. Normal 0–1-layer URLs are unaffected. Fix this →

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Fix the review issues. Use your Read tool to read the local file .github/workflows/claude-PR-code-review-auto-apply.md for instructions. The file is in the checked-out repository.

Context

Review to Address

See the review comment above this one for the issues to fix.

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 19m 34s —— View job


Auto-fixing review issues — PR #870 (attempt 2 of 5) ✅

  • Read auto-apply instructions & CLAUDE.md guidelines
  • Examine the review (0 high, 1 medium, 2 low)
  • Trace the view-route unwrap flow to confirm the fix asserts the right thing
  • Medium: Strengthen the vacuous view-route unwrap test (assert resolved articleUrl in the Save CTA href)
  • Run pnpm check — ✅ full monorepo green (pre-commit hook, 36 projects); hutch:check re-verified post-rebase; coverage functions 100%, branches 97.33%, statements/lines 99.6%
  • Resolve rebase conflict with a concurrent landed fix (db47f942)
  • Push (db47f942..5e7b2311) — local & origin both at 5e7b2311

Summary

Medium — view-route unwrap test was vacuous. The test "unwraps a nested Readplace self-URL…" asserted only the article title, which buildReaderHarness's fake parser returns ("Hello World") for every URL — so it passed even if the route stopped unwrapping. Fixed by asserting on the resolved articleUrl carried in the Save CTA href (view.page.ts:141:237): the url param must equal https://example.com/post (ARTICLE_URL). If unwrapping regresses, the href carries the …/view/example.com/post self-URL and the test fails.

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL);

Why this shape: mirrors the file's existing canonical-path convention (view.route.test.ts:296-334), reuses the ctaAction helper + ARTICLE_URL constant, and derives the origin from the resolved URL rather than hard-coding it.

Note on the concurrent commit. While this attempt was running, a parallel auto-apply landed db47f942 on the branch with the same href assertion — so it had already made the test regression-detecting. The review, however, asked to replace the title assertion; db47f942 only appended the href check and left the redundant "Hello World" line in. After rebasing onto it, my commit's net effect is simply removing that 3-line vacuous assertion, leaving the exact clean state the review specified. No competing logic — both fixes assert the same thing; this just finishes the de-duplication.

Left for the author (LOW, per the auto-apply policy of fixing only HIGH/MEDIUM):

  1. Negative assertion in the queue test (.not.toContain)
  2. Worst-case O(n²) on adversarially deep nested self-URLs in preparse-readplace-url.ts

The auto-apply prompt suggests posting the summary as a separate gh pr comment, but the harness constraint here is to communicate only by updating this single comment — so the summary lives here.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 4m 27s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · db47f942

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts changes (originalUrlFromViewPath)
  • Review composition-root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Verify whether the prior MEDIUM (vacuous view-route test) landed this time → it did
  • Review test coverage & remaining LOW items
  • Post final review

Verdict: 0 high, 0 medium, 2 low. The previously-flagged MEDIUM is now resolved at db47f942, and CI is green (run 28432935074).

✅ Resolved — view-route unwrap test is no longer vacuous

view.route.test.ts:144-147 now asserts on the resolved articleUrl carried in the Save CTA href, not just the always-"Hello World" title:

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL); // "https://example.com/post"

I traced it end-to-end: view.page.ts:136 runs parsed.articleUrl through the wrapped validator, :141 takes validation.url (unwrapped), and :237/:245 build the CTA href from it. If unwrapping regressed, the href's url param would carry the self-URL (…/view/example.com/post) and the assertion fails — exactly the regression the prior reviews flagged. CI green on db47f942 confirms it passes.

Design re-confirmed

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl. The only raw uses (app.ts:221/:402, link extraction) are benign: import commit re-validates each URL through the wrapped deps.validateSaveableUrl (import.page.ts:289), so the stored URL is unwrapped. No production path bypasses preparse.
  • parseViewPath reuse keeps the %3F?, %23#, %2525%25, explicit-http:// rules as a single source of truth.
  • Termination holds: each path-unwrap strips one /view/<self> layer and each ?url= extraction returns a strict substring, so current shrinks.
  • Security improvement, no bypass: a /view/127.0.0.1/… wrapper now resolves to 127.0.0.1 and is rejected by the SSRF guard; preparse only collapses self-host wrappers and feeds the inner target back through validate.

🟡 Remaining LOW (author's discretion — auto-apply only fixes HIGH/MEDIUM)

  1. Negative assertion in queue.save-articles.route.test.ts (.not.toContain(...), also hard-codes the origin).
  2. Worst-case O(n²) on adversarially deep nesting in preparse-readplace-url.ts (validateSaveableUrl has no length cap, so the bound is only the HTTP body limit — authenticated, self-inflicted, timeout-bounded).

The formatted review block has been emitted to the run output for the review automation to post.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Note — the prior MEDIUM is now resolved at db47f942. The view-route unwrap test (projects/hutch/src/runtime/web/pages/view/view.route.test.ts:131-148, "unwraps a nested Readplace self-URL and renders the underlying article") no longer asserts only on the always-"Hello World" title. It now asserts on the resolved articleUrl carried in the Save CTA href:

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL); // "https://example.com/post"

Traced end-to-end: view.page.ts:136 runs parsed.articleUrl through the wrapped validator, :141 takes validation.url (unwrapped), :237/:245 build the CTA href from it. If unwrapping regressed, the href's url param would carry the self-URL and this fails. CI is green on db47f942 (run 28432935074), confirming it passes.

Low Priority Suggestions

1. Negative test assertion in the queue test

projects/hutch/src/runtime/web/pages/queue/queue.save-articles.route.test.ts ends the unwrap test with expect(urls).not.toContain("http://localhost:3000/view/fagnerbrack.com/business-success"). The test-driven-design skill calls out Avoid Negative Test Assertions (goes stale on refactor). Given the test already asserts saved: 1 and the positive toContain on the unwrapped URL, the negative line is redundant — and it hard-codes the http://localhost:3000 origin rather than deriving it (the new view.route.test.ts derives the host from TEST_APP_ORIGIN). Drop it or assert the exact stored URL set. Fix this →

2. Worst-case O(n²) on adversarially deep nesting

projects/hutch/src/runtime/web/pages/view/preparse-readplace-url.ts — the for (;;) loop re-parses current with new URL once per /view/<self> layer. Termination is correct (the path strictly shrinks), but validateSaveableUrl (src/packages/domain/src/article/saveable-url.ts:86) imposes no URL length cap, so a pathologically deep single URL (reachable via an authenticated import file / queue manifest) is ~O(n²) bounded only by the HTTP body limit, and could burn CPU until the Lambda times out. Impact is limited (authenticated, self-inflicted, timeout-bounded), but cheap to harden — bound the input length or cap the unwrap-iteration count. Normal 0–1-layer URLs are unaffected. Fix this →

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 3m 17s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · 5e7b2311

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main (HEAD 5e7b2311)
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts changes (originalUrlFromViewPath)
  • Review composition-root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage & verify prior LOW items
  • Post final review

Verdict: 0 high, 0 medium, 2 low. The previously-flagged MEDIUM is now fully resolved at 5e7b2311.

✅ Resolved — view-route unwrap test is no longer vacuous

The latest commit cleanly removes the redundant always-"Hello World" title assertion (buildReaderHarness's fake parser returns it for any URL), leaving only the regression-detecting href assertion at view.route.test.ts:141-144:

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL); // "https://example.com/post"

Traced end-to-end: view.page.ts:136 runs parsed.articleUrl through the wrapped validator, :141 takes validation.url (unwrapped), and the CTA href is built from it. If unwrapping regressed, the href's url param would carry the self-URL (…/view/example.com/post) and the assertion fails. Title rendering is still covered by the sibling iframe test (:119-121), so no coverage loss.

Design re-confirmed

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl (app.ts:605, test-app.ts:91, e2e-server.main.ts:143). The only raw uses are benign: import commit re-validates each URL through the wrapped deps.validateSaveableUrl (import.page.ts), so the stored URL is unwrapped. No production path bypasses preparse.
  • parseViewPath reuse keeps the %3F?, %23#, %2525%25, explicit-http:// rules as a single source of truth.
  • Termination holds: each path-unwrap strips one /view/<self> layer and each ?url= extraction returns a strict substring, so current shrinks.
  • Security improvement, no bypass: a /view/127.0.0.1/… wrapper now resolves to 127.0.0.1 and is rejected by the SSRF guard; preparse only collapses self-host wrappers and feeds the inner target back through validate.

🟡 Remaining LOW (author's discretion — auto-apply only fixes HIGH/MEDIUM)

  1. Negative assertion in queue.save-articles.route.test.ts:128 (.not.toContain(...)) — redundant given saved: 1 + the positive toContain; also hard-codes the origin.
  2. Worst-case O(n²) on adversarially deep nesting in preparse-readplace-url.ts — confirmed validateSaveableUrl (saveable-url.ts:86) has no length cap, so the bound is only the HTTP body limit (authenticated, self-inflicted, timeout-bounded).

The formatted review block has been emitted to the run output for the review automation to post.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Note — the prior MEDIUM is now fully resolved at 5e7b2311. The view-route unwrap test (projects/hutch/src/runtime/web/pages/view/view.route.test.ts:131-145, "unwraps a nested Readplace self-URL and renders the underlying article") previously also asserted the always-"Hello World" title; that redundant 3-line assertion was removed in this commit, leaving only the regression-detecting href assertion:

const href = ctaAction(doc).getAttribute("href");
assert(href, "cta action must have an href");
const parsed = new URL(href, "http://localhost");
expect(parsed.searchParams.get("url")).toBe(ARTICLE_URL); // "https://example.com/post"

Traced end-to-end: view.page.ts:136 runs parsed.articleUrl through the wrapped validator, :141 takes validation.url (unwrapped), and the Save CTA href is built from it. If unwrapping regressed, the href's url param would carry the self-URL and this fails. Title rendering remains covered by the sibling iframe test (:119-121), so there is no coverage loss.

Low Priority Suggestions

1. Negative test assertion in the queue test

projects/hutch/src/runtime/web/pages/queue/queue.save-articles.route.test.ts:128 ends the unwrap test with expect(urls).not.toContain("http://localhost:3000/view/fagnerbrack.com/business-success"). The test-driven-design skill calls out Avoid Negative Test Assertions (goes stale on refactor). Given the test already asserts saved: 1 and the positive toContain("https://fagnerbrack.com/business-success"), the negative line is redundant — if unwrapping regressed, the positive assertion already fails. It also hard-codes the http://localhost:3000 origin rather than deriving it (the new view.route.test.ts:133 derives the host from TEST_APP_ORIGIN). Drop it or assert the exact stored URL set. Fix this →

2. Worst-case O(n²) on adversarially deep nesting

projects/hutch/src/runtime/web/pages/view/preparse-readplace-url.ts — the for (;;) loop re-parses current with new URL once per /view/<self> layer. Termination is correct (the path strictly shrinks), but validateSaveableUrl (src/packages/domain/src/article/saveable-url.ts:86) imposes no URL length cap — confirmed it only rejects empty/hostname-less input — so a pathologically deep single URL (reachable via an authenticated import file / queue manifest) is ~O(n²) bounded only by the HTTP body limit, and could burn CPU until the Lambda times out. Impact is limited (authenticated, self-inflicted, timeout-bounded), but cheap to harden — bound the input length or cap the unwrap-iteration count. Normal 0–1-layer URLs are unaffected. Fix this →

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude CI has FAILED. Use your Read tool to read the local file .github/workflows/claude-PR-CI-failure-fixer.md for instructions. The file is in the checked-out repository.

Context

Failed Jobs

  • web-tests: Run pnpm check

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 20m 52s —— View job


CI failure fixed — PR #870

  • Read CI-failure-fixer instructions & CLAUDE.md guidelines
  • Fetch failed job logs (run 28514604682, job web-tests)
  • Diagnose root cause
  • Apply fix
  • Verify with full pnpm check (36 projects green)
  • Commit & push (e88de324)

Root cause — transient CI infra flake, not the PR's code

hutch:test-with-coverage failed with exit 130 (SIGINT, not an assertion failure). The Playwright browser install shells out to apt-get, which couldn't acquire the dpkg lock — the runner's boot-time apt-get (PID 5354) held it:

=== Readplace - Running E2E tests ===
E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 5354 (apt-get)
=== Readplace - Running E2E tests - retrying browser install once after failure ===
E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 5354 (apt-get)
Test run failed: Command failed: node_modules/.bin/playwright install --with-deps chromium

The existing retry fired immediately, hit the same still-held lock, and failed again → Nx bailed with --nx-bail → SIGINT'd the sibling tasks → aggregate exit 130. None of this PR's URL-unwrap changes are involved.

Fix

src/packages/test-phase-runner/src/run-test-phases.ts — replaced the single immediate retry with a bounded wait-and-retry back-off: 4 attempts with 10s/20s/30s waits (via a new injected sleep dep, defaulting to node:timers/promises). The retry now lands after the runner's background apt process releases the lock. Normal installs succeed on the first attempt and are unaffected. Updated the two browser-install retry tests to assert the wait schedule and bounded attempt count.

Why hardening rather than just re-running: the prior single retry was written specifically for this dpkg-lock race but retried with no delay, so it could never absorb a sustained lock. This is the load-bearing crawler-health-style philosophy applied to CI infra — fix the flake, don't paper over it.

Verification

Full pnpm check green locally (mirroring the pre-commit hook): all 36 projects, hutch:test-with-coverage = 2629 unit + 11 integration + 1 E2E, coverage thresholds met, browser install succeeded first-try. @packages/test-phase-runner:check (the only project with source changes) passes tsc + biome + knip + 39 tests. Nx flagged hutch:test-with-coverage as a flaky task — exactly the flake this hardens against.
· branch claude/readplace-self-url-unwrap-nu8u1h · commit e88de324

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude fix all reported low prio issues

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 24m 29s —— View job


Fixed all reported low-priority issues — PR #870

  • Read context (review, code-comments + test-driven-design skills, affected files)
  • Low 1: Negative assertion in queue.save-articles.route.test.ts → positive exact-set assertion
  • Low 2: Depth-cap the unwrap paths (O(n²) → O(n), and kill the uncaught RangeError)
  • Low 3: Trim the history clause from the CI-fix comment in run-test-phases.ts
  • Run pnpm check — full monorepo green
  • Commit & push (22fb90f9)

🟡 Low 1 — negative test assertion

queue.save-articles.route.test.ts — replaced the redundant negative assertion (which also hard-coded http://localhost:3000) with a stronger positive exact-set assertion:

const urls = stored.articles.map((a) => a.url);
expect(urls).toEqual(["https://fagnerbrack.com/business-success"]);

This proves the unwrapped original is the only thing stored — it fails if unwrapping regresses (per the skill's Avoid Negative Test Assertionstest the actual expected value), with no origin hard-coding.

🟡 Low 2 — O(n²) + unbounded recursion on deep nesting

Added MAX_VIEW_UNWRAP_DEPTH = 16 in view-path.ts (0–1 layers is the legitimate case; 16 is generous headroom) and applied it at both facets the review named:

  • preparse-readplace-url.ts — the for (;;) loop is now for (let depth = 0; depth < MAX_VIEW_UNWRAP_DEPTH; …), converting the worst-case O(n²) re-parse to O(n). The now-redundant termination comment is dropped (the bounded loop self-documents).
  • view-path.tsoriginalUrlFromViewPath was recursive with no depth bound, and the recursion sat outside the decodeURIComponent try/catch, so a deeply-stacked https://https://… payload could throw an uncaught RangeError (→ 500). Converted to a bounded loop, which eliminates the stack-overflow path entirely and caps CPU.

Two boundary tests pin the caps (past the cap → residual wrapper left intact / undefined), both derived from the constant so they track it. Normal 0–1-layer URLs are unaffected.

🟡 Low 3 — CI-fix comment referenced prior code state

run-test-phases.ts — kept the dpkg-lock why, dropped the "why the prior single, no-wait retry still failed" history clause (git blame already carries it, per the code-comments skill):

// apt-get (via --with-deps) needs the dpkg frontend lock, which the
// runner's boot-time unattended-upgrades holds for its first minute or
// so. Wait for it to drain, longer each attempt.

Verification

pnpm check green across all 36 projects (pre-commit hook, 47 tasks). hutch: 2631 unit (+2 new) + 11 integration pass; coverage functions 100%, branches 97.47%, statements/lines 99.68%. @packages/test-phase-runner: lint + 39 tests pass. E2E skipped locally via CLAUDE_CODE_REMOTE; CI's web-tests job runs the full E2E suite. No new code comments were added (approval-gated) — the MAX_VIEW_UNWRAP_DEPTH name carries the intent; the why lives here + in git blame.
· branch claude/readplace-self-url-unwrap-nu8u1h · commit 22fb90f9

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 5m 7s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · 22fb90f9

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Verify the 3 low-priority fixes from the last round landed (22fb90f9)
  • Review preparse-readplace-url.ts + view-path.ts depth-cap changes
  • Review test changes (queue exact-set, boundary tests)
  • Review run-test-phases.ts comment trim
  • Confirm CI status + trace targeted tests
  • Post final review

Verdict: 0 high, 0 medium, 0 low. All three previously-flagged LOW items are resolved in 22fb90f9, CI is green (run 28645472416), and I found no new issues. Ready for human review.

✅ Resolved — all three LOW items

  1. Negative assertion (queue.save-articles.route.test.ts:127) → replaced with a positive exact-set assertion expect(urls).toEqual(["https://fagnerbrack.com/business-success"]). No hard-coded origin, and strictly stronger (proves the unwrapped original is the only stored URL).
  2. O(n²) + unbounded recursion → new MAX_VIEW_UNWRAP_DEPTH = 16 caps both unwrap paths. preparse's for(;;) is now a bounded loop (O(n²)→O(n)), and originalUrlFromViewPath is converted from unbounded recursion to a bounded loop with the decode try/catch inside it — removing the uncaught RangeError/stack-overflow path the last review flagged. Two boundary tests pin the caps, both derived from the constant so they track it.
  3. CI-fix comment (run-test-phases.ts:220-222) → dropped the "why the prior single, no-wait retry still failed" history clause, kept the load-bearing dpkg-lock rationale. Compliant with the code-comments skill.

Traced both new boundary tests end-to-end

  • preparse cap: https://readplace.com/view/ + readplace.com/view/×16 + fagnerbrack.com/x → 16 capped iterations strip 16 layers, leaving exactly https://readplace.com/view/fagnerbrack.com/x. Matches the assertion.
  • scheme-redirect cap: https://×17 + example.com/x → one scheme stripped per iteration; the cap trips with one prefix left, returning undefined. Matches the assertion.

Design re-confirmed

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl; validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original (queue integration test proves it end-to-end).
  • parseViewPath reuse keeps %3F?, %23#, %2525%25, explicit-http:// as one source of truth. Security: /view/127.0.0.1/… resolves to 127.0.0.1 and is SSRF-rejected.
  • No new comments addedMAX_VIEW_UNWRAP_DEPTH (name) + the two boundary tests carry the intent; the why lives in git blame.

Design note (intentional, not a defect)

Past the 16-layer cap, preparse returns the residual wrapper (a still-valid readplace.com self-URL), which would then be stored as a self-URL — the very thing the PR avoids. This is reachable only at ≥16 adversarial nesting (authenticated, self-inflicted); the residual still resolves correctly through /view and is not a security issue. Bounding CPU/stack over perfectly unwrapping pathological input is the right trade, and the test name ("leaving the residual wrapper intact") documents it. No change recommended.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

None

All three previously-flagged LOW items are resolved in 22fb90f9, and CI is green (run 28645472416).

  1. Negative assertionqueue.save-articles.route.test.ts:127 now uses a positive exact-set assertion expect(urls).toEqual(["https://fagnerbrack.com/business-success"]): no hard-coded origin, and strictly stronger than the prior toContain + .not.toContain pair (it proves the unwrapped original is the only stored URL). Aligns with the test-driven-design skill's Avoid Negative Test Assertions.
  2. O(n²) + unbounded recursion — new MAX_VIEW_UNWRAP_DEPTH = 16 caps both unwrap paths. preparse's for(;;) becomes a bounded loop (O(n²)→O(n) on adversarially deep nesting), and originalUrlFromViewPath is converted from unbounded recursion to a bounded loop with the decodeURIComponent try/catch now inside it — eliminating the uncaught RangeError/stack-overflow path the prior review flagged. Two boundary tests pin the caps, both derived from the constant so they track it. I traced both: the preparse test resolves to exactly https://readplace.com/view/fagnerbrack.com/x (one residual layer after 16 iterations); the scheme-redirect test returns undefined (17 stacked https:// exceeds the cap).
  3. CI-fix commentrun-test-phases.ts:220-222 drops the "why the prior single, no-wait retry still failed" history clause and keeps the load-bearing dpkg-lock rationale, per the code-comments skill.

Design re-confirmed: unwrap-before-validate at the composition root (every save path receives the wrapped validator via deps.validateSaveableUrl; validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original — proven end-to-end by the queue integration test); parseViewPath reuse keeps the encoding rules as one source of truth; /view/127.0.0.1/… is correctly SSRF-rejected. No new code comments were added.

Intentional design note (not a defect): past the 16-layer cap, preparse returns the residual wrapper (a still-valid readplace.com self-URL), which would then be stored as a self-URL — reachable only at ≥16 adversarial, authenticated, self-inflicted nesting. The residual still resolves through /view and is not a security issue; bounding CPU/stack over perfectly unwrapping pathological input is the right trade, documented by the test name "leaving the residual wrapper intact". No change recommended.

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Resolve conflicts. Use your Read tool to read the local file .github/workflows/claude-PR-conflict-fixer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 13m 47s —— View job


Resolving merge conflicts — PR #870

  • Read conflict-fixer instructions & CLAUDE.md guidelines
  • Read full commit history to understand PR intent
  • Rebase onto origin/main and identify conflicts
  • Resolve conflicts (cross-referencing PR intent)
  • Run pnpm check to verify — green
  • Push with --force-with-lease

Rebased the 5 PR commits onto origin/main (86fdbbdb) and force-pushed (22fb90f9c0a03847, lease-protected).

Conflict resolved — 1 file

projects/hutch/src/runtime/web/pages/view/view.route.test.ts — both branches added a new it(...) test at the same spot (right after the reader-iframe test):

Side Test added
origin/main (#871, "Last crawled at" feature) "renders the Last crawled at bookmark once a crawl timestamp exists"
This PR "unwraps a nested Readplace self-URL and renders the underlying article"

They're independent tests, so I kept both — inserting the }); that separates them (each side had relied on the shared closing brace). I preserved the PR's test in its original b56b6759 form so the later PR commits that strengthen its assertion replayed cleanly; verified the final assertion is the regression-detecting href check (parsed.searchParams.get("url")).toBe(ARTICLE_URL)), not the earlier vacuous title-only one. No PR intent was reverted.

One extra step: relink a new workspace package

After the rebase, pnpm check failed at @packages/domain:compile"Cannot find module @packages/supported-clients". That package is new in origin/main (commit 61cc6150, "single-source client registry"), and @packages/domain now imports it; the pre-rebase node_modules predated it. Running pnpm install --frozen-lockfile created the per-project symlink (lockfile already matched — no tracked-file changes), and the re-run passed. This was environment relinking, not a code conflict.

Verification — pnpm check green

  • 37 projects / 48 tasks succeeded (exit 0)
  • 2876 unit + 11 integration + 2 E2E tests pass
  • Coverage thresholds met: statements 99.51%, branches 97.19%, functions 100%, lines 99.51%

Note: Nx flagged @packages/domain:compile (and siblings) as "flaky" only because it failed on the pre-install run and passed after the symlink was added — the inputs genuinely changed; it is not an actual flake.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack FagnerMartinsBrack force-pushed the claude/readplace-self-url-unwrap-nu8u1h branch from 22fb90f to c0a0384 Compare July 5, 2026 13:13
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 3m 58s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · c0a03847

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts changes (originalUrlFromViewPath + depth cap)
  • Review composition-root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage
  • Review CI-flake fix (run-test-phases.ts)
  • Post final review

Verdict: 0 high, 0 medium, 0 low. The only change since the last review (22fb90f9, which was already 0/0/0) is the conflict-resolution rebase onto origin/main landing as c0a03847. I reviewed the rebase and re-audited the whole PR — it's clean and CI is green (run 28741986449).

✅ Conflict resolution is clean

view.route.test.ts was the only conflicted file — both branches added an it(...) at the same spot. Both tests are preserved and well-formed: the "Last crawled at" test (from origin/main #871, :131-178) and this PR's "unwraps a nested Readplace self-URL" test (:180-194). No leftover conflict markers anywhere in the changed files, and the PR's unwrap test kept its final regression-detecting form (asserts the Save CTA href's url param equals ARTICLE_URL, not the always-"Hello World" title).

Design re-confirmed end-to-end

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl (app.ts:694, test-app.ts:95, e2e-server.main.ts:143). validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original — the queue integration test proves it (urls).toEqual(["https://fagnerbrack.com/business-success"]), derived, no hard-coded origin).
  • Bounded, terminating unwrapMAX_VIEW_UNWRAP_DEPTH = 16 caps both facets: preparse's loop (O(n²)→O(n)) and originalUrlFromViewPath (converted from unbounded recursion to a bounded loop with the decodeURIComponent try/catch inside it, so no uncaught RangeError). I traced both boundary tests — the preparse cap resolves to exactly https://readplace.com/view/fagnerbrack.com/x (17 layers, 16 stripped), and the scheme-redirect cap returns undefined (17 stacked https://). Both are derived from the constant so they track it.
  • parseViewPath reuse keeps %3F?, %23#, %2525%25, and explicit-http:// as one source of truth.
  • Security improvement, no bypass — a /view/127.0.0.1/… wrapper now resolves to 127.0.0.1 and is SSRF-rejected; preparse only collapses self-host wrappers and feeds the inner target back through validate.
  • CI-flake fix (run-test-phases.ts) — the bounded browser-install back-off (4 attempts, 10s/20s/30s) is required-sleep-injected and pinned by two tests asserting the exact wait schedule and attempt count. Comment trimmed to the load-bearing dpkg-lock why, per the code-comments skill.

No new issues. Ready for human review.

The formatted review block has been emitted to the run output for the review automation to post.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

None

Re-review at c0a03847. The only change since the last review (22fb90f9, already 0/0/0) is the conflict-resolution rebase onto origin/main. I audited the rebase and re-reviewed the whole PR; CI is green (run 28741986449).

Conflict resolution is clean. view.route.test.ts was the sole conflicted file — both branches added an it(...) at the same spot. Both are preserved and well-formed: the "Last crawled at" test (from origin/main #871) and this PR's "unwraps a nested Readplace self-URL" test, which kept its regression-detecting form (asserts the Save CTA href's url param equals ARTICLE_URL, not the always-"Hello World" title). No leftover conflict markers in any changed file.

Design re-confirmed: unwrap-before-validate at the composition root (every save path gets the wrapped validator via deps.validateSaveableUrl; validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original — proven by the queue integration test's toEqual(["https://fagnerbrack.com/business-success"])); MAX_VIEW_UNWRAP_DEPTH = 16 bounds both the preparse loop (O(n²)→O(n)) and originalUrlFromViewPath (bounded loop, decode try/catch inside, no uncaught RangeError) — I traced both boundary tests and they match; parseViewPath reuse keeps the encoding rules as one source of truth; /view/127.0.0.1/… is correctly SSRF-rejected with no validation bypass. The CI-flake fix (run-test-phases.ts) has a required-injected sleep and two tests pinning the exact back-off schedule and attempt count.

All three previously-flagged LOW items remain resolved. No new issues.

@FagnerMartinsBrack FagnerMartinsBrack force-pushed the claude/readplace-self-url-unwrap-nu8u1h branch from c0a0384 to 4181a9c Compare July 9, 2026 09:48
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 4m 50s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · 4181a9c0

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts (originalUrlFromViewPath + depth cap)
  • Review composition-root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage
  • Review CI-flake fix (run-test-phases.ts)
  • Trace both boundary tests + view-route unwrap end-to-end
  • Confirm CI status & post final review

Verdict: 0 high, 0 medium, 0 low. CI is green (run 29009499074). This is the rebased branch (5 clean commits); 4181a9c0 folds in all three previously-flagged LOW fixes, and I re-audited the full PR fresh — no new issues.

✅ All three prior LOW items resolved in 4181a9c0

  1. Negative assertionqueue.save-articles.route.test.ts:127 now uses expect(urls).toEqual(["https://fagnerbrack.com/business-success"]) — positive, no hard-coded origin, and strictly stronger (proves the unwrapped original is the only stored URL).
  2. O(n²) + unbounded recursion → new MAX_VIEW_UNWRAP_DEPTH = 16 caps both facets: preparse's for(;;) is now bounded (O(n²)→O(n)), and originalUrlFromViewPath is converted from unbounded recursion to a bounded loop with the decodeURIComponent try/catch inside it — removing the uncaught RangeError/stack-overflow path. Both caps are pinned by boundary tests derived from the constant.
  3. CI-fix commentrun-test-phases.ts:220-222 dropped the prior-code-state history clause, keeping the load-bearing dpkg-lock why. Compliant with the code-comments skill.

Traced both boundary tests + the view-route unwrap

  • preparse cap (preparse-readplace-url.test.ts:108): https://readplace.com/view/ + readplace.com/view/×16 + fagnerbrack.com/x → each iteration strips exactly one layer; after 16 capped iterations one residual layer remains → resolves to https://readplace.com/view/fagnerbrack.com/x. Matches the assertion.
  • scheme-redirect cap (view-path.test.ts:225): https://×17 + example.com/x needs 18 iterations to fully resolve; the 16-cap trips first → undefined. Matches.
  • view-route unwrap (view.route.test.ts:186): GET /view/<selfHost>/view/example.com/postview.page.ts:150 validates through the wrapped validator, :155 takes validation.url (unwrapped), :259/:267 build the Save CTA href from it. The test asserts new URL(href).searchParams.get("url") === ARTICLE_URL — the regression-detecting form (fails if unwrapping regresses, since the href would carry the self-URL). Not vacuous.

Design re-confirmed

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl (app.ts:718, test-app.ts:95, e2e-server.main.ts:144); server.ts threads deps.validateSaveableUrl to every page. validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original — the queue integration test proves it end-to-end.
  • parseViewPath reuse keeps %3F?, %23#, %2525%25, and explicit-http:// as one source of truth; the iterative rewrite is behavior-preserving for normal 0–1-layer URLs.
  • Security improvement, no bypass — a /view/127.0.0.1/… wrapper resolves to 127.0.0.1 and is SSRF-rejected; preparse only collapses self-host wrappers and feeds the inner target back through validate.
  • CI-flake fixrunPlaywrightPhase is async and awaited at its single call site; sleep is a required dep; the bounded back-off (4 attempts, 10s/20s/30s) is pinned by two tests.

Design note (intentional, not a defect)

Past the 16-layer cap, preparse returns a residual readplace.com/view/… self-URL that would then be stored as-is — reachable only via ≥16 adversarial, authenticated, self-inflicted nesting. The residual still resolves through /view (no loop, and a later re-save unwraps the single remaining layer), so bounding CPU/stack over perfectly unwrapping pathological input is the right trade. The test name ("leaving the residual wrapper intact") documents it. No change recommended.

cc @FagnerMartinsBrack — No high or medium priority issues found. This PR is ready for human review.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

None

Re-review at 4181a9c0 (rebased branch, 5 clean commits). CI is green (run 29009499074). 4181a9c0 folds in all three previously-flagged LOW items; I re-audited the full PR fresh and found no new issues.

All three prior LOW items resolved:

  1. Negative assertionqueue.save-articles.route.test.ts:127 now uses expect(urls).toEqual(["https://fagnerbrack.com/business-success"]): positive, no hard-coded origin, strictly stronger than the prior toContain + .not.toContain pair. Aligns with the test-driven-design skill's Avoid Negative Test Assertions.
  2. O(n²) + unbounded recursionMAX_VIEW_UNWRAP_DEPTH = 16 caps both unwrap paths. preparse's for(;;) is now bounded (O(n²)→O(n)), and originalUrlFromViewPath is converted from unbounded recursion to a bounded loop with the decodeURIComponent try/catch inside it — removing the uncaught RangeError/stack-overflow path. Two boundary tests pin the caps, both derived from the constant. Traced both: the preparse cap resolves to https://readplace.com/view/fagnerbrack.com/x (one residual layer after 16 iterations); the scheme-redirect cap returns undefined (17 stacked https:// exceeds the cap).
  3. CI-fix commentrun-test-phases.ts:220-222 drops the prior-code-state clause, keeping the load-bearing dpkg-lock rationale, per the code-comments skill.

Design re-confirmed: unwrap-before-validate at the composition root (every save path gets the wrapped validator via deps.validateSaveableUrlapp.ts:718, test-app.ts:95, e2e-server.main.ts:144; validateSaveableUrl returns parsed.toString() of its unwrapped input, so the stored URL is the original — proven end-to-end by the queue integration test's toEqual([...])); the view-route unwrap test asserts the resolved articleUrl carried in the Save CTA href (regression-detecting, not vacuous); parseViewPath reuse keeps the encoding rules as one source of truth; /view/127.0.0.1/… is correctly SSRF-rejected with no validation bypass. The CI-flake fix has a required-injected sleep and two tests pinning the exact back-off schedule and attempt count.

Intentional design note (not a defect): past the 16-layer cap, preparse returns the residual readplace.com/view/… self-URL — reachable only at ≥16 adversarial, authenticated, self-inflicted nesting. The residual still resolves through /view (no loop; a later re-save unwraps the single remaining layer), so bounding CPU/stack over perfectly unwrapping pathological input is the right trade, documented by the test name "leaving the residual wrapper intact". No change recommended.

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Resolve conflicts. Use your Read tool to read the local file .github/workflows/claude-PR-conflict-fixer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 11m 28s —— View job


Resolving merge conflicts — PR #870

  • Read conflict-fixer instructions & CLAUDE.md guidelines
  • Read full commit history to understand PR intent
  • Rebase onto origin/main and identify conflicts
  • Resolve conflicts (cross-referencing PR intent)
  • Run pnpm check to verify — green
  • Push with --force-with-lease

Rebased the PR onto origin/main (28f223b0) and force-pushed (4181a9c0f3bc1436, lease-protected).

Conflict resolved — 2 files, both superseded by main

The conflict was entirely in the CI-flake fix, not the PR's feature: while this PR was open, main independently landed the same browser-install retry fix the PR's f2b12029 had added (the refactor(crawl) / ci: move web-tests back to ubuntu-latest line of work). Both touched the same two files:

File PR's version (f2b12029 + 4181a9c0) main's version
run-test-phases.ts async for(;;) loop, sleep: delay (Promise), escalating 10s/20s/30s back-off synchronous recursive installBrowsers, sleep: Atomics.wait (blocking), fixed 10s back-off + a defaultDeps.sleep test
run-test-phases.test.ts async-sleep test doubles sync-sleep test doubles matching the above

Both implementations solve the identical problem (survive dpkg-lock contention so browser install doesn't fail CI). Per the conflict-fixer rule "never reverse intentional changes, but update the PR to be compatible with main" — and since the PR's real feature (self-URL unwrapping) lives entirely in separate, non-conflicting files — I took main's version of both files. This satisfies the PR's CI-fix intent (green CI) without the PR carrying a now-redundant, divergent copy.

Result: the rebase went from 5 commits → 4 — git automatically dropped f2b12029 (its diff became empty once both files matched main). The PR is now cleanly scoped to just the self-URL feature:

f3bc1436 fix(view): bound self-URL unwrap depth; address low-priority review nits
521ca849 fix: address high/medium priority review issues
ee06eedc fix: address high/medium priority review issues
7d1b5225 feat(view): unwrap Readplace self-URLs when saving

PR feature intact

git diff origin/main --stat = 9 files, all self-URL-unwrap content. Verified the 4181a9c0 low-priority fixes survived the rebase: MAX_VIEW_UNWRAP_DEPTH = 16 depth cap (bounded loops in both preparse-readplace-url.ts and view-path.ts) and the queue positive exact-set assertion (toEqual(["https://fagnerbrack.com/business-success"])). run-test-phases.{ts,test.ts} are now byte-identical to main.

Verification — pnpm check green

  • 40 projects / 55 tasks succeeded (exit 0)
  • hutch: 2998 unit + 11 integration + 9 E2E tests pass
  • Coverage thresholds met: statements 99.47%, branches 97.08%, functions 100%, lines 99.47%
  • E2E browser install succeeded (via main's retry logic)
    · branch claude/readplace-self-url-unwrap-nu8u1h

claude and others added 4 commits July 10, 2026 10:13
Saving a Readplace reader URL (e.g. /view/fagnerbrack.com/x) now saves the
underlying original article (https://fagnerbrack.com/x) rather than the
self-referential wrapper. Without this, a /view/... URL pasted into any save
surface was stored verbatim, producing a double-nested article that crawls
Readplace as a third-party site.

The fix is a pure URL→URL preparser composed into the injected
validateSaveableUrl at each composition root, so it runs before the stub row
is written and applies to every client (web, extension, iOS, MCP, import)
with no per-route code. It recognises /view/<host>/<path> and /view/?url=
shapes, recursively collapses nesting, and matches only the deployment's own
host. Because /view rendering also routes through validateSaveableUrl,
visiting a nested self-URL now resolves to the real article too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoT1snrsX53BSK7Q5FYubH
Strengthen the view-route self-URL unwrap test so it can detect a
regression. The fake parseArticle returns title "Hello World" for any
URL, so the title-only assertion passed even if the route stopped
unwrapping. Assert on the resolved articleUrl surfaced in the Save CTA
href (parsed url param == the unwrapped original) — this fails if
unwrapping regresses, since the href would carry the self-URL instead.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strengthen the view-route self-URL unwrap test so it can detect a regression. The previous assertion checked only the article title, which the harness's fake parser returns ("Hello World") for every URL — so it passed even if the route stopped unwrapping. Assert instead on the resolved articleUrl carried in the Save CTA href (url param must equal https://example.com/post), mirroring the canonical-path test convention.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cap the /view self-URL unwrap at MAX_VIEW_UNWRAP_DEPTH so an adversarially
nested wrapper (reachable via an authenticated import/manifest) can no longer
drive O(n^2) reparsing or overflow the stack: originalUrlFromViewPath now
resolves scheme redirects in a bounded loop instead of unbounded recursion,
removing the uncaught RangeError path, and the preparse loop is likewise
capped. Normal 0-1 layer URLs are unaffected.

Also addresses the remaining review LOW items:
- queue.save-articles test: replace the negative .not.toContain assertion
  (and its hard-coded origin) with a positive exact-set assertion.
- run-test-phases browser-install comment: drop the prior-code-state clause,
  keeping the dpkg-lock rationale.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FagnerMartinsBrack FagnerMartinsBrack force-pushed the claude/readplace-self-url-unwrap-nu8u1h branch from 4181a9c to f3bc143 Compare July 10, 2026 10:23
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 4m 46s —— View job


Review complete — PR #870 (Unwrap Readplace self-URLs before saving) · f3bc1436

  • Read review instructions & CLAUDE.md guidelines
  • Examine the PR diff against origin/main
  • Review preparse-readplace-url.ts (core logic)
  • Review view-path.ts (originalUrlFromViewPath + depth cap)
  • Review composition-root integration (app.ts, test-app.ts, e2e-server.main.ts)
  • Review test coverage & trace boundary/route tests end-to-end
  • Confirm no conflict markers, CI status
  • Post final review

Verdict: 0 high, 0 medium, 0 low. CI is green (run 29086203850). Since the last review (4181a9c0, already 0/0/0), the only change is the 2026-07-10 conflict-resolution rebase landing as f3bc1436. The rebase dropped the PR's CI-flake commit (f2b12029) because main landed an equivalent browser-install retry independently, so run-test-phases.{ts,test.ts} are now byte-identical to main and out of the diff — the PR is cleanly scoped to the 9 self-URL files. I re-audited the whole PR fresh.

Verified clean

  • No conflict markers in any changed file; git diff origin/main --stat = 9 files, all self-URL content.
  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl (app.ts:716, test-app.ts:92, e2e-server.main.ts:145). validateSaveableUrl returns parsed.toString() of its unwrapped input (saveable-url.ts:100), so the stored URL is the original — the queue integration test proves it end-to-end (toEqual(["https://fagnerbrack.com/business-success"]), derived from TEST_APP_ORIGIN, no hard-coded origin).
  • parseViewPath reuse keeps %3F?, %23#, %2525%25, and explicit-http:// as one source of truth.
  • Bounded & terminatingMAX_VIEW_UNWRAP_DEPTH = 16 caps both facets: preparse's loop (O(n²)→O(n)) and originalUrlFromViewPath (bounded loop with decodeURIComponent try/catch inside it → no uncaught RangeError). Traced both boundary tests: preparse cap resolves to https://readplace.com/view/fagnerbrack.com/x (one residual layer after 16 iterations); scheme-redirect cap returns undefined (17 stacked https:// needs 18 iterations). Both derived from the constant.
  • Security improvement, no bypass — a /view/127.0.0.1/… wrapper resolves to 127.0.0.1 and is SSRF-rejected by validateSaveableUrl; preparse only collapses self-host wrappers and feeds the inner target back through validate.

All three prior LOW items remain resolved

  1. Negative assertion → positive exact-set expect(urls).toEqual([…]) (queue.save-articles.route.test.ts:127).
  2. O(n²) + unbounded recursion → MAX_VIEW_UNWRAP_DEPTH caps both paths, pinned by two boundary tests.
  3. CI-fix comment → moot: the file was reverted to main's version during the rebase and is no longer in the PR.

Intentional design note (not a defect)

Past the 16-layer cap, preparse returns the residual readplace.com/view/… self-URL — reachable only at ≥16 adversarial, authenticated, self-inflicted nesting. The residual still resolves through /view, so bounding CPU/stack over perfectly unwrapping pathological input is the right trade, documented by the test name "leaving the residual wrapper intact". No change recommended.

No new issues. Ready for human review.
· branch claude/readplace-self-url-unwrap-nu8u1h

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

None

Re-review at f3bc1436. CI is green (run 29086203850). Since the last review (4181a9c0, already 0/0/0), the only change is the 2026-07-10 conflict-resolution rebase. The rebase dropped the PR's CI-flake commit (f2b12029) because main landed an equivalent browser-install retry independently, so run-test-phases.{ts,test.ts} are now byte-identical to main and out of the diff — the PR is cleanly scoped to the 9 self-URL files. I re-audited the whole PR fresh; no conflict markers remain in any changed file.

Design re-confirmed end-to-end:

  • Unwrap-before-validate at the composition root — every save path receives the wrapped validator via deps.validateSaveableUrl (app.ts:716, test-app.ts:92, e2e-server.main.ts:145). validateSaveableUrl returns parsed.toString() of its unwrapped input (saveable-url.ts:100), so the stored URL is the original — proven by the queue integration test (toEqual(["https://fagnerbrack.com/business-success"]), derived from TEST_APP_ORIGIN, no hard-coded origin).
  • parseViewPath reuse keeps %3F?, %23#, %2525%25, and explicit-http:// as one source of truth.
  • Bounded & terminatingMAX_VIEW_UNWRAP_DEPTH = 16 caps both facets: preparse's loop (O(n²)→O(n)) and originalUrlFromViewPath (bounded loop with the decodeURIComponent try/catch inside it → no uncaught RangeError). Traced both boundary tests — preparse cap resolves to https://readplace.com/view/fagnerbrack.com/x (one residual layer after 16 iterations); scheme-redirect cap returns undefined (17 stacked https://). Both derived from the constant so they track it.
  • Security improvement, no bypass — a /view/127.0.0.1/… wrapper resolves to 127.0.0.1 and is SSRF-rejected; preparse only collapses self-host wrappers and feeds the inner target back through validate.
  • The view-route unwrap test asserts the resolved articleUrl carried in the Save CTA href (view.page.ts:152:157:261/:269), so it is regression-detecting, not vacuous.

All three previously-flagged LOW items remain resolved: (1) negative assertion → positive exact-set toEqual; (2) O(n²)+unbounded recursion → depth cap on both paths; (3) CI-fix comment → moot (file reverted to main in the rebase, no longer in the PR).

Intentional design note (not a defect): past the 16-layer cap, preparse returns the residual readplace.com/view/… self-URL — reachable only at ≥16 adversarial, authenticated, self-inflicted nesting. The residual still resolves through /view, so bounding CPU/stack over perfectly unwrapping pathological input is the right trade, documented by the test name "leaving the residual wrapper intact". No change recommended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants