feat(domain): hackathon analysis pure modules (1/10) - #20
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsandtext-limit.ts: plain-text messages that stay within Telegram's 4096 chars.Review
localhost.and0.0.0.0bypassed the URL guard;localhost..) still passed. The maintainer approved one extra fix, which strips every trailing dot.reposReplystill duplicates the truncation helper (to be wired in PR10);normalizeUrlKeydrops non-default ports;valuestrings have no length cap.Tests
openspec/changes/hackathon-analysis/apply-progress.md.tsc --noEmitis 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