@inerrata-corporation/errata 2.0.2-dev.245 → 2.0.2-dev.249

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.2-dev.245",
3
+ "version": "2.0.2-dev.249",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -24081,7 +24081,22 @@ var LOCAL_RULE_OVERRIDES = {
24081
24081
  REVEALED_BY: null,
24082
24082
  PRODUCED: null
24083
24083
  };
24084
- var SCHEMA_VERSION = 5;
24084
+ function localEdgeViolation(fromLabel, type, toLabel) {
24085
+ if (type in LOCAL_RULE_OVERRIDES) {
24086
+ const rule = LOCAL_RULE_OVERRIDES[type];
24087
+ if (!rule) return null;
24088
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24089
+ return `${type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24090
+ }
24091
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24092
+ return `${type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24093
+ }
24094
+ return null;
24095
+ }
24096
+ const verdict = isValidEdge(fromLabel, type, toLabel);
24097
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24098
+ }
24099
+ var SCHEMA_VERSION = 6;
24085
24100
  var SCHEMA_SQL = `
24086
24101
  CREATE TABLE IF NOT EXISTS schema_version (
24087
24102
  version INTEGER PRIMARY KEY
@@ -24096,6 +24111,21 @@ CREATE TABLE IF NOT EXISTS store_meta (
24096
24111
  value TEXT NOT NULL
24097
24112
  );
24098
24113
 
24114
+ -- Durable ledger of edges the ontology gate REFUSED, keyed by edge type.
24115
+ -- Durable rather than in-memory for one specific reason: the status command runs
24116
+ -- in a SEPARATE process and opens its own store handle, so a counter living on
24117
+ -- the instance reads 0 there forever. That is exactly how a producer rejecting
24118
+ -- 100% of its output stayed invisible for 17 days. Persisting it also survives
24119
+ -- the daemon restart that would otherwise erase the evidence.
24120
+ -- Keyed by type because a systematic producer bug shows up as ONE type
24121
+ -- dominating; sample keeps the latest reason so the count is actionable.
24122
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24123
+ type TEXT PRIMARY KEY,
24124
+ count INTEGER NOT NULL DEFAULT 0,
24125
+ last_at INTEGER NOT NULL,
24126
+ sample TEXT
24127
+ );
24128
+
24099
24129
  CREATE TABLE IF NOT EXISTS nodes (
24100
24130
  id TEXT PRIMARY KEY,
24101
24131
  label TEXT NOT NULL,
@@ -24472,6 +24502,16 @@ var SqliteGraphStore = class {
24472
24502
  if (!cols.has(name2)) this.db.exec(ddl);
24473
24503
  }
24474
24504
  }
24505
+ if (from < 6) {
24506
+ this.db.exec(`
24507
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24508
+ type TEXT PRIMARY KEY,
24509
+ count INTEGER NOT NULL DEFAULT 0,
24510
+ last_at INTEGER NOT NULL,
24511
+ sample TEXT
24512
+ )
24513
+ `);
24514
+ }
24475
24515
  this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
24476
24516
  });
24477
24517
  }
@@ -24553,6 +24593,7 @@ var SqliteGraphStore = class {
24553
24593
  const violation = this.edgeRuleViolation(edge);
24554
24594
  if (violation) {
24555
24595
  this.rejectedEdgeCount++;
24596
+ this.recordEdgeRejection(edge.type, violation, edge.lastSeenAt || edge.createdAt || 0);
24556
24597
  console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24557
24598
  return;
24558
24599
  }
@@ -24577,22 +24618,51 @@ var SqliteGraphStore = class {
24577
24618
  * overlay consulted first. Returns the reason string on a documented-
24578
24619
  * forbidden combination, else null. Point lookups on the id PK — negligible
24579
24620
  * next to the insert itself. */
24580
- edgeRuleViolation(edge) {
24581
- const fromLabel = this.getNode(edge.from)?.label;
24582
- const toLabel = this.getNode(edge.to)?.label;
24583
- if (edge.type in LOCAL_RULE_OVERRIDES) {
24584
- const rule = LOCAL_RULE_OVERRIDES[edge.type];
24585
- if (!rule) return null;
24586
- if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24587
- return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24588
- }
24589
- if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24590
- return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24591
- }
24592
- return null;
24621
+ /** Upsert one refusal into the durable ledger. Best-effort: a bookkeeping
24622
+ * failure must never turn a refused edge into a thrown write. */
24623
+ recordEdgeRejection(type, reason, at) {
24624
+ try {
24625
+ this.db.prepare(
24626
+ `INSERT INTO edge_rejections (type, count, last_at, sample) VALUES (?, 1, ?, ?)
24627
+ ON CONFLICT(type) DO UPDATE SET
24628
+ count = count + 1, last_at = excluded.last_at, sample = excluded.sample`
24629
+ ).run(type, at, reason.slice(0, 200));
24630
+ } catch {
24631
+ }
24632
+ }
24633
+ /** Refusals recorded by the ontology gate, per edge type, newest activity first.
24634
+ * Durable across restarts and readable from any process (see the table note). */
24635
+ edgeRejections() {
24636
+ try {
24637
+ return this.db.prepare(
24638
+ "SELECT type, count, last_at AS lastAt, sample FROM edge_rejections ORDER BY count DESC, last_at DESC"
24639
+ ).all();
24640
+ } catch {
24641
+ return [];
24642
+ }
24643
+ }
24644
+ /** Drop ledger entries whose last refusal predates `cutoff`. A still-misbehaving
24645
+ * producer keeps refreshing `last_at` and survives; a fixed one fades out. */
24646
+ pruneEdgeRejections(cutoff) {
24647
+ try {
24648
+ this.db.prepare("DELETE FROM edge_rejections WHERE last_at < ?").run(cutoff);
24649
+ } catch {
24593
24650
  }
24594
- const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24595
- return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24651
+ }
24652
+ /** Clear the ledger outright, whole or per type — operator escape hatch. */
24653
+ clearEdgeRejections(type) {
24654
+ try {
24655
+ if (type) this.db.prepare("DELETE FROM edge_rejections WHERE type = ?").run(type);
24656
+ else this.db.exec("DELETE FROM edge_rejections");
24657
+ } catch {
24658
+ }
24659
+ }
24660
+ edgeRuleViolation(edge) {
24661
+ return localEdgeViolation(
24662
+ this.getNode(edge.from)?.label,
24663
+ edge.type,
24664
+ this.getNode(edge.to)?.label
24665
+ );
24596
24666
  }
24597
24667
  updateEdge(id, patch) {
24598
24668
  this.stmts.updateEdge.run({
@@ -24729,6 +24799,32 @@ var SqliteGraphStore = class {
24729
24799
  const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
24730
24800
  return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
24731
24801
  }
24802
+ /**
24803
+ * Live edges WITH their endpoint labels and ids, resolved in ONE join.
24804
+ *
24805
+ * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
24806
+ * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
24807
+ * 550,000 on this store — and each one deserializes the node's embedding blob.
24808
+ * Measured: the sweep did not finish in 10 minutes. As a single join it is one
24809
+ * query over an index-covered scan. Labels only; nothing here touches embeddings.
24810
+ */
24811
+ scanLiveEdgeRows() {
24812
+ const rows = this.db.prepare(
24813
+ `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
24814
+ FROM edges e
24815
+ LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
24816
+ LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
24817
+ WHERE e.valid_to IS NULL`
24818
+ ).all();
24819
+ return rows.map((r) => ({
24820
+ id: r.id,
24821
+ from: r.from_id,
24822
+ to: r.to_id,
24823
+ type: r.type,
24824
+ fromLabel: r.from_label ?? void 0,
24825
+ toLabel: r.to_label ?? void 0
24826
+ }));
24827
+ }
24732
24828
  /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
24733
24829
  * expression index so the incremental reindex fetches only the changed files'
24734
24830
  * symbols instead of scanning every versioned node. */