From edf9114731408b0a427098b3ca142d3533ddc31d Mon Sep 17 00:00:00 2001 From: NeilJo-GY <43027886+NeilJo-GY@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:56:05 +0800 Subject: [PATCH 1/4] Add Nemotron Personas USA as second attribute-census seed provider Register nvidia/Nemotron-Personas-USA with offline fixtures and keep the provider registry as the source of truth for seed-capable HF repos. Co-authored-by: Cursor --- skills/persona-seed/SKILL.md | 13 +- skills/persona-seed/providers/README.md | 3 + .../fixtures/sample-corpus.json | 127 +++++++ .../nemotron-personas-usa/mapping.md | 27 ++ .../nemotron-personas-usa/provider.js | 325 ++++++++++++++++++ skills/persona-seed/providers/registry.json | 12 + .../persona-seed/tests/persona-seed.test.js | 38 ++ 7 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 skills/persona-seed/providers/nemotron-personas-usa/fixtures/sample-corpus.json create mode 100644 skills/persona-seed/providers/nemotron-personas-usa/mapping.md create mode 100644 skills/persona-seed/providers/nemotron-personas-usa/provider.js 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..7f6826f 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 `frontend/lib/persona-seed-capable.json` in sync when adding `hfRepos` (datasets UI badge). `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..7cd68a1 --- /dev/null +++ b/skills/persona-seed/providers/nemotron-personas-usa/provider.js @@ -0,0 +1,325 @@ +'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 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 (r && text.includes(norm(r))) { + 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-'); + const usa = norm(row.country) === 'usa' || norm(row.country) === 'us'; + if ((isEn && usa) || (!isEn && text.includes(n))) { + 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/tests/persona-seed.test.js b/skills/persona-seed/tests/persona-seed.test.js index e753aa5..4b23047 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,45 @@ 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 frontend badge list', () => { + assert.equal(resolveEntry('nvidia/Nemotron-Personas-USA').id, 'nemotron-personas-usa'); + const frontendPath = path.join( + __dirname, + '../../../frontend/lib/persona-seed-capable.json' + ); + const frontend = JSON.parse(fs.readFileSync(frontendPath, 'utf8')); + const regRepos = new Set(seedCapableRepos().map((r) => r.repo.toLowerCase())); + for (const row of frontend) { + assert.ok(regRepos.has(String(row.repo).toLowerCase()), `missing registry for ${row.repo}`); + } + for (const row of seedCapableRepos()) { + assert.ok( + frontend.some((f) => String(f.repo).toLowerCase() === row.repo.toLowerCase()), + `frontend badge list 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')); + }); }); + 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`); From 5c67ad0d47c51f82fcbabef806732b4c2f11448f Mon Sep 17 00:00:00 2001 From: NeilJo-GY <43027886+NeilJo-GY@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:16:57 +0800 Subject: [PATCH 2/4] Fix Nemotron region/locale matching for geo fields Match region against city/state/country only (exact for short codes) and treat United States as an English locale country value. Co-authored-by: Cursor --- .../nemotron-personas-usa/provider.js | 39 +++++++++++++++++-- .../persona-seed/tests/persona-seed.test.js | 31 +++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/skills/persona-seed/providers/nemotron-personas-usa/provider.js b/skills/persona-seed/providers/nemotron-personas-usa/provider.js index 7cd68a1..1fa575b 100644 --- a/skills/persona-seed/providers/nemotron-personas-usa/provider.js +++ b/skills/persona-seed/providers/nemotron-personas-usa/provider.js @@ -97,6 +97,40 @@ 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.' + ); +} + +/** 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…): exact state/city only — avoid "or" in "new york" + if (want.length <= 3) { + return state === want || city === want; + } + + if (city && (city === want || city.includes(want) || want.includes(city))) return true; + if (state && (state === want || state.includes(want) || want.includes(state))) return true; + if (country && (country === want || country.includes(want) || want.includes(country))) return true; + if (isUsaCountry(row.country) && (want === 'usa' || want === 'united states')) { + return true; + } + return false; +} + function haystack(row) { return [ row.persona, @@ -151,7 +185,7 @@ function scoreRecord(row, intent) { } } for (const r of asArray(intent.region)) { - if (r && text.includes(norm(r))) { + if (regionMatches(r, row)) { score += 2; hits.push(`region:${r}`); } @@ -159,8 +193,7 @@ function scoreRecord(row, intent) { for (const loc of asArray(intent.locale)) { const n = norm(loc); const isEn = n === 'en' || n === 'english' || n.startsWith('en-'); - const usa = norm(row.country) === 'usa' || norm(row.country) === 'us'; - if ((isEn && usa) || (!isEn && text.includes(n))) { + if (isEn && isUsaCountry(row.country)) { score += 2; hits.push(`locale:${loc}`); } diff --git a/skills/persona-seed/tests/persona-seed.test.js b/skills/persona-seed/tests/persona-seed.test.js index 4b23047..b38cfd6 100644 --- a/skills/persona-seed/tests/persona-seed.test.js +++ b/skills/persona-seed/tests/persona-seed.test.js @@ -247,6 +247,37 @@ describe('persona-seed / nemotron-personas-usa', () => { 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, []); + }); + + 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 }); + } + }); }); From dd201c7bda10da08ea212c293a66bb349a995a1f Mon Sep 17 00:00:00 2001 From: NeilJo-GY <43027886+NeilJo-GY@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:22:15 +0800 Subject: [PATCH 3/4] Keep seed-capable public list inside persona-seed for CI frontend/ is a separate repo, so sync tests must not open its paths. Co-authored-by: Cursor --- skills/persona-seed/providers/README.md | 2 +- .../providers/seed-capable.public.json | 14 ++++++++++++++ skills/persona-seed/tests/persona-seed.test.js | 15 ++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 skills/persona-seed/providers/seed-capable.public.json diff --git a/skills/persona-seed/providers/README.md b/skills/persona-seed/providers/README.md index 7f6826f..0f64235 100644 --- a/skills/persona-seed/providers/README.md +++ b/skills/persona-seed/providers/README.md @@ -9,7 +9,7 @@ Adapters that turn corpus rows into `SeedProfile`. Orchestration: `../SKILL.md`. | `matraix-persona-1m` | attribute-census | ga | `MatrAIx2026/MatrAIx_Persona_1M` | | `nemotron-personas-usa` | attribute-census | ga | `nvidia/Nemotron-Personas-USA` | -Keep `frontend/lib/persona-seed-capable.json` in sync when adding `hfRepos` (datasets UI badge). +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/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 b38cfd6..143da12 100644 --- a/skills/persona-seed/tests/persona-seed.test.js +++ b/skills/persona-seed/tests/persona-seed.test.js @@ -212,21 +212,18 @@ describe('persona-seed / registry', () => { assert.throws(() => resolveEntry('not-a-provider'), /Unknown provider/); }); - it('resolves Nemotron HF repo and stays in sync with frontend badge list', () => { + 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 frontendPath = path.join( - __dirname, - '../../../frontend/lib/persona-seed-capable.json' - ); - const frontend = JSON.parse(fs.readFileSync(frontendPath, 'utf8')); + 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 frontend) { + 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( - frontend.some((f) => String(f.repo).toLowerCase() === row.repo.toLowerCase()), - `frontend badge list missing ${row.repo}` + published.some((f) => String(f.repo).toLowerCase() === row.repo.toLowerCase()), + `seed-capable.public.json missing ${row.repo}` ); } }); From 13b5fd84f9b72234f93e36d4061d158519f8ce01 Mon Sep 17 00:00:00 2001 From: NeilJo-GY <43027886+NeilJo-GY@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:31:37 +0800 Subject: [PATCH 4/4] Harden Nemotron region matching against short geo codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Block substring matches on 2–3 letter state codes and let US/USA region intents hit country fields. Co-authored-by: Cursor --- .../nemotron-personas-usa/provider.js | 23 ++++++++++++++----- .../persona-seed/tests/persona-seed.test.js | 5 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/skills/persona-seed/providers/nemotron-personas-usa/provider.js b/skills/persona-seed/providers/nemotron-personas-usa/provider.js index 1fa575b..8b1fbd5 100644 --- a/skills/persona-seed/providers/nemotron-personas-usa/provider.js +++ b/skills/persona-seed/providers/nemotron-personas-usa/provider.js @@ -109,6 +109,15 @@ function isUsaCountry(country) { ); } +/** 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; @@ -117,15 +126,17 @@ function regionMatches(intentRegion, row) { const state = norm(row.state); const country = norm(row.country); - // Short codes (WA, OR, CA, NY…): exact state/city only — avoid "or" in "new york" + // Short codes (WA, OR, CA, NY, US…): exact city/state/country only if (want.length <= 3) { - return state === want || city === want; + if (state === want || city === want || country === want) return true; + if (want === 'us' || want === 'usa') return isUsaCountry(row.country); + return false; } - if (city && (city === want || city.includes(want) || want.includes(city))) return true; - if (state && (state === want || state.includes(want) || want.includes(state))) return true; - if (country && (country === want || country.includes(want) || want.includes(country))) return true; - if (isUsaCountry(row.country) && (want === 'usa' || want === 'united states')) { + 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; diff --git a/skills/persona-seed/tests/persona-seed.test.js b/skills/persona-seed/tests/persona-seed.test.js index 143da12..2a97eb1 100644 --- a/skills/persona-seed/tests/persona-seed.test.js +++ b/skills/persona-seed/tests/persona-seed.test.js @@ -251,6 +251,11 @@ describe('persona-seed / nemotron-personas-usa', () => { 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', () => {