Matchmaking v0 — Feature Spec
Matchmaking v0 — Feature Spec
This is a standalone feature spec for Meshi’s matchmaking system. It is separate from the foundational V0.1 Spec, which governs identity, evidence, traits, and context. Matchmaking is downstream of that substrate (Doctrine G).
Revision history:
- v0.0 (2026-03-22): Initial heuristic scoring model with archetype weight vectors
- v0.1 (2026-03-22): Goal-centric architecture — goals promoted to own table, derived needs/offers replace general offer/need dimensions, three universal scoring dimensions replace archetype weights
1. Scope and Non-Goals
In scope (v0)
- Goal-centric matching: scoring is driven by goal-derived needs and offers, not general traits
- Goals as first-class entities: dedicated
goaltable with derived needs/offers, per-goal embeddings, and goal-specific lifecycle - Three scoring dimensions: goal-complementarity, value-alignment, general-similarity
- Precomputed embeddings: per-goal needs/offers embeddings plus entity-level aggregates — zero embedding calls at match time
- ANN prefiltering: targeted candidate selection using goal needs/offers embeddings
- Global matching only: all matchable entities across the platform, no event-scoping
- Candidate tiers 1 and 2: Active Users and High-Quality Unclaimed entities
- Paginated API: cursor-based pagination for match results
- Full transparency: all scores and candidate metadata exposed
Non-goals (v0)
- Event-scoped matching (Tier 3 inclusion)
- Personality-similarity or social-similarity (circles) scoring dimensions
- Match-unit projection tables or dedicated per-type HNSW indexes
last_affirmed_atfreshness weighting- Bookmarks, introductions, shortlists, or any post-match actions
- Goal-to-goal matching (A’s goal-specific needs vs B’s goal-specific offers per-goal, not aggregate)
- Cross-score normalization across different goal types
- MCP tool wrapper or AgentKit integration
- Goal priority ordering or completion workflows
- Diversity/coverage scoring for multi-goal aggregation
2. Goal Architecture
Goals are promoted from the trait system to a first-class entity. A goal represents what a person is trying to achieve, and it generates two derived fields — needs and offers — that are the primary matching signal.
Why goals are not traits
Traits are semi-permanent descriptions (skills, values, personality). Goals are temporal, intent-bearing, and the primary matching signal. Goals have unique lifecycle semantics that don’t fit the trait state machine:
- Goals can be completed (traits cannot)
- Goals generate derived needs and offers (traits do not derive sub-fields)
- Goals have per-goal embeddings (traits are entity-level)
- Goals are the primary matching entity and deserve first-class schema treatment
Goal-derived needs and offers
Every goal implies what the person needs to achieve it and what they can offer because they’re pursuing it:
-
Goal: “Raise Series A for my climate tech startup”
- Needs: “investor introductions”, “term sheet expertise”, “financial modeling”
- Offers: “deep climate domain expertise”, “technical co-founder perspective”
-
Goal: “Join an early-stage climate startup as CTO”
- Needs: “founder with climate mission”, “equity stake”, “technical leadership role”
- Offers: “15 years enterprise engineering”, “team scaling experience”, “AI/ML architecture”
These derived fields are extracted by an LLM (§10) and stored as structured columns on the goal row. Their embeddings — not the raw goal text — are the primary input to goal-complementarity scoring.
Goal-derived needs/offers replace general offer and need dimensions
The offer and need trait dimensions are removed from the system. Goal-specific needs/offers
supersede their functionality:
- More precise: scoped to active intent, not lifetime inventory
- More matchable: “need investor introductions” (from a fundraising goal) is actionable; “offers video editing” (from a 2008 LinkedIn role) is noise
- Bypasses trust-profile visibility: the foundational spec flagged
needclaims as invisible under scoring trust profiles (exclusively LLM-inferred). Goal-derived needs live on the goal row itself, not as trait claims, so trust profile filtering doesn’t apply
Trait dimensions that survive (for search and profile use only): skill, experience, values,
circles, personality, location. The goal, offer, and need dimensions are deleted from
trait_dimension.
Goal lifecycle
proposed → confirmed (user confirms an inferred/imported goal)proposed → archived (user dismisses)proposed → superseded (re-inference replaces with new version)confirmed → completed (user marks goal as achieved)confirmed → archived (user abandons goal)confirmed → superseded (user edits, creating new goal version)Terminal states: completed, archived, superseded. These goals are excluded from matching.
Active states for matching:
- confirmed: iterable for the principal’s own goal selection
- proposed: contributes to entity aggregate embeddings (inferred goals add signal)
Storage
See §13 for the complete goal and goal_embedding table DDL.
3. Scoring Model
Matching evaluates how well two entities fit each other. The model uses three universal scoring dimensions with fixed weights.
Dimensions
| Dimension | What it measures | Signal source | Symmetric? |
|---|---|---|---|
| Goal-Complementarity | Can B fulfill A’s needs? Can A fulfill B’s needs? | Goal-derived needs/offers embeddings | No (directional) |
| Value-Alignment | Do A and B share values and operating philosophy? | Values-dimension trait embeddings | Yes |
| General-Similarity | Are A and B holistically similar people? | Brief embeddings | Yes |
Final score
finalScore = w_gc * goalComplementarity + w_va * valueAlignment + w_gs * generalSimilarityFixed weights for v0:
| Weight | Value | Rationale |
|---|---|---|
w_gc | 0.55 | Goal complementarity is the primary matching signal |
w_va | 0.25 | Shared values indicate sustainable relationships |
w_gs | 0.20 | Holistic similarity captures signals beyond goals and values |
Weights sum to 1.0. Future versions may make weights tunable per query.
Key constraints
- No scoring-time LLM calls. All embeddings are precomputed. The match pipeline performs only lookups and arithmetic.
- No scoring-time embedding calls. Per-goal embeddings plus the
agg_values/briefentity-level sub-score embeddings are precomputed by background workers. Match execution is pure computation over stored vectors. - Canonical briefs are never scoring inputs (Doctrine: briefs are derived, never authoritative). Brief embeddings are used only for general-similarity.
4. Goal-Complementarity
The primary scoring dimension. Measures bilateral need/offer fit between two entities’ goals.
Elemental scores
Goal-complementarity is composed of two elemental scores that are tracked separately:
| Score | Computation | What it measures |
|---|---|---|
needsToOffers | sim(A.goal.needs, B.goal.offers) | Can B offer what A needs? |
offersToNeeds | sim(A.goal.offers, B.goal.needs) | Does B need what A offers? |
Both use cosine similarity on precomputed embedding vectors. The combined goal-complementarity for a single direction is:
directionScore = 0.5 * needsToOffers + 0.5 * offersToNeedsIteration model
Goal-complementarity is fully bilateral. For each candidate, scoring evaluates the best single goal-pair on each directional pass:
for each principalGoal in A.confirmedReadyGoals: for each candidateGoal in B.matchableGoals: needsScore = cosineSim(principalGoal.needs_embedding, candidateGoal.offers_embedding) offersScore = cosineSim(principalGoal.offers_embedding, candidateGoal.needs_embedding) score = 0.5 * needsScore + 0.5 * offersScore track max(score) for A→B
for each candidateGoal in B.matchableGoals: for each principalGoal in A.confirmedReadyGoals: needsScore = cosineSim(candidateGoal.needs_embedding, principalGoal.offers_embedding) offersScore = cosineSim(candidateGoal.offers_embedding, principalGoal.needs_embedding) score = 0.5 * needsScore + 0.5 * offersScore track max(score) for B→APrincipal goal scope:
- If
goal_idis provided, use only that confirmed goal. - Otherwise use all confirmed goals that have both
needsandoffersembeddings ready.
Candidate goal scope:
- Confirmed goals
- Proposed goals with confidence
>= MIN_PROPOSED_GOAL_CONFIDENCE
Combined goal-complementarity
goalComplementarity = intentMix.aToB * A→B_score + intentMix.bToA * B→A_scoreWhere intentMix is determined by intent strength (§9).
Missing-vector behavior
cosineSimilarity() returns 0 for empty vectors. This means candidates or goals missing one side of
their per-goal embeddings degrade safely to a zero contribution rather than throwing.
There is no aggregate-goal or values-only fallback. Returned matches must have positive goal-complementarity.
5. Value-Alignment
Symmetric scoring dimension measuring shared values and operating philosophy.
valueAlignment = cosineSim(A.agg_values_embedding, B.agg_values_embedding)Where agg_values is the embedded concatenation of an entity’s values-dimension trait claims. See
§11 for computation.
If either entity has no agg_values embedding (no values-dimension claims), value-alignment is 0.
Trust profiles for values claims
Values claims are still trait claims in trait_claim. The aggregate embedding worker loads claims
using review_surface to maximize signal. This means both confirmed and proposed values claims
contribute to the embedding, regardless of tier.
6. General-Similarity
Symmetric scoring dimension measuring holistic person similarity.
generalSimilarity = cosineSim(A.brief_embedding, B.brief_embedding)Both embeddings are precomputed entity-level brief embeddings from entity_embedding (type
"brief"). This is the same embedding used for the existing search API.
If either entity has no brief embedding, general-similarity is 0.
7. Candidate Tiers
Unchanged from the original spec. Entities are classified into tiers based on data quality and account status.
Tier definitions
| Tier | Name | Criteria | Match pool (v0) |
|---|---|---|---|
| 1 | Active User | Has a row in auth_user_person_link | Yes |
| 2 | High-Quality Unclaimed | Has a person_external_identity with identity_type IN ('linkedin_id', 'linkedin_url') and status NOT IN ('revoked', 'superseded', 'disputed') | Yes |
| 3 | Low-Quality Import | Neither of the above | No (v0) |
Materialization
Tier is materialized as entity.match_tier SMALLINT:
1= Active User2= High-Quality UnclaimedNULL= not in match pool
Maintained automatically by database triggers on auth_user_person_link and
person_external_identity. Trigger precedence: Tier 1 wins over Tier 2.
8. ANN Prefiltering
The first phase of matching uses Approximate Nearest Neighbor search to identify a targeted candidate pool using goal needs/offers embeddings.
Pipeline
For each ready principal goal:
- ANN query
goal_embedding WHERE embedding_type = 'offers'using the principal goal’sneedsembedding - ANN query
goal_embedding WHERE embedding_type = 'needs'using the principal goal’soffersembedding - Restrict hits to candidate goals that are matchable under the same policy used by scoring:
confirmed goals plus proposed goals with confidence
>= MIN_PROPOSED_GOAL_CONFIDENCE - Union + dedupe the resulting candidate entity IDs, keeping the closest distance
- Tier filter to
entity.match_tier IN (1, 2)
ANN is per-goal only. There is no aggregate-goal ANN supplement and no values-based ANN fallback.
Self-exclusion
The principal’s entity_id (and all linked entities via auth_user_person_link) is passed as
excludeEntityIds to both ANN queries. See §14 invariant 4.
Why goal embeddings for ANN
Brief-based ANN asks “who is generally similar to this goal text?” — a weak signal. Goal needs/offers ANN asks “who can offer what I need?” and “who needs what I offer?” — directly matching on complementarity. This produces a more targeted candidate pool, which matters at scale (10,000+ entity pools).
Entities without matchable goal embeddings
Entities without matchable per-goal embeddings are invisible to matchmaking ANN. This is correct: matchmaking requires an active goal-level need/offer signal. They remain discoverable via search and other product surfaces.
9. Intent Strength
Intent strength models confidence in an entity’s expressed goals. It determines how much each direction contributes to the combined goal-complementarity score.
Classification
type IntentStrength = "confirmed" | "inferred" | "none";| Strength | Condition | Meaning |
|---|---|---|
confirmed | Entity has ≥1 goal with status: "confirmed" | Entity actively stated their goals |
inferred | Entity has goals but none confirmed (all proposed) | Goals are LLM-inferred or imported |
none | Entity has no active goals | No goal signal |
Direction mixing weights
| Intent Strength | A→B weight | B→A weight |
|---|---|---|
confirmed | 0.6 | 0.4 |
inferred | 0.8 | 0.2 |
none | 1.0 | 0.0 |
Rationale: When B has confirmed goals, reciprocity matters — mutual fit is rewarded (40% weight to B→A). When goals are inferred, we discount B→A to 20%. When B has no goals, only A→B contributes.
10. Goal Needs/Offers Extraction Pipeline
An async Inngest function extracts needs and offers from goal text using an LLM.
Trigger events
- Goal created (proposed from inference or import)
- Goal confirmed by user
- Goal superseded (new version created via edit)
Extraction flow
- Load the goal by ID from the event payload
- Skip if goal status is terminal (
completed,archived,superseded) - Send the goal’s
valuetext to an LLM with an extraction prompt - Parse the response as
{ needs: string[], offers: string[] } - Generate
needs_text(join needs with\n) andoffers_text(join offers with\n) - Write
needs,offers,needs_text,offers_textto the goal row - Embed
needs_textandoffers_textseparately - Upsert per-goal embeddings into
goal_embedding(types:needs,offers) - Trigger
agg_valuesrecomputation for the entity when trait-side signals change (§11)
Extraction prompt contract
The LLM receives the goal text and must return:
needs: 3–8 concise phrases describing what the person requires to achieve this goaloffers: 3–8 concise phrases describing what the person can provide because of this goal
The extraction uses raw value text (not normalized) to preserve semantic richness.
Idempotency
Per-goal embeddings use the same (goal_id, embedding_type, input_hash) idempotency mechanism as
entity embeddings. If needs/offers text hasn’t changed, the embedding upsert is a no-op.
Debouncing
Debounce key: event.data.goalId, period: 15s. Multiple rapid events for the same goal collapse
into a single extraction run.
11. Aggregate Embedding Pipeline
An async Inngest function computes agg_values only.
Trigger
Fires after trait inference and trait review events (TRAITS_INFERRED, TRAIT_CONFIRMED,
TRAIT_REJECTED, TRAIT_EDITED, TRAIT_ASSERTED).
Aggregate computed
| Embedding type | Computation | Used for |
|---|---|---|
agg_values | Embed concatenated value from all values-dimension trait claims | Value-alignment scoring |
Goal aggregates (agg_goal_needs, agg_goal_offers) were removed. Matchmaking uses only per-goal
embeddings for ANN and complementarity scoring.
Debouncing
Debounce key: event.data.entityId, period: 15s. Multiple goal changes for the same entity collapse
into a single aggregate recomputation.
Trust profile for values aggregation
Values claims are loaded using review_surface (all proposed + confirmed, any authority). This
gives unclaimed entities the widest safe signal set for value-alignment matching.
12. API Contract
All endpoints require authentication and are mounted under /api/v0/me/match/.
Goal lifecycle API (entry point to matchmaking)
Matchmaking operates over the principal’s goals. The goal lifecycle (create, confirm, archive,
edit/supersede) is handled by the goals API under /api/v0/me/goals (implemented in
packages/api/src/routes/goals.ts). Clients generally:
- Create or edit goals via
/api/v0/me/goals - Confirm goals (making them eligible for matching) via
/api/v0/me/goals/:id/confirm - Then run matchmaking via
/api/v0/me/match/run
GET /api/v0/me/match/goals
Returns the principal’s confirmed goals with their derived needs and offers.
Response 200 OK:
{ "goals": [ { "id": "uuid", "value": "Raise Series A for my climate tech startup", "status": "confirmed", "needs": ["investor introductions", "term sheet expertise", "financial modeling"], "offers": ["deep climate domain expertise", "technical co-founder perspective"], "authority": "user_confirmed", "createdAt": "2026-03-10T12:00:00Z" } ]}If needs/offers extraction hasn’t completed yet (async pipeline), needs and offers are null.
GET /api/v0/me/match/run
Executes a match against a selected goal (or all confirmed ready goals) and returns paginated ranked results.
Query parameters:
| Param | Required | Default | Description |
|---|---|---|---|
goal_id | No | — | UUID of a specific confirmed goal to match against. If omitted, uses all confirmed ready goals. |
limit | No | 20 | Results per page (1–50) |
cursor | No | — | Opaque base64 cursor from previous response |
Response 200 OK:
{ "goalId": "uuid-or-null", "totalCandidates": 47, "results": [ { "entityId": "uuid", "tier": 1, "finalScore": 0.812, "scores": { "goalComplementarity": 0.88, "goalNeedsToOffers": 0.92, "goalOffersToNeeds": 0.84, "valueAlignment": 0.75, "generalSimilarity": 0.68 }, "bestGoalForPrincipal": { "goalId": "uuid", "goalText": "Raise Series A for my climate tech startup", "matchedGoalId": "uuid", "matchedGoalText": "Invest in early-stage climate tech companies", "needsToOffers": 0.92, "offersToNeeds": 0.84 }, "bestGoalForCandidate": { "goalId": "uuid", "goalText": "Join an early-stage climate startup as CTO", "matchedGoalId": "uuid", "matchedGoalText": "Raise Series A for my climate tech startup", "needsToOffers": 0.85, "offersToNeeds": 0.79 }, "intentStrength": "confirmed", "person": { "displayName": "Jane Smith", "currentRole": "CTO", "currentCompany": "Acme Climate Inc" }, "brief": { "content": "Jane is a seasoned CTO with 15 years in enterprise software...", "synthesisMethod": "llm_synthesis", "createdAt": "2026-03-15T10:00:00Z" } } ], "nextCursor": "eyJvZmZzZXQiOjIwfQ==", "matchedAt": "2026-03-22T14:00:00Z", "readiness": {}, "usedGoalIds": ["uuid"], "unreadyGoalIds": ["uuid"]}Field semantics:
scores.goalComplementarity: combined goal-complementarity (intent-weighted blend of A→B and B→A directions)scores.goalNeedsToOffers: elemental — how well B’s offerings match A’s needs (A→B direction, or combined if no specific goal selected)scores.goalOffersToNeeds: elemental — how well B’s needs match A’s offeringsscores.valueAlignment: symmetric values similarityscores.generalSimilarity: symmetric brief similaritybestGoalForPrincipal: which confirmed principal goal paired best with which candidate goal. Candidate goals may be confirmed or high-confidence proposed.nullonly when the A→B direction has no positive winning pair.bestGoalForCandidate: which candidate goal paired best with which principal goal.nullonly when the B→A direction has no positive winning pair.intentStrength: B’s goal intent strength (determines direction mixing)readiness: a readiness summary for the principal’s profile at match time (used by the client to explain blocking/missing inputs and overall matchability).usedGoalIds: the confirmed goal IDs that were actually used for matching in this run (selected goal ID, or the subset of confirmed goals that were embedding-ready).unreadyGoalIds: confirmed goals that were in-scope for matching but were excluded because their needs/offers embeddings were not yet complete.
Pagination: The cursor encodes { offset: number, issuedAt: string } as base64 JSON.
Match execution is not re-run on every page request. Instead, the server caches the full
scored result set in-process for 60 seconds (keyed by principal entityId, optional goalId,
and excludeEntityIds) and serves subsequent pages by re-slicing the cached array. When the cache
entry expires (or is evicted), a new request re-runs the full pipeline and produces a new
matchedAt (and thus a new issuedAt in the next cursor).
nextCursor is null when no more results.
totalCandidates is the count after the actionability gate (i.e., after applying
MIN_GOAL_COMPLEMENTARITY_THRESHOLD behavior) and reflects the size of the gated result set prior
to pagination.
Pending 409 Conflict:
{ "code": "match_not_ready", "reason": "goal_embeddings_pending", "goalId": "uuid-or-null", "error": "That goal is still being prepared for matchmaking. Try again once needs/offers extraction finishes."}Reasons:
goal_embeddings_pending: the selected confirmed goal, or all confirmed goals in all-goals mode, are still waiting on complete per-goal embeddingsno_match_ready_goals: the principal has no confirmed match-ready goals
Errors: 400 if cursor is invalid. 403 if the goal doesn’t belong to the authenticated user.
404 if the specified goal doesn’t exist.
Removed endpoints
The following endpoints from v0.0 are removed:
POST /api/v0/me/match/goals/:goalClaimId/scratchpad— scratchpad is superseded by goal-derived needs/offers which users and agents can edit directly
13. Schema
New tables
goal
CREATE TABLE goal ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_id UUID NOT NULL REFERENCES entity(id), value TEXT NOT NULL, normalized_value TEXT, status TEXT NOT NULL DEFAULT 'proposed' CHECK (status IN ('proposed', 'confirmed', 'completed', 'archived', 'superseded')), authority TEXT NOT NULL CHECK (authority IN ( 'user_confirmed', 'user_asserted', 'organizer_asserted', 'imported', 'inferred' )), origin TEXT NOT NULL CHECK (origin IN ('inferred', 'imported', 'user_asserted', 'organizer_asserted')),
-- Confidence in proposed goals (0..1). Used for matchable-goal policy and taxonomy backfills. confidence FLOAT CHECK (confidence >= 0 AND confidence <= 1),
-- LLM-assigned goal taxonomy categories (application-layer validated). goal_categories TEXT[],
-- Kickoff fields (optional, user-facing) description TEXT, user_notes TEXT,
-- Derived by LLM extraction (§10) needs TEXT[], offers TEXT[], needs_text TEXT, offers_text TEXT,
-- Provenance evidence_refs UUID[], pipeline_version TEXT, supersedes_id UUID REFERENCES goal(id),
-- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ);
-- Active goals per entity (for listing and candidate goal loading)CREATE INDEX idx_goal_entity_active ON goal (entity_id, status, created_at DESC) WHERE status IN ('proposed', 'confirmed');
-- Confirmed goals per entity (for iteration in scoring)CREATE INDEX idx_goal_entity_confirmed ON goal (entity_id, created_at DESC) WHERE status = 'confirmed';goal_embedding
CREATE TABLE goal_embedding ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), goal_id UUID NOT NULL REFERENCES goal(id) ON DELETE CASCADE, entity_id UUID NOT NULL REFERENCES entity(id), embedding_type TEXT NOT NULL CHECK (embedding_type IN ('needs', 'offers')), embedding_model TEXT NOT NULL CHECK (char_length(embedding_model) > 0), embedding vector(1024) NOT NULL, input_hash TEXT NOT NULL, -- Denormalized for indexing and fast status-filtering during ANN queries goal_status TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (goal_id, embedding_type, input_hash));
-- Lookup per-goal embeddingsCREATE INDEX idx_goal_embedding_goal ON goal_embedding (goal_id, embedding_type, created_at DESC);
-- Entity-scoped goal embedding lookup by typeCREATE INDEX idx_goal_embedding_entity_type ON goal_embedding (entity_id, embedding_type, created_at DESC);Goal lifecycle triggers
-- State machine enforcementCREATE OR REPLACE FUNCTION enforce_goal_lifecycle()RETURNS TRIGGER AS $$BEGIN IF TG_OP = 'UPDATE' THEN -- Immutable content fields (same principle as trait_claim) IF OLD.value IS DISTINCT FROM NEW.value OR OLD.normalized_value IS DISTINCT FROM NEW.normalized_value OR OLD.origin IS DISTINCT FROM NEW.origin OR OLD.evidence_refs IS DISTINCT FROM NEW.evidence_refs OR OLD.entity_id IS DISTINCT FROM NEW.entity_id OR OLD.supersedes_id IS DISTINCT FROM NEW.supersedes_id THEN RAISE EXCEPTION 'goal immutable fields cannot be changed'; END IF;
-- Valid state transitions IF OLD.status IS DISTINCT FROM NEW.status THEN IF NOT ( (OLD.status = 'proposed' AND NEW.status IN ('confirmed', 'archived', 'superseded')) OR (OLD.status = 'confirmed' AND NEW.status IN ('completed', 'archived', 'superseded')) ) THEN RAISE EXCEPTION 'invalid goal status transition: % → %', OLD.status, NEW.status; END IF; END IF; END IF; NEW.updated_at = now(); RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_goal_lifecycle BEFORE UPDATE ON goal FOR EACH ROW EXECUTE FUNCTION enforce_goal_lifecycle();Entity embedding additions
No schema change to entity_embedding. agg_values is used by convention for value-alignment.
brief continues to be used for general-similarity.
Trait dimension changes
Remove three dimensions from trait_dimension:
-- Goals are now in the goal tableDELETE FROM trait_dimension WHERE key = 'goal';-- Offers and needs are derived from goalsDELETE FROM trait_dimension WHERE key IN ('offer', 'need');Surviving dimensions: skill, experience, values, circles, personality, location.
Existing trait_claim rows referencing the deleted dimensions should be archived or deleted as part
of the migration. Since we are pre-production with only seed data, a clean db:reset is acceptable.
14. Invariants
These constraints must hold across all matchmaking code. Violations are bugs.
- Goals are the primary matching entity. Scoring is driven by goal-derived needs/offers, not general trait dimensions. Trait dimensions participate only in value-alignment (values) and general-similarity (briefs).
- Entity_id is the root identifier. No
person_idin matchmaking interfaces. Entities are the matchable unit. - Consumer trust uses named profiles only for trait_claim queries. No ad hoc WHERE clauses on status/authority.
- Self-exclusion operates at auth-principal level. Via
auth_user_person_link, not just entity_id. A user may have multiple linked entities; all must be excluded. - No scoring-time LLM or embedding calls. All embeddings are precomputed by background workers. Match execution performs only lookups and arithmetic.
- Per-goal embeddings are the only goal-match source. Matchmaking ANN and goal-complementarity
scoring operate directly on
goal_embedding, not on entity-level goal aggregates. - Goal-derived needs/offers are the only complementarity signal. General
offer/needtrait dimensions do not exist. Complementarity analysis uses exclusively goal-derived data. - Elemental scores are always recorded.
goalNeedsToOffersandgoalOffersToNeedsare tracked separately even though they combine intogoalComplementarityfor the end user. - No values-only matchmaking results.
goalComplementaritymust be part of every returned match. Values and brief similarity are sub-scores only.
15. Deferred Items
| Item | Rationale |
|---|---|
| Event-scoped matching (Tier 3) | Requires scope parameter, trust policy for minimal-data entities, organizer auth model |
| Personality/social similarity | Future scoring dimensions (personality-similarity, circle-similarity) for niche use cases |
| Goal priority ordering | Column exists as nullable; UI/API for setting priority deferred |
| Goal completion workflow | completed status exists; UI/API for marking goals complete deferred |
| Additional ANN tuning | Per-goal HNSW is in place. Oversampling, rerank strategies, and budget tuning remain open evaluation work |
last_affirmed_at freshness | Recency weighting for goals and traits. Nothing blocks this addition |
| Post-match actions | Bookmarks, introductions, shortlists. v0 is informational only |
| Cross-score normalization | Different goal types may produce different score ranges. Normalization requires evaluation data |
| MCP tool wrapper | Agent SDK integration. HTTP API is sufficient for v0 |
| Profile completeness threshold | Minimum data to qualify as matchable. Sparse entities score lower naturally |
| Search surface migration | Agent search currently uses offer/need/skill trait dimensions. Migrating search to use goal-derived needs/offers for complementarity queries is follow-up work |
16. Relation to Foundational Spec
This feature spec is governed by the following foundational doctrines:
- Doctrine G (§4.G): “Matching is downstream.” — Matchmaking builds on the trait/evidence
substrate. The
goaltable extends the substrate with a first-class entity for matching. - Doctrine M (§4.M): “Every trait is an atomic statement.” — Goals are no longer traits, but the surviving trait dimensions (values, skill, etc.) still follow this doctrine.
- Doctrine N (§4.N): “Consumer trust policy is downstream policy.” — Trust profiles are used for values-dimension trait loading. Goals have their own status-based filtering.
- §15.2: Trait dimension registry. The
goal,offer, andneeddimensions are removed. The foundational spec §25 seed data must be updated to reflect this. - §19: Matching is directional, between typed projections, under a given lens. This spec implements that with goal-derived needs/offers as the projection and goal selection as the lens.
Foundational spec updates required
The following sections of meshi-v0.1-spec.md need updating to reflect goal promotion:
- §15.2 — Remove
goal,offer,needfrom the starter dimensions list - §25 — Add
goalandgoal_embeddingtable DDL; removegoal/offer/needfrom trait_dimension seed data - §19 — Update matching model description to reference goal-centric architecture
- §26 — Update deferred items regarding matching
The implementation addendum §7.1 (normalization for goal/offer/need) and §8.3 (trait inference for goal/offer dimensions) also need updating.