Three-axis decomposed scoring (Scale, Cost, Quality) for each candidate. Transparent, evidenced, no composite rank. Reads candidates.json + entity articles + pricing reference. Writes scored_candidates.json.
You take a candidates.json (output of query/discover) plus the wiki entity articles those candidates point to, and produce scored_candidates.json. Each candidate gets three independent axes: Scale, Cost, Quality. Every score is decomposable, every score carries its evidence, and there is NO composite "winner" rank. The buyer sees all three axes and decides their own weighting.
This is the platform's core differentiator. Brokers and competitors return a single ranked list. vCRO returns three transparent axes the buyer can filter and weight themselves.
candidates_path: absolute path to a candidates.json.request_path: absolute path to the corresponding request.json (so you can apply request-specific weighting hints to evidence quality, never to the score itself).wiki_root: absolute path to store/wiki/.pricing_ref: absolute path to . Fallback cache for cost estimates — use entity articles and live research first, only when no better source exists.references/pricing-data.mdpricing-data.mdout_path: absolute path where you write scored_candidates.json. Conventionally <query_dir>/scored_candidates.json.candidates.json carries two types of candidates:
wiki_entity — has entity_id, resolves to a wiki article at store/wiki/<type>/<entity_id>.md. Full scoring: read the entity article, all dimension sections, frontmatter, linked entities. This is the existing behavior.
search_lead — has lead_id (not entity_id), a source (pubmed/ctgov/web), and inline fields from the search (canonical_name, specimen_type, headline_n, access_route, source_url, notes). No wiki article exists on disk. Score from the lead's inline fields only. Do NOT attempt to read a wiki file.
Branching rule: For each candidate in candidates.json:
candidate_type == "wiki_entity" (or entity_id is present and no lead_id): → full scoring path (read wiki article)candidate_type == "search_lead" (or lead_id is present): → thin scoring path (inline fields only)provenance_depth: 0.0 (no wiki article, no dimension sections)scale.usable_n: from lead's headline_n if present, otherwise unknownscale.usable_n_for_request: from lead's inline fields (specimen_type match, treatment status, N) — heavily caveatedscale.n_confidence: low (search-derived, not primary-source verified)quality.pre_analytical: missing — no dimension sections to read. If prior_art.json has a direct hit on the same matrix, cite it as the only fitness signal.quality.confounders: missing — no entity body to read. Address request hard_negatives from lead's inline fields if available (e.g. treatment_naive_confirmed: true), otherwise open_question.quality.platform_validation: from prior_art.json only. If prior art exists for this assay × specimen × indication, cite it.cost: from providers.json and references/pricing-data.md only. Lead's access_route informs the source leg.sourcing_chain: populate from lead fields + providers.json + prior_art.json. Works the same as for wiki entities — each link carries its own evidence state.card_for_delivery: construct from lead's canonical_name, access_route, notes. risk MUST include: "Search lead — not yet compiled into wiki. Claims not verified against primary sources."axis_confidences: all low unless a specific axis has strong evidence from providers.json or prior_art.jsonfinding_type: determined by lead's source and specimen info, same table as wiki entitiescandidates.json — wiki entities from discover AND search leads from Gate 3. Score all of them.request.json — for n_target, hard_negatives, use_case_type, and scope_notes.store/wiki/<type>/<entity_id>.md — frontmatter + every dimension section. Also linked institution and platform articles.references/pricing-data.md — fallback pricing cache for both types.references/providers/<provider>-<assay>.md — provider-specific requirements for both types.store/queries/<slug>/search/prior_art.json — prior art for both types.store/queries/<slug>/search/providers.json — assay providers for both types.store/raw/ and NO web search. NOTHING from your training data.Read request.json field intent (access | commission | mixed). The three axes are structurally identical regardless of intent. The EVIDENCE that populates them changes:
For access intent (default): axes score existing data availability and quality. This is the current behavior -- usable_n_for_request = subjects with existing data matching the request. Cost source leg = data access fee. Quality pre-analytical = existing data QC.
For commission intent: axes score specimen availability and fitness for the intended assay.
usable_n_for_request = estimated banked specimens of the requested type, NOT existing data points. Read the entity's specimens.estimated_available_n or dimension 15 section.references/pricing-data.md or the entity's access route. Assay leg = provider quote for the intended assay. Screening_qa leg = specimen validation/QC cost (e.g. low-input DNA extraction QC). Every cost figure must cite its source — if no source exists in the entity or references, the leg is "quote required" [open_question].references/providers/<assay>.md exists, compare the entity's documented specimen attributes against the assay's stated requirements. If the reference file doesn't exist, state what the entity documents and verdict = missing — no assay requirements reference available. Do NOT fill assay requirements from training data. Questions to answer from the entity article: freeze-thaw history? Volume per aliquot? Storage temperature? Expected DNA/RNA yield? Dimension 20 (collection protocol detail) is the key evidence. The load-bearing question is: "will this banked specimen produce signal when subjected to the buyer's assay?" If dim 20 is not covered, pre-analytical verdict is missing for commission intent.For mixed intent: score BOTH. Include two parallel assessments: existing-data score and specimen-sourcing score. The deliver skill renders both for the buyer.
Use this table. Do NOT guess. Read intent from request.json and specimen_match from candidates.json:
| intent | specimen_match | finding_type |
|---|---|---|
| access | any | direct_match |
| commission | has_banked_specimens | sourcing_path |
| commission | has_existing_data_only | direct_match (comparator — existing data proves feasibility) |
| commission | no_specimen_info | pivot |
| mixed | has_banked_specimens | sourcing_path |
| mixed | any other | direct_match |
The schema is constant across domains. The locked A/B/C example rotation in .claude/rules/example-rotation.md populates the per-domain values. The shape:
{
"request_id": "<from request.json>",
"scored_at": "<ISO datetime>",
"candidates": [
{
"entity_id": "<wiki slug>",
"scale": {
"usable_n": 0,
"usable_n_evidence": "<entity article section + quote>",
"usable_n_for_request": 0,
"usable_n_for_request_reason": "<derivation: how the request's filter narrows headline N>",
"n_confidence": "low | medium | high",
"multi_site_potential": "<one line>",
"axis_summary": "<one line>"
},
"cost": {
"legs": {
"source": { "estimate": "...", "currency": "...", "evidence": "...", "note": "..." },
"screening_qa": { "estimate": "...", "currency": "...", "evidence": "...", "note": "..." },
"assay": { "estimate": "...", "currency": "...", "evidence": "...", "note": "..." }
},
"total_known_low": null, "total_known_high": null, "currency": "USD",
"within_budget": "yes | no | unknown",
"unknowns": ["..."],
"timeline": "<calendar weeks or months>",
"axis_summary": "<one line>"
},
"quality": {
"provenance_depth": 0.0,
"depth_decomposition": {
"covered_dimensions": ["...names from references/intelligence-dimensions.md only..."],
"missing_dimensions": ["...names from references/intelligence-dimensions.md only..."]
},
"pre_analytical": { "...request-relevant attributes...": "...", "verdict": "good | partial | weak | missing" },
"confounders": { "...request-relevant attributes...": "...", "verdict": "..." },
"platform_validation": { "platform": "...", "verdict": "..." },
"axis_summary": "<one line>"
},
"axis_confidences": { "scale": "...", "cost": "...", "quality": "..." },
"finding_type": "direct_match | sourcing_path | pivot",
"card_for_delivery": {
"primary_signal": "<<= 200 chars, request-specific>",
"action": "<verb phrase, addresses the buyer's gaps>",
"risk": "<single biggest unknown for THIS request>"
},
"sourcing_chain": null
}
],
"finding_type_legend": "`finding_type` classifies what this candidate offers. `direct_match` = existing data matches the request. `sourcing_path` = banked specimens exist, assay must be commissioned. `pivot` = neither data nor specimens match directly; alternative approach needed. The deliver skill uses `finding_type` to select the output format for each candidate.",
"axis_legend": {
"scale": "Usable N for THIS request, with confidence. Not headline N.",
"cost": "Three legs (source + screening_qa + assay), explicit unknowns, no composite total when unknowns exist.",
"quality": "Pre-analytical, confounders, platform validation, and provenance depth. The buyer's hard_negatives drive which sub-dimensions are surfaced."
}
}
The pre_analytical and confounders sub-axes are where the de-bias matters most: the load-bearing attributes are domain-specific. The frame is constant; the attribute names rotate.
Example A — neuro fluid biomarker (AD plasma metabolomics).
"pre_analytical": {
"fasting": "documented (overnight fasting, study protocol)",
"tube_type": "plasma; anticoagulant (EDTA / heparin / citrate) often implied not explicit",
"freeze_thaw": "cycle count usually not documented in primary paper; cited protocol paper may have it",
"verdict": "partial — fasting is the load-bearing pre-analytical fact for lipidomics and it IS documented; FT cycles are a gap"
},
"confounders": {
"medication": "statin use as a covariate, % of cohort affected, adjustment in model",
"demographics": "age, sex, ancestry, APOE genotype",
"verdict": "good if the request's named confounder (statins) is addressed; downgrade to partial if silent"
}
Example B — oncology tissue genomics (NSCLC FFPE bulk RNA-seq).
"pre_analytical": {
"fixation_time": "10% NBF for 24-48h, documented in Methods",
"block_age": "median age at sectioning ~4 years; older blocks lose extractability",
"tumor_purity": "median 70% by ABSOLUTE; deconvolution recommended below 50%",
"verdict": "good — the load-bearing pre-analytical facts (fixation time, purity) are documented"
},
"confounders": {
"treatment_history": "neoadjuvant chemotherapy or radiation prior to biopsy; % affected, exclusion vs adjustment",
"stage_distribution": "stage I-IIIA enriched; metastatic underrepresented",
"verdict": "good if neoadjuvant exposure is documented and matches the request's exclusion stance"
}
Example C — microbiome stool sequencing (IBD shotgun).
"pre_analytical": {
"time_to_freeze": "self-collection at home; median time to first freeze 4 hours, documented",
"preservative": "OMNIgene-GUT vs dry tube vs ethanol — community composition differs by container",
"cold_chain": "documented for processing lab handoff; gap exists for the at-home leg",
"verdict": "partial — preservative documented, cold chain only documented post-handoff"
},
"confounders": {
"antibiotic_exposure": "30-day washout protocol; 18% of cases on antibiotics in 15-30 day window, captured as covariate",
"ppi_use": "22% of cases, not in exclusion criteria, not consistently adjusted in published analyses",
"diet": "not documented",
"verdict": "good on antibiotics (the dominant microbiome confounder); partial on PPI; gap on diet"
}
If your Example A rendering has fixation time, or your Example C rendering talks about EDTA, you have leaked an example into the wrong domain. The pre_analytical attributes ARE the domain signature.
The single most important number is usable_n_for_request, NOT headline N. The headline N is the published cohort size; usable N is the count of subjects that survive filtering by the request's criteria (sample type, longitudinal requirement, disease subset, etc.).
usable_n = the cohort's stated headline figure (ground truth, with quote).usable_n_for_request = your best estimate of the subset that satisfies the request, with the reason.n_confidence = high if both numbers are quoted directly, medium if one is inferred from the cohort body, low if you had to estimate from analogues.multi_site_potential = a one-line description of whether this cohort can be aggregated with other entities in the wiki (same platform, same protocol, related consortium). This is where the wiki graph pays off.Three legs, always. Even if one is zero or already-performed, list it explicitly so the buyer sees the structure.
Each leg has:
estimate: a USD/EUR figure, a range, "already performed", "free", or "quote required".currency: the currency (or null if not applicable).evidence: where this number came from (entity article, pricing-data.md line, or "wiki has no entry").note: one sentence on the load-bearing assumption or alternative scenario.After the legs:
total_known_low and total_known_high: numeric bounds if at least one leg has a real number; null if all legs are "quote required".within_budget: yes / no / unknown. Tied to request.budget.unknowns: explicit list of every cost component you could not value.timeline: the realistic end-to-end calendar weeks/months. This is part of cost in spirit because it determines opportunity cost.Never invent a composite cost number. If two legs are known and one is "quote required", the composite is unknown — report the two knowns and the one gap, not a fake total.
Four sub-axes, each with its own evidence:
referenced_by count.missing_dimensions, draw names from the 21 canonical dimensions in references/intelligence-dimensions.md. Do NOT invent dimension names; gaps that don't map to one of the 21 belong in pre_analytical.verdict or confounders.verdict instead.Each sub-axis has a one-line verdict (good, partial, weak, or missing) and an evidence reference. The axis_summary is your one-sentence rollup that tells the buyer where the load-bearing weakness is.
For each scored candidate, refresh the card. The card in the entity article was written by merge from the original fragments; the card_for_delivery is request-specific:
primary_signal: the standout fact for THIS request (cite the usable_n_for_request, not headline N).action: the specific next step that addresses the buyer's hard_negatives and gaps from discover.risk: the single biggest unknown for THIS request, not the original entity's generic risk.Deliver renders this card, not the entity's stored card.
For sourcing_path candidates: the card fields reframe: primary_signal = banked specimen count + type + access route (not existing data count). action = specimen request step + assay provider contact (not data portal login). risk = specimen fitness uncertainty for the intended assay (not existing data quality caveat).
For commission and mixed intent, each candidate gets a sourcing_chain — the full path from specimen to data. For access intent, set sourcing_chain: null.
The chain is an ordered list of links. Each link answers one question, cites its evidence, and carries a state. The link types flex per query — not every chain has the same links. The structure is constant:
"sourcing_chain": [
{
"link": "<link type>",
"question": "<what this link answers>",
"answer": "<concise answer or 'unknown'>",
"evidence_source": "<entity article section | provider file | prior_art.json entry | pricing-data.md line | 'none found'>",
"evidence": "PUBLISHED | DERIVED | open_question",
"note": "<one sentence on what the buyer should do if evidence is open_question>"
}
]
Build the chain from these link types. Include a link ONLY if it's relevant to the query. Skip links that don't apply. Each link carries its own cost field where relevant — cost is not a standalone link type.
references/providers/<provider>-<assay>.md). If prior art exists (prior_art.json), cite it — a successful prior study on the same matrix is the strongest fitness evidence.providers.json and references/providers/ files. If multiple providers were found, pick the best match for this candidate (geography, matrix validation, pricing) and note alternatives.prior_art.json. A direct hit with outcome=success is the single strongest signal that the path works.Read providers.json (if it exists). For each scored candidate:
specimen_types_accepted, pair them.note field.evidence: open_question with note="No provider found for this assay × specimen combination; the gap-resolution step may have searched and failed — check search_history.jsonl."Geography and logistics. When pairing, check whether the specimen source and provider are in the same country (read provider location from providers.json, specimen source location from the parent institution entity or entity card).
note field.logistics link must carry evidence: open_question with a note flagging customs, import permits, and international biological specimen shipping requirements. This is a blocking concern, not a ranking factor — the buyer needs to resolve it before proceeding.data_delivery link: populate from the candidate entity's card.action field (access mechanism, documented timeline). If data comes via portal download after DUA, timeline = access processing + download. If physical specimens require a separate data delivery step, note it.note field says what was tried and what the buyer should do next.Three-bool struct: how confident you are in EACH axis independently.
references/providers/. No vibes. No training-data fills.usable_n_for_request always comes first.missing or open_question, NOT a number from your parametric memory. Specifically:
references/providers/ or a PMC paper in the entity article.references/pricing-data.md with its own source URL.[open_question — no grounded reference] and move on. The gap is more valuable than a plausible guess.
The entire value proposition is "every fact is traceable to a source the buyer can verify." A training-data fill disguised as evidence is the exact broker opacity the system exists to dissolve.If an entity is so thinly evidenced that you cannot fill any axis with confidence (e.g. provenance_depth < 0.2 and no real_numbers section), set all three axis_confidences to low and write a axis_summary for each that explicitly states "insufficient evidence in current wiki — recommend lint/enrich before scoring this entity again". Do NOT drop the candidate; deliver decides whether to surface or hide it.
Each candidate's score block is 80 to 200 lines of JSON. For 5 candidates, the file is 500 to 1000 lines. Keep it scannable; deliver renders the readable view.