Skip to content

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 goal table 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_at freshness 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 need claims 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

DimensionWhat it measuresSignal sourceSymmetric?
Goal-ComplementarityCan B fulfill A’s needs? Can A fulfill B’s needs?Goal-derived needs/offers embeddingsNo (directional)
Value-AlignmentDo A and B share values and operating philosophy?Values-dimension trait embeddingsYes
General-SimilarityAre A and B holistically similar people?Brief embeddingsYes

Final score

finalScore = w_gc * goalComplementarity
+ w_va * valueAlignment
+ w_gs * generalSimilarity

Fixed weights for v0:

WeightValueRationale
w_gc0.55Goal complementarity is the primary matching signal
w_va0.25Shared values indicate sustainable relationships
w_gs0.20Holistic 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 / brief entity-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:

ScoreComputationWhat it measures
needsToOfferssim(A.goal.needs, B.goal.offers)Can B offer what A needs?
offersToNeedssim(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 * offersToNeeds

Iteration 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→A

Principal goal scope:

  • If goal_id is provided, use only that confirmed goal.
  • Otherwise use all confirmed goals that have both needs and offers embeddings 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_score

Where 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

TierNameCriteriaMatch pool (v0)
1Active UserHas a row in auth_user_person_linkYes
2High-Quality UnclaimedHas a person_external_identity with identity_type IN ('linkedin_id', 'linkedin_url') and status NOT IN ('revoked', 'superseded', 'disputed')Yes
3Low-Quality ImportNeither of the aboveNo (v0)

Materialization

Tier is materialized as entity.match_tier SMALLINT:

  • 1 = Active User
  • 2 = High-Quality Unclaimed
  • NULL = 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:

  1. ANN query goal_embedding WHERE embedding_type = 'offers' using the principal goal’s needs embedding
  2. ANN query goal_embedding WHERE embedding_type = 'needs' using the principal goal’s offers embedding
  3. 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
  4. Union + dedupe the resulting candidate entity IDs, keeping the closest distance
  5. 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";
StrengthConditionMeaning
confirmedEntity has ≥1 goal with status: "confirmed"Entity actively stated their goals
inferredEntity has goals but none confirmed (all proposed)Goals are LLM-inferred or imported
noneEntity has no active goalsNo goal signal

Direction mixing weights

Intent StrengthA→B weightB→A weight
confirmed0.60.4
inferred0.80.2
none1.00.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

  1. Load the goal by ID from the event payload
  2. Skip if goal status is terminal (completed, archived, superseded)
  3. Send the goal’s value text to an LLM with an extraction prompt
  4. Parse the response as { needs: string[], offers: string[] }
  5. Generate needs_text (join needs with \n) and offers_text (join offers with \n)
  6. Write needs, offers, needs_text, offers_text to the goal row
  7. Embed needs_text and offers_text separately
  8. Upsert per-goal embeddings into goal_embedding (types: needs, offers)
  9. Trigger agg_values recomputation 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 goal
  • offers: 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 typeComputationUsed for
agg_valuesEmbed concatenated value from all values-dimension trait claimsValue-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:

ParamRequiredDefaultDescription
goal_idNoUUID of a specific confirmed goal to match against. If omitted, uses all confirmed ready goals.
limitNo20Results per page (1–50)
cursorNoOpaque 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 offerings
  • scores.valueAlignment: symmetric values similarity
  • scores.generalSimilarity: symmetric brief similarity
  • bestGoalForPrincipal: which confirmed principal goal paired best with which candidate goal. Candidate goals may be confirmed or high-confidence proposed. null only when the A→B direction has no positive winning pair.
  • bestGoalForCandidate: which candidate goal paired best with which principal goal. null only 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 embeddings
  • no_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 embeddings
CREATE INDEX idx_goal_embedding_goal
ON goal_embedding (goal_id, embedding_type, created_at DESC);
-- Entity-scoped goal embedding lookup by type
CREATE INDEX idx_goal_embedding_entity_type
ON goal_embedding (entity_id, embedding_type, created_at DESC);

Goal lifecycle triggers

-- State machine enforcement
CREATE 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 table
DELETE FROM trait_dimension WHERE key = 'goal';
-- Offers and needs are derived from goals
DELETE 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.

  1. 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).
  2. Entity_id is the root identifier. No person_id in matchmaking interfaces. Entities are the matchable unit.
  3. Consumer trust uses named profiles only for trait_claim queries. No ad hoc WHERE clauses on status/authority.
  4. 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.
  5. No scoring-time LLM or embedding calls. All embeddings are precomputed by background workers. Match execution performs only lookups and arithmetic.
  6. 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.
  7. Goal-derived needs/offers are the only complementarity signal. General offer/need trait dimensions do not exist. Complementarity analysis uses exclusively goal-derived data.
  8. Elemental scores are always recorded. goalNeedsToOffers and goalOffersToNeeds are tracked separately even though they combine into goalComplementarity for the end user.
  9. No values-only matchmaking results. goalComplementarity must be part of every returned match. Values and brief similarity are sub-scores only.

15. Deferred Items

ItemRationale
Event-scoped matching (Tier 3)Requires scope parameter, trust policy for minimal-data entities, organizer auth model
Personality/social similarityFuture scoring dimensions (personality-similarity, circle-similarity) for niche use cases
Goal priority orderingColumn exists as nullable; UI/API for setting priority deferred
Goal completion workflowcompleted status exists; UI/API for marking goals complete deferred
Additional ANN tuningPer-goal HNSW is in place. Oversampling, rerank strategies, and budget tuning remain open evaluation work
last_affirmed_at freshnessRecency weighting for goals and traits. Nothing blocks this addition
Post-match actionsBookmarks, introductions, shortlists. v0 is informational only
Cross-score normalizationDifferent goal types may produce different score ranges. Normalization requires evaluation data
MCP tool wrapperAgent SDK integration. HTTP API is sufficient for v0
Profile completeness thresholdMinimum data to qualify as matchable. Sparse entities score lower naturally
Search surface migrationAgent 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 goal table 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, and need dimensions 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:

  1. §15.2 — Remove goal, offer, need from the starter dimensions list
  2. §25 — Add goal and goal_embedding table DDL; remove goal/offer/need from trait_dimension seed data
  3. §19 — Update matching model description to reference goal-centric architecture
  4. §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.