Fragrantica
Fragrantica for swamp — search perfumes, list them by designer/house or by note, fetch full perfume details, and read the "People who like this also like" similar-perfumes list, all from the public Fragrantica pages (no credentials).
Model:
@magistr/fragrantica—searchresolves a free-text query (name/brand) to Fragrantica perfume pages via a web search engine (Fragrantica's own search is Cloudflare/Algolia-gated).get-perfumeparses a perfume page into brand, gender, year, rating, perfumers, main accords (with strength %), the top/middle/base notes pyramid, and similar perfumes.similarreturns just the "also like" list.list-by-designerandlist-by-noteenumerate the perfumes on a designer/house or note page, andfind-by-notesintersects several note pages to hunt a note combination (e.g. licorice + oud).
| Argument | Type | Description |
|---|---|---|
| query | string | Perfume name and/or brand, e.g. 'Creed Aventus' |
Resources
2026.08.02.1
Real-fixes the 9 remaining latent bugs (LB4–LB12) pinned during the wave-3
quality backfill (tracked in the local fragrantica-latent-bugs
@magistr/issue-lifecycle model — not filed to the swamp.club Lab, per
CLAUDE.md's Anti-Bypass rule). LB1–LB3 (closed in 2026.07.31.1) are untouched
and stay green.
Adds three optional, code-defaulted global args — timeoutMs (default 15000),
maxNotes (default 20), imageBaseUrl (default https://fimgs.net) — so every
existing caller stays byte-identical unless it opts in to an override. No
resource-schema change.
MEDIUM:
- LB4, redirect-follow bypasses host intent, closed —
fetchPage'sfetch()now passesredirect: "manual"and follows redirects itself in a loop bounded to 5 hops, re-validating eachLocationtarget against the host allowlist (assertHostAllowed) before ever fetching it. An opaque/locationless redirect throws instead of being silently followed. - LB5, second-order SSRF via DuckDuckGo poisoning, closed —
resolveNoteUrl's andlist-by-designer's DuckDuckGo-resolved hit is now host-allowlisted before use, reusing the sameassertHostAllowedguard as LB1/LB4. The stale "#4/#5 deferred" comments are removed throughout. - LB6, unbounded note fan-out, closed —
find-by-notesnow capsnotes[]at amaxNotesglobal arg (default 20), throwing before any fetch when exceeded. - LB7, no fetch timeout, closed —
fetchPageandduckDuckGonow run under a sharedwithTimeouthelper: oneAbortController+setTimeoutper call (spanning every redirect hop forfetchPage),clearTimeoutalways infinally. Governed by the newtimeoutMsglobal arg (default 15000ms). - LB8, duplicate-note double-count, closed —
find-by-notesnow dedups by the RESOLVED note URL (a repeated note is fetched at most once) and computes itsmode=allthreshold from the distinct note count, notargs.notes.length.
LOW:
- LB9, instanceSlug resource-name collision, closed —
instanceSlugnow appends a deterministic 8-hex-char FNV-1a hash of the RAW (pre-slugify) input, so distinct inputs that used to collapse to the same lossy slug (e.g."A/B"vs"A B") get distinctwriteResourceinstance names. Injective and deterministic, not merely idempotent — the property test was rewritten accordingly. - LB10, unclamped accord strength, closed (with an accepted residual) —
parseAccordsnow clamps strength to[0, 100]. The false-positive selector match (matching ANY coloredwidth:div, not just real accord-bar markup) is a SEPARATE, accepted residual — it isn't separable from synthetic markup without breaking the byte-frozenperfume.htmlaccords contract pin, so it stays pinned as a documented, un-fixed risk. - LB11, hardcoded fimgs.net thumbnail, closed —
refFromPerfumeUrl/collectPerfumeRefs/parsePerfumenow thread an optionalimageBaseparameter (defaulthttps://fimgs.net, driven by the newimageBaseUrlglobal arg) through to the thumbnail URL. The default stays byte-identical everywhere the override isn't set. - LB12, unsanitized stored values, accepted by decision (not fixed) —
parsePerfumestill stores parsed text (brand/notes/description) VERBATIM. This is now documented as an intentional lossless-storage contract (seeparsePerfume's doc comment infragrantica.ts): sanitizing at storage time would corrupt legitimate brand/note names, and this model never renders its stored data — encoding is a render-time responsibility for whichever downstream consumer displays it.
Model version and manifest.yaml bumped 2026.07.31.1 -> 2026.08.02.1 (in
sync), with an upgrades entry (upgradeAttributes: (old) => old — no
resource-schema change).
Test flips: fragrantica_adversarial_test.ts — 9 pins flipped from
pinned-broken to pinned-fixed (LB4/LB5/LB6/LB7/LB8/LB9/LB10/LB11) plus 6 new
tests (2 non-vacuous LB4 redirect tests, 1 symmetric LB5 list-by-designer test,
2 LB6 boundary/override tests, 1 LB11 unchanged-default test); LB10's
false-positive-selector pin and LB12's verbatim-storage pin stay as documented,
un-fixed/accepted residuals (retitled, same assertions).
fragrantica_contract_test.ts — the accord-over-100.html pin flips 120
-> 100. fragrantica_coverage_test.ts — the two instanceSlug tests updated
for the new hash-suffixed shape. fragrantica_methods_test.ts — the
expectedSlug mirror updated to match instanceSlug's new algorithm (the 4
tests that call it need no other changes). fragrantica_property_test.ts —
Notes trimmed at a line boundary to fit the registry's 4900-byte per-version cap. Full section: https://github.com/umag/swamp-workspace/blob/d850d8e8372d4c0008c9245959a090b37095de7a/fragrantica/CHANGELOG.md
Release 2026.07.16.2 — align model versions with manifests
Maintenance release across the @magistr extensions. For most packages this
carries no functional change: the only edit is the model's version: field,
brought back in line with its manifest version so the published model type
version and the package version no longer drift.
Functional changes in this release are limited to:
anime-cron: normalizeTitle now strips a ": subtitle" suffix and a trailing parenthesized year before comparison, fixing dedup false-misses where the torrent title carries a subtitle or year that the AniList romaji does not.
arckit: first publish. Standalone ArcKit port — a 12-phase architecture governance state machine with 65 bundled templates, driven by a bundled skill.
Also tracks three extensions (kaiten, observability-agent, music-library) that previously existed only as untracked working-tree directories, recovered from stashes.
Merge pull request #45 from umag/add-fragrantica-extension
Add @magistr/fragrantica extension
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned