[object Object]

← back to Lifestyle Asset Intel

yolo tick #22: source-weight-aware comparable_similarity_weight + methodology v0-stub.1

69ece512bde664b872a08cb722973b7d9d68bdaa · 2026-05-13 17:15:43 -0700 · Steve Abrams

- 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.

Files touched

Diff

commit 69ece512bde664b872a08cb722973b7d9d68bdaa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 13 17:15:43 2026 -0700

    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.
---
 METHODOLOGY.md      |  9 +++++----
 lib/audit.js        |  5 +++--
 lib/valuation.js    | 40 ++++++++++++++++++++++++++++++++++++----
 routes/api.js       |  6 ++++--
 tests/audit.test.js |  3 ++-
 tests/smoke.test.js |  3 ++-
 6 files changed, 52 insertions(+), 14 deletions(-)

diff --git a/METHODOLOGY.md b/METHODOLOGY.md
index 003c17d..8257adc 100644
--- a/METHODOLOGY.md
+++ b/METHODOLOGY.md
@@ -158,7 +158,7 @@ clamped to `[0, 1]`.
 | Component                          | Range          | v0 source                                        |
 |------------------------------------|----------------|--------------------------------------------------|
 | `source_quality_weight`            | 0 to +0.30     | Comps' `sources.weight`-weighted average × 0.30  |
-| `comparable_similarity_weight`     | 0 to +0.18     | Constant 0.18 (no embeddings until v0.3)         |
+| `comparable_similarity_weight`     | 0 to +0.18     | Avg comp source weight × 0.18 (full CLIP cosine arrives in v0.3) |
 | `sample_depth_weight`              | 0 to +0.20     | log-saturating in n_comps over 20-comp ceiling    |
 | `recency_weight`                   | 0 to +0.15     | Linear decay over 365 days from newest comp       |
 | `image_match_weight`               | 0 to +0.09     | Constant 0.09 (no image intake until v0.3)        |
@@ -234,6 +234,7 @@ imply coverage.
 
 ## 14. Change log
 
-| Version  | Date       | Change                                                              |
-|----------|------------|---------------------------------------------------------------------|
-| v0-stub  | 2026-05-09 | Initial methodology — confidence, liquidity, dts all live-computed. |
+| Version    | Date       | Change                                                              |
+|------------|------------|---------------------------------------------------------------------|
+| v0-stub    | 2026-05-09 | Initial methodology — confidence, liquidity, dts all live-computed. |
+| v0-stub.1  | 2026-05-13 | `comparable_similarity_weight` is now source-weight-aware (was constant 0.18). Tier-1 comp pools score the full 0.18; tier-4 pools drop to ~0.04. Full CLIP embeddings still scheduled for v0.3. |
diff --git a/lib/audit.js b/lib/audit.js
index d54b381..b4d28f9 100644
--- a/lib/audit.js
+++ b/lib/audit.js
@@ -10,6 +10,7 @@
 const crypto = require('crypto');
 const { randomUUID } = require('crypto');
 const { pool } = require('./db');
+const { METHODOLOGY_VERSION } = require('./valuation');
 
 const SKIP_PATHS = new Set(['/health']);  // relative to /api mount
 
@@ -43,7 +44,7 @@ async function recordCall(row) {
       row.method,
       row.asset_slug || null,
       row.response_status,
-      row.methodology_version || 'v0-stub',
+      row.methodology_version || METHODOLOGY_VERSION,
       row.latency_ms == null ? null : row.latency_ms,
       row.user_agent || null,
       row.request_id || null
@@ -74,7 +75,7 @@ function auditMiddleware(req, res, next) {
         method: req.method,
         asset_slug: extractAssetSlug(req),
         response_status: res.statusCode,
-        methodology_version: 'v0-stub',
+        methodology_version: METHODOLOGY_VERSION,
         latency_ms: latencyMs,
         user_agent: req.get('user-agent') || null,
         request_id: requestId
diff --git a/lib/valuation.js b/lib/valuation.js
index 3673daa..6fe547d 100644
--- a/lib/valuation.js
+++ b/lib/valuation.js
@@ -14,6 +14,13 @@ const { many, one } = require('./db');
 // Tier-tier fallback weights when a source.weight column is unreachable.
 const TIER_FALLBACK = { 1: 1.0, 2: 0.7, 3: 0.4, 4: 0.2 };
 
+// Single source of truth for the methodology stamp emitted by /api/version,
+// the audit log middleware, and any newly written valuation snapshots. Bump
+// this whenever a confidence-breakdown component changes meaning, per
+// METHODOLOGY § 7. v0-stub.1: comparable_similarity_weight became
+// source-weight-aware (was constant 0.18).
+const METHODOLOGY_VERSION = 'v0-stub.1';
+
 async function valueAsset(slug) {
   const asset = await one(
     `SELECT ca.*, mf.name AS family_name, mf.slug AS family_slug,
@@ -210,9 +217,33 @@ function computeBreakdown({ asset, comps, snapshot, sourceWeights }) {
     source_quality_weight = clamp(avg * 0.30, 0, 0.30);
   }
 
-  // 2. comparable_similarity_weight — constant for v0; embeddings come later.
-  // TODO v0.3: replace with cosine-sim against a reference vector.
-  const comparable_similarity_weight = n > 0 ? 0.18 : 0;
+  // 2. comparable_similarity_weight — source-weight-aware proxy for v0.
+  // Until real CLIP embeddings land in v0.3, the best signal we have for
+  // "how similar are these comps to the target" is how trusted the comp
+  // SOURCES are: a tier-1 auction-house lot describing the exact
+  // configuration is a tighter similarity signal than a tier-3 aggregator
+  // listing. We scale the 0.18 cap by the avg source weight already
+  // computed for source_quality_weight (re-derived here for purity),
+  // so a comp pool of all tier-1 lots scores the full 0.18, a mix of
+  // tier-1/2 scores ~0.15, and a tier-4 pool drops to ~0.04. Bumps
+  // methodology_version per METHODOLOGY § 7.
+  let comparable_similarity_weight = 0;
+  if (n > 0) {
+    let acc = 0;
+    for (const c of compArr) {
+      let w;
+      if (c.source_weight != null) {
+        w = parseFloat(c.source_weight);
+      } else if (weights[c.source_tier] != null) {
+        w = weights[c.source_tier];
+      } else {
+        w = TIER_FALLBACK[c.source_tier] != null ? TIER_FALLBACK[c.source_tier] : 0.2;
+      }
+      acc += isFinite(w) ? w : 0.2;
+    }
+    const avg = clamp(acc / n, 0, 1);
+    comparable_similarity_weight = clamp(avg * 0.18, 0, 0.18);
+  }
 
   // 3. sample_depth_weight — log-saturating in comp count, capped at 20 comps.
   const sample_depth_weight = clamp(
@@ -615,5 +646,6 @@ function quantile(sorted, q) {
 
 module.exports = {
   valueAsset, listAssets, getIndex, getFamily, listFamilies,
-  computeBreakdown, computeLiquidity, computeExpectedDts, quantile
+  computeBreakdown, computeLiquidity, computeExpectedDts, quantile,
+  METHODOLOGY_VERSION
 };
diff --git a/routes/api.js b/routes/api.js
index 01ec3ad..c3d867a 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -3,7 +3,9 @@ const fs = require('fs');
 const path = require('path');
 const { execSync } = require('child_process');
 const { pool } = require('../lib/db');
-const { valueAsset, listAssets, getIndex, getFamily, listFamilies } = require('../lib/valuation');
+const {
+  valueAsset, listAssets, getIndex, getFamily, listFamilies, METHODOLOGY_VERSION
+} = require('../lib/valuation');
 const {
   isValidEmail,
   listHoldings,
@@ -96,7 +98,7 @@ router.get('/version', setCache(NO_CACHE), async (req, res, next) => {
     res.json({
       service: 'lifestyle-asset-intel',
       package_version: PKG_VERSION,
-      methodology_version: 'v0-stub',
+      methodology_version: METHODOLOGY_VERSION,
       git: GIT_SHA ? { sha: GIT_SHA, dirty: GIT_DIRTY } : null,
       runtime: {
         node: process.version,
diff --git a/tests/audit.test.js b/tests/audit.test.js
index 7fb4d88..39e30b7 100644
--- a/tests/audit.test.js
+++ b/tests/audit.test.js
@@ -12,6 +12,7 @@ const assert = require('node:assert/strict');
 const app = require('../server');
 const { pool } = require('../lib/db');
 const { hashToken } = require('../lib/audit');
+const { METHODOLOGY_VERSION } = require('../lib/valuation');
 
 let server;
 let baseUrl;
@@ -78,7 +79,7 @@ test('GET /api/valuation/:slug records a row in valuation_calls', async () => {
   assert.equal(r0.method, 'GET');
   assert.equal(r0.asset_slug, 'birkin-30-togo-gold-ghw-us');
   assert.equal(r0.response_status, 200);
-  assert.equal(r0.methodology_version, 'v0-stub');
+  assert.equal(r0.methodology_version, METHODOLOGY_VERSION);
   assert.equal(r0.user_agent, TEST_UA);
   assert.match(r0.caller_token_hash, /^[0-9a-f]{64}$/);
   assert.ok(typeof r0.latency_ms === 'number' && r0.latency_ms >= 0);
diff --git a/tests/smoke.test.js b/tests/smoke.test.js
index 557cf54..c7dafd4 100644
--- a/tests/smoke.test.js
+++ b/tests/smoke.test.js
@@ -15,6 +15,7 @@ process.env.PGDATABASE = process.env.PGDATABASE || 'lifestyle_asset_intel';
 
 const app = require('../server');
 const { pool } = require('../lib/db');
+const { METHODOLOGY_VERSION } = require('../lib/valuation');
 
 let server;
 let baseUrl;
@@ -157,7 +158,7 @@ test('GET /api/version returns build + schema + data introspection', async () =>
   const r = await get('/api/version');
   assert.equal(r.status, 200);
   assert.equal(r.json.service, 'lifestyle-asset-intel');
-  assert.equal(r.json.methodology_version, 'v0-stub');
+  assert.equal(r.json.methodology_version, METHODOLOGY_VERSION);
   assert.ok(r.json.runtime && r.json.runtime.node, 'runtime.node present');
   assert.ok(typeof r.json.runtime.uptime_seconds === 'number');
   assert.ok(Array.isArray(r.json.schema.migrations));

← 3a8e130 yolo tick #21: /family HTML index page (families list with c  ·  back to Lifestyle Asset Intel  ·  yolo tick #23: source citation links on comp rows 0e64230 →