diff --git a/skills/persona-seed/SKILL.md b/skills/persona-seed/SKILL.md index 004d911..9df278c 100644 --- a/skills/persona-seed/SKILL.md +++ b/skills/persona-seed/SKILL.md @@ -62,16 +62,17 @@ Set `SEED_DIR` to this skill directory (path containing `SKILL.md`). Default corpus is the offline **fixture** (5 rows) under `providers/matraix-persona-1m/fixtures/`. `capabilities()` / provenance report `corpusMode: "fixture"` and a fixture dataset id — not the Hugging Face 1M release. -To use a larger decoded corpus: +Larger corpora (per provider): ```bash -# optional download + decode first N rows (needs python3 + pyarrow [+ huggingface-cli]) +# MatrAIx — optional download + decode (python3 + pyarrow [+ huggingface-cli]) bash ${SEED_DIR}/scripts/decode-matraix.sh --download --limit 1000 -export MATRAIX_CORPUS_PATH=./matraix-1m.decoded.json # or .jsonl -node ${SEED_DIR}/scripts/prepare-corpus.js --validate "$MATRAIX_CORPUS_PATH" --limit 20 -``` +export MATRAIX_CORPUS_PATH=./matraix-1m.decoded.json -Decoder: `providers/matraix-persona-1m/scripts/decode_parquet.py` +# Nemotron Personas USA — export HF rows to JSON/JSONL, then: +export NEMOTRON_CORPUS_PATH=./nemotron-usa.jsonl +node ${SEED_DIR}/scripts/search.js --repo nvidia/Nemotron-Personas-USA --intent '{"domain":["software"]}' +``` Empty filter results return `[]` (no silent unfiltered fallback). Re-check intent if search is empty. diff --git a/skills/persona-seed/providers/README.md b/skills/persona-seed/providers/README.md index 90a17dd..0f64235 100644 --- a/skills/persona-seed/providers/README.md +++ b/skills/persona-seed/providers/README.md @@ -7,6 +7,9 @@ Adapters that turn corpus rows into `SeedProfile`. Orchestration: `../SKILL.md`. | Provider id | Family | Status | HF repos | |-------------|--------|--------|----------| | `matraix-persona-1m` | attribute-census | ga | `MatrAIx2026/MatrAIx_Persona_1M` | +| `nemotron-personas-usa` | attribute-census | ga | `nvidia/Nemotron-Personas-USA` | + +Keep `seed-capable.public.json` in sync when adding `hfRepos` (mirror into the frontend repo’s `lib/persona-seed-capable.json` for `/datasets` badges). `openpersona.co/datasets` may list many more corpora. Only registry rows are usable with persona-seed. diff --git a/skills/persona-seed/providers/nemotron-personas-usa/fixtures/sample-corpus.json b/skills/persona-seed/providers/nemotron-personas-usa/fixtures/sample-corpus.json new file mode 100644 index 0000000..398fe54 --- /dev/null +++ b/skills/persona-seed/providers/nemotron-personas-usa/fixtures/sample-corpus.json @@ -0,0 +1,127 @@ +[ + { + "uuid": "nemo-fixture-001", + "persona": "A pragmatic mid-career software engineer who prefers clear specs, short feedback loops, and mentorship over heroics.", + "professional_persona": "Backend engineer focused on reliability and API design.", + "sports_persona": "Weekend trail runner.", + "arts_persona": "Amateur photographer.", + "travel_persona": "Plans trips around national parks.", + "culinary_persona": "Home cook who meal-preps on Sundays.", + "cultural_background": "First-generation American; bilingual household growing up.", + "skills_and_expertise": "Distributed systems, PostgreSQL, incident response.", + "skills_and_expertise_list": "python;sql;kubernetes;incident response", + "hobbies_and_interests": "Trail running, photography, science fiction.", + "hobbies_and_interests_list": "trail running;photography;sci-fi", + "career_goals_and_ambitions": "Grow into a staff engineer role while mentoring juniors.", + "sex": "Female", + "age": 34, + "marital_status": "Married", + "education_level": "Master's", + "bachelors_field": "Computer Science", + "occupation": "Software engineer", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + "country": "USA" + }, + { + "uuid": "nemo-fixture-002", + "persona": "A patient high-school history teacher who asks Socratic questions and keeps language plain.", + "professional_persona": "Public school educator specializing in US history.", + "sports_persona": "Coach of the school debate team more than athletics.", + "arts_persona": "Community theater volunteer.", + "travel_persona": "Museum-first city trips.", + "culinary_persona": "Potluck organizer.", + "cultural_background": "Midwest upbringing; values civic engagement.", + "skills_and_expertise": "Curriculum design, classroom facilitation.", + "skills_and_expertise_list": "curriculum;facilitation;public speaking", + "hobbies_and_interests": "History podcasts, coaching, gardening.", + "hobbies_and_interests_list": "podcasts;coaching;gardening", + "career_goals_and_ambitions": "Become a department lead and write a teaching handbook.", + "sex": "Male", + "age": 41, + "marital_status": "Married", + "education_level": "Bachelor's", + "bachelors_field": "History", + "occupation": "High school teacher", + "city": "Columbus", + "state": "OH", + "zipcode": "43215", + "country": "USA" + }, + { + "uuid": "nemo-fixture-003", + "persona": "A cautious clinical research coordinator who hedges claims and refuses medical authority cosplay.", + "professional_persona": "Clinical research coordinator at an academic medical center.", + "sports_persona": "Yoga twice a week.", + "arts_persona": "Classical piano.", + "travel_persona": "Conference travel only when necessary.", + "culinary_persona": "Vegetarian home cooking.", + "cultural_background": "Immigrant family; strong emphasis on education.", + "skills_and_expertise": "Protocol compliance, patient scheduling, literature review.", + "skills_and_expertise_list": "GCP;literature review;scheduling", + "hobbies_and_interests": "Biostatistics blogs, classical music.", + "hobbies_and_interests_list": "biostats;classical music", + "career_goals_and_ambitions": "Move into clinical operations management.", + "sex": "Female", + "age": 48, + "marital_status": "Single", + "education_level": "PhD", + "bachelors_field": "Biology", + "occupation": "Clinical research coordinator", + "city": "Boston", + "state": "MA", + "zipcode": "02115", + "country": "USA" + }, + { + "uuid": "nemo-fixture-004", + "persona": "An energetic indie e-commerce seller who ships fast experiments and talks in short upbeat paragraphs.", + "professional_persona": "Shopify store operator and brand builder.", + "sports_persona": "Pick-up basketball.", + "arts_persona": "Product photography and Canva design.", + "travel_persona": "Wholesale market trips.", + "culinary_persona": "Street-food explorer.", + "cultural_background": "West Coast creative community.", + "skills_and_expertise": "Social commerce, creative ads, customer support.", + "skills_and_expertise_list": "shopify;canva;meta ads", + "hobbies_and_interests": "Design, street food, community markets.", + "hobbies_and_interests_list": "design;street food;markets", + "career_goals_and_ambitions": "Grow a lifestyle brand to seven figures.", + "sex": "Male", + "age": 23, + "marital_status": "Single", + "education_level": "Some college", + "bachelors_field": "", + "occupation": "E-commerce seller", + "city": "Los Angeles", + "state": "CA", + "zipcode": "90012", + "country": "USA" + }, + { + "uuid": "nemo-fixture-005", + "persona": "A structured product manager who frames tradeoffs clearly and stays oriented to user outcomes.", + "professional_persona": "B2B SaaS product manager.", + "sports_persona": "Indoor climbing.", + "arts_persona": "Design critique hobbyist.", + "travel_persona": "Customer onsite visits.", + "culinary_persona": "Coffee snob.", + "cultural_background": "Suburban Northeast; pragmatic communicator.", + "skills_and_expertise": "Roadmapping, UX research synthesis, SQL for analytics.", + "skills_and_expertise_list": "roadmap;ux research;sql;notion", + "hobbies_and_interests": "Systems thinking, climbing, espresso.", + "hobbies_and_interests_list": "systems thinking;climbing;coffee", + "career_goals_and_ambitions": "Lead a product org focused on developer tools.", + "sex": "Female", + "age": 29, + "marital_status": "Partnered", + "education_level": "Bachelor's", + "bachelors_field": "Economics", + "occupation": "Product manager", + "city": "New York", + "state": "NY", + "zipcode": "10001", + "country": "USA" + } +] diff --git a/skills/persona-seed/providers/nemotron-personas-usa/mapping.md b/skills/persona-seed/providers/nemotron-personas-usa/mapping.md new file mode 100644 index 0000000..a1137a9 --- /dev/null +++ b/skills/persona-seed/providers/nemotron-personas-usa/mapping.md @@ -0,0 +1,27 @@ +# nemotron-personas-usa → SeedProfile + +Source: [`nvidia/Nemotron-Personas-USA`](https://huggingface.co/datasets/nvidia/Nemotron-Personas-USA) +Family: `attribute-census` + +## Field map + +| SeedProfile | Nemotron field(s) | +|-------------|-------------------| +| `identity.summary` | `persona` (fallback: `professional_persona`) | +| `identity.occupation` | `occupation` | +| `identity.education` | `education_level` (+ `bachelors_field` when present) | +| `identity.region` | `city`, `state`, `country` joined | +| `identity.ageBracket` | derived from `age` | +| `identity.locale` | `["en"]` for USA rows | +| `character.traits` | light keywords from persona texts (optional) | +| `character.interests` | `hobbies_and_interests_list` (split on `;` / `,`) | +| `character.motivations` | `career_goals_and_ambitions` (single entry) | +| `capabilities.skills` / `tools` | `skills_and_expertise_list` | +| `capabilities.domains` | occupation / bachelor field tokens | +| `evidence.rawDescriptions` | persona + professional_persona + cultural_background | +| `constraints.sensitiveFlags` | `healthcare_domain` when occupation/persona mentions clinical/medical | + +## Corpus + +- Default: offline fixture (5 rows) +- External: `NEMOTRON_CORPUS_PATH` → JSON array or `.jsonl` of raw Nemotron rows diff --git a/skills/persona-seed/providers/nemotron-personas-usa/provider.js b/skills/persona-seed/providers/nemotron-personas-usa/provider.js new file mode 100644 index 0000000..8b1fbd5 --- /dev/null +++ b/skills/persona-seed/providers/nemotron-personas-usa/provider.js @@ -0,0 +1,369 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const PROVIDER_ID = 'nemotron-personas-usa'; +const DEFAULT_CORPUS = path.join(__dirname, 'fixtures', 'sample-corpus.json'); +const FIXTURE_DATASET = 'persona-seed/providers/nemotron-personas-usa/fixtures/sample-corpus'; +const PUBLIC_DATASET = 'nvidia/Nemotron-Personas-USA'; + +/** @type {{ path: string, mtimeMs: number, data: object[] } | null} */ +let corpusCache = null; + +function corpusPath() { + return process.env.NEMOTRON_CORPUS_PATH + ? path.resolve(process.env.NEMOTRON_CORPUS_PATH) + : DEFAULT_CORPUS; +} + +function isFixtureCorpus() { + return corpusPath() === path.resolve(DEFAULT_CORPUS); +} + +function resetCorpusCache() { + corpusCache = null; +} + +function loadJsonl(filePath) { + const text = fs.readFileSync(filePath, 'utf8'); + const rows = []; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + rows.push(JSON.parse(trimmed)); + } + return rows; +} + +function loadCorpus() { + const resolved = corpusPath(); + const stat = fs.statSync(resolved); + if ( + corpusCache && + corpusCache.path === resolved && + corpusCache.mtimeMs === stat.mtimeMs + ) { + return corpusCache.data; + } + + let data; + if (resolved.endsWith('.jsonl')) { + data = loadJsonl(resolved); + } else { + data = JSON.parse(fs.readFileSync(resolved, 'utf8')); + } + if (!Array.isArray(data)) { + throw new Error(`nemotron-personas-usa: corpus must be a JSON array or .jsonl: ${resolved}`); + } + corpusCache = { path: resolved, mtimeMs: stat.mtimeMs, data }; + return data; +} + +function asArray(v) { + if (v == null || v === '') return []; + if (Array.isArray(v)) return v; + return [v]; +} + +function splitList(v) { + if (v == null || v === '') return []; + if (Array.isArray(v)) return v.map(String).map((s) => s.trim()).filter(Boolean); + return String(v) + .split(/[;,|]/) + .map((s) => s.trim()) + .filter(Boolean); +} + +function norm(s) { + return String(s || '') + .toLowerCase() + .trim(); +} + +function ageBracket(age) { + const n = Number(age); + if (!Number.isFinite(n)) return null; + if (n < 18) return 'under-18'; + if (n <= 24) return '18-24'; + if (n <= 34) return '25-34'; + if (n <= 44) return '35-44'; + if (n <= 54) return '45-54'; + if (n <= 64) return '55-64'; + return '65+'; +} + +function regionOf(row) { + return [row.city, row.state, row.country].filter(Boolean).join(', ') || null; +} + +function isUsaCountry(country) { + const n = norm(country); + return ( + n === 'usa' || + n === 'us' || + n === 'united states' || + n === 'united states of america' || + n === 'u.s.' || + n === 'u.s.a.' + ); +} + +/** Geo field match: exact, or substring only when both sides are long enough. */ +function geoFieldMatches(want, field) { + if (!field) return false; + if (field === want) return true; + // Avoid short state codes (WA/OR/CA) matching inside unrelated words (aware/history) + if (want.length <= 3 || field.length <= 3) return false; + return field.includes(want) || want.includes(field); +} + +/** Match region intent against city/state/country only (not full haystack). */ +function regionMatches(intentRegion, row) { + if (!intentRegion) return false; + const want = norm(intentRegion); + const city = norm(row.city); + const state = norm(row.state); + const country = norm(row.country); + + // Short codes (WA, OR, CA, NY, US…): exact city/state/country only + if (want.length <= 3) { + if (state === want || city === want || country === want) return true; + if (want === 'us' || want === 'usa') return isUsaCountry(row.country); + return false; + } + + if (geoFieldMatches(want, city) || geoFieldMatches(want, state) || geoFieldMatches(want, country)) { + return true; + } + if (isUsaCountry(row.country) && (want === 'united states' || want === 'united states of america')) { + return true; + } + return false; +} + +function haystack(row) { + return [ + row.persona, + row.professional_persona, + row.sports_persona, + row.arts_persona, + row.travel_persona, + row.culinary_persona, + row.cultural_background, + row.skills_and_expertise, + row.skills_and_expertise_list, + row.hobbies_and_interests, + row.hobbies_and_interests_list, + row.career_goals_and_ambitions, + row.occupation, + row.education_level, + row.bachelors_field, + row.city, + row.state, + row.country, + String(row.age ?? ''), + ] + .map(norm) + .join(' '); +} + +function hasPositiveFilters(intent) { + return ['traits', 'domain', 'region', 'locale', 'ageBracket'].some( + (k) => asArray(intent[k]).length > 0 + ); +} + +function isExcluded(text, intent) { + return asArray(intent.exclude).some((ex) => ex && text.includes(norm(ex))); +} + +function scoreRecord(row, intent) { + const text = haystack(row); + let score = 0; + const hits = []; + + for (const t of asArray(intent.traits)) { + if (t && text.includes(norm(t))) { + score += 3; + hits.push(`trait:${t}`); + } + } + for (const d of asArray(intent.domain)) { + if (d && text.includes(norm(d))) { + score += 3; + hits.push(`domain:${d}`); + } + } + for (const r of asArray(intent.region)) { + if (regionMatches(r, row)) { + score += 2; + hits.push(`region:${r}`); + } + } + for (const loc of asArray(intent.locale)) { + const n = norm(loc); + const isEn = n === 'en' || n === 'english' || n.startsWith('en-'); + if (isEn && isUsaCountry(row.country)) { + score += 2; + hits.push(`locale:${loc}`); + } + } + for (const age of asArray(intent.ageBracket)) { + if (age && norm(ageBracket(row.age)) === norm(age)) { + score += 1; + hits.push(`age:${age}`); + } + } + if (intent.query) { + for (const tok of norm(intent.query) + .split(/[^a-z0-9\u4e00-\u9fff]+/) + .filter((x) => x.length > 2)) { + if (text.includes(tok)) { + score += 1; + hits.push(`query:${tok}`); + } + } + } + return { score, hits, text }; +} + +function toCandidate(row, score, hits) { + return { + id: row.uuid || row.id, + provider: PROVIDER_ID, + summary: row.persona || row.professional_persona || `${row.occupation || 'persona'} · ${regionOf(row) || 'USA'}`, + highlights: { + region: regionOf(row), + traits: splitList(row.hobbies_and_interests_list).slice(0, 5), + domain: asArray(row.occupation), + groundingType: 'synthetic', + corpusMode: isFixtureCorpus() ? 'fixture' : 'external', + hits, + }, + score, + }; +} + +function capabilities() { + const fixture = isFixtureCorpus(); + return { + id: PROVIDER_ID, + name: 'Nemotron Personas USA', + dataset: fixture ? FIXTURE_DATASET : PUBLIC_DATASET, + corpusMode: fixture ? 'fixture' : 'external', + licenseNotes: fixture + ? 'Offline fixture (5 sample rows). Set NEMOTRON_CORPUS_PATH to a decoded JSON/JSONL export of nvidia/Nemotron-Personas-USA.' + : 'External corpus derived from nvidia/Nemotron-Personas-USA. Respect NVIDIA dataset license/terms. Archetype seed only.', + offline: fixture, + corpusPath: corpusPath(), + }; +} + +function search(intent = {}) { + const limit = Math.min(Math.max(intent.limit || 5, 1), 20); + const corpus = loadCorpus(); + const requirePositive = hasPositiveFilters(intent); + const ranked = []; + + for (const row of corpus) { + const { score, hits, text } = scoreRecord(row, intent); + if (isExcluded(text, intent)) continue; + if (requirePositive && score <= 0) continue; + ranked.push(toCandidate(row, score, hits)); + } + + ranked.sort((x, y) => y.score - x.score); + return ranked.slice(0, limit); +} + +function fetch(id) { + const row = loadCorpus().find((r) => r.uuid === id || r.id === id); + if (!row) throw new Error(`nemotron-personas-usa: record not found: ${id}`); + return row; +} + +function toSeed(raw) { + const skills = splitList(raw.skills_and_expertise_list); + const interests = splitList(raw.hobbies_and_interests_list); + const sensitiveFlags = []; + const blob = `${raw.persona || ''} ${raw.occupation || ''} ${raw.professional_persona || ''}`; + if (/clinic|medical|health|physician|nurse|patient/i.test(blob)) { + sensitiveFlags.push('healthcare_domain'); + } + + const education = [raw.education_level, raw.bachelors_field].filter(Boolean).join(' · ') || null; + const fixture = isFixtureCorpus(); + const summary = + raw.persona || + raw.professional_persona || + 'Seed profile from Nemotron Personas USA.'; + + return { + schemaVersion: '0.1.0', + provenance: { + provider: PROVIDER_ID, + recordId: String(raw.uuid || raw.id), + dataset: fixture ? FIXTURE_DATASET : PUBLIC_DATASET, + corpusMode: fixture ? 'fixture' : 'external', + licenseNotes: fixture + ? 'Offline fixture row — not a Hugging Face uuid.' + : 'Row from nvidia/Nemotron-Personas-USA export. Archetype only — not a real-person twin.', + groundingType: 'synthetic', + retrievedAt: new Date().toISOString(), + }, + identity: { + suggestedName: null, + suggestedSlug: null, + summary, + locale: ['en'], + region: regionOf(raw), + ageBracket: ageBracket(raw.age), + occupation: raw.occupation || null, + education, + }, + character: { + traits: [], + values: [], + motivations: raw.career_goals_and_ambitions ? [raw.career_goals_and_ambitions] : [], + speakingHints: [], + interests, + riskTolerance: null, + formalityBaseline: 0, + }, + capabilities: { + domains: asArray(raw.occupation), + skills, + tools: skills, + }, + constraints: { + hardExclusions: [], + sensitiveFlags, + suggestedImmutableTraits: ['honest', 'helpful'], + }, + evidence: { + attributeHighlights: [ + raw.occupation && { field: 'occupation', value: String(raw.occupation) }, + raw.age != null && { field: 'age', value: String(raw.age) }, + regionOf(raw) && { field: 'region', value: regionOf(raw) }, + education && { field: 'education', value: education }, + ].filter(Boolean), + rawDescriptions: [ + raw.persona, + raw.professional_persona, + raw.cultural_background, + ].filter(Boolean), + }, + gaps: ['personaName', 'slug', 'role', 'boundaries'], + }; +} + +module.exports = { + PROVIDER_ID, + capabilities, + search, + fetch, + toSeed, + resetCorpusCache, + isFixtureCorpus, +}; diff --git a/skills/persona-seed/providers/registry.json b/skills/persona-seed/providers/registry.json index e8a9258..efc4dd3 100644 --- a/skills/persona-seed/providers/registry.json +++ b/skills/persona-seed/providers/registry.json @@ -19,6 +19,18 @@ ], "status": "ga", "notes": "Fixture offline by default; set MATRAIX_CORPUS_PATH for decoded JSON/JSONL." + }, + { + "id": "nemotron-personas-usa", + "family": "attribute-census", + "name": "Nemotron Personas USA", + "module": "./nemotron-personas-usa/provider.js", + "hfRepos": ["nvidia/Nemotron-Personas-USA"], + "directoryUrls": [ + "https://openpersona.co/datasets/nvidia/nemotron-personas-usa" + ], + "status": "ga", + "notes": "Fixture offline by default; set NEMOTRON_CORPUS_PATH for JSON/JSONL export of HF rows." } ] } diff --git a/skills/persona-seed/providers/seed-capable.public.json b/skills/persona-seed/providers/seed-capable.public.json new file mode 100644 index 0000000..5904df1 --- /dev/null +++ b/skills/persona-seed/providers/seed-capable.public.json @@ -0,0 +1,14 @@ +[ + { + "repo": "MatrAIx2026/MatrAIx_Persona_1M", + "providerId": "matraix-persona-1m", + "family": "attribute-census", + "status": "ga" + }, + { + "repo": "nvidia/Nemotron-Personas-USA", + "providerId": "nemotron-personas-usa", + "family": "attribute-census", + "status": "ga" + } +] diff --git a/skills/persona-seed/tests/persona-seed.test.js b/skills/persona-seed/tests/persona-seed.test.js index e753aa5..2a97eb1 100644 --- a/skills/persona-seed/tests/persona-seed.test.js +++ b/skills/persona-seed/tests/persona-seed.test.js @@ -7,6 +7,7 @@ const os = require('os'); const path = require('path'); const matraix = require('../providers/matraix-persona-1m/provider'); +const nemotron = require('../providers/nemotron-personas-usa/provider'); const { mapSeedToPersona, slugify } = require('../scripts/map-seed-to-persona'); const { writeProvenance } = require('../scripts/write-provenance'); const { run: runPipeline } = require('../scripts/run-pipeline'); @@ -210,8 +211,78 @@ describe('persona-seed / registry', () => { it('rejects unknown provider ids', () => { assert.throws(() => resolveEntry('not-a-provider'), /Unknown provider/); }); + + it('resolves Nemotron HF repo and stays in sync with seed-capable.public.json', () => { + assert.equal(resolveEntry('nvidia/Nemotron-Personas-USA').id, 'nemotron-personas-usa'); + const publicPath = path.join(__dirname, '..', 'providers', 'seed-capable.public.json'); + const published = JSON.parse(fs.readFileSync(publicPath, 'utf8')); + const regRepos = new Set(seedCapableRepos().map((r) => r.repo.toLowerCase())); + for (const row of published) { + assert.ok(regRepos.has(String(row.repo).toLowerCase()), `missing registry for ${row.repo}`); + } + for (const row of seedCapableRepos()) { + assert.ok( + published.some((f) => String(f.repo).toLowerCase() === row.repo.toLowerCase()), + `seed-capable.public.json missing ${row.repo}` + ); + } + }); }); +describe('persona-seed / nemotron-personas-usa', () => { + it('searches fixture by domain and maps to SeedProfile', () => { + const hits = nemotron.search({ domain: ['software'], limit: 5 }); + assert.ok(hits.some((h) => h.id === 'nemo-fixture-001')); + const seed = nemotron.toSeed(nemotron.fetch('nemo-fixture-001')); + assert.equal(seed.provenance.provider, 'nemotron-personas-usa'); + assert.equal(seed.provenance.corpusMode, 'fixture'); + assert.ok(seed.identity.summary.length > 0); + assert.ok(seed.gaps.includes('personaName')); + }); + + it('flags healthcare-sensitive occupations', () => { + const seed = nemotron.toSeed(nemotron.fetch('nemo-fixture-003')); + assert.ok(seed.constraints.sensitiveFlags.includes('healthcare_domain')); + }); + + it('region filter uses geo fields only (no haystack false positives)', () => { + // "WA" must not match via substring inside "software" + const wa = nemotron.search({ region: ['WA'], limit: 10 }); + assert.ok(wa.every((h) => h.id === 'nemo-fixture-001')); + const orHits = nemotron.search({ region: ['OR'], limit: 10 }); + assert.deepEqual(orHits, []); + // Long intent must not match via short state code substring (aware ⊃ wa) + assert.deepEqual(nemotron.search({ region: ['aware'], limit: 10 }), []); + // Country short codes must still match USA rows + const usa = nemotron.search({ region: ['USA'], limit: 10 }); + assert.ok(usa.length >= 1); + }); + + it('locale en matches United States country values', () => { + const prev = process.env.NEMOTRON_CORPUS_PATH; + const tmp = path.join(os.tmpdir(), `nemo-usa-${Date.now()}.json`); + const row = { + ...nemotron.fetch('nemo-fixture-001'), + uuid: 'nemo-united-states', + country: 'United States', + }; + fs.writeFileSync(tmp, `${JSON.stringify([row])}\n`); + process.env.NEMOTRON_CORPUS_PATH = tmp; + nemotron.resetCorpusCache(); + try { + const hits = nemotron.search({ locale: ['en'], limit: 5 }); + assert.equal(hits.length, 1); + assert.equal(hits[0].id, 'nemo-united-states'); + } finally { + if (prev === undefined) delete process.env.NEMOTRON_CORPUS_PATH; + else process.env.NEMOTRON_CORPUS_PATH = prev; + nemotron.resetCorpusCache(); + fs.rmSync(tmp, { force: true }); + } + }); +}); + + describe('persona-seed / jsonl corpus', () => { it('search works when MATRAIX_CORPUS_PATH is jsonl', () => { const jsonlPath = path.join(os.tmpdir(), `persona-seed-corpus-${Date.now()}.jsonl`);