@inerrata-corporation/errata 2.0.2-dev.227 → 2.0.2-dev.247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15774,7 +15774,22 @@ var LOCAL_RULE_OVERRIDES = {
15774
15774
  REVEALED_BY: null,
15775
15775
  PRODUCED: null
15776
15776
  };
15777
- var SCHEMA_VERSION = 5;
15777
+ function localEdgeViolation(fromLabel, type, toLabel) {
15778
+ if (type in LOCAL_RULE_OVERRIDES) {
15779
+ const rule = LOCAL_RULE_OVERRIDES[type];
15780
+ if (!rule) return null;
15781
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
15782
+ return `${type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
15783
+ }
15784
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
15785
+ return `${type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
15786
+ }
15787
+ return null;
15788
+ }
15789
+ const verdict = isValidEdge(fromLabel, type, toLabel);
15790
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
15791
+ }
15792
+ var SCHEMA_VERSION = 6;
15778
15793
  var SCHEMA_SQL = `
15779
15794
  CREATE TABLE IF NOT EXISTS schema_version (
15780
15795
  version INTEGER PRIMARY KEY
@@ -15789,6 +15804,21 @@ CREATE TABLE IF NOT EXISTS store_meta (
15789
15804
  value TEXT NOT NULL
15790
15805
  );
15791
15806
 
15807
+ -- Durable ledger of edges the ontology gate REFUSED, keyed by edge type.
15808
+ -- Durable rather than in-memory for one specific reason: the status command runs
15809
+ -- in a SEPARATE process and opens its own store handle, so a counter living on
15810
+ -- the instance reads 0 there forever. That is exactly how a producer rejecting
15811
+ -- 100% of its output stayed invisible for 17 days. Persisting it also survives
15812
+ -- the daemon restart that would otherwise erase the evidence.
15813
+ -- Keyed by type because a systematic producer bug shows up as ONE type
15814
+ -- dominating; sample keeps the latest reason so the count is actionable.
15815
+ CREATE TABLE IF NOT EXISTS edge_rejections (
15816
+ type TEXT PRIMARY KEY,
15817
+ count INTEGER NOT NULL DEFAULT 0,
15818
+ last_at INTEGER NOT NULL,
15819
+ sample TEXT
15820
+ );
15821
+
15792
15822
  CREATE TABLE IF NOT EXISTS nodes (
15793
15823
  id TEXT PRIMARY KEY,
15794
15824
  label TEXT NOT NULL,
@@ -16165,6 +16195,16 @@ var SqliteGraphStore = class {
16165
16195
  if (!cols.has(name)) this.db.exec(ddl);
16166
16196
  }
16167
16197
  }
16198
+ if (from < 6) {
16199
+ this.db.exec(`
16200
+ CREATE TABLE IF NOT EXISTS edge_rejections (
16201
+ type TEXT PRIMARY KEY,
16202
+ count INTEGER NOT NULL DEFAULT 0,
16203
+ last_at INTEGER NOT NULL,
16204
+ sample TEXT
16205
+ )
16206
+ `);
16207
+ }
16168
16208
  this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
16169
16209
  });
16170
16210
  }
@@ -16246,6 +16286,7 @@ var SqliteGraphStore = class {
16246
16286
  const violation = this.edgeRuleViolation(edge);
16247
16287
  if (violation) {
16248
16288
  this.rejectedEdgeCount++;
16289
+ this.recordEdgeRejection(edge.type, violation, edge.lastSeenAt || edge.createdAt || 0);
16249
16290
  console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
16250
16291
  return;
16251
16292
  }
@@ -16270,22 +16311,51 @@ var SqliteGraphStore = class {
16270
16311
  * overlay consulted first. Returns the reason string on a documented-
16271
16312
  * forbidden combination, else null. Point lookups on the id PK — negligible
16272
16313
  * next to the insert itself. */
16273
- edgeRuleViolation(edge) {
16274
- const fromLabel = this.getNode(edge.from)?.label;
16275
- const toLabel = this.getNode(edge.to)?.label;
16276
- if (edge.type in LOCAL_RULE_OVERRIDES) {
16277
- const rule = LOCAL_RULE_OVERRIDES[edge.type];
16278
- if (!rule) return null;
16279
- if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
16280
- return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
16281
- }
16282
- if (toLabel && rule.to && !rule.to.includes(toLabel)) {
16283
- return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
16284
- }
16285
- return null;
16314
+ /** Upsert one refusal into the durable ledger. Best-effort: a bookkeeping
16315
+ * failure must never turn a refused edge into a thrown write. */
16316
+ recordEdgeRejection(type, reason, at) {
16317
+ try {
16318
+ this.db.prepare(
16319
+ `INSERT INTO edge_rejections (type, count, last_at, sample) VALUES (?, 1, ?, ?)
16320
+ ON CONFLICT(type) DO UPDATE SET
16321
+ count = count + 1, last_at = excluded.last_at, sample = excluded.sample`
16322
+ ).run(type, at, reason.slice(0, 200));
16323
+ } catch {
16324
+ }
16325
+ }
16326
+ /** Refusals recorded by the ontology gate, per edge type, newest activity first.
16327
+ * Durable across restarts and readable from any process (see the table note). */
16328
+ edgeRejections() {
16329
+ try {
16330
+ return this.db.prepare(
16331
+ "SELECT type, count, last_at AS lastAt, sample FROM edge_rejections ORDER BY count DESC, last_at DESC"
16332
+ ).all();
16333
+ } catch {
16334
+ return [];
16335
+ }
16336
+ }
16337
+ /** Drop ledger entries whose last refusal predates `cutoff`. A still-misbehaving
16338
+ * producer keeps refreshing `last_at` and survives; a fixed one fades out. */
16339
+ pruneEdgeRejections(cutoff) {
16340
+ try {
16341
+ this.db.prepare("DELETE FROM edge_rejections WHERE last_at < ?").run(cutoff);
16342
+ } catch {
16343
+ }
16344
+ }
16345
+ /** Clear the ledger outright, whole or per type — operator escape hatch. */
16346
+ clearEdgeRejections(type) {
16347
+ try {
16348
+ if (type) this.db.prepare("DELETE FROM edge_rejections WHERE type = ?").run(type);
16349
+ else this.db.exec("DELETE FROM edge_rejections");
16350
+ } catch {
16286
16351
  }
16287
- const verdict = isValidEdge(fromLabel, edge.type, toLabel);
16288
- return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16352
+ }
16353
+ edgeRuleViolation(edge) {
16354
+ return localEdgeViolation(
16355
+ this.getNode(edge.from)?.label,
16356
+ edge.type,
16357
+ this.getNode(edge.to)?.label
16358
+ );
16289
16359
  }
16290
16360
  updateEdge(id, patch) {
16291
16361
  this.stmts.updateEdge.run({
@@ -16422,6 +16492,32 @@ var SqliteGraphStore = class {
16422
16492
  const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
16423
16493
  return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
16424
16494
  }
16495
+ /**
16496
+ * Live edges WITH their endpoint labels and ids, resolved in ONE join.
16497
+ *
16498
+ * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
16499
+ * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
16500
+ * 550,000 on this store — and each one deserializes the node's embedding blob.
16501
+ * Measured: the sweep did not finish in 10 minutes. As a single join it is one
16502
+ * query over an index-covered scan. Labels only; nothing here touches embeddings.
16503
+ */
16504
+ scanLiveEdgeRows() {
16505
+ const rows = this.db.prepare(
16506
+ `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
16507
+ FROM edges e
16508
+ LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
16509
+ LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
16510
+ WHERE e.valid_to IS NULL`
16511
+ ).all();
16512
+ return rows.map((r) => ({
16513
+ id: r.id,
16514
+ from: r.from_id,
16515
+ to: r.to_id,
16516
+ type: r.type,
16517
+ fromLabel: r.from_label ?? void 0,
16518
+ toLabel: r.to_label ?? void 0
16519
+ }));
16520
+ }
16425
16521
  /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
16426
16522
  * expression index so the incremental reindex fetches only the changed files'
16427
16523
  * symbols instead of scanning every versioned node. */