Docs/Skills and code/src/

src/stages/10-review-vocabulary.js

Code100 lines5.5 KBsrc/

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.