Docs/Skills and code/src/
src/stages/10-review-vocabulary.js
import { getDb } from "../db.js";
import { config } from "../../config.js";
import { ensureResearchSchema } from "../analysis/research-schema.js";
const db = getDb();
ensureResearchSchema();
const args = process.argv.slice(2);
const valueOf = name => args.find(arg => arg.startsWith(`--${name}=`))?.slice(name.length + 3) ?? null;
const has = name => args.includes(`--${name}`);
const ecosystem = config.ecosystem.name;
function parseId(value, label) {
const id = Number(value);
if (!Number.isInteger(id) || id < 1) throw new Error(`${label} must be a positive integer.`);
return id;
}
function ensureTerm(id) {
const term = db.prepare(`SELECT * FROM vocabulary_terms WHERE term_id=? AND ecosystem=?`).get(id, ecosystem);
if (!term) throw new Error(`Vocabulary term ${id} was not found in ecosystem ${ecosystem}.`);
return term;
}
try {
const promote = valueOf("promote");
const reject = valueOf("reject");
const alias = valueOf("alias");
const acceptRelation = valueOf("accept-relation");
const rejectRelation = valueOf("reject-relation");
if (promote) {
const id = parseId(promote, "--promote");
const term = ensureTerm(id);
db.prepare(`UPDATE vocabulary_terms SET status='canonical', canonical_term_id=NULL, updated_at=? WHERE term_id=?`).run(new Date().toISOString(), id);
console.log(`[vocabulary-review] Promoted term #${id} to canonical: ${term.term_type} / ${term.label}`);
} else if (reject) {
const id = parseId(reject, "--reject");
const term = ensureTerm(id);
if (term.status === "canonical") throw new Error("Refusing to reject a canonical term; demote or remap its aliases first.");
db.prepare(`UPDATE vocabulary_terms SET status='rejected', canonical_term_id=NULL, updated_at=? WHERE term_id=?`).run(new Date().toISOString(), id);
console.log(`[vocabulary-review] Rejected term #${id}: ${term.term_type} / ${term.label}`);
} else if (alias) {
const match = alias.match(/^(\d+):(\d+)$/);
if (!match) throw new Error("Use --alias=ALIAS_TERM_ID:CANONICAL_TERM_ID");
const aliasId = parseId(match[1], "alias term id");
const canonicalId = parseId(match[2], "canonical term id");
const source = ensureTerm(aliasId);
const target = ensureTerm(canonicalId);
if (aliasId === canonicalId) throw new Error("A term cannot alias itself.");
if (source.term_type !== target.term_type) throw new Error(`Type mismatch: ${source.term_type} cannot alias ${target.term_type}.`);
if (target.status !== "canonical") throw new Error(`Target #${canonicalId} must first be promoted to canonical.`);
db.prepare(`UPDATE vocabulary_terms SET status='alias', canonical_term_id=?, updated_at=? WHERE term_id=?`)
.run(canonicalId, new Date().toISOString(), aliasId);
console.log(`[vocabulary-review] Mapped #${aliasId} (${source.label}) as an alias of #${canonicalId} (${target.label}).`);
} else if (acceptRelation || rejectRelation) {
const isAccept = Boolean(acceptRelation);
const id = parseId(isAccept ? acceptRelation : rejectRelation, isAccept ? "--accept-relation" : "--reject-relation");
const relation = db.prepare(`SELECT * FROM vocabulary_relations WHERE relation_id=?`).get(id);
if (!relation) throw new Error(`Relation #${id} was not found.`);
db.prepare(`UPDATE vocabulary_relations SET status=?, updated_at=? WHERE relation_id=?`)
.run(isAccept ? "accepted" : "rejected", new Date().toISOString(), id);
console.log(`[vocabulary-review] Relation #${id} marked ${isAccept ? "accepted" : "rejected"}. Acceptance records the relationship; it does not merge terms.`);
} else {
const relationLimit = Math.max(1, Number(valueOf("limit") || 30));
if (has("terms")) {
const rows = db.prepare(`
SELECT t.term_id, t.term_type, t.label, t.status, t.canonical_term_id,
COUNT(DISTINCT e.evidence_id) AS evidence_count,
MAX(CASE WHEN a.important=1 THEN 1 ELSE 0 END) AS reference_evidence
FROM vocabulary_terms t
LEFT JOIN vocabulary_evidence e ON e.term_id=t.term_id
LEFT JOIN editorial_annotations a ON a.object_type='channel' AND a.object_id=e.object_id AND e.object_type='channel'
WHERE t.ecosystem=?
GROUP BY t.term_id
ORDER BY reference_evidence DESC, evidence_count DESC, t.term_type, t.label
LIMIT ?
`).all(ecosystem, relationLimit);
console.log(`[vocabulary-review] Terms (ecosystem=${ecosystem}, limit=${relationLimit})`);
for (const row of rows) console.log(JSON.stringify(row));
} else {
const rows = db.prepare(`
SELECT r.relation_id, a.term_id AS term_a_id, a.term_type, a.label AS term_a,
b.term_id AS term_b_id, b.label AS term_b, r.relation_type, r.similarity, r.status,
r.method, r.evidence_json
FROM vocabulary_relations r
JOIN vocabulary_terms a ON a.term_id=r.term_a_id
JOIN vocabulary_terms b ON b.term_id=r.term_b_id
WHERE a.ecosystem=? AND b.ecosystem=? AND r.status='proposed'
ORDER BY r.similarity DESC, r.relation_id
LIMIT ?
`).all(ecosystem, ecosystem, relationLimit);
console.log(`[vocabulary-review] Proposed same-type similarities (ecosystem=${ecosystem}, limit=${relationLimit})`);
for (const row of rows) console.log(JSON.stringify(row));
console.log("\nActions: --promote=TERM_ID | --reject=TERM_ID | --alias=ALIAS_ID:CANONICAL_ID | --accept-relation=RELATION_ID | --reject-relation=RELATION_ID | --terms");
}
}
} catch (error) {
console.error("[vocabulary-review] Action failed:", error instanceof Error ? error.message : error);
process.exitCode = 1;
}
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.