Creative ideas + design notes
Commits with substantial prose (≥120 chars) — the rationale behind each move.
ae0a5ef · 2026-05-19 · add density slider to /family grid
Families cohort grid was hardcoded to --cols:3 with no density control.
Adds a density range input (3-8) and an id="grid" hook so the existing
public/js/app.js wiring picks it up — applies, persists to localStorage
key lai-density, and hydrates from storage on next page load. Sort stays
server-side via the existing form-submit select (no id, so app.js leaves
it alone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bf3e286 · 2026-05-19 · add noreferrer to topnav external-target links
The Sources / OpenAPI / API links open in a new tab and had rel="noopener"
but no noreferrer, leaking the originating page URL via Referer. Hardens
them to rel="noopener noreferrer" — matches the cite-link convention in
views/asset.ejs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f402188 · 2026-05-19 · gitignore snapshot files + 404-guard .bak/.pre-/.orig paths under static
Broadens .gitignore to exclude editor/snapshot backups (*.bak, *.bak.*,
*.pre-*, *.orig, *.rej, *.swp, *~) and adds an Express middleware that
404s any request matching those patterns BEFORE express.static can serve
them, so a stray backup landing in public/ can never leak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a2a7ff0 · 2026-05-13 · yolo tick #26: /api/index list endpoint with sort + latest-point summary
Returns one row per benchmark index: identity (slug/name/definition/
methodology_version), point_count, first_as_of, latest_as_of,
latest_value, latest_change_pct. Single SQL pass with two lateral
subqueries so it stays cheap as indices and index_points grow.
Sort modes: slug (default), name, value-desc, value-asc, change-desc,
change-asc, points-desc, recent-desc. Unknown sort values fall back to
default ordering instead of erroring.
OpenAPI 3.1 spec updated. Three new smoke tests cover the list shape,
value-desc ordering with nulls-last, and bogus-sort fallback.
58/58 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41093f5 · 2026-05-13 · yolo tick #25: family-detail grid gets sort + density + search controls
Family detail page (/family/:slug) was missing the standing-rule controls
that index.ejs has — Steve's MEMORY says every grid view needs sort +
density + grid-search. Added the same controls block + data-* attrs on
each card + id=grid so the existing public/js/app.js machinery wires
itself up automatically. No server changes; client-side only.
0e64230 · 2026-05-13 · yolo tick #23: source citation links on comp rows
LEFT JOIN raw_observations in valueAsset() to expose payload.source_url
per comp. Asset detail view renders an external ↗ cite link (target=_blank,
rel=noopener noreferrer nofollow) next to each comp's source slug when a
URL is available. Adds .cite styling to app.css. Sotheby's editorial scrape
already populates raw_observations.payload.source_url, so 2 comps on the
birkin-20 detail page now resolve to live article URLs.
69ece51 · 2026-05-13 · yolo tick #22: source-weight-aware comparable_similarity_weight + methodology v0-stub.1
- comparable_similarity_weight scales the 0.18 cap by avg comp source weight
(tier-1 pools score full 0.18; tier-4 pools drop to ~0.04). Real CLIP cosine
similarity still scheduled for v0.3.
- METHODOLOGY_VERSION constant exported from lib/valuation.js; audit middleware,
/api/version, and tests now read from one source of truth.
- Bumps methodology_version to v0-stub.1 per METHODOLOGY § 7 (component
semantics changed).
- METHODOLOGY.md change log + table row updated.
- 52/52 tests green.
3a8e130 · 2026-05-13 · yolo tick #21: /family HTML index page (families list with cohort summary)
Adds GET /family — HTML listing of every model family with one-row
cohort summary cards (brand, family name, avg Q50, asset count, total
comps 24mo). Each card links to /family/:slug detail page.
- routes/console.js — new /family handler ordered before /family/:slug
- views/families.ejs — new view, reuses .grid + .card styles
- views/partials/head.ejs — adds 'Families' nav link beside Console
- tests/smoke.test.js — new smoke test (52/52 green)
Author: steve@designerwallcoverings.com
39ba614 · 2026-05-13 · yolo tick #20: sparkline Y-axis min/max labels + descriptive aria-label
Adds a left-side label gutter to the SSR sparkline partial showing the
formatted min and max values ($1.2K / $14M style), plus a vertical
axis tick line. aria-label now describes the range and point count
rather than the static word "sparkline".
ROADMAP: 'Sparkline Y-axis labels' — done.
Author: Steve Abrams <steve@designerwallcoverings.com>
ced9794 · 2026-05-13 · yolo tick #18: /admin/audit HTML view over recent valuation_calls
Read-only inspector for the immutable audit log shipped in tick #7.
Supports ?slug=<asset-slug> and ?limit=<1..1000> filters, no-store
cache header, 4xx/5xx status-class coloring. Two new smoke tests
(render + slug filter).
447537f · 2026-05-10 · yolo tick #16: exotic / special-edition badges + audit-test race fix
Surface canonical_assets.attrs.exotic and attrs.special_edition on the
console grid, family pages, and asset detail h1 — quick visual signal
for the 14 Sotheby's-sourced exotic configs added in tick #13. Pure
EJS conditional + CSS, no schema changes.
.exotic-badge — accent-tinted pill (e.g., "EXOTIC" on Niloticus
Himalaya, Crocodile, Alligator, Lizard configs)
.edition-badge — warn-tinted pill (e.g., "FAUBOURG", "RAINY DAYS",
"MIDAS" on the Sotheby's special-edition lots)
lib/valuation.listAssets now returns ca.attrs in the SELECT so the
templates can read attrs.exotic and attrs.special_edition without a
second query. valueAsset() already returned attrs as part of the
asset block.
Audit test fix: tests/audit.test.js "GET /api/health is NOT logged"
was racing against parallel test files (node --test runs files
concurrently) — other tests' audit rows landed during the 200ms
sleep, inflating the unfiltered COUNT(*). Tightened the assertion
to filter `WHERE route = '/api/health'` so the test only counts
its own writes. Caught a real flake, not a regression.
44/44 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
db84b94 · 2026-05-10 · yolo tick #15: launchd-driven overnight autonomous loop
Steve is going to sleep and wants the lifestyle-asset-intel build
running autonomously overnight. ScheduleWakeup-based loop dies when
the active Claude Code session closes, so set up a more durable
launchd-driven path that fires every 30 minutes regardless of
session state.
Files (all under .yolo/):
tick-prompt.md — self-contained instructions for each fire:
working dir, PG env, pm2 process, standing-rule
guardrails (no DNS, no email, no eBay scrape,
local-only), candidate next-tick items, hard
exit conditions ("≥8 turns elapsed → wrap up").
run-tick.sh — shell wrapper. mkdir-atomic lock (no flock on
macOS), 12-minute stale-lock recovery, claude
CLI invoke with --print --dangerously-skip-
permissions --max-turns 12, append to log.
Unsets ANTHROPIC_API_KEY/AUTH_TOKEN per Steve's
standing rule (Max plan only).
README.md — operator runbook: where the plist lives, how
to pause/resume/tail, why launchd over
ScheduleWakeup, safety knobs.
LaunchAgent: ~/Library/LaunchAgents/com.steve.lai-yolo.plist (not in
this repo — per-machine config). StartCalendarInterval = :15 and :45
of every hour. WorkingDirectory pinned to project root. PATH includes
/Users/stevestudio2/.local/bin so the claude binary resolves
correctly.
.gitignore extended to keep .yolo/yolo-loop.log,
.yolo/launchd-{out,err}.log, and .yolo/.lock.d/ out of the repo;
scripts and prompt stay tracked.
Validated: lock acquires + refuses + releases correctly; launchctl
list shows the entry loaded with LastExitStatus 0; next fire 28
minutes from this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
012be8e · 2026-05-10 · yolo tick #14: auto-generate valuation_snapshots from real comps
The 14 exotic / Birkin 20 / Constance canonical_assets added in tick
#13 had real Sotheby's comps but no valuation_snapshot rows, so they
showed up in the grid as null Q10/Q50/Q90. Adding a one-shot seed
block that auto-fills v0-stub snapshots for any canonical_asset that
has ≥1 transaction but no snapshot for today's date.
Algorithm (per METHODOLOGY § 6 heuristic):
q50 = PERCENTILE_CONT(0.5) of gross_transaction_price (all comps)
q10 = q50 * 0.85
q90 = q50 * 1.27
liquidity_score, expected_dts, confidence stay null — those are
live-recomputed in lib/valuation.js from the 24-month window, so
any seeded constant would be inert. The grid + asset detail pages
pick up live values automatically.
auth_risk = 0.06 (low default for tier-1 auction provenance)
breakdown = { source: 'auto-from-real-comps', generator: 'tick-14',
method: 'median q50 + 0.85/1.27 quantile heuristic' }
No 24-month filter on the snapshot-input window (intentionally broader
than the live liquidity calc), so configs whose only comps are 2+
years stale still surface — they show the historical median Q50 with
a low live liquidity score, which is the correct signal.
Effect on Birkin family rollup:
asset_count 9 → 19
avg_q50 $24,367 → $85,658
total_comps 26 → 37 (24mo filter still applies in this metric)
29 snapshots / 29 assets = zero orphans. 44/44 tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d9aa2ad · 2026-05-10 · yolo tick #13: real Sotheby's auction-result data ingest
Pivoted from feature polish to actual data acquisition. Per Steve's
"build and look anywhere for data" + "always stay legal for scraping"
instructions, I went after Sotheby's editorial pages — public records
of realized auction prices, robots-friendly, no auth bypass, no
images. NOT eBay sold-history (Marketplace Insights API gated, HTML
scrape is TOS-prohibited). NOT third-party aggregators without
license review.
Sources (cited in raw_observations.payload.source_url for every row):
1. /articles/the-10-most-expensive-hermes-bags-sold-at-auction-in-2024
2. /articles/the-11-most-expensive-hermes-bags-sold-at-auction-in-the-first-half-2024
3. /articles/the-newest-hermes-bag-collector-obsession-the-birkin-20
4. /articles/secondary-market-signals-after-hermes-impressive-2024-results
Schema additions:
- 1 new model_family: Constance
- 14 new canonical_assets (mostly exotic Niloticus Himalaya configs +
Faubourg / Midas / Rainy Days / Vert d'Eau / Vanille / Havane —
the Sotheby's headline lots)
- 20 raw_observations with full source_url + lot description in
payload jsonb (idempotent via UNIQUE source+external_id+kind)
- 22 transactions linked back to their raw_observation. Net seller
proceeds computed as gross * 0.82 per METHODOLOGY § 4 Sotheby's
hammer-to-net haircut.
Region attribution: transactions.region carries the auction city
(HK / FR), so per METHODOLOGY § 11 they're stored as cross-region
comps and excluded from US comp pools — the canonical assets retain
region='US' (buyer region we model).
Top realized prices now in DB:
$330k Kelly 25 Sellier Niloticus Himalaya paved diamond, HK 2024
$284k Kelly 25 Sellier Niloticus Himalaya 14K + diamond, Paris 2024
$234k Kelly 28 Niloticus Himalaya 18K + diamond, HK 2024
$234k Birkin 20 Faubourg Snow, HK 2024
$227k Birkin 20 Faubourg Snow, Paris 2024
$203k Birkin 20 Faubourg Rainy Days, HK 2025
$187k Birkin 25 Niloticus Himalaya palladium, HK 2024
$181k Birkin 20 Faubourg Midnight, HK 2024
$157k Kelly 25 Sellier Midas (yellow gold), HK 2024
…
Counts after: 29 canonical_assets (13 exotic), 61 transactions,
20 raw_observations. 44/44 tests still green.
Note: new exotic assets have comps but no valuation_snapshot rows —
detail page renders "No snapshot yet for this asset" honestly. Auto-
snapshot generation from comps is a future tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41d8213 · 2026-05-10 · yolo tick #12: family taxonomy pages (/family/:slug)
Cohort-level discovery surface, symmetric to /indices/:slug. Each
Hermès model family becomes a first-class page with summary stats,
asset grid, and related indices.
lib/valuation.getFamily(slug) joins model_families + brands, reuses
listAssets() enrichment via post-filter, pulls related indices via
slug-substring match (v0 heuristic), and computes a 4-key summary:
asset_count, avg_q50, total_comps (24mo), avg_liquidity
API: GET /api/family/{slug} → FamilyResponse (Cache-Control: public,
max-age=300). 404s on unknown slugs.
Console: GET /family/{slug} → views/family.ejs with breadcrumb, KPI
cohort block, related-indices list, 4-column asset grid.
asset.ejs h1 links family name to /family/{slug} so navigation closes
the loop. Top-level "Birkin" and "Kelly" nav entries with active-
state highlighting. OpenAPI 3.1 spec gains the path + FamilyResponse
schema.
Live shape (Birkin): 9 assets, $24,367 avg Q50, 26 trailing-24mo
comps, 0.615 avg liquidity, 2 related indices.
44/44 tests green: 3 new (api JSON, 404, HTML).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25a6082 · 2026-05-10 · yolo tick #11: per-route Cache-Control discipline
CDN + client cache hygiene matters for any enterprise API. Without
explicit headers, intermediaries either over-cache (serving stale
valuations) or under-cache (defeating CDN economics on the
list endpoints).
Discipline (lib/audit must record every valuation hit, so caching
those is non-negotiable):
no-store /api/valuation/{slug}, /api/audit/recent, /api/portfolio*
— auditable + per-user; must hit live every time
no-cache /api/health, /api/version
— conditional GETs OK, never serve stale to monitors
public 60s /api/assets
— list + filter results; refresh quickly
public 300s /api/sources, /api/index/{slug}
— slow-moving reference data
public 1h /api/openapi.json
— only changes on deploy
Plus stale-while-revalidate on the public ones so a brief origin
hiccup doesn't propagate to clients.
Helper: setCache(value) mini-middleware factory inside routes/api.js
so every route declares its own cache policy on the same line as the
handler — easy to audit at code-review time.
Smoke test asserts five representative paths (one per cache class).
41/41 tests green; live curl probe confirms all 8 documented routes
emit the expected Cache-Control header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6d5a46c · 2026-05-10 · yolo tick #10: server-side /api/assets?q= filter
Catalog discovery scaling. Until now the grid filter was DOM-only —
lib/valuation.listAssets returned the entire catalog and the client-
side JS hid non-matching cards. That works at 15 assets but breaks
when a paying API consumer pulls /api/assets at 1,500+ assets.
listAssets now accepts opts.q — a single token matched
case-insensitively across slug, brand, family, size, material, color,
hardware, and construction via parameterized SQL LIKE. Returns
{ count, q, assets[] } so callers can confirm the filter was honoured.
routes/console.js GET / pulls req.query.q through to listAssets and
passes it to the EJS view, which SSR-renders the filter into the
search input plus a "Clear ×" link when q is set. Pressing
Enter in the search box now navigates to /?q=… for server-side
scoping; typing keeps the existing DOM-side live filter for
sub-second feedback.
40/40 tests green; new tests assert ?q=birkin returns only Birkins
and ?q=clemence filters by material.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8639f38 · 2026-05-10 · yolo tick #9: OpenAPI 3.1 spec at /api/openapi.json
The enterprise-pitch artifact. Lenders, insurers, and marketplaces
need a machine-readable contract to generate clients in any language;
without one, every customer integration is a custom interview.
lib/openapi.js exports buildSpec({ version, gitSha }) which assembles
a hand-written 3.1 document covering all 9 current routes (health,
version, sources, assets, valuation/{slug}, index/{slug}, portfolio
GET/POST/PATCH/DELETE, audit/recent) and 14 schemas (Source,
CanonicalAsset, ValuationSnapshot, ConfidenceBreakdown, Comp,
MsrpObservation, ValuationResponse, IndexPoint, IndexResponse,
Holding, AuditEntry, ErrorResponse, HealthResponse, VersionResponse).
Hand-written rather than introspected so the source of truth is one
file the team reviews. Spec version is 'package_version+gitSha[7]'
so deploys are pinpointed.
Cached at boot in routes/api.js; served as application/json. Spec is
~12 KB, 5 tags (ops/catalog/valuation/indices/portfolio). Smoke test
asserts every documented path is present and that the v0.2-introduced
msrp + premium_to_msrp fields surface in the ValuationResponse schema.
Nav adds an 'OpenAPI' link next to 'Sources' for one-click discovery.
38/38 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19cb967 · 2026-05-10 · yolo tick #8: MSRP series + premium-to-retail ratio
BLUEPRINT.md flagged MSRP backfill as the central v0 problem for
Hermès — they sell Birkin/Kelly/Constance "exclusively in stores",
so there's no e-commerce feed to harvest. Migration 0005 lays in
msrp_observations with a CHECK-constrained source enum
(boutique_receipt / specialist_tracker / auction_house_commentary /
user_upload / client_pdf_invoice) and a (asset, source, date) unique
key so the same observation can't be double-counted across sources.
Seed: only the Birkin 30 Togo 2022–2026 series from BLUEPRINT.md's
own table, attributed to 'auction_house_commentary' (Sotheby's
report). The other 14 canonical assets stay null until real
observations land — Steve's standing rule against synthesized data
applies here too, so no fabricated MSRPs.
valueAsset() now joins the latest msrp_observation per asset and
computes premium_to_msrp = q50 / msrp_usd to two decimals. Returns
null when no MSRP exists. Live values for seeded B30 Togo:
q50=22300, msrp_usd=14900 (2026 reading), premium=1.50×
The number is naturally lower than Sotheby's 2024-quoted 2.4× because
2026 MSRP has caught up while 2025 secondary held — exactly the
drift signal METHODOLOGY § 1 calls out.
Asset detail page renders a "Primary-market reference" KPI block
(MSRP $ + observed year + source) with a teal-tinted "Q50 ÷ MSRP"
cell when the ratio is computable, and a contextual blurb pointing
to /methodology when MSRP is missing. CSS adds .msrp-block + the
accent-color premium cell.
37/37 tests green; smoke test now asserts the MSRP fields too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44cae3a · 2026-05-10 · yolo tick #7: immutable valuation_calls audit log + middleware
Fulfill the BLUEPRINT and METHODOLOGY commitment to immutable audit
logging. Every paying customer (lender / insurer / marketplace) needs
to point at a specific call, see what we returned, and reproduce it.
Without this, no enterprise contract closes.
Migration 0004 lays in valuation_calls — INSERT-only by app convention.
Indexes on (called_at DESC), (asset_slug, called_at DESC), and
(caller_ip, called_at DESC) cover the standard "last 100 calls",
"calls for this asset", and "what's this IP doing" queries. There is
deliberately no updated_at / deleted_at column; the v1.0 role split
will withhold UPDATE/DELETE grants from the app role.
lib/audit.js wires three pieces:
hashToken — sha256 redaction of any Bearer token before storage. Raw
tokens never touch the DB.
auditMiddleware — Express middleware that hooks res.finish, computes
latency via process.hrtime.bigint, sets x-request-id
response header, and writes via setImmediate so the user
never waits on the audit insert. Errors swallowed so
logging can't break a request. /api/health excluded so
monitoring traffic doesn't pollute the table.
recentCalls — paged SELECT with optional asset_slug filter.
Endpoint /api/audit/recent serves the last N calls for ops dashboards
and customer-side reconciliation. Self-recording (asks itself appear
in subsequent calls) — acceptable v0 behaviour.
7 new audit tests covering: hashToken edge cases, end-to-end audit-row
insertion via the middleware (using a per-test unique user-agent so
files running in parallel don't race), /api/health skip, /api/audit/recent
ordering + filtering, and a schema sanity check that the table stays
INSERT-only (no updated_at/deleted_at columns). 37/37 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bd3a76 · 2026-05-10 · yolo tick #6: /api/version introspection endpoint
Add the standard ops-hygiene introspection endpoint paying customers
expect: package_version, methodology_version, git SHA + dirty flag (so
ops can answer "is prod the same as my local?"), full runtime block
(node + platform + arch + pid + boot_time + uptime_seconds), schema
state (every applied migration with timestamp), and data snapshot
(asset / transaction / snapshot / index / holding / image counts +
latest snapshot as_of date).
Git SHA + dirty flag resolved at boot via execSync('git rev-parse'); if
git isn't installed (or the deploy is from a tarball not a checkout)
the field returns null without crashing the route.
Tightened /api/health to read PKG_VERSION from package.json (was
hardcoded). Added a /api/version smoke test asserting all the top-level
keys are present and that ≥3 migrations have applied.
30/30 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63e0563 · 2026-05-09 · yolo tick #5: METHODOLOGY.md + /methodology route
Freeze the formal methodology document the BLUEPRINT calls for. Anchors
every rule the v0 stub and future ML iterations must respect: source
eligibility/weighting, comparable selection, four-tier price
normalization (gross→all-in→net→normalized), the 7-grade ordinal
condition system + facet jsonb, quantile interpretation, the live
liquidity/dts formulas (with units and clamps), the 9-component
confidence breakdown table with v0 sources called out per row, the
authenticity-risk three-band escalation rule, current outlier-filtering
posture (none — by design), region/currency scope, index rebasing
rules, and an explicit "what v0 does NOT model" section so enterprise
buyers don't assume coverage that isn't shipped.
methodology_version field is the version pin every published snapshot
and index point references; v0 stamps everything 'v0-stub'. Versioning
discipline: any rule change moving a published quantile by >1% MUST
bump.
Render path: marked@18 (added) parses the .md at boot into HTML; cached
across requests; reload requires pm2 restart. /methodology added to
nav and to console.js routes. New views/methodology.ejs is a thin
wrapper that injects body_html through partials/head + foot. CSS
rules added for typographic long-form: H2 underlines, pre/code
backgrounds, accent-color blockquote bar, link underline-offset.
29/29 tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
660aa14 · 2026-05-09 · yolo tick #4: image_assets + image_embeddings (prep for v0.3 CLIP intake)
Lay in the schema for the image-intake pipeline BLUEPRINT.md calls for
in v0.3 (image-first valuation flow):
image_assets:
Provenance-rich row per image. Tracks canonical_asset linkage,
origin (upload/auction_lot/marketplace/crawl/reference), source_url,
storage_url, sha256 for content-addressed dedupe, mime/width/height,
and an attribution string per Steve's standing public-domain-only
rule. UNIQUE (sha256) lets the connector idempotently store images
without re-uploading the same bytes.
image_embeddings:
One row per (image, model) pair so we can run multiple embedders
side-by-side (CLIP ViT-B/32 baseline + SigLIP fine re-rank, etc.).
Stored as real[] with explicit dim column — PG-native, no extension
required. When pgvector lands locally the migration to vector(dim) +
HNSW is a 2-line ALTER.
No data seeded; v0.3 connector lands in a future tick.
29/29 tests still green. 3/3 migrations recorded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9b63e84 · 2026-05-09 · yolo tick #3: live liquidity_score + expected_dts (no more constants)
Replace the seed's constant 0.78 liquidity / 21 dts with per-asset live
calculations from the trailing 24-month transaction window.
computeLiquidity(stats) — pure function combining:
0.4 * count_factor log-saturating in n_comps over a 20-comp ceiling
0.4 * recency_factor linear decay from newest comp's age over 365 days
0.2 * spread_factor 1 − (Q90−Q10)/avg_gross, gross-normalized so a
$30k asset with $5k spread scores the same as a
$3k asset with $500 spread
computeExpectedDts(liquidity) — inverse of liquidity, clamped [7, 120]:
liquidity=1.0 → 10 days, =0.5 → 35 days, =0 → 60 days
valueAsset() and listAssets() both compute these live and override the
snapshot's stored constants before returning. listAssets does it in SQL
via a second LATERAL aggregating PERCENTILE_CONT(0.9/0.1) and AVG —
mirrors the JS quantile() helper so server + DB agree on numbers.
Across the 15 seeded assets: liquidity ranges 0.349–0.741, dts 23–43.
The console grid's sort-by-liquidity now produces meaningful order; the
asset detail page shows real, per-config days-to-sell.
8 new pure-function tests in tests/valuation.test.js (29/29 total green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ea1adfb · 2026-05-09 · yolo tick #2: portfolio nav link + view styling
Add /portfolio to the hamburger nav (was previously only reachable via
direct URL) and an /api/sources/api/health pair as external-link buttons.
Active-state highlighting works for the indices and portfolio sections,
not just the console root.
Wire CSS for the classes the portfolio.ejs view was already emitting but
that had no rules: pl-pos/pl-neg coloring (good/bad CSS vars), flash
banners (ok/err with color-mix tinting), owner-form + holding-form grid
layouts, inline-remove link button, and a thicker top border on the
table totals row. color-mix in srgb keeps the tints theme-aware in both
light and dark.
21/21 tests still green. /portfolio renders with active nav state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1d24570 · 2026-05-09 · yolo tick #1: enforce transactions natural-key + seed idempotency
Migration 0002 dedupes transactions on the natural key
(canonical_asset_id, source_id, transacted_at, gross_transaction_price)
and adds a UNIQUE constraint so future re-seeds are no-ops. Seed.sql
already had ON CONFLICT DO NOTHING in place (commit 917d044) — those
clauses were inert until this constraint existed.
Verified: txns went 128 → 41 after dedupe, re-seed held at 41.
tests/valuation.test.js: dotenv + PGHOST=/tmp guards so tests work in
the Mac peer-auth environment without needing PGUSER/PGPASSWORD set.
21/21 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
917d044 · 2026-05-09 · real confidence calc + sparklines + portfolio CRUD (Max It batch)
Three parallel improvements landed via the Max It dispatcher:
1. lib/valuation.js — replace constant-stub confidence_breakdown with a
compositional calculator (computeBreakdown). Source quality weights from
the joined sources.weight column (tier fallback {1:1.0, 2:0.7, 3:0.4,
4:0.2}); sample depth log-saturates over 20 comps; recency decays
linearly over a year on the newest comp; auth/condition/region risk
penalties are bounded. Latest_snapshot.confidence is recomputed from
the live breakdown sum, clamped [0,1]. computeBreakdown is exported as
a pure function so tests don't need to boot Express.
2. SVG sparklines — new views/partials/sparkline.ejs renders inline path +
circles from {as_of, value} points. Integrated into asset.ejs (price
trend over comps' transacted_at → gross) and index_detail.ejs (time
series of index points). Pure SSR, no client-side chart library. CSS
adds .spark { color: var(--accent); } and .trend section spacing.
3. Portfolio CRUD — routes/api.js gains GET/POST/PATCH/DELETE on
/api/portfolio with email-format validation, asset_slug existence check,
parameterized queries, and LATERAL-joined latest snapshots so each
holding ships with current Q50 + unrealized P&L. routes/console.js adds
/portfolio?owner= page using a shared lib/portfolio.js helper. New
views/portfolio.ejs renders the holdings table + add-holding form +
per-row remove form with totals row at bottom. Datalist of all asset
slugs powers the add-form autocomplete.
Tests: 21 total now (7 smoke + 5 valuation + 9 portfolio); all green.
package.json test script switched to glob tests/*.test.js so future test
files auto-pick up. tests/valuation.test.js needed dotenv + PGHOST=/tmp
guards to match smoke.test.js's local-socket pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0877b89 · 2026-05-09 · catalog breadth: 14 more assets + 3 indices (Max It batch)
Expand the seed catalog from 1 → 15 canonical Hermès assets covering Birkin
25/30/35 and Kelly 25/28/32 across Togo/Epsom/Clemence/Swift in Black, Gold,
Etoupe, Vert Verone, with both GHW and PHW and Sellier/Retourne. Each new
asset gets 2-4 transactions across 2024-2026 with realistic gross→net
spreads (Sotheby's hammer-to-net ~18%, FASHIONPHILE/Rebag ~22%) and a v0
valuation snapshot whose breakdown jsonb mirrors the compositional-
confidence formula.
Pricing anchored to the BLUEPRINT.md Sotheby's reference (Birkin 30 Togo
neutral $22,300 Q50): Birkin 25 +30-40%, Birkin 35 -10-20%, Sellier +15-25%
over Retourne, Pristine/store-fresh +25-40%, recent stamps +10-20%.
Three new indices: birkin-25-neutral, kelly-sellier-premium, recent-stamp-
premium, each with 4 historical points 2022-12-31 → 2025-12-31.
Known issue: transactions table has no natural unique key, so re-running
seed.sql duplicates txn rows (87 → 128 on second run). Logged for v0.x —
fix is to add UNIQUE (canonical_asset_id, source_id, transacted_at,
gross_transaction_price) constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cfa428e · 2026-05-09 · server + console + valuation API + smoke tests
Express bootstrap (server.js) follows NPH middleware order: helmet CSP →
compression → morgan → body parsers → static → locals → routes → 404 →
error. Three route surfaces:
GET / internal market-ops console (EJS)
GET /asset/:slug detail view w/ value band + comps + Schema.org Product
GET /indices/:slug benchmark detail
GET /api/health liveness + db_ok
GET /api/sources tiered source registry
GET /api/assets catalog list
GET /api/valuation/:slug blueprint-shaped { asset, latest_snapshot, comps[], confidence_breakdown }
GET /api/index/:slug time-series points
GET /api/portfolio?owner=… stub holdings list
lib/valuation.js is a SQL-only stub — its file-level docstring locks down
that the response shape is the future-API contract; only its body changes
when real ML lands in v0.5.
Console enforces every standing-rule grid control: 8-option sort <select>,
3–8 column density slider, live grid-search input, sun/moon dark/light
toggle (anti-flash inline + CSS-var inversion), hamburger nav, and
Schema.org JSON-LD (WebSite root + Product on detail). All persisted to
localStorage. Footer renders an explicit TODO until BRAND_DOMAIN is set,
so v0.1 (info@<domain> Purelymail provisioning) can't be silently skipped.
7 smoke tests via node --test cover health, sources, valuation contract
shape, index time-series, console controls, and asset-page schema.org.
All green against the seeded database.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5afcf7c · 2026-05-09 · canonical schema + Birkin sample seed + pg pool wrapper
Migration 0001 lays out the 10-table canonical model from BLUEPRINT.md:
sources / brands / model_families / canonical_assets / raw_observations
(bronze) / transactions (silver) / valuation_snapshots (gold) / indices /
index_points / portfolio_holdings, plus schema_migrations bookkeeping.
Seed data is the blueprint's normalized 2022–2025 secondary-market series
for the Birkin 30 Togo Gold GHW (US), 8 source rows across tiers 1–3, the
birkin-30-togo-neutral index with 4 historical points, and a v0-stub
valuation snapshot whose breakdown jsonb mirrors the compositional
confidence formula in the blueprint.
lib/db.js carries the pg-pool gotcha guards (empty PGPASSWORD strip,
omit password key when blank) that NPH learned the hard way; macOS dev
defaults PGHOST=/tmp so unix-socket peer auth Just Works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18d04fe · 2026-05-09 · initial scaffold — package, env, pm2, gitignore, blueprint, roadmap
v0 of lifestyle-asset-intel — enterprise valuation/liquidity/ownership-cost
platform per the pasted blueprint. This commit lays in the project shell:
package.json + lockfile, .env.example with the macOS-socket PG pattern, pm2
ecosystem on port 9820, .gitignore per Steve's standing rule, full
BLUEPRINT.md verbatim, and ROADMAP.md mapping the path from this skeleton
to the v2 multi-vertical (handbags → watches → sneakers → jewelry) target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>