Skip to content

feat(domain): hackathon analysis pure modules (1/10) - #20

Merged
TOMOKI977 merged 13 commits into
mainfrom
feat/hackathon-domain
Sep 26, 2026
Merged

TOMOKI977 merged 13 commits into
mainfrom
feat/hackathon-domain

Conversation

@TOMOKI977

Copy link
Copy Markdown
Contributor

Summary

PR 1 of 10 for change 3, hackathon-analysis: the pure domain modules. Nothing is wired yet, and nothing imports Workers AI, Puppeteer, grammY or D1.

  • argument.ts: decides whether the argument is a URL or a slug.
  • url.ts: the SSRF guard (schemes, userinfo, ports, localhost, IPv4/IPv6 private literals, 0.0.0.0/8, trailing dots, private suffixes) and the URL normalization key.
  • slug.ts: builds the slug from the hackathon name or host, with a collision suffix.
  • extraction.ts: validates untrusted model output. The shape and the type of each field are strict, and each snippet must appear verbatim in the page and be at most 200 chars.
  • suggest.ts: ranks linked repos for re-compete suggestions.
  • format.ts and text-limit.ts: plain-text messages that stay within Telegram's 4096 chars.

Review

  • A full 4R review ran on the first version. Four findings were confirmed and fixed:
    • localhost. and 0.0.0.0 bypassed the URL guard;
    • wrong-typed values in the model output were accepted;
    • a snippet-length test passed for the wrong reason.
  • One scoped validator approved the fix.
  • A final runtime probe of the real guard found that several trailing dots (localhost..) still passed. The maintainer approved one extra fix, which strips every trailing dot.
  • Warnings deferred to later PRs:
    • reposReply still duplicates the truncation helper (to be wired in PR10);
    • normalizeUrlKey drops non-default ports;
    • a surrogate pair can be split at the 4096 cut;
    • the truncation fallback is O(n²);
    • model value strings have no length cap.

Tests

  • Strict TDD. The RED and GREEN evidence for each task is in openspec/changes/hackathon-analysis/apply-progress.md.
  • Full suite: 402/402. tsc --noEmit is clean.

Size exception: about 436 lines of source and 590 of tests (plus the progress docs). This PR is only the pure domain; the tests are where it went over the ~350-line estimate.

🤖 Generated with Claude Code

Pure classifier so the command layer can route to a slug re-show or a
fresh URL analysis without touching either lookup or fetch code.
Refuses non-http(s) schemes, userinfo, non-default ports, IP-literal
and private-suffix hosts before any fetch is attempted, and derives a
stable normalization key so re-runs of the same event page refresh
the existing analysis instead of creating a duplicate.
Derives a base slug from the extracted name or the URL host (NFKD,
lowercased, capped at 40 chars) and appends the numeric or hex
collision suffix design.md specifies for a duplicate slug.
Rejects a malformed model response outright, and demotes any
individual field to null when its snippet is oversized or not found
verbatim in the page text, so a fabricated or ungrounded field can
never reach the domain layer.

Note: the stored snippet cap is 200 chars per the llm-extraction
spec (the plan's "≤160" was a task-doc shorthand for the verbatim
check window, not the storage cap).
Ranks the team's known repos against the extracted hackathon name by
shared-token count, top 3, with no LLM call and no recomputation on
later shows (design.md "Suggestions").
Extracts the /repos truncation pattern (whole lines kept under the
4096-char limit, "...and N more" summary past it) into a reusable
pure helper so /hackathons can reuse it without duplicating the logic.
Renders a stored analysis's non-null fields and suggestions as plain
text, and the /hackathons listing (slug, name, deadline, linked
status) truncated with the shared line-list helper — both bounded
to Telegram's 4096-char limit.
Phase 1 (Domain Foundation) is fully implemented and tested; records
TDD cycle evidence, work-unit evidence, and the over-budget changed
line count for the maintainer to review before continuing to PR2.
…uard

RISK-002: a trailing root dot (localhost., foo.localhost.,
metadata.google.internal.) bypassed the localhost/private-suffix/
single-label checks in assertSafeUrl because URL.hostname preserves it
verbatim. Strip one trailing dot before all host checks.

RISK-003: 0.0.0.0 was accepted by isUnsafeIpv4. Refuse the whole
0.0.0.0/8 block.
RELI-002: the old "snippet exceeds limit" test used a 197-char
snippet that was also not verbatim in the page, so it passed for the
wrong reason (non-verbatim, not length). Rewrote it so the snippet IS
verbatim in pageText and is exactly 201 chars (nulled for length
alone), and added a boundary test for an exactly-200-char verbatim
snippet that must be kept. Fixed the title: it said 160, the spec
says 200.
RELI-001/RESI-001: validateExtraction only checked snippet and
confidence types, so a string teamSize, a numeric name, or an
undefined value passed through as ok: true with the bad value stored.
Added hasValidFieldType to check candidate.value against each field's
declared type (teamSize: finite number; every other field: string)
and fold it into the existing shape guard, so a wrong type rejects the
whole response as invalid-shape per the file's existing semantics.
@TOMOKI977 TOMOKI977 added the size:exception PR over the 400-line review budget, accepted explicitly label Sep 26, 2026
@TOMOKI977
TOMOKI977 merged commit 4a4d03a into main Sep 26, 2026
2 checks passed
@TOMOKI977
TOMOKI977 deleted the feat/hackathon-domain branch September 26, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:exception PR over the 400-line review budget, accepted explicitly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant