JuliusBrussee/caveman · error · Error
theme ids are not in canonical order
Error message
${where} theme ids are not in canonical order What it means
Relationship endpoints must be stored in a canonical order: the validator requires relationship.theme_a_id < relationship.theme_b_id (lexicographic string comparison) so the same pair of themes always serializes one way and is not double-counted as two distinct relationships. It throws when the ids are equal or stored in descending order.
Solutions
- Swap the two ids so theme_a_id is the lexicographically smaller one (keeping counts/probabilities attached to the correct endpoints).
- Sort theme ids when emitting relationships in the report generator so ordering is deterministic.
- Deduplicate reversed pairs in the generator before writing the report.
- Re-run the validator to confirm canonical ordering.
Example fix
// before
{ "theme_a_id": "theme-retry", "theme_b_id": "theme-guard" }
// after
{ "theme_a_id": "theme-guard", "theme_b_id": "theme-retry" } Defensive patterns
Strategy: validation
Validate before calling
if (report.relationships.some((r) => !(r.theme_a_id < r.theme_b_id))) {
throw new Error("relationship endpoints not in canonical order");
} Type guard
const isCanonicallyOrdered = (r) => r.theme_a_id < r.theme_b_id;
Try / catch
try {
await runValidator([reportPath, spansPath]);
} catch (err) {
if (String(err.message).includes("theme ids are not in canonical order")) {
console.error("Sort the pair lexicographically and swap counts accordingly.");
}
throw err;
} Prevention
- Sort theme ids ([a, b].sort()) before emitting any relationship.
- Deduplicate pairs via a normalized `${min}:${max}` key.
- Keep endpoint ordering logic in one shared helper used by all detectors.
When it happens
Trigger: Running the validator on a report where a relationship has theme_a_id >= theme_b_id, e.g. {"theme_a_id": "theme-guard", "theme_b_id": "theme-retry"} with "theme-retry" < "theme-guard", or both fields equal.
Common situations: Relationship emitted while iterating themes in discovery order instead of sorted order; deduplication logic absent so a pair appears as both (a,b) and (b,a); hand-written fixtures.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- references a theme that is not in this report
- shares more units than either theme has
- canonical span
- expected exactly 2 agent conformance fixtures, found
- ${fixtureFiles[index]}…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/6e6a1e41f17de10c.
Report an issue: GitHub.
Appendix: source
Thrown at packages/shared/contracts/scripts/validate-continuous-improvement.mjs:85
if (opportunity.type === "safety") {
if (opportunity.alternative_variant_id) throw new Error(`${where} is a safety finding with an alternative variant`);
if (opportunity.expected_value !== 0) throw new Error(`${where} is a safety finding carrying expected value ${opportunity.expected_value}`);
if (opportunity.alternative_metrics.cost_per_outcome_usd !== null || opportunity.alternative_metrics.eligible_runs !== 0) {
throw new Error(`${where} is a safety finding carrying another workflow's metrics`);
}
}
}
// A relationship is a count, so it must be recomputable from the counts it
// carries. Anything a reader cannot re-derive is a claim, not evidence.
const themeIds = new Set(report.themes.map((theme) => theme.id));
for (const relationship of report.relationships) {
const where = `report fixture ${reportPaths[index]}: relationship ${relationship.id}`;
if (!themeIds.has(relationship.theme_a_id) || !themeIds.has(relationship.theme_b_id)) {
throw new Error(`${where} references a theme that is not in this report`);
}
if (!(relationship.theme_a_id < relationship.theme_b_id)) {
throw new Error(`${where} theme ids are not in canonical order`);
}
if (relationship.shared_unit_count > relationship.theme_a_unit_count || relationship.shared_unit_count > relationship.theme_b_unit_count) {
throw new Error(`${where} shares more units than either theme has`);
}
if (relationship.shared_unit_ids.length > relationship.shared_unit_count) {
throw new Error(`${where} carries more shared unit ids than its shared unit count`);
}
const expectedBGivenA = relationship.shared_unit_count / relationship.theme_a_unit_count;
const expectedAGivenB = relationship.shared_unit_count / relationship.theme_b_unit_count;
if (Math.abs(relationship.probability_b_given_a - expectedBGivenA) > 1e-9) {
throw new Error(`${where} probability_b_given_a ${relationship.probability_b_given_a} != ${expectedBGivenA}`);
}
if (Math.abs(relationship.probability_a_given_b - expectedAGivenB) > 1e-9) {
throw new Error(`${where} probability_a_given_b ${relationship.probability_a_given_b} != ${expectedAGivenB}`);
}
}
relationshipCount += report.relationships.length;
View on GitHub (pinned to 3ee70a1026)