When to Use
- Changing the risk model, the evidence bundle, the review prompt or its validation.
- Adding a writer of any
triage_*property: a run step, a route, an MCP tool, a migration. - Adding a reader of the board state: a report, an MCP tool, a filter, a UI chip.
For the Cypher itself, also follow graph-db-writes; for a new MCP tool,
mcp-server-tools.
Critical Rules
-
NOBODY writes a final score.
triage_priority_score,triage_tier,triage_tier_rule,triage_risk,triage_factors,triage_stateandtriage_decided_by(the mixin's_FINAL_SET) come ONLY fromscore_model.combine_layers(), reached throughlayers.combine_props(). A writer changes ITS layer and askscombinefor the final inside the same transaction. Setting a final directly is how a review or a verdict used to vanish at the next run. -
Each writer touches one layer.
Writer Layer Method a run's publish BASE (+ a builtin REVIEW it made) publish_triage_layersan MCP agent's review REVIEW, channel mcpwrite_reviewa person (app, or MCP with channel mcp)DECISION set_human_verdictA publish never writes a decision,
:Mutedorupdated_at; a review never writes a decision or the base; a verdict never writes the base or a review. -
NEVER write
updated_atfrom triage. It is the scan's "last seen" stamp: the prune reads it, and the publish's guard skips a node whoseupdated_atmoved since the run read it. A triage write that bumped it would make every finding look re-ingested. -
Every single-finding write is lock-then-read, in one managed transaction.
SET n._triage_lock = true REMOVE n._triage_lockfirst, then read the node, the live proof (_LIVE_PROOF) and the current layers, then decide and write. Match exactly one node, elseambiguous. A timeout raisesTriageWriteBusy, which the API returns as 503busy. Never read proof from a run's snapshot. -
Live proof is proof of THIS finding.
_LIVE_PROOFmirrorsscore_model.is_proven; it must not readtriage_proof, which records proof on the finding's HOST. Reading it lifted every finding on a compromised host to T1 "proven" at its next rescore. (The mute guards'_PROVENdoes read it, on purpose: refusing a mute on a compromised host is the safe direction.) -
An
appdecision is never changed or reset over MCP (decided_in_app). Onlyapp-channel decisions by the user themself teach a detector's prior. -
A review is valid only while
triage_ai_evidence_hash == triage_evidence_hash. The hash isbundle_hash(build_bundle(...)): normalised, secret-redacted, capped. Changing the bundle, the redaction or the cap changes every hash and expires every review: bumpREVIEW_PROMPT_VERSIONdeliberately, never by accident. -
A run never reviews over a valid
mcpreview, and re-reviews its own (builtin) only on a model or prompt-version change. Fix-item text (fix_lever,ai_quote) comes from builtin reviews only. -
Keep the legacy readers tolerant. Unknown or legacy values mean "no layer", never a decision.
triage_source = 'ai'is a builtin false-positive review; a missingtriage_verdict_channelmeansapp. The first publish after an upgrade adopts an unchanged review-v1 review throughbuild_bundle_legacy, and unmute applies the same cleanup to muted nodes. Readers that branch on this:score_model.finding_state,reportData.ts,projectRisk.ts,tooling/scripts/triage_eval.py. -
A new stored
triage_*property goes inTRIAGE_PROPS(triage_mixin.py), which feedstriage_graph_migrate.py, and inTRIAGE_PROPERTIES(webapp/src/lib/triage/properties.ts), which keeps it out of the Recon Delta. Agent-writable text also goes inTRIAGE_TEXT_PROPERTIES, sobuildNodeContextnever hands it to the in-app agent. Declare it ingraph_db/schema_sections.mdand re-seed the catalog.
Pattern: a writer asks combine for the final
python# graph_db/mixins/recon/triage_mixin.py - write_review, trimmed def work(tx): found = self._lock_findings(tx, user_id, project_id, node_id, label) # lock, THEN read ... # not_found / ambiguous props = dict(rec["props"]) outcome = decide(props, rec["proven_now"], rec["updated_at"], rec["label"]) if outcome.get("refused"): return {"written": False, "reason": outcome["refused"]} # nothing written review = self._clean_review(outcome["review"]) # only THIS layer's keys tx.run("MATCH (n) WHERE elementId(n) = $eid SET n += $review", ...) props.update(review) final = self._clean_final(combine(props, rec["proven_now"])) # layers.combine_props tx.run(f"MATCH (n) WHERE elementId(n) = $eid SET {self._FINAL_SET}", ...) return self._write_tx(self.driver, work, timeout) # TriageWriteBusy on timeout
Commands
bash./agentic/run_tests.sh tests/test_score_model.py # combine_layers + the model ./agentic/run_tests.sh tests/test_triage_orchestrator_flow.py ./agentic/run_tests.sh tests/test_triage_finding_ops.py # root tests run in the agent image; the *_live suites need Neo4j and self-skip without it ./redamon.sh test unit cd webapp && npx vitest run src/lib/triage src/lib/mcp src/app/api/triage
Resources
- Priority-Board.md - the user-facing model: layers, Decided by, Reset, the detail panel, MCP
- README.CYPHERFIX_AGENTS.md - run steps, publish, run protocol
- graph_db/schema_sections.md - every
triage_*property, by layer - Related skills:
graph-db-writes,mcp-server-tools,redamon-testing


