@inerrata-corporation/errata 2.0.0-dev.79 → 2.0.0-dev.82

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
 
@@ -15274,6 +15355,13 @@ var init_project_symbol = __esm({
15274
15355
  }
15275
15356
  });
15276
15357
 
15358
+ // ../../packages/shared/src/lockfile.ts
15359
+ var init_lockfile = __esm({
15360
+ "../../packages/shared/src/lockfile.ts"() {
15361
+ "use strict";
15362
+ }
15363
+ });
15364
+
15277
15365
  // ../../packages/shared/src/nlp/canonicalize.ts
15278
15366
  var init_canonicalize = __esm({
15279
15367
  "../../packages/shared/src/nlp/canonicalize.ts"() {
@@ -15310,6 +15398,7 @@ var init_src = __esm({
15310
15398
  init_symbol_intent();
15311
15399
  init_repo_locator();
15312
15400
  init_project_symbol();
15401
+ init_lockfile();
15313
15402
  init_hash();
15314
15403
  init_error_signature();
15315
15404
  init_canonicalize();
@@ -15623,6 +15712,15 @@ import { parentPort } from "node:worker_threads";
15623
15712
 
15624
15713
  // ../../packages/local-graph/src/store.ts
15625
15714
  init_src2();
15715
+ init_src();
15716
+ var LOCAL_RULE_OVERRIDES = {
15717
+ ANCHORED_AT: {
15718
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
15719
+ to: [...CODE_NODE_LABELS, "Symbol"]
15720
+ },
15721
+ REVEALED_BY: null,
15722
+ PRODUCED: null
15723
+ };
15626
15724
  var SCHEMA_VERSION = 5;
15627
15725
  var SCHEMA_SQL = `
15628
15726
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -15772,6 +15870,10 @@ var SqliteGraphStore = class {
15772
15870
  * every node/edge upsert — code-index and capture alike — so the consolidation
15773
15871
  * trigger can read "how much changed" without caring what changed it. */
15774
15872
  mutations = 0;
15873
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
15874
+ * visible counter so a misbehaving extractor shows up in stats instead of
15875
+ * silently thinning the graph. */
15876
+ rejectedEdgeCount = 0;
15775
15877
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
15776
15878
  * from store_meta on open, advanced once per knowledge ingest event via
15777
15879
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -16081,6 +16183,12 @@ var SqliteGraphStore = class {
16081
16183
  this.stmts.setCommunity.run({ id, v: community });
16082
16184
  }
16083
16185
  mergeEdge(edge) {
16186
+ const violation = this.edgeRuleViolation(edge);
16187
+ if (violation) {
16188
+ this.rejectedEdgeCount++;
16189
+ console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
16190
+ return;
16191
+ }
16084
16192
  this.mutations++;
16085
16193
  this.stmts.insertEdge.run({
16086
16194
  id: edge.id,
@@ -16098,6 +16206,27 @@ var SqliteGraphStore = class {
16098
16206
  attrs_json: JSON.stringify(edge.attrs ?? {})
16099
16207
  });
16100
16208
  }
16209
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
16210
+ * overlay consulted first. Returns the reason string on a documented-
16211
+ * forbidden combination, else null. Point lookups on the id PK — negligible
16212
+ * next to the insert itself. */
16213
+ edgeRuleViolation(edge) {
16214
+ const fromLabel = this.getNode(edge.from)?.label;
16215
+ const toLabel = this.getNode(edge.to)?.label;
16216
+ if (edge.type in LOCAL_RULE_OVERRIDES) {
16217
+ const rule = LOCAL_RULE_OVERRIDES[edge.type];
16218
+ if (!rule) return null;
16219
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
16220
+ return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
16221
+ }
16222
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
16223
+ return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
16224
+ }
16225
+ return null;
16226
+ }
16227
+ const verdict = isValidEdge(fromLabel, edge.type, toLabel);
16228
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16229
+ }
16101
16230
  updateEdge(id, patch) {
16102
16231
  this.stmts.updateEdge.run({
16103
16232
  id,
package/errata.mjs CHANGED
@@ -15656,6 +15656,144 @@ var init_project_symbol = __esm({
15656
15656
  }
15657
15657
  });
15658
15658
 
15659
+ // ../../packages/shared/src/lockfile.ts
15660
+ function npmPurl(name2, version2) {
15661
+ return `pkg:npm/${name2}@${version2}`;
15662
+ }
15663
+ function component(name2, version2) {
15664
+ return { purl: npmPurl(name2, version2), name: name2, version: version2, ecosystem: "npm" };
15665
+ }
15666
+ function parsePackageLockJson(jsonText) {
15667
+ let doc;
15668
+ try {
15669
+ doc = JSON.parse(jsonText);
15670
+ } catch {
15671
+ return { components: [], dependencies: [] };
15672
+ }
15673
+ const packages = doc.packages ?? {};
15674
+ const components = [];
15675
+ const hoisted = /* @__PURE__ */ new Map();
15676
+ const byPath = /* @__PURE__ */ new Map();
15677
+ for (const [path2, meta3] of Object.entries(packages)) {
15678
+ if (path2 === "" || meta3.link === true) continue;
15679
+ const ix = path2.lastIndexOf("node_modules/");
15680
+ if (ix === -1) continue;
15681
+ const name2 = path2.slice(ix + "node_modules/".length);
15682
+ const version2 = typeof meta3.version === "string" ? meta3.version : "";
15683
+ if (!name2 || !version2) continue;
15684
+ components.push(component(name2, version2));
15685
+ byPath.set(path2, { name: name2, version: version2 });
15686
+ const depth = path2.split("node_modules/").length;
15687
+ const prev = hoisted.get(name2);
15688
+ if (!prev || depth < prev.depth) hoisted.set(name2, { version: version2, depth });
15689
+ }
15690
+ const dependencies = [];
15691
+ for (const [path2, meta3] of Object.entries(packages)) {
15692
+ const self = byPath.get(path2);
15693
+ if (!self) continue;
15694
+ const deps = meta3.dependencies;
15695
+ if (!deps || typeof deps !== "object") continue;
15696
+ const dependsOn = [];
15697
+ for (const depName of Object.keys(deps)) {
15698
+ const target = hoisted.get(depName);
15699
+ if (target) dependsOn.push(npmPurl(depName, target.version));
15700
+ }
15701
+ if (dependsOn.length > 0)
15702
+ dependencies.push({ ref: npmPurl(self.name, self.version), dependsOn });
15703
+ }
15704
+ return { components, dependencies: dedupeRefs(dependencies) };
15705
+ }
15706
+ function splitPnpmKey(raw2) {
15707
+ let k = raw2.trim().replace(/^['"]|['"]$/g, "");
15708
+ if (k.startsWith("/")) k = k.slice(1);
15709
+ const paren = k.indexOf("(");
15710
+ if (paren !== -1) k = k.slice(0, paren);
15711
+ if (k.startsWith("link:") || k.startsWith("file:") || k.includes("://")) return null;
15712
+ const at = k.lastIndexOf("@");
15713
+ if (at <= 0) return null;
15714
+ const name2 = k.slice(0, at);
15715
+ const version2 = k.slice(at + 1);
15716
+ if (!name2 || !version2) return null;
15717
+ return { name: name2, version: version2 };
15718
+ }
15719
+ function parsePnpmLock(text) {
15720
+ const lines = text.split(/\r?\n/);
15721
+ const components = [];
15722
+ const seen = /* @__PURE__ */ new Set();
15723
+ const dependencies = [];
15724
+ let section = null;
15725
+ let currentPkg = null;
15726
+ let inDepsBlock = false;
15727
+ let currentDeps = [];
15728
+ const flushDeps = () => {
15729
+ if (currentPkg && currentDeps.length > 0) {
15730
+ dependencies.push({
15731
+ ref: npmPurl(currentPkg.name, currentPkg.version),
15732
+ dependsOn: currentDeps.slice()
15733
+ });
15734
+ }
15735
+ currentDeps = [];
15736
+ };
15737
+ for (const line of lines) {
15738
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
15739
+ const indent = line.length - line.trimStart().length;
15740
+ if (indent === 0) {
15741
+ flushDeps();
15742
+ currentPkg = null;
15743
+ inDepsBlock = false;
15744
+ section = line === "packages:" ? "packages" : line === "snapshots:" ? "snapshots" : null;
15745
+ continue;
15746
+ }
15747
+ if (!section) continue;
15748
+ if (indent === 2 && line.trimEnd().endsWith(":")) {
15749
+ flushDeps();
15750
+ inDepsBlock = false;
15751
+ const key = line.trim().slice(0, -1);
15752
+ currentPkg = splitPnpmKey(key);
15753
+ if (currentPkg && section === "packages") {
15754
+ const purl = npmPurl(currentPkg.name, currentPkg.version);
15755
+ if (!seen.has(purl)) {
15756
+ seen.add(purl);
15757
+ components.push(component(currentPkg.name, currentPkg.version));
15758
+ }
15759
+ }
15760
+ continue;
15761
+ }
15762
+ if (!currentPkg) continue;
15763
+ if (indent === 4) {
15764
+ inDepsBlock = line.trim() === "dependencies:";
15765
+ continue;
15766
+ }
15767
+ if (inDepsBlock && indent === 6) {
15768
+ const t = line.trim();
15769
+ const at = t.indexOf(": ");
15770
+ if (at === -1) continue;
15771
+ const depName = t.slice(0, at).replace(/^['"]|['"]$/g, "");
15772
+ let depVersion = t.slice(at + 2).trim().replace(/^['"]|['"]$/g, "");
15773
+ const paren = depVersion.indexOf("(");
15774
+ if (paren !== -1) depVersion = depVersion.slice(0, paren);
15775
+ if (depName && depVersion && !depVersion.startsWith("link:"))
15776
+ currentDeps.push(npmPurl(depName, depVersion));
15777
+ }
15778
+ }
15779
+ flushDeps();
15780
+ return { components, dependencies: dedupeRefs(dependencies) };
15781
+ }
15782
+ function dedupeRefs(deps) {
15783
+ const byRef = /* @__PURE__ */ new Map();
15784
+ for (const d of deps) {
15785
+ const set2 = byRef.get(d.ref) ?? /* @__PURE__ */ new Set();
15786
+ for (const t of d.dependsOn) set2.add(t);
15787
+ byRef.set(d.ref, set2);
15788
+ }
15789
+ return [...byRef.entries()].map(([ref, set2]) => ({ ref, dependsOn: [...set2] }));
15790
+ }
15791
+ var init_lockfile = __esm({
15792
+ "../../packages/shared/src/lockfile.ts"() {
15793
+ "use strict";
15794
+ }
15795
+ });
15796
+
15659
15797
  // ../../packages/shared/src/nlp/canonicalize.ts
15660
15798
  function canonicalize(value) {
15661
15799
  return value.normalize("NFC").trim().toLowerCase().replace(/\s+/g, " ");
@@ -15774,9 +15912,12 @@ __export(src_exports, {
15774
15912
  normalizePredicate: () => normalizePredicate,
15775
15913
  normalizeRepoLocator: () => normalizeRepoLocator,
15776
15914
  normalizeSymbolPath: () => normalizeSymbolPath,
15915
+ npmPurl: () => npmPurl,
15777
15916
  packageCanonicalId: () => packageCanonicalId,
15778
15917
  pagerankEdgeTypes: () => pagerankEdgeTypes,
15918
+ parsePackageLockJson: () => parsePackageLockJson,
15779
15919
  parsePackageRef: () => parsePackageRef,
15920
+ parsePnpmLock: () => parsePnpmLock,
15780
15921
  prefilterEntities: () => prefilterEntities,
15781
15922
  problemIdFromError: () => problemIdFromError,
15782
15923
  projectSymbolId: () => projectSymbolId,
@@ -15785,6 +15926,7 @@ __export(src_exports, {
15785
15926
  resolveCanonicalId: () => resolveCanonicalId,
15786
15927
  sha256: () => sha256,
15787
15928
  sha256Short: () => sha256Short,
15929
+ splitPnpmKey: () => splitPnpmKey,
15788
15930
  substituteSymbolSummaries: () => substituteSymbolSummaries,
15789
15931
  summarizeIngestResult: () => summarizeIngestResult,
15790
15932
  summaryRejectionReason: () => summaryRejectionReason,
@@ -15802,6 +15944,7 @@ var init_src = __esm({
15802
15944
  init_symbol_intent();
15803
15945
  init_repo_locator();
15804
15946
  init_project_symbol();
15947
+ init_lockfile();
15805
15948
  init_hash();
15806
15949
  init_error_signature();
15807
15950
  init_canonicalize();
@@ -16188,11 +16331,20 @@ function rowToEdge(r) {
16188
16331
  function openGraphStore(opts) {
16189
16332
  return new SqliteGraphStore(opts);
16190
16333
  }
16191
- var SCHEMA_VERSION, SCHEMA_SQL, TEMPORAL_INDEX_SQL, SqliteGraphStore;
16334
+ var LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SCHEMA_SQL, TEMPORAL_INDEX_SQL, SqliteGraphStore;
16192
16335
  var init_store = __esm({
16193
16336
  "../../packages/local-graph/src/store.ts"() {
16194
16337
  "use strict";
16195
16338
  init_src2();
16339
+ init_src();
16340
+ LOCAL_RULE_OVERRIDES = {
16341
+ ANCHORED_AT: {
16342
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
16343
+ to: [...CODE_NODE_LABELS, "Symbol"]
16344
+ },
16345
+ REVEALED_BY: null,
16346
+ PRODUCED: null
16347
+ };
16196
16348
  SCHEMA_VERSION = 5;
16197
16349
  SCHEMA_SQL = `
16198
16350
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -16285,6 +16437,10 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16285
16437
  * every node/edge upsert — code-index and capture alike — so the consolidation
16286
16438
  * trigger can read "how much changed" without caring what changed it. */
16287
16439
  mutations = 0;
16440
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
16441
+ * visible counter so a misbehaving extractor shows up in stats instead of
16442
+ * silently thinning the graph. */
16443
+ rejectedEdgeCount = 0;
16288
16444
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
16289
16445
  * from store_meta on open, advanced once per knowledge ingest event via
16290
16446
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -16594,6 +16750,12 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16594
16750
  this.stmts.setCommunity.run({ id, v: community });
16595
16751
  }
16596
16752
  mergeEdge(edge2) {
16753
+ const violation = this.edgeRuleViolation(edge2);
16754
+ if (violation) {
16755
+ this.rejectedEdgeCount++;
16756
+ console.warn(`[local-graph] rejected edge ${edge2.from}-[:${edge2.type}]->${edge2.to}: ${violation}`);
16757
+ return;
16758
+ }
16597
16759
  this.mutations++;
16598
16760
  this.stmts.insertEdge.run({
16599
16761
  id: edge2.id,
@@ -16611,6 +16773,27 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
16611
16773
  attrs_json: JSON.stringify(edge2.attrs ?? {})
16612
16774
  });
16613
16775
  }
16776
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
16777
+ * overlay consulted first. Returns the reason string on a documented-
16778
+ * forbidden combination, else null. Point lookups on the id PK — negligible
16779
+ * next to the insert itself. */
16780
+ edgeRuleViolation(edge2) {
16781
+ const fromLabel = this.getNode(edge2.from)?.label;
16782
+ const toLabel = this.getNode(edge2.to)?.label;
16783
+ if (edge2.type in LOCAL_RULE_OVERRIDES) {
16784
+ const rule = LOCAL_RULE_OVERRIDES[edge2.type];
16785
+ if (!rule) return null;
16786
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
16787
+ return `${edge2.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
16788
+ }
16789
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
16790
+ return `${edge2.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
16791
+ }
16792
+ return null;
16793
+ }
16794
+ const verdict = isValidEdge(fromLabel, edge2.type, toLabel);
16795
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
16796
+ }
16614
16797
  updateEdge(id, patch) {
16615
16798
  this.stmts.updateEdge.run({
16616
16799
  id,
@@ -37675,6 +37858,17 @@ async function flushOutbox(paths, client, store) {
37675
37858
  if (res.accepted > 0) uploaded++;
37676
37859
  if (res.rejected > 0) {
37677
37860
  failed++;
37861
+ const rejections = [
37862
+ ...(res.result?.nodes ?? []).filter((n) => n.decision === "rejected"),
37863
+ ...(res.result?.edges ?? []).filter((e) => e.decision === "rejected")
37864
+ ];
37865
+ for (const r of rejections.slice(0, 5)) {
37866
+ const what = "type" in r ? `${r.from}-[:${r.type}]->${r.to}` : r.canonicalId ?? "node";
37867
+ console.warn(`[errata] outbox: cloud rejected ${what}: ${r.reason ?? "no reason given"}`);
37868
+ }
37869
+ if (rejections.length > 5) {
37870
+ console.warn(`[errata] outbox: \u2026and ${rejections.length - 5} more rejections in this batch`);
37871
+ }
37678
37872
  quarantine(abs, `semantic rejection (${res.rejected} item${res.rejected === 1 ? "" : "s"})`);
37679
37873
  continue;
37680
37874
  }
@@ -43328,7 +43522,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
43328
43522
  }
43329
43523
 
43330
43524
  // src/engine.ts
43331
- var DAEMON_VERSION = true ? "2.0.0-dev.79" : "2.0.0-alpha.0";
43525
+ var DAEMON_VERSION = true ? "2.0.0-dev.82" : "2.0.0-alpha.0";
43332
43526
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
43333
43527
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
43334
43528
  var GIT_OP_MUTE_MS = 4e3;
@@ -44772,6 +44966,7 @@ async function syncPrinciples(store, cloud, opts) {
44772
44966
  init_reconcile();
44773
44967
 
44774
44968
  // src/lockfile-auto.ts
44969
+ init_src();
44775
44970
  import { existsSync as existsSync20, readFileSync as readFileSync18 } from "node:fs";
44776
44971
  import { join as join24 } from "node:path";
44777
44972
 
@@ -44898,137 +45093,7 @@ function buildContextIngest(store, profile, daemonVersion, ignorePatterns = [],
44898
45093
  }
44899
45094
 
44900
45095
  // src/lockfile-auto.ts
44901
- function npmPurl(name2, version2) {
44902
- return `pkg:npm/${name2}@${version2}`;
44903
- }
44904
- function component(name2, version2) {
44905
- return { purl: npmPurl(name2, version2), name: name2, version: version2, ecosystem: "npm" };
44906
- }
44907
- function parsePackageLockJson(jsonText) {
44908
- let doc;
44909
- try {
44910
- doc = JSON.parse(jsonText);
44911
- } catch {
44912
- return { components: [], dependencies: [] };
44913
- }
44914
- const packages = doc.packages ?? {};
44915
- const components = [];
44916
- const hoisted = /* @__PURE__ */ new Map();
44917
- const byPath = /* @__PURE__ */ new Map();
44918
- for (const [path2, meta3] of Object.entries(packages)) {
44919
- if (path2 === "" || meta3.link === true) continue;
44920
- const ix = path2.lastIndexOf("node_modules/");
44921
- if (ix === -1) continue;
44922
- const name2 = path2.slice(ix + "node_modules/".length);
44923
- const version2 = typeof meta3.version === "string" ? meta3.version : "";
44924
- if (!name2 || !version2) continue;
44925
- components.push(component(name2, version2));
44926
- byPath.set(path2, { name: name2, version: version2 });
44927
- const depth = path2.split("node_modules/").length;
44928
- const prev = hoisted.get(name2);
44929
- if (!prev || depth < prev.depth) hoisted.set(name2, { version: version2, depth });
44930
- }
44931
- const dependencies = [];
44932
- for (const [path2, meta3] of Object.entries(packages)) {
44933
- const self = byPath.get(path2);
44934
- if (!self) continue;
44935
- const deps = meta3.dependencies;
44936
- if (!deps || typeof deps !== "object") continue;
44937
- const dependsOn = [];
44938
- for (const depName of Object.keys(deps)) {
44939
- const target = hoisted.get(depName);
44940
- if (target) dependsOn.push(npmPurl(depName, target.version));
44941
- }
44942
- if (dependsOn.length > 0)
44943
- dependencies.push({ ref: npmPurl(self.name, self.version), dependsOn });
44944
- }
44945
- return { components, dependencies: dedupeRefs(dependencies) };
44946
- }
44947
- function splitPnpmKey(raw2) {
44948
- let k = raw2.trim().replace(/^['"]|['"]$/g, "");
44949
- if (k.startsWith("/")) k = k.slice(1);
44950
- const paren = k.indexOf("(");
44951
- if (paren !== -1) k = k.slice(0, paren);
44952
- if (k.startsWith("link:") || k.startsWith("file:") || k.includes("://")) return null;
44953
- const at = k.lastIndexOf("@");
44954
- if (at <= 0) return null;
44955
- const name2 = k.slice(0, at);
44956
- const version2 = k.slice(at + 1);
44957
- if (!name2 || !version2) return null;
44958
- return { name: name2, version: version2 };
44959
- }
44960
- function parsePnpmLock(text) {
44961
- const lines = text.split(/\r?\n/);
44962
- const components = [];
44963
- const seen = /* @__PURE__ */ new Set();
44964
- const dependencies = [];
44965
- let section = null;
44966
- let currentPkg = null;
44967
- let inDepsBlock = false;
44968
- let currentDeps = [];
44969
- const flushDeps = () => {
44970
- if (currentPkg && currentDeps.length > 0) {
44971
- dependencies.push({
44972
- ref: npmPurl(currentPkg.name, currentPkg.version),
44973
- dependsOn: currentDeps.slice()
44974
- });
44975
- }
44976
- currentDeps = [];
44977
- };
44978
- for (const line of lines) {
44979
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
44980
- const indent = line.length - line.trimStart().length;
44981
- if (indent === 0) {
44982
- flushDeps();
44983
- currentPkg = null;
44984
- inDepsBlock = false;
44985
- section = line === "packages:" ? "packages" : line === "snapshots:" ? "snapshots" : null;
44986
- continue;
44987
- }
44988
- if (!section) continue;
44989
- if (indent === 2 && line.trimEnd().endsWith(":")) {
44990
- flushDeps();
44991
- inDepsBlock = false;
44992
- const key = line.trim().slice(0, -1);
44993
- currentPkg = splitPnpmKey(key);
44994
- if (currentPkg && section === "packages") {
44995
- const purl = npmPurl(currentPkg.name, currentPkg.version);
44996
- if (!seen.has(purl)) {
44997
- seen.add(purl);
44998
- components.push(component(currentPkg.name, currentPkg.version));
44999
- }
45000
- }
45001
- continue;
45002
- }
45003
- if (!currentPkg) continue;
45004
- if (indent === 4) {
45005
- inDepsBlock = line.trim() === "dependencies:";
45006
- continue;
45007
- }
45008
- if (inDepsBlock && indent === 6) {
45009
- const t = line.trim();
45010
- const at = t.indexOf(": ");
45011
- if (at === -1) continue;
45012
- const depName = t.slice(0, at).replace(/^['"]|['"]$/g, "");
45013
- let depVersion = t.slice(at + 2).trim().replace(/^['"]|['"]$/g, "");
45014
- const paren = depVersion.indexOf("(");
45015
- if (paren !== -1) depVersion = depVersion.slice(0, paren);
45016
- if (depName && depVersion && !depVersion.startsWith("link:"))
45017
- currentDeps.push(npmPurl(depName, depVersion));
45018
- }
45019
- }
45020
- flushDeps();
45021
- return { components, dependencies: dedupeRefs(dependencies) };
45022
- }
45023
- function dedupeRefs(deps) {
45024
- const byRef = /* @__PURE__ */ new Map();
45025
- for (const d of deps) {
45026
- const set2 = byRef.get(d.ref) ?? /* @__PURE__ */ new Set();
45027
- for (const t of d.dependsOn) set2.add(t);
45028
- byRef.set(d.ref, set2);
45029
- }
45030
- return [...byRef.entries()].map(([ref, set2]) => ({ ref, dependsOn: [...set2] }));
45031
- }
45096
+ init_src();
45032
45097
  function runLockfilePass(opts) {
45033
45098
  const ts = Date.now();
45034
45099
  const languages = mintLanguageNodes(opts.store, opts.languages, opts.workspaceId, ts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.0-dev.79",
3
+ "version": "2.0.0-dev.82",
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
 
@@ -15129,6 +15210,13 @@ var init_project_symbol = __esm({
15129
15210
  }
15130
15211
  });
15131
15212
 
15213
+ // ../../packages/shared/src/lockfile.ts
15214
+ var init_lockfile = __esm({
15215
+ "../../packages/shared/src/lockfile.ts"() {
15216
+ "use strict";
15217
+ }
15218
+ });
15219
+
15132
15220
  // ../../packages/shared/src/nlp/canonicalize.ts
15133
15221
  var init_canonicalize = __esm({
15134
15222
  "../../packages/shared/src/nlp/canonicalize.ts"() {
@@ -15155,6 +15243,7 @@ var init_src = __esm({
15155
15243
  init_symbol_intent();
15156
15244
  init_repo_locator();
15157
15245
  init_project_symbol();
15246
+ init_lockfile();
15158
15247
  init_hash();
15159
15248
  init_error_signature();
15160
15249
  init_canonicalize();
@@ -23930,6 +24019,15 @@ import { parentPort, workerData } from "node:worker_threads";
23930
24019
 
23931
24020
  // ../../packages/local-graph/src/store.ts
23932
24021
  init_src2();
24022
+ init_src();
24023
+ var LOCAL_RULE_OVERRIDES = {
24024
+ ANCHORED_AT: {
24025
+ from: EDGE_RULES["ANCHORED_AT"]?.from,
24026
+ to: [...CODE_NODE_LABELS, "Symbol"]
24027
+ },
24028
+ REVEALED_BY: null,
24029
+ PRODUCED: null
24030
+ };
23933
24031
  var SCHEMA_VERSION = 5;
23934
24032
  var SCHEMA_SQL = `
23935
24033
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -24079,6 +24177,10 @@ var SqliteGraphStore = class {
24079
24177
  * every node/edge upsert — code-index and capture alike — so the consolidation
24080
24178
  * trigger can read "how much changed" without caring what changed it. */
24081
24179
  mutations = 0;
24180
+ /** Edges refused by the local ontology gate (EDGE_RULES) since open — a
24181
+ * visible counter so a misbehaving extractor shows up in stats instead of
24182
+ * silently thinning the graph. */
24183
+ rejectedEdgeCount = 0;
24082
24184
  /** Persistent monotonic ingest-sequence high-water (KN-decay-momentum). Loaded
24083
24185
  * from store_meta on open, advanced once per knowledge ingest event via
24084
24186
  * nextIngestSeq(). Distinct from `mutations` (in-memory, per-session, bumped by
@@ -24388,6 +24490,12 @@ var SqliteGraphStore = class {
24388
24490
  this.stmts.setCommunity.run({ id, v: community });
24389
24491
  }
24390
24492
  mergeEdge(edge) {
24493
+ const violation = this.edgeRuleViolation(edge);
24494
+ if (violation) {
24495
+ this.rejectedEdgeCount++;
24496
+ console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24497
+ return;
24498
+ }
24391
24499
  this.mutations++;
24392
24500
  this.stmts.insertEdge.run({
24393
24501
  id: edge.id,
@@ -24405,6 +24513,27 @@ var SqliteGraphStore = class {
24405
24513
  attrs_json: JSON.stringify(edge.attrs ?? {})
24406
24514
  });
24407
24515
  }
24516
+ /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
24517
+ * overlay consulted first. Returns the reason string on a documented-
24518
+ * forbidden combination, else null. Point lookups on the id PK — negligible
24519
+ * next to the insert itself. */
24520
+ edgeRuleViolation(edge) {
24521
+ const fromLabel = this.getNode(edge.from)?.label;
24522
+ const toLabel = this.getNode(edge.to)?.label;
24523
+ if (edge.type in LOCAL_RULE_OVERRIDES) {
24524
+ const rule = LOCAL_RULE_OVERRIDES[edge.type];
24525
+ if (!rule) return null;
24526
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24527
+ return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24528
+ }
24529
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24530
+ return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24531
+ }
24532
+ return null;
24533
+ }
24534
+ const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24535
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24536
+ }
24408
24537
  updateEdge(id, patch) {
24409
24538
  this.stmts.updateEdge.run({
24410
24539
  id,