@inerrata-corporation/errata 2.0.0-dev.78 → 2.0.0-dev.80

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.
@@ -15110,9 +15110,90 @@ var init_zod = __esm({
15110
15110
  });
15111
15111
 
15112
15112
  // ../../packages/shared/src/edge-rules.ts
15113
+ function isValidEdge(from, type, to) {
15114
+ const rule = EDGE_RULES[type];
15115
+ if (!rule) return { ok: true };
15116
+ if (from && rule.from && !rule.from.includes(from)) {
15117
+ return {
15118
+ ok: false,
15119
+ reason: `${type} cannot originate from ${from} (allowed: ${rule.from.join(", ")})`
15120
+ };
15121
+ }
15122
+ if (to && rule.to && !rule.to.includes(to)) {
15123
+ return {
15124
+ ok: false,
15125
+ reason: `${type} cannot target ${to} (allowed: ${rule.to.join(", ")})`
15126
+ };
15127
+ }
15128
+ return { ok: true };
15129
+ }
15130
+ var EDGE_RULES;
15113
15131
  var init_edge_rules = __esm({
15114
15132
  "../../packages/shared/src/edge-rules.ts"() {
15115
15133
  "use strict";
15134
+ EDGE_RULES = {
15135
+ // ── Project anchors (PJ-anchors · PLAN_PROJECT_STORE §6c) ──
15136
+ // ANCHORED_AT is the ONE bridge edge admitted to the cloud, and it exists for
15137
+ // exactly one purpose: point a piece of knowledge at an OPAQUE project `Symbol`
15138
+ // so two teammates' observations about the same function converge on one node.
15139
+ // Constrain it to that purpose — an unconstrained ANCHORED_AT (its state before
15140
+ // this rule, since a missing entry means "anything goes") would let a batch
15141
+ // anchor anything at anything, e.g. at a public `Package`, quietly re-creating
15142
+ // the code↔collective bridge the membrane exists to prevent.
15143
+ ANCHORED_AT: {
15144
+ from: ["Problem", "Solution", "RootCause", "Claim"],
15145
+ to: ["Symbol"]
15146
+ },
15147
+ // ── Security spine (v1 taxonomy.ts SECURITY_FORWARD_ONLY commentary) ──
15148
+ TARGETS: { from: ["Exploit"], to: ["Vulnerability"] },
15149
+ ENABLES: { from: ["AntiPattern"], to: ["Vulnerability"] },
15150
+ MITIGATES: { from: ["Solution"], to: ["Vulnerability"] },
15151
+ PRESENTS: { from: ["Solution"], to: ["Problem", "Vulnerability"] },
15152
+ // SOLVED_BY sources: Vulnerability (v1 comment) + Problem/RootCause
15153
+ // (upstream design-problem.ts + triage.ts mint both).
15154
+ SOLVED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15155
+ // FIXED_BY sources mirror SOLVED_BY's (same ailment→fix semantics; live-graph
15156
+ // census 2026-07-14: 512 Problem + 13 RootCause sources, zero others besides
15157
+ // 3 REVERSED Solution→Problem edges the missing `from` let through — the
15158
+ // "Problem solved by Problem" splash bug. A reversed resolution edge is NOT
15159
+ // mechanically flippable (half the sample were PoC-not-fix claims), so the
15160
+ // door rejects rather than repairs; a correct re-emission lands via
15161
+ // recognition-at-the-door.
15162
+ FIXED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15163
+ // ── Triage family (upstream castalia.ts: Problem —TRIAGED_BY→ Triage —{INDICATES|CONFIRMS}→ cause) ──
15164
+ // Two-edge differential model (T3-rename): INDICATES = provisional candidate,
15165
+ // CONFIRMS = promoted router (a discriminator exists). Same endpoint matrix.
15166
+ // ROUTES_TO is the deprecated pre-split alias, kept readable during migration.
15167
+ TRIAGED_BY: { from: ["Problem"], to: ["Triage"] },
15168
+ CONFIRMS: { from: ["Triage"], to: ["RootCause", "Problem"] },
15169
+ INDICATES: { from: ["Triage"], to: ["RootCause", "Problem"] },
15170
+ ROUTES_TO: { from: ["Triage"], to: ["RootCause", "Problem"] },
15171
+ // ── Context grounding (v1 taxonomy.ts comments) ──
15172
+ WRITTEN_IN: { from: ["Component", "Package"], to: ["Language"] },
15173
+ OCCURS_IN: { to: ["Language"] },
15174
+ OCCURS_ON: { to: ["OperatingSystem"] },
15175
+ RUNS_ON: { to: ["OperatingSystem"] },
15176
+ OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15177
+ INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15178
+ // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
15179
+ IS_A: { from: ["Weakness"], to: ["Weakness"] },
15180
+ // ── Conceptual (v1 taxonomy.ts: instance → motif/pattern reference) ──
15181
+ INSTANCE_OF: { to: ["Pattern", "Weakness", "Technique"] },
15182
+ IMPLEMENTS: { from: ["Solution", "Language", "Component"], to: ["Pattern", "Technique"] },
15183
+ MATCHES: { to: ["Pattern"] },
15184
+ // ── Artifact evidence (v1 taxonomy.ts artifact rows) ──
15185
+ PRODUCED: { from: ["Report"], to: ["Artifact"] },
15186
+ MANIFESTED_IN: { from: ["Problem"], to: ["Artifact"] },
15187
+ REVEALED_BY: { from: ["RootCause"], to: ["Artifact"] },
15188
+ // ── Belief / abstraction gradients (upstream castalia.ts) ──
15189
+ CORROBORATES: { from: ["Claim"], to: ["Claim"] },
15190
+ GENERALIZES: { from: ["Claim"] },
15191
+ DERIVED_FROM: { from: ["Claim"] },
15192
+ // ── Decomposition (castalia.ts: a Problem fans out into independent root
15193
+ // problems; §5.1 also attaches SPLIT_INTO to Claim when one conflates two).
15194
+ // `to` left unconstrained — the replacement set's labels vary by case. ──
15195
+ SPLIT_INTO: { from: ["Problem", "Claim"] }
15196
+ };
15116
15197
  }
15117
15198
  });
15118
15199
 
@@ -15623,6 +15704,15 @@ import { parentPort } from "node:worker_threads";
15623
15704
 
15624
15705
  // ../../packages/local-graph/src/store.ts
15625
15706
  init_src2();
15707
+ init_src();
15708
+ var LOCAL_RULE_OVERRIDES = {
15709
+ ANCHORED_AT: {
15710
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
15711
+ to: [...CODE_NODE_LABELS, "Symbol"]
15712
+ },
15713
+ REVEALED_BY: null,
15714
+ PRODUCED: null
15715
+ };
15626
15716
  var SCHEMA_VERSION = 5;
15627
15717
  var SCHEMA_SQL = `
15628
15718
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -15772,6 +15862,10 @@ var SqliteGraphStore = class {
15772
15862
  * every node/edge upsert — code-index and capture alike — so the consolidation
15773
15863
  * trigger can read "how much changed" without caring what changed it. */
15774
15864
  mutations = 0;
15865
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
15866
+ * visible counter so a misbehaving extractor shows up in stats instead of
15867
+ * silently thinning the graph. */
15868
+ rejectedEdgeCount = 0;
15775
15869
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
15776
15870
  * from store_meta on open, advanced once per knowledge ingest event via
15777
15871
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -16081,6 +16175,12 @@ var SqliteGraphStore = class {
16081
16175
  this.stmts.setCommunity.run({ id, v: community });
16082
16176
  }
16083
16177
  mergeEdge(edge) {
16178
+ const violation = this.edgeRuleViolation(edge);
16179
+ if (violation) {
16180
+ this.rejectedEdgeCount++;
16181
+ console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
16182
+ return;
16183
+ }
16084
16184
  this.mutations++;
16085
16185
  this.stmts.insertEdge.run({
16086
16186
  id: edge.id,
@@ -16098,6 +16198,27 @@ var SqliteGraphStore = class {
16098
16198
  attrs_json: JSON.stringify(edge.attrs ?? {})
16099
16199
  });
16100
16200
  }
16201
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
16202
+ * overlay consulted first. Returns the reason string on a documented-
16203
+ * forbidden combination, else null. Point lookups on the id PK — negligible
16204
+ * next to the insert itself. */
16205
+ edgeRuleViolation(edge) {
16206
+ const fromLabel = this.getNode(edge.from)?.label;
16207
+ const toLabel = this.getNode(edge.to)?.label;
16208
+ if (edge.type in LOCAL_RULE_OVERRIDES) {
16209
+ const rule = LOCAL_RULE_OVERRIDES[edge.type];
16210
+ if (!rule) return null;
16211
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
16212
+ return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
16213
+ }
16214
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
16215
+ return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
16216
+ }
16217
+ return null;
16218
+ }
16219
+ const verdict = isValidEdge(fromLabel, edge.type, toLabel);
16220
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16221
+ }
16101
16222
  updateEdge(id, patch) {
16102
16223
  this.stmts.updateEdge.run({
16103
16224
  id,
package/errata.mjs CHANGED
@@ -15290,7 +15290,14 @@ var init_edge_rules = __esm({
15290
15290
  // SOLVED_BY sources: Vulnerability (v1 comment) + Problem/RootCause
15291
15291
  // (upstream design-problem.ts + triage.ts mint both).
15292
15292
  SOLVED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15293
- FIXED_BY: { to: ["Solution"] },
15293
+ // FIXED_BY sources mirror SOLVED_BY's (same ailment→fix semantics; live-graph
15294
+ // census 2026-07-14: 512 Problem + 13 RootCause sources, zero others besides
15295
+ // 3 REVERSED Solution→Problem edges the missing `from` let through — the
15296
+ // "Problem solved by Problem" splash bug. A reversed resolution edge is NOT
15297
+ // mechanically flippable (half the sample were PoC-not-fix claims), so the
15298
+ // door rejects rather than repairs; a correct re-emission lands via
15299
+ // recognition-at-the-door.
15300
+ FIXED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15294
15301
  // ── Triage family (upstream castalia.ts: Problem —TRIAGED_BY→ Triage —{INDICATES|CONFIRMS}→ cause) ──
15295
15302
  // Two-edge differential model (T3-rename): INDICATES = provisional candidate,
15296
15303
  // CONFIRMS = promoted router (a discriminator exists). Same endpoint matrix.
@@ -16181,11 +16188,20 @@ function rowToEdge(r) {
16181
16188
  function openGraphStore(opts) {
16182
16189
  return new SqliteGraphStore(opts);
16183
16190
  }
16184
- var SCHEMA_VERSION, SCHEMA_SQL, TEMPORAL_INDEX_SQL, SqliteGraphStore;
16191
+ var LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SCHEMA_SQL, TEMPORAL_INDEX_SQL, SqliteGraphStore;
16185
16192
  var init_store = __esm({
16186
16193
  "../../packages/local-graph/src/store.ts"() {
16187
16194
  "use strict";
16188
16195
  init_src2();
16196
+ init_src();
16197
+ LOCAL_RULE_OVERRIDES = {
16198
+ ANCHORED_AT: {
16199
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
16200
+ to: [...CODE_NODE_LABELS, "Symbol"]
16201
+ },
16202
+ REVEALED_BY: null,
16203
+ PRODUCED: null
16204
+ };
16189
16205
  SCHEMA_VERSION = 5;
16190
16206
  SCHEMA_SQL = `
16191
16207
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -16278,6 +16294,10 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16278
16294
  * every node/edge upsert — code-index and capture alike — so the consolidation
16279
16295
  * trigger can read "how much changed" without caring what changed it. */
16280
16296
  mutations = 0;
16297
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
16298
+ * visible counter so a misbehaving extractor shows up in stats instead of
16299
+ * silently thinning the graph. */
16300
+ rejectedEdgeCount = 0;
16281
16301
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
16282
16302
  * from store_meta on open, advanced once per knowledge ingest event via
16283
16303
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -16587,6 +16607,12 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16587
16607
  this.stmts.setCommunity.run({ id, v: community });
16588
16608
  }
16589
16609
  mergeEdge(edge2) {
16610
+ const violation = this.edgeRuleViolation(edge2);
16611
+ if (violation) {
16612
+ this.rejectedEdgeCount++;
16613
+ console.warn(`[local-graph] rejected edge ${edge2.from}-[:${edge2.type}]->${edge2.to}: ${violation}`);
16614
+ return;
16615
+ }
16590
16616
  this.mutations++;
16591
16617
  this.stmts.insertEdge.run({
16592
16618
  id: edge2.id,
@@ -16604,6 +16630,27 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16604
16630
  attrs_json: JSON.stringify(edge2.attrs ?? {})
16605
16631
  });
16606
16632
  }
16633
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
16634
+ * overlay consulted first. Returns the reason string on a documented-
16635
+ * forbidden combination, else null. Point lookups on the id PK — negligible
16636
+ * next to the insert itself. */
16637
+ edgeRuleViolation(edge2) {
16638
+ const fromLabel = this.getNode(edge2.from)?.label;
16639
+ const toLabel = this.getNode(edge2.to)?.label;
16640
+ if (edge2.type in LOCAL_RULE_OVERRIDES) {
16641
+ const rule = LOCAL_RULE_OVERRIDES[edge2.type];
16642
+ if (!rule) return null;
16643
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
16644
+ return `${edge2.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
16645
+ }
16646
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
16647
+ return `${edge2.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
16648
+ }
16649
+ return null;
16650
+ }
16651
+ const verdict = isValidEdge(fromLabel, edge2.type, toLabel);
16652
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16653
+ }
16607
16654
  updateEdge(id, patch) {
16608
16655
  this.stmts.updateEdge.run({
16609
16656
  id,
@@ -37668,6 +37715,17 @@ async function flushOutbox(paths, client, store) {
37668
37715
  if (res.accepted > 0) uploaded++;
37669
37716
  if (res.rejected > 0) {
37670
37717
  failed++;
37718
+ const rejections = [
37719
+ ...(res.result?.nodes ?? []).filter((n) => n.decision === "rejected"),
37720
+ ...(res.result?.edges ?? []).filter((e) => e.decision === "rejected")
37721
+ ];
37722
+ for (const r of rejections.slice(0, 5)) {
37723
+ const what = "type" in r ? `${r.from}-[:${r.type}]->${r.to}` : r.canonicalId ?? "node";
37724
+ console.warn(`[errata] outbox: cloud rejected ${what}: ${r.reason ?? "no reason given"}`);
37725
+ }
37726
+ if (rejections.length > 5) {
37727
+ console.warn(`[errata] outbox: \u2026and ${rejections.length - 5} more rejections in this batch`);
37728
+ }
37671
37729
  quarantine(abs, `semantic rejection (${res.rejected} item${res.rejected === 1 ? "" : "s"})`);
37672
37730
  continue;
37673
37731
  }
@@ -43321,7 +43379,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
43321
43379
  }
43322
43380
 
43323
43381
  // src/engine.ts
43324
- var DAEMON_VERSION = true ? "2.0.0-dev.78" : "2.0.0-alpha.0";
43382
+ var DAEMON_VERSION = true ? "2.0.0-dev.80" : "2.0.0-alpha.0";
43325
43383
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
43326
43384
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
43327
43385
  var GIT_OP_MUTE_MS = 4e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.0-dev.78",
3
+ "version": "2.0.0-dev.80",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -14965,9 +14965,90 @@ var init_zod = __esm({
14965
14965
  });
14966
14966
 
14967
14967
  // ../../packages/shared/src/edge-rules.ts
14968
+ function isValidEdge(from, type, to) {
14969
+ const rule = EDGE_RULES[type];
14970
+ if (!rule) return { ok: true };
14971
+ if (from && rule.from && !rule.from.includes(from)) {
14972
+ return {
14973
+ ok: false,
14974
+ reason: `${type} cannot originate from ${from} (allowed: ${rule.from.join(", ")})`
14975
+ };
14976
+ }
14977
+ if (to && rule.to && !rule.to.includes(to)) {
14978
+ return {
14979
+ ok: false,
14980
+ reason: `${type} cannot target ${to} (allowed: ${rule.to.join(", ")})`
14981
+ };
14982
+ }
14983
+ return { ok: true };
14984
+ }
14985
+ var EDGE_RULES;
14968
14986
  var init_edge_rules = __esm({
14969
14987
  "../../packages/shared/src/edge-rules.ts"() {
14970
14988
  "use strict";
14989
+ EDGE_RULES = {
14990
+ // ── Project anchors (PJ-anchors · PLAN_PROJECT_STORE §6c) ──
14991
+ // ANCHORED_AT is the ONE bridge edge admitted to the cloud, and it exists for
14992
+ // exactly one purpose: point a piece of knowledge at an OPAQUE project `Symbol`
14993
+ // so two teammates' observations about the same function converge on one node.
14994
+ // Constrain it to that purpose — an unconstrained ANCHORED_AT (its state before
14995
+ // this rule, since a missing entry means "anything goes") would let a batch
14996
+ // anchor anything at anything, e.g. at a public `Package`, quietly re-creating
14997
+ // the code↔collective bridge the membrane exists to prevent.
14998
+ ANCHORED_AT: {
14999
+ from: ["Problem", "Solution", "RootCause", "Claim"],
15000
+ to: ["Symbol"]
15001
+ },
15002
+ // ── Security spine (v1 taxonomy.ts SECURITY_FORWARD_ONLY commentary) ──
15003
+ TARGETS: { from: ["Exploit"], to: ["Vulnerability"] },
15004
+ ENABLES: { from: ["AntiPattern"], to: ["Vulnerability"] },
15005
+ MITIGATES: { from: ["Solution"], to: ["Vulnerability"] },
15006
+ PRESENTS: { from: ["Solution"], to: ["Problem", "Vulnerability"] },
15007
+ // SOLVED_BY sources: Vulnerability (v1 comment) + Problem/RootCause
15008
+ // (upstream design-problem.ts + triage.ts mint both).
15009
+ SOLVED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15010
+ // FIXED_BY sources mirror SOLVED_BY's (same ailment→fix semantics; live-graph
15011
+ // census 2026-07-14: 512 Problem + 13 RootCause sources, zero others besides
15012
+ // 3 REVERSED Solution→Problem edges the missing `from` let through — the
15013
+ // "Problem solved by Problem" splash bug. A reversed resolution edge is NOT
15014
+ // mechanically flippable (half the sample were PoC-not-fix claims), so the
15015
+ // door rejects rather than repairs; a correct re-emission lands via
15016
+ // recognition-at-the-door.
15017
+ FIXED_BY: { from: ["Vulnerability", "Problem", "RootCause"], to: ["Solution"] },
15018
+ // ── Triage family (upstream castalia.ts: Problem —TRIAGED_BY→ Triage —{INDICATES|CONFIRMS}→ cause) ──
15019
+ // Two-edge differential model (T3-rename): INDICATES = provisional candidate,
15020
+ // CONFIRMS = promoted router (a discriminator exists). Same endpoint matrix.
15021
+ // ROUTES_TO is the deprecated pre-split alias, kept readable during migration.
15022
+ TRIAGED_BY: { from: ["Problem"], to: ["Triage"] },
15023
+ CONFIRMS: { from: ["Triage"], to: ["RootCause", "Problem"] },
15024
+ INDICATES: { from: ["Triage"], to: ["RootCause", "Problem"] },
15025
+ ROUTES_TO: { from: ["Triage"], to: ["RootCause", "Problem"] },
15026
+ // ── Context grounding (v1 taxonomy.ts comments) ──
15027
+ WRITTEN_IN: { from: ["Component", "Package"], to: ["Language"] },
15028
+ OCCURS_IN: { to: ["Language"] },
15029
+ OCCURS_ON: { to: ["OperatingSystem"] },
15030
+ RUNS_ON: { to: ["OperatingSystem"] },
15031
+ OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15032
+ INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15033
+ // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
15034
+ IS_A: { from: ["Weakness"], to: ["Weakness"] },
15035
+ // ── Conceptual (v1 taxonomy.ts: instance → motif/pattern reference) ──
15036
+ INSTANCE_OF: { to: ["Pattern", "Weakness", "Technique"] },
15037
+ IMPLEMENTS: { from: ["Solution", "Language", "Component"], to: ["Pattern", "Technique"] },
15038
+ MATCHES: { to: ["Pattern"] },
15039
+ // ── Artifact evidence (v1 taxonomy.ts artifact rows) ──
15040
+ PRODUCED: { from: ["Report"], to: ["Artifact"] },
15041
+ MANIFESTED_IN: { from: ["Problem"], to: ["Artifact"] },
15042
+ REVEALED_BY: { from: ["RootCause"], to: ["Artifact"] },
15043
+ // ── Belief / abstraction gradients (upstream castalia.ts) ──
15044
+ CORROBORATES: { from: ["Claim"], to: ["Claim"] },
15045
+ GENERALIZES: { from: ["Claim"] },
15046
+ DERIVED_FROM: { from: ["Claim"] },
15047
+ // ── Decomposition (castalia.ts: a Problem fans out into independent root
15048
+ // problems; §5.1 also attaches SPLIT_INTO to Claim when one conflates two).
15049
+ // `to` left unconstrained — the replacement set's labels vary by case. ──
15050
+ SPLIT_INTO: { from: ["Problem", "Claim"] }
15051
+ };
14971
15052
  }
14972
15053
  });
14973
15054
 
@@ -23930,6 +24011,15 @@ import { parentPort, workerData } from "node:worker_threads";
23930
24011
 
23931
24012
  // ../../packages/local-graph/src/store.ts
23932
24013
  init_src2();
24014
+ init_src();
24015
+ var LOCAL_RULE_OVERRIDES = {
24016
+ ANCHORED_AT: {
24017
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
24018
+ to: [...CODE_NODE_LABELS, "Symbol"]
24019
+ },
24020
+ REVEALED_BY: null,
24021
+ PRODUCED: null
24022
+ };
23933
24023
  var SCHEMA_VERSION = 5;
23934
24024
  var SCHEMA_SQL = `
23935
24025
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -24079,6 +24169,10 @@ var SqliteGraphStore = class {
24079
24169
  * every node/edge upsert — code-index and capture alike — so the consolidation
24080
24170
  * trigger can read "how much changed" without caring what changed it. */
24081
24171
  mutations = 0;
24172
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
24173
+ * visible counter so a misbehaving extractor shows up in stats instead of
24174
+ * silently thinning the graph. */
24175
+ rejectedEdgeCount = 0;
24082
24176
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
24083
24177
  * from store_meta on open, advanced once per knowledge ingest event via
24084
24178
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -24388,6 +24482,12 @@ var SqliteGraphStore = class {
24388
24482
  this.stmts.setCommunity.run({ id, v: community });
24389
24483
  }
24390
24484
  mergeEdge(edge) {
24485
+ const violation = this.edgeRuleViolation(edge);
24486
+ if (violation) {
24487
+ this.rejectedEdgeCount++;
24488
+ console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24489
+ return;
24490
+ }
24391
24491
  this.mutations++;
24392
24492
  this.stmts.insertEdge.run({
24393
24493
  id: edge.id,
@@ -24405,6 +24505,27 @@ var SqliteGraphStore = class {
24405
24505
  attrs_json: JSON.stringify(edge.attrs ?? {})
24406
24506
  });
24407
24507
  }
24508
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
24509
+ * overlay consulted first. Returns the reason string on a documented-
24510
+ * forbidden combination, else null. Point lookups on the id PK — negligible
24511
+ * next to the insert itself. */
24512
+ edgeRuleViolation(edge) {
24513
+ const fromLabel = this.getNode(edge.from)?.label;
24514
+ const toLabel = this.getNode(edge.to)?.label;
24515
+ if (edge.type in LOCAL_RULE_OVERRIDES) {
24516
+ const rule = LOCAL_RULE_OVERRIDES[edge.type];
24517
+ if (!rule) return null;
24518
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24519
+ return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24520
+ }
24521
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24522
+ return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24523
+ }
24524
+ return null;
24525
+ }
24526
+ const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24527
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24528
+ }
24408
24529
  updateEdge(id, patch) {
24409
24530
  this.stmts.updateEdge.run({
24410
24531
  id,