Skip to main content

Fragrantica

@magistr/fragranticav2026.08.19.1· 27d agoMODELS
01README

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/fragranticasearch resolves a free-text query (name/brand) to Fragrantica perfume pages via a web search engine (Fragrantica's own search is Cloudflare/Algolia-gated). get-perfume parses a perfume page into brand, gender, year, rating, perfumers, main accords (with strength %), the top/middle/base notes pyramid, and similar perfumes. similar returns just the "also like" list. list-by-designer and list-by-note enumerate the perfumes on a designer/house or note page, and find-by-notes intersects several note pages to hunt a note combination (e.g. licorice + oud).
02Models1
@magistr/fragranticav2026.08.19.1extensions/models/fragrantica.ts
fn search(query: string)
Search perfumes by free-text query (name and/or brand). Resolves to
ArgumentTypeDescription
querystringPerfume name and/or brand, e.g. 'Creed Aventus'
fn similar()
Get the 'People who like this also like' similar perfumes for a given

Resources

search(infinite)— Perfume search results (name → Fragrantica perfumes)
listing(infinite)— Perfumes listed by designer/house or by note
similar(infinite)— 'People who like this also like' similar perfumes
perfume(infinite)— Full perfume details: accords, notes pyramid, rating, perfumers, similar
noteIntersection(infinite)— Perfumes that share several notes at once (intersection of note pages)
03Previous Versions3
2026.08.02.1

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, closedfetchPage's fetch() now passes redirect: "manual" and follows redirects itself in a loop bounded to 5 hops, re-validating each Location target 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, closedresolveNoteUrl's and list-by-designer's DuckDuckGo-resolved hit is now host-allowlisted before use, reusing the same assertHostAllowed guard as LB1/LB4. The stale "#4/#5 deferred" comments are removed throughout.
  • LB6, unbounded note fan-out, closedfind-by-notes now caps notes[] at a maxNotes global arg (default 20), throwing before any fetch when exceeded.
  • LB7, no fetch timeout, closedfetchPage and duckDuckGo now run under a shared withTimeout helper: one AbortController + setTimeout per call (spanning every redirect hop for fetchPage), clearTimeout always in finally. Governed by the new timeoutMs global arg (default 15000ms).
  • LB8, duplicate-note double-count, closedfind-by-notes now dedups by the RESOLVED note URL (a repeated note is fetched at most once) and computes its mode=all threshold from the distinct note count, not args.notes.length.

LOW:

  • LB9, instanceSlug resource-name collision, closedinstanceSlug now 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 distinct writeResource instance names. Injective and deterministic, not merely idempotent — the property test was rewritten accordingly.
  • LB10, unclamped accord strength, closed (with an accepted residual)parseAccords now clamps strength to [0, 100]. The false-positive selector match (matching ANY colored width: div, not just real accord-bar markup) is a SEPARATE, accepted residual — it isn't separable from synthetic markup without breaking the byte-frozen perfume.html accords contract pin, so it stays pinned as a documented, un-fixed risk.
  • LB11, hardcoded fimgs.net thumbnail, closedrefFromPerfumeUrl / collectPerfumeRefs / parsePerfume now thread an optional imageBase parameter (default https://fimgs.net, driven by the new imageBaseUrl global 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)parsePerfume still stores parsed text (brand/notes/description) VERBATIM. This is now documented as an intentional lossless-storage contract (see parsePerfume's doc comment in fragrantica.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

2026.07.16.2

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.

2026.07.15.1

Merge pull request #45 from umag/add-fragrantica-extension

Add @magistr/fragrantica extension

04Stats
A
100 / 100
Downloads
0
Archive size
131.7 KB
  • 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
05Platforms
06Labels