@inerrata-corporation/errata 2.0.15 → 2.0.16-dev.3343

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.
@@ -802,6 +802,12 @@ function identityId(input) {
802
802
  }
803
803
  case "Claim":
804
804
  return canonicalClaimId(input.text, input.scope);
805
+ case "WitnessedVulnerability":
806
+ return genericCanonicalId("dvuln", {
807
+ workspaceId: input.workspaceId,
808
+ target: input.target.replace(/\\/g, "/").replace(/^\.\//, ""),
809
+ statement: canonicalClaimId(input.statement.trim())
810
+ });
805
811
  case "DesignProblem":
806
812
  return `${DESIGN_PROBLEM_PREFIX}${canonicalClaimId(input.statement.trim())}`.slice(
807
813
  0,
@@ -15258,7 +15264,7 @@ var init_edge_rules = __esm({
15258
15264
  // anchor anything at anything, e.g. at a public `Package`, quietly re-creating
15259
15265
  // the code↔collective bridge the membrane exists to prevent.
15260
15266
  ANCHORED_AT: {
15261
- from: ["Problem", "Solution", "RootCause", "Claim"],
15267
+ from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim"],
15262
15268
  to: ["Symbol"]
15263
15269
  },
15264
15270
  // ── Security spine (v1 taxonomy.ts SECURITY_FORWARD_ONLY commentary) ──
@@ -15303,7 +15309,7 @@ var init_edge_rules = __esm({
15303
15309
  // `Component` joined the target set with OM-agent-anchors: an agent-named
15304
15310
  // component ("React Router") is the same knowledge→named-unit anchor shape as
15305
15311
  // a Tool — the knowledge is ABOUT it, not dependent on it.
15306
- CONCERNS: { from: ["Problem", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15312
+ CONCERNS: { from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15307
15313
  OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15308
15314
  INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15309
15315
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
@@ -16410,7 +16416,8 @@ CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
16410
16416
  -- its table row. Partial, so it maintains a few thousand rows instead of adding
16411
16417
  -- a write to every node write. Its label list is FINGERPRINT_INDEXED_LABELS.
16412
16418
  DROP INDEX IF EXISTS nodes_live_fingerprint;
16413
- CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause');
16419
+ DROP INDEX IF EXISTS nodes_instance_fingerprint;
16420
+ CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause');
16414
16421
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
16415
16422
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
16416
16423
  -- Expression index on the per-node relPath (stored in attrs_json) so the
@@ -16481,7 +16488,7 @@ function rowToEdge(r) {
16481
16488
  attrs: JSON.parse(r.attrs_json)
16482
16489
  };
16483
16490
  }
16484
- var FINGERPRINT_INDEXED_LABELS = ["Problem", "Solution", "RootCause"];
16491
+ var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
16485
16492
  var SqliteGraphStore = class {
16486
16493
  db;
16487
16494
  // Typed edge lookups build their SQL from the number of types asked for, and
@@ -17159,8 +17166,8 @@ var SqliteGraphStore = class {
17159
17166
  const r2 = this.db.prepare(
17160
17167
  `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
17161
17168
  MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
17162
- FROM nodes INDEXED BY nodes_instance_fingerprint
17163
- WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause')`
17169
+ FROM nodes INDEXED BY nodes_instance_fingerprint_v2
17170
+ WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
17164
17171
  ).get();
17165
17172
  return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
17166
17173
  }
@@ -19389,7 +19396,7 @@ function generalizeFileRefs(text, fileLexicon, level = 1) {
19389
19396
  }
19390
19397
 
19391
19398
  // src/instance-ingest.ts
19392
- var INSTANCE_LABELS = ["Problem", "Solution", "RootCause"];
19399
+ var INSTANCE_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
19393
19400
  var INSTANCE_EDGES = ["CAUSED_BY", "SOLVED_BY"];
19394
19401
  var ANCHOR_EDGES = ["OCCURS_IN", "DEPENDS_ON", "PERTAIN_TO", "CONCERNS"];
19395
19402
  var ANCHOR_TARGET_LABELS = ["Language", "Package", "Domain", "Component"];
@@ -19509,6 +19516,10 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19509
19516
  attrs: {
19510
19517
  scope: stripCodebaseScope(n.attrs["scope"]),
19511
19518
  ...n.attrs["kind"] ? { kind: n.attrs["kind"] } : {},
19519
+ ...n.attrs["privateFinding"] ? { privateFinding: true } : {},
19520
+ ...n.attrs["assessment"] ? { assessment: n.attrs["assessment"] } : {},
19521
+ ...n.attrs["sourceClass"] ? { sourceClass: n.attrs["sourceClass"] } : {},
19522
+ ...n.attrs["resolutionWitnessed"] ? { resolutionWitnessed: true, resolvedAt: n.attrs["resolvedAt"] } : {},
19512
19523
  ...n.attrs["resolvedAs"] ? { resolvedAs: n.attrs["resolvedAs"] } : {},
19513
19524
  ...preserved.length > 0 ? { preservedSymbols: preserved } : {},
19514
19525
  ...internal.length > 0 ? { internalSymbols: internal } : {}
@@ -19523,6 +19534,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19523
19534
  const semanticEndpoints = /* @__PURE__ */ new Map();
19524
19535
  const includedByLabel = /* @__PURE__ */ new Map();
19525
19536
  const composition = {
19537
+ privateTargetMissing: 0,
19526
19538
  candidates: 0,
19527
19539
  shipped: 0,
19528
19540
  cloudSourced: 0,
@@ -19568,11 +19580,15 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19568
19580
  if (walked.has(n.id)) continue;
19569
19581
  walked.add(n.id);
19570
19582
  composition.candidates++;
19583
+ if ((n.attrs["privateFinding"] || n.label === "Vulnerability") && !opts.project) {
19584
+ composition.privateTargetMissing++;
19585
+ continue;
19586
+ }
19571
19587
  if (n.attrs["source"] === "cloud") {
19572
19588
  composition.cloudSourced++;
19573
19589
  continue;
19574
19590
  }
19575
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
19591
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
19576
19592
  composition.falsePositive++;
19577
19593
  continue;
19578
19594
  }
@@ -19595,7 +19611,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19595
19611
  composition.terminalRefused++;
19596
19612
  continue;
19597
19613
  }
19598
- if (!noveltyAgainst(n, ref, noveltyOpts).ready) {
19614
+ if (!n.attrs["privateFinding"] && !noveltyAgainst(n, ref, noveltyOpts).ready) {
19599
19615
  composition.notNovel++;
19600
19616
  continue;
19601
19617
  }
@@ -19638,7 +19654,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19638
19654
  }
19639
19655
  if (targets.length === 0) continue;
19640
19656
  const shipped = shippedById.get(sourceId);
19641
- if (shipped) {
19657
+ if (shipped && !shipped.attrs["privateFinding"]) {
19642
19658
  const anchors = (label) => targets.filter((x) => x.t.label === label).map(
19643
19659
  (x) => label === "Language" ? langAnchor(x.t) : label === "Component" ? `component:${x.wireId}` : x.wireId
19644
19660
  ).filter((a) => a != null).sort();
package/errata.mjs CHANGED
@@ -139,6 +139,7 @@ var init_castalia = __esm({
139
139
  ROUTING_REASONS = [
140
140
  /** Harness org (CTF lane, e2e battery, canary) — forced org-private. */
141
141
  "harness-clamp",
142
+ "witnessed-vulnerability-private",
142
143
  /** A signed gateway write target named the destination. */
143
144
  "signed-target-public",
144
145
  "signed-target-org",
@@ -1080,6 +1081,12 @@ function identityId(input) {
1080
1081
  }
1081
1082
  case "Claim":
1082
1083
  return canonicalClaimId(input.text, input.scope);
1084
+ case "WitnessedVulnerability":
1085
+ return genericCanonicalId("dvuln", {
1086
+ workspaceId: input.workspaceId,
1087
+ target: input.target.replace(/\\/g, "/").replace(/^\.\//, ""),
1088
+ statement: canonicalClaimId(input.statement.trim())
1089
+ });
1083
1090
  case "DesignProblem":
1084
1091
  return `${DESIGN_PROBLEM_PREFIX}${canonicalClaimId(input.statement.trim())}`.slice(
1085
1092
  0,
@@ -15849,7 +15856,7 @@ var init_edge_rules = __esm({
15849
15856
  // anchor anything at anything, e.g. at a public `Package`, quietly re-creating
15850
15857
  // the code↔collective bridge the membrane exists to prevent.
15851
15858
  ANCHORED_AT: {
15852
- from: ["Problem", "Solution", "RootCause", "Claim"],
15859
+ from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim"],
15853
15860
  to: ["Symbol"]
15854
15861
  },
15855
15862
  // ── Security spine (v1 taxonomy.ts SECURITY_FORWARD_ONLY commentary) ──
@@ -15894,7 +15901,7 @@ var init_edge_rules = __esm({
15894
15901
  // `Component` joined the target set with OM-agent-anchors: an agent-named
15895
15902
  // component ("React Router") is the same knowledge→named-unit anchor shape as
15896
15903
  // a Tool — the knowledge is ABOUT it, not dependent on it.
15897
- CONCERNS: { from: ["Problem", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15904
+ CONCERNS: { from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15898
15905
  OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15899
15906
  INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15900
15907
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
@@ -18268,7 +18275,8 @@ CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
18268
18275
  -- its table row. Partial, so it maintains a few thousand rows instead of adding
18269
18276
  -- a write to every node write. Its label list is FINGERPRINT_INDEXED_LABELS.
18270
18277
  DROP INDEX IF EXISTS nodes_live_fingerprint;
18271
- CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause');
18278
+ DROP INDEX IF EXISTS nodes_instance_fingerprint;
18279
+ CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause');
18272
18280
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
18273
18281
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
18274
18282
  -- Expression index on the per-node relPath (stored in attrs_json) so the
@@ -18282,7 +18290,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18282
18290
  -- every edited file of every turn, and a scan parses every Problem's attrs.
18283
18291
  CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
18284
18292
  `;
18285
- FINGERPRINT_INDEXED_LABELS = ["Problem", "Solution", "RootCause"];
18293
+ FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
18286
18294
  SqliteGraphStore = class {
18287
18295
  db;
18288
18296
  // Typed edge lookups build their SQL from the number of types asked for, and
@@ -18960,8 +18968,8 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
18960
18968
  const r2 = this.db.prepare(
18961
18969
  `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
18962
18970
  MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
18963
- FROM nodes INDEXED BY nodes_instance_fingerprint
18964
- WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause')`
18971
+ FROM nodes INDEXED BY nodes_instance_fingerprint_v2
18972
+ WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
18965
18973
  ).get();
18966
18974
  return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
18967
18975
  }
@@ -20257,7 +20265,7 @@ function mergeContextEdge(store, from, to, type, ts) {
20257
20265
  function deriveContextFromAnchors(store, problemId, ts, pkgIndex) {
20258
20266
  const report = { languages: [], packages: [] };
20259
20267
  const p = store.getNode(problemId);
20260
- if (!p || p.label !== "Problem") return report;
20268
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return report;
20261
20269
  const files = store.outEdges(problemId, ["ANCHORED_AT"]).map((e) => store.getNode(e.to)).filter((n) => !!n && n.label === "File");
20262
20270
  if (files.length === 0) return report;
20263
20271
  const idx = pkgIndex ?? buildPackageIndex(store);
@@ -20895,6 +20903,81 @@ function ingestDesignProblem(store, flag, opts) {
20895
20903
  ...reopenedFromRegression ? { reopenedFromRegression } : {}
20896
20904
  };
20897
20905
  }
20906
+ function ingestWitnessedVulnerability(store, flag, opts) {
20907
+ const statement = flag.statement.trim();
20908
+ if (isPlaceholderStatement(statement)) return { id: "", created: false, corroborated: false, upgraded: false, rejected: "statement" };
20909
+ const target = flag.target?.trim().replace(/\\/g, "/").replace(/^\.\//, "") ?? "";
20910
+ const file2 = target ? resolveFileNode(store, target, opts.workspaceId) : null;
20911
+ const symbol2 = file2 && flag.line !== void 0 && flag.fileText !== void 0 ? resolveEnclosingSymbol(store, file2.id, flag.line, flag.fileText) : null;
20912
+ const identityTarget = symbol2 ? String(symbol2.attrs["logicalId"] ?? `${target}#${symbol2.attrs["qname"] ?? symbol2.description}`) : target;
20913
+ const compatibleTarget = (n) => !symbol2 || !n.attrs["identityTarget"] || n.attrs["identityTarget"] === target || n.attrs["identityTarget"] === identityTarget;
20914
+ const bound = flag.existingId ? store.getNode(flag.existingId) : null;
20915
+ if (flag.existingId && (!bound || bound.label !== "Vulnerability" || bound.attrs["workspaceId"] !== opts.workspaceId || target && bound.attrs["affectedTarget"] && bound.attrs["affectedTarget"] !== target || !compatibleTarget(bound))) {
20916
+ return { id: "", created: false, corroborated: false, upgraded: false, rejected: "binding-conflict" };
20917
+ }
20918
+ const statementKey = identityId({ kind: "DesignProblem", statement });
20919
+ const restated = bound ? null : store.findNodesByLabel("Vulnerability").find((n) => n.attrs["workspaceId"] === opts.workspaceId && n.attrs["affectedTarget"] === target && compatibleTarget(n) && [n.description, ...n.attrs["statementAliases"] ?? []].some((s) => identityId({ kind: "DesignProblem", statement: s }) === statementKey));
20920
+ const id = bound?.id ?? restated?.id ?? identityId({ kind: "WitnessedVulnerability", workspaceId: opts.workspaceId, target: identityTarget, statement });
20921
+ const existing = bound ?? store.getNode(id);
20922
+ const sources = existing?.attrs["sources"] ?? [];
20923
+ const corroborated = !!existing && !sources.includes(opts.source);
20924
+ const upgraded = !!existing && existing.attrs["assessment"] !== "asserted" && flag.assessment === "asserted";
20925
+ const assessment = existing?.attrs["assessment"] === "asserted" ? "asserted" : flag.assessment;
20926
+ if (!existing) {
20927
+ store.mergeNode(buildNode(store, id, "Vulnerability", statement, opts.ts, {
20928
+ source: "convo",
20929
+ sourceClass: "agent-observation",
20930
+ privateFinding: id,
20931
+ workspaceId: opts.workspaceId,
20932
+ affectedTarget: target,
20933
+ identityTarget,
20934
+ assessment,
20935
+ anchorDisposition: target ? "unresolved" : "absent",
20936
+ provisional: true,
20937
+ sources: [opts.source],
20938
+ corroborations: 0,
20939
+ ...opts.hostHarness ? { hostHarness: opts.hostHarness } : {}
20940
+ }));
20941
+ } else if (corroborated || upgraded || !existing.attrs["affectedTarget"] && target || symbol2 && existing.attrs["identityTarget"] !== identityTarget) {
20942
+ store.updateNode(id, {
20943
+ attrs: {
20944
+ ...existing.attrs,
20945
+ assessment,
20946
+ ...upgraded && existing.description !== statement ? { statementAliases: [...existing.attrs["statementAliases"] ?? [], existing.description].slice(-5) } : {},
20947
+ affectedTarget: existing.attrs["affectedTarget"] || target,
20948
+ ...symbol2 ? { identityTarget } : {},
20949
+ sources: corroborated ? [...sources, opts.source] : sources,
20950
+ corroborations: (Number(existing.attrs["corroborations"]) || 0) + (corroborated ? 1 : 0)
20951
+ },
20952
+ // Assessment strengthens on evidence, but does not certify exploitability.
20953
+ ...upgraded ? { description: statement } : {},
20954
+ ...corroborated ? { cumulativeHits: existing.cumulativeHits + 1 } : {},
20955
+ ...reinforcedSeqAttrs(store, "Vulnerability"),
20956
+ lastUpdatedAt: opts.ts
20957
+ });
20958
+ }
20959
+ return { id, created: !existing, corroborated, upgraded };
20960
+ }
20961
+ function recordVulnerabilityCause(store, findingId, causes, ts) {
20962
+ const finding = store.getNode(findingId);
20963
+ if (finding?.label !== "Vulnerability") return 0;
20964
+ let from = findingId;
20965
+ let count = 0;
20966
+ for (const description of causes) {
20967
+ if (isPlaceholderStatement(description)) continue;
20968
+ const id = genericCanonicalId("dcause", { findingId, description: description.trim() });
20969
+ if (!store.getNode(id)) store.mergeNode(buildNode(store, id, "RootCause", description.trim(), ts, {
20970
+ source: "convo",
20971
+ privateFinding: findingId,
20972
+ sources: finding.attrs["sources"],
20973
+ assessment: finding.attrs["assessment"]
20974
+ }));
20975
+ mergeEdge(store, from, id, "CAUSED_BY", ts);
20976
+ from = id;
20977
+ count++;
20978
+ }
20979
+ return count;
20980
+ }
20898
20981
  function canonicalizePatternText(name2) {
20899
20982
  const stripped = name2.replace(/^\s*pattern(?:\s*[×x]\s*\d+)?\s*(?:\(exemplar\))?\s*[:—–-]\s*/i, "").replace(/\s+/g, " ").trim();
20900
20983
  return stripped.length > 0 ? stripped : name2.replace(/\s+/g, " ").trim();
@@ -21205,6 +21288,7 @@ function findReusableSolution(store, fixText) {
21205
21288
  const key = solutionTextKey(fixText);
21206
21289
  if (!key) return null;
21207
21290
  for (const s of store.findNodesByLabel("Solution")) {
21291
+ if (s.attrs["privateFinding"]) continue;
21208
21292
  if (s.validTo != null) continue;
21209
21293
  if (isAutoMinted(s)) continue;
21210
21294
  if (solutionTextKey(s.description ?? "") === key) return s;
@@ -21229,7 +21313,7 @@ function priorsForFile(store, relPath) {
21229
21313
  for (const e of store.inEdges(file2.id, ["ANCHORED_AT"])) {
21230
21314
  const n = store.getNode(e.from);
21231
21315
  if (!n) continue;
21232
- if (n.label === "Problem") {
21316
+ if (n.label === "Problem" || n.label === "Vulnerability") {
21233
21317
  if (seenProblem.has(n.id)) continue;
21234
21318
  seenProblem.add(n.id);
21235
21319
  if (!n.attrs["resolvedAt"]) {
@@ -21299,7 +21383,8 @@ function solutionForProblem(store, problemId) {
21299
21383
  }
21300
21384
  function anchorProblemToWorkingFile(store, problemId, relPath, workspaceId2, ts, provenance = "edited", opts = {}) {
21301
21385
  const p = store.getNode(problemId);
21302
- if (!p || p.label !== "Problem") return false;
21386
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
21387
+ if (p.label === "Vulnerability" && provenance !== "witnessed") return false;
21303
21388
  if (!isAnchorableCodePath(relPath)) return false;
21304
21389
  const file2 = resolveFileNode(store, relPath, workspaceId2);
21305
21390
  if (!file2) return false;
@@ -21478,18 +21563,22 @@ function resolveDesignProblemById(store, id, fixNote, t) {
21478
21563
  }
21479
21564
  function closeDesignProblem(store, problemId, fixNote, t) {
21480
21565
  const p = store.getNode(problemId);
21481
- if (!p || p.label !== "Problem") return false;
21566
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
21482
21567
  if (p.attrs["resolvedAs"]) return false;
21483
21568
  if (fixNote?.trim() && !isFragmentFixNote(fixNote)) {
21484
21569
  const sols = store.outEdges(problemId, ["SOLVED_BY"]).map((e) => store.getNode(e.to)).filter((n) => n !== null);
21485
21570
  if (sols.every((s) => isAutoMinted(s))) {
21486
- const reuse = findReusableSolution(store, fixNote);
21571
+ const reuse = p.label === "Vulnerability" ? null : findReusableSolution(store, fixNote);
21487
21572
  if (reuse) {
21488
21573
  mergeEdge(store, problemId, reuse.id, "SOLVED_BY", t);
21489
21574
  } else {
21490
21575
  const solId = `dfix_${digest({ problemId, fix: fixNote })}`.slice(0, 56);
21491
21576
  store.mergeNode(
21492
- buildNode(store, solId, "Solution", fixNote.trim(), t, { source: "convo", provisional: false })
21577
+ buildNode(store, solId, "Solution", fixNote.trim(), t, {
21578
+ source: "convo",
21579
+ provisional: false,
21580
+ ...p.label === "Vulnerability" ? { privateFinding: problemId, sources: p.attrs["sources"] } : {}
21581
+ })
21493
21582
  );
21494
21583
  mergeEdge(store, problemId, solId, "SOLVED_BY", t);
21495
21584
  }
@@ -21503,6 +21592,7 @@ function closeDesignProblem(store, problemId, fixNote, t) {
21503
21592
  // mints no Solution, and a legacy placeholder beside it reads as
21504
21593
  // all-heuristic either way.
21505
21594
  attrs: { ...p.attrs, provisional: false, resolvedAt: t, resolutionWitnessed: true },
21595
+ ...reinforcedSeqAttrs(store, p.label),
21506
21596
  lastUpdatedAt: t
21507
21597
  });
21508
21598
  return true;
@@ -23854,7 +23944,7 @@ function detectLocalCommunities(store, opts = {}) {
23854
23944
  const ids = /* @__PURE__ */ new Set();
23855
23945
  for (const label of JOINT_COMMUNITY_LABELS) {
23856
23946
  for (const n of store.findNodesByLabel(label)) {
23857
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
23947
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
23858
23948
  ids.add(n.id);
23859
23949
  }
23860
23950
  }
@@ -23991,6 +24081,7 @@ var init_community2 = __esm({
23991
24081
  init_src4();
23992
24082
  JOINT_COMMUNITY_LABELS = [
23993
24083
  "Problem",
24084
+ "Vulnerability",
23994
24085
  "Solution",
23995
24086
  "RootCause",
23996
24087
  "Pattern"
@@ -24852,6 +24943,7 @@ __export(src_exports2, {
24852
24943
  induceAbstractions: () => induceAbstractions,
24853
24944
  induceTriage: () => induceTriage,
24854
24945
  ingestDesignProblem: () => ingestDesignProblem,
24946
+ ingestWitnessedVulnerability: () => ingestWitnessedVulnerability,
24855
24947
  isConstraintProblem: () => isConstraintProblem,
24856
24948
  isFragmentFixNote: () => isFragmentFixNote,
24857
24949
  isPlaceholderStatement: () => isPlaceholderStatement,
@@ -24899,6 +24991,7 @@ __export(src_exports2, {
24899
24991
  recordMisreadPrior: () => recordMisreadPrior,
24900
24992
  recordRenderLedger: () => recordRenderLedger,
24901
24993
  recordTriageObservation: () => recordTriageObservation,
24994
+ recordVulnerabilityCause: () => recordVulnerabilityCause,
24902
24995
  reinforceSameAnchorProblem: () => reinforceSameAnchorProblem,
24903
24996
  relateClaims: () => relateClaims,
24904
24997
  reopenAutoClosedProblems: () => reopenAutoClosedProblems,
@@ -24968,7 +25061,7 @@ function isPassiveInjectable(node2, atCommit) {
24968
25061
  return false;
24969
25062
  }
24970
25063
  function buildSnapshot(opts) {
24971
- const problems = opts.store.findNodesByLabel("Problem").filter((p) => p.attrs["mergedInto"] === void 0 && historyReadPolicy(p, { mode: "passive", atCommit: opts.atCommit }).allow);
25064
+ const problems = [...opts.store.findNodesByLabel("Problem"), ...opts.store.findNodesByLabel("Vulnerability")].filter((p) => p.attrs["mergedInto"] === void 0 && historyReadPolicy(p, { mode: "passive", atCommit: opts.atCommit }).allow);
24972
25065
  const stillOpen = problems.filter((p) => p.attrs["resolvedAt"] == null);
24973
25066
  const allProblems = stillOpen.filter((p) => !isConstraintProblem(p));
24974
25067
  allProblems.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
@@ -25123,11 +25216,11 @@ function sliceForFile(store, relPath, atCommit) {
25123
25216
  const priors = fp ? {
25124
25217
  openProblems: fp.openProblems.slice(0, 3).map((p) => ({
25125
25218
  id: p.id,
25126
- description: p.description,
25219
+ description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description,
25127
25220
  ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
25128
25221
  })),
25129
25222
  constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
25130
- resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
25223
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description })),
25131
25224
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
25132
25225
  solutionsByProblem: Object.fromEntries(
25133
25226
  [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [
@@ -25245,7 +25338,8 @@ function renderSnapshot(s) {
25245
25338
  if (s.recentProblems.length > 0) {
25246
25339
  lines.push("_Open \u2014 recently observed here, derived from your work (a recency slice, not an inventory \u2014 burst an id for its full neighborhood):_");
25247
25340
  for (const r of s.recentProblems) {
25248
- lines.push(`- **${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25341
+ const badge = r.node.label === "Vulnerability" ? `Vulnerability (${r.node.attrs["assessment"] ?? "unassessed"}): ` : "";
25342
+ lines.push(`- **${badge}${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25249
25343
  const hits = r.node.cumulativeHits ?? 0;
25250
25344
  const surprise = (r.node.cumulativeSurprise ?? 0).toFixed(2);
25251
25345
  lines.push(` - observed ${hits}\xD7 \xB7 cumulativeSurprise ${surprise}`);
@@ -25286,7 +25380,8 @@ function renderSnapshot(s) {
25286
25380
  if (s.recentResolved.length > 0) {
25287
25381
  lines.push("_Recently resolved \u2014 solved here; jumping-off points, not live defects:_");
25288
25382
  for (const r of s.recentResolved) {
25289
- lines.push(`- \u2713 **${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25383
+ const badge = r.node.label === "Vulnerability" ? `Vulnerability (${r.node.attrs["assessment"] ?? "unassessed"}): ` : "";
25384
+ lines.push(`- \u2713 **${badge}${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25290
25385
  if (r.solution) {
25291
25386
  lines.push(` - solved: ${r.solution.description.slice(0, 160)} \u2014 \`${r.solution.id}\`${tagOf(r.solution)}`);
25292
25387
  }
@@ -27146,8 +27241,8 @@ var init_config = __esm({
27146
27241
  "use strict";
27147
27242
  init_paths();
27148
27243
  LEGACY_CLOUD_URL = "https://inerrata-gateway.onrender.com";
27149
- DEFAULT_CLOUD_URL = true ? "https://inerrata.dev" : DEV_CLOUD_URL;
27150
- DEFAULT_UPDATE_CHANNEL = true ? "latest" : "dev";
27244
+ DEFAULT_CLOUD_URL = true ? "https://dev.inerrata.dev" : DEV_CLOUD_URL;
27245
+ DEFAULT_UPDATE_CHANNEL = false ? "latest" : "dev";
27151
27246
  }
27152
27247
  });
27153
27248
 
@@ -31707,7 +31802,7 @@ var init_src8 = __esm({
31707
31802
 
31708
31803
  // src/agent-signals.ts
31709
31804
  function buildFileRecallInstruction(referent = "\u2026") {
31710
- return `Touching one? paste its token: ${TAG_EXAMPLE.fix(referent)} resolved \xB7 ${TAG_EXAMPLE.prior(referent)} leaned-on \xB7 ${TAG_EXAMPLE.problem()} new. We harvest it.`;
31805
+ return `Touching one? paste its token: ${TAG_EXAMPLE.fix(referent)} resolved \xB7 ${TAG_EXAMPLE.prior(referent)} leaned-on \xB7 ${TAG_EXAMPLE.problem()} new \xB7 ${TAG_EXAMPLE.vulnerability()} security. We harvest it.`;
31711
31806
  }
31712
31807
  function triageBullet(ref) {
31713
31808
  return [
@@ -31743,15 +31838,15 @@ function attemptBullet() {
31743
31838
  ].join("\n");
31744
31839
  }
31745
31840
  function buildAgentInstruction(opts = {}) {
31746
- const signals = opts.signals ?? ["prior", "problem", "domain", "fix", "constraint", "triage", "attempt", "link"];
31841
+ const signals = opts.signals ?? ["prior", "problem", "vulnerability", "domain", "fix", "constraint", "triage", "attempt", "link"];
31747
31842
  const ref = opts.referent ?? "its-handle";
31748
31843
  const token = (s) => s === "problem" ? TAG_EXAMPLE.problem() : s === "constraint" ? TAG_EXAMPLE.constraint() : s === "attempt" ? TAG_EXAMPLE.tried() : s === "domain" ? TAG_EXAMPLE.domain() : s === "link" ? TAG_EXAMPLE.pattern() : s === "verdict" ? TAG_EXAMPLE.confirm(ref) : TAG_EXAMPLE[s](ref);
31749
- const head2 = `We tag the priors we show you with a short handle like [${opts.handleExample ?? "chokidar-glob"}]. Untagged prose is silently lost. Put each tag inline in prose (no fences or backticks). Requested errata-* answer blocks are the exception: keep their fences in your turn-final reply. Tag everything, we filter downstream:`;
31750
- const tail = "Mid-turn text (between tool calls) can be dropped by the harness \u2014 RESTATE every tag in your turn-final message; the final message is what reliably survives \u2014 EXCEPT one already shown above with an id: that is captured, so cite `[its-handle]` rather than retyping it (drift mints a duplicate).";
31844
+ const head2 = "Tag inline (no fences or backticks). Untagged findings are silently lost. Cite short handles; tag everything, we filter downstream. Requested errata-* answer blocks are the exception: keep their fences in your turn-final reply:";
31845
+ const tail = "Restate mid-turn tags in your final reply; the harness can drop mid-turn text. EXCEPT one already shown with an id: cite [its-handle] instead of retyping (drift mints a duplicate).";
31751
31846
  return [
31752
31847
  head2,
31753
31848
  ...signals.map(
31754
- (s) => s === "triage" ? triageBullet(ref) : s === "attempt" ? attemptBullet() : s === "link" ? linkBullet(ref) : s === "verdict" ? verdictBullet(ref) : ` \u2022 ${GLOSS[s]} \u2192 ${token(s)}`
31849
+ (s) => s === "triage" ? triageBullet(ref) : s === "vulnerability" ? ` \u2022 ${GLOSS.vulnerability} \u2192 ${TAG_EXAMPLE.vulnerability()}. Uncertain? ${TAG_EXAMPLE.suspectedVulnerability()}. Code review counts, even if immediately fixed. No CVE/exploit needed. For fixes: ${TAG_EXAMPLE.vulnerabilityThread()} ${TAG_EXAMPLE.fixThread("v")}.` : s === "attempt" ? attemptBullet() : s === "link" ? linkBullet(ref) : s === "verdict" ? verdictBullet(ref) : ` \u2022 ${GLOSS[s]} \u2192 ${token(s)}`
31755
31850
  ),
31756
31851
  tail
31757
31852
  ].join("\n");
@@ -31763,6 +31858,9 @@ var init_agent_signals = __esm({
31763
31858
  TAG_EXAMPLE = {
31764
31859
  prior: (ref) => `([${ref}])`,
31765
31860
  problem: () => `[!one line on what's wrong]`,
31861
+ vulnerability: () => `[!vuln: who can do what; observed basis @ path:line]`,
31862
+ suspectedVulnerability: () => `[?vuln: consequence; missing check]`,
31863
+ vulnerabilityThread: () => `[!#v vuln: finding here]`,
31766
31864
  fix: (ref) => `(fix:[${ref}])`,
31767
31865
  // CONSTRAINT — the native capture path for a DIFFUSE problem (a constraint/tension/
31768
31866
  // conflict that shaped a decision). Free text, attaches to the decision sentence the
@@ -31783,7 +31881,7 @@ var init_agent_signals = __esm({
31783
31881
  // tags never need one — the id is for diagnoses that unfold over distance.
31784
31882
  problemThread: () => `[!#slug one line on what's wrong]`,
31785
31883
  causeThread: () => `(cause:#slug the mechanism)`,
31786
- fixThread: () => `(fix:#slug what you changed)`,
31884
+ fixThread: (thread = "slug") => `(fix:#${thread} what you changed)`,
31787
31885
  // ATTEMPT arc — what's being tried and what didn't work. A failed attempt is a
31788
31886
  // witnessed NEGATIVE result: it saves the next agent the same dead end.
31789
31887
  tried: () => `(tried: the approach you're attempting)`,
@@ -31825,8 +31923,9 @@ var init_agent_signals = __esm({
31825
31923
  // problems, and narrated them as progress (a pick-best-before-scoring bug lost).
31826
31924
  // Every example here was diagnosis-framed, so nothing contradicted that sorting.
31827
31925
  // Kept to 29 chars: the instruction sits at the 2600 passive-budget ceiling.
31828
- problem: "a problem you notice or report \u2014 any bug, finding, or thing wrong, even in code you just wrote; about specific code? name the file: [!what's wrong @ src/file.ts:42]",
31829
- fix: "your edit fixed a PROBLEM we showed you; for one you flagged yourself: (fix: what you changed) \u2014 a fix resolves a stated problem, completing routine work is not a fix",
31926
+ problem: "any bug or finding, even in code you just wrote; name affected code: [!what's wrong @ path:42]",
31927
+ vulnerability: "unauthorized actions, data exposure, or execution/disruption an attacker can trigger",
31928
+ fix: "you fixed a shown finding; for your own non-security finding: (fix: what changed) \u2014 a fix resolves a stated problem, completing routine work is not a fix",
31830
31929
  constraint: "a design decision forced by conflicting requirements or constraints \u2014 tag the tension, especially when your fix makes it 'only look' resolved",
31831
31930
  triage: "you diagnosed a cause \u2014 fold the chain into the flag with <- (caused-by), deepest sure mechanism rightmost",
31832
31931
  attempt: "attempting an approach / an attempt failed \u2014 a failed attempt is knowledge, it rules out a path",
@@ -31848,7 +31947,7 @@ var init_agent_signals = __esm({
31848
31947
  // Sim note (2026-08-02 shrink): domain emission held 4/4 with just the trigger
31849
31948
  // clause; the pattern convergence hook now rides here too (it fell 0/4 when it
31850
31949
  // only appeared in the link bullet's tail).
31851
- domain: "a problem with no single file \u2014 name the area it's about (Title Case). Also name the general shape it instantiates: (pattern: the shape) \u2014 needs no code anchor, and two agents naming the same shape converge on one node",
31950
+ domain: "no single file? name the area (Title Case), and (pattern: the shape). No code anchor needed; two agents naming the same shape converge on one node",
31852
31951
  link: "the problem you flagged is an instance of priors/abstractions you can name \u2014 (instance:[h1],[h2],\u2026) / (pattern: \u2026) / (aids:[h]); rendered whole via linkBullet",
31853
31952
  verdict: "you can judge a prior we SHOWED you \u2014 confirm it if it held, refute it (with why) if it didn't; rendered whole via verdictBullet"
31854
31953
  };
@@ -32085,7 +32184,7 @@ function readContextCard(root, hash2) {
32085
32184
  function openProblems(store, now = Date.now()) {
32086
32185
  const hit = openProblemsCache.get(store);
32087
32186
  if (hit && now - hit.at < OPEN_PROBLEMS_TTL_MS) return hit.nodes;
32088
- const nodes = store.findNodesByLabel("Problem").filter((p) => live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p));
32187
+ const nodes = [...store.findNodesByLabel("Problem"), ...store.findNodesByLabel("Vulnerability")].filter((p) => live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p));
32089
32188
  openProblemsCache.set(store, { at: now, nodes });
32090
32189
  return nodes;
32091
32190
  }
@@ -43329,47 +43428,9 @@ var init_mcp = __esm({
43329
43428
  init_reconcile();
43330
43429
  init_agent_signals();
43331
43430
  INSTRUCTIONS = [
43332
- // KEEP THIS UNDER THE CLIENT'S ~2 KB instruction-injection budget (the client
43333
- // truncates `instructions` on connect, ~2008 chars observed). Capture no longer
43334
- // depends on a standing emit-instruction block: the daemon derives problems/fixes
43335
- // from your WORK deterministically, and the one thing it asks of the agent — a
43336
- // one-line root cause at a fix — rides INLINE on the next tool response (the ⚡
43337
- // errata line; see the dispatch's enrichment piggyback) instead of a fenced
43338
- // protocol the agent has to remember. So this stays short: tool-routing steering
43339
- // plus a single line about the inline nudge. Per-tool routing lives in each tool's
43340
- // own `description` (delivered via tools/list). Don't re-add a tool table here.
43341
- "errata is a LIVE, bi-temporal code graph of this workspace, kept current by a",
43342
- "file watcher \u2014 a SEPARATE DIAGNOSTIC AXIS to run ALONGSIDE grep/glob/read, not",
43343
- "instead of them. For STRUCTURAL questions (callers of callers, blast-radius,",
43344
- "rename lineage) prefer it: faster, more complete, and it reaches relationships",
43345
- "grep can't. Each tool's description says when to reach for it. Fall back to",
43346
- 'grep/read when a tool returns found:false (ESPECIALLY reason:"unresolved" \u2014 the',
43347
- "name was not recognized, NOT an empty answer; an empty list like callers:[] is a",
43348
- 'confident "none" \u2014 trust it), when you need EXACT bytes before a high-stakes',
43349
- "edit, or for an unindexed file/language. Always confirm exact bytes with a read",
43350
- "before editing.",
43351
- "",
43352
- "errata is a cloud knowledge substrate \u2014 AND ALSO this workspace's code graph.",
43353
- "The problems you hit and the fixes you make become shared knowledge other agents",
43354
- "recall. That capture is automatic: derived from your edits/commands/commits \u2014 no",
43355
- "emit needed. Just after you resolve something a tool result may carry a one-line",
43356
- "\u26A1 errata note asking the ROOT CAUSE; answer via errata.annotate_resolution when",
43357
- "you know it, skip it otherwise.",
43358
- "",
43359
- "Two things it CAN'T derive \u2014 only you witness them, and this is a CAPTURE",
43360
- "PROTOCOL, not optional formatting: a problem or fix you state only in prose, a",
43361
- "list, or your own numbering is SILENTLY LOST \u2014 the tag is the ONLY thing recorded.",
43362
- "So the moment you state one, wrap it inline (no fence, no extra call):",
43363
- // Signal tokens come from the single source (agent-signals.ts) so this always-on
43364
- // block can't teach a grammar the parser rejects. Referent is "its-id" here — the
43365
- // raw MCP channel has no primed handles, so the agent cites a tool-result id.
43366
- ` \u2022 ${TAG_EXAMPLE.problem()} \u2014 EVERY problem you notice or report ([?\u2026] = TODO)`,
43367
- ` \u2022 ${TAG_EXAMPLE.fix("its-id")} \u2014 your edit fixed a problem errata showed you`,
43368
- "Tag every one you state \u2014 we filter downstream, so don't self-censor or batch.",
43369
- "",
43370
- "Graph-as-files: the graph is ALSO a virtual FS \u2014 Read (or `cat`)",
43371
- ".errata/g/<verb>/<seed> to run a query as a file read (e.g. .errata/g/burst/",
43372
- "<symbol>, /why/<sym>, /trace/<a>..<b>). Read .errata/g/index for the vocabulary."
43431
+ "errata is a live code and knowledge graph, a SEPARATE DIAGNOSTIC AXIS alongside grep/read for callers, blast radius, rename lineage and prior fixes. Fall back to read/search on found:false (unresolved is not an empty answer), or for unindexed code. Confirm exact bytes before editing.",
43432
+ buildAgentInstruction({ signals: ["prior", "problem", "vulnerability", "fix"], referent: "its-id" }),
43433
+ "Read .errata/g/index for graph-as-files query paths. A tool result may ask for a root cause; answer via errata.annotate_resolution when known."
43373
43434
  ].join("\n");
43374
43435
  AUDIT_RUN_THRESHOLD = 5;
43375
43436
  AUDIT_NUDGE_COOLDOWN = 12;
@@ -43961,7 +44022,7 @@ var init_mcp = __esm({
43961
44022
  },
43962
44023
  {
43963
44024
  name: "errata.problems",
43964
- description: "List the workspace's Problem backlog \u2014 the triage view that search/why/impact can't give because they need a seed. status: 'open' (default) | 'resolved' (a real fix) | 'retracted' (false alarms \u2014 false_positive/dissolution) | 'constraint' (design TENSIONS captured via `(constraint: \u2026)` \u2014 standing context, not open work, so they're excluded from 'open') | 'all'. Also returns a `retracted` breakdown so the false-positive rate stays visible.",
44025
+ description: "List the workspace's Problem and Vulnerability backlog \u2014 the triage view that search/why/impact can't give because they need a seed. status: 'open' (default) | 'resolved' (a real fix) | 'retracted' (false alarms \u2014 false_positive/dissolution) | 'constraint' (design TENSIONS captured via `(constraint: \u2026)` \u2014 standing context, not open work, so they're excluded from 'open') | 'all'. Also returns a `retracted` breakdown so the false-positive rate stays visible.",
43965
44026
  inputSchema: {
43966
44027
  type: "object",
43967
44028
  properties: {
@@ -43973,13 +44034,15 @@ var init_mcp = __esm({
43973
44034
  const want = String(args2["status"] ?? "open");
43974
44035
  const limit = Math.max(1, Math.min(200, Number(args2["limit"] ?? 50)));
43975
44036
  const RETRACTIONS = /* @__PURE__ */ new Set(["false_positive", "invalid", "duplicate"]);
43976
- const rows = store.findAllVersionsByLabel("Problem").map((p) => {
44037
+ const rows = [...store.findAllVersionsByLabel("Problem"), ...store.findAllVersionsByLabel("Vulnerability")].map((p) => {
43977
44038
  const ra = p.attrs["resolvedAs"];
43978
44039
  const status = ra && RETRACTIONS.has(ra) ? ra : p.attrs["resolvedAt"] != null ? "resolved" : isConstraintProblem(p) ? "constraint" : "open";
43979
44040
  const anchorEdge = store.outEdges(p.id, ["ANCHORED_AT"])[0];
43980
44041
  const anchor = anchorEdge ? store.getNode(anchorEdge.to)?.description : void 0;
43981
44042
  return {
43982
44043
  id: p.id,
44044
+ label: p.label,
44045
+ ...p.label === "Vulnerability" ? { assessment: p.attrs["assessment"] } : {},
43983
44046
  problem: p.description,
43984
44047
  status,
43985
44048
  createdAt: p.createdAt,
@@ -52544,7 +52607,8 @@ function errorRecallTerms(errorText) {
52544
52607
  }
52545
52608
  function recallLine(n, token) {
52546
52609
  const desc = (n.description ?? "").replace(/\s+/g, " ").slice(0, 120);
52547
- return `- [${n.label}] ${desc} \u2192 ${token}`;
52610
+ const label = n.label === "Vulnerability" ? `Vulnerability; ${n.attrs["assessment"] ?? "unassessed"}` : n.label;
52611
+ return `- [${label}] ${desc} \u2192 ${token}`;
52548
52612
  }
52549
52613
  function recallForFile(store, relPath) {
52550
52614
  return fileRecall(store, relPath)?.context ?? null;
@@ -52565,7 +52629,7 @@ function fileRecall(store, relPath) {
52565
52629
  const shownRelated = priors.related.slice(0, 4);
52566
52630
  for (const p of shownProblems) {
52567
52631
  const hasCause = store.outEdges(p.id, ["CAUSED_BY"]).some((e) => store.getNode(e.to)?.label === "RootCause");
52568
- const causeCue = hasCause ? "" : ` \xB7 ${TAG_EXAMPLE.triageMint()} to diagnose its cause`;
52632
+ const causeCue = hasCause ? "" : ` \xB7 ${p.label === "Vulnerability" ? `(cause:#${p.id} the mechanism)` : TAG_EXAMPLE.triageMint()} to diagnose its cause`;
52569
52633
  lines.push(
52570
52634
  recallLine(p, `${TAG_EXAMPLE.fix(p.id)} if you resolved it \xB7 ${TAG_EXAMPLE.prior(p.id)} to cite${causeCue}`)
52571
52635
  );
@@ -75829,7 +75893,7 @@ init_src5();
75829
75893
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "node:fs";
75830
75894
  var NEG = /n['']t|\b(?:not|never|no|none|neither|nor|unrelated|irrelevant|would|might|could|if)\b/i;
75831
75895
  var CITE_GROUP_RE = /\(((?:[^()\n]|\([^()\n]*\)){1,200})\)/g;
75832
- var HANDLE_RE = /\[([a-z0-9][\w-]{0,80})\]|((?:pat|sol|drc|dcause|dfix|dprob|prob|claim|err)_[0-9a-f]{6,})/gi;
75896
+ var HANDLE_RE = /\[([a-z0-9][\w-]{0,80})\]|((?:pat|sol|drc|dcause|dfix|dprob|dvuln|prob|claim|err)_[0-9a-f]{6,})/gi;
75833
75897
  var PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
75834
75898
  "handle",
75835
75899
  "its-handle",
@@ -75844,7 +75908,7 @@ var PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
75844
75908
  "b"
75845
75909
  ]);
75846
75910
  var isPlaceholderHandle = (h) => PLACEHOLDER_HANDLES.has(h.toLowerCase());
75847
- var FLAG_BODY_HANDLE_RE = /^(?:[a-z0-9]+(?:-[a-z0-9]+){2,}-(?=[0-9a-f]{0,3}\d)[0-9a-f]{4}|(?:pat|sol|drc|dcause|dfix|dprob|prob|claim|err)_[0-9a-f]{6,})$/i;
75911
+ var FLAG_BODY_HANDLE_RE = /^(?:[a-z0-9]+(?:-[a-z0-9]+){2,}-(?=[0-9a-f]{0,3}\d)[0-9a-f]{4}|(?:pat|sol|drc|dcause|dfix|dprob|dvuln|prob|claim|err)_[0-9a-f]{6,})$/i;
75848
75912
  var FIX_PREFIX = /^\s*fix:(?:#([a-z][\w-]{0,63}))?\s*/i;
75849
75913
  var CONSTRAINT_RE = /\(\s*constraint:\s*([^()\n]{8,}?)\s*\)/gi;
75850
75914
  var CAUSE_PREFIX = /^\s*cause:(?:#([a-z][\w-]{0,63}))?\s*/i;
@@ -75869,7 +75933,7 @@ var DOMAIN_RE = /\(\s*domain:\s*([^()\n]{3,}?)\s*\)/gi;
75869
75933
  var PACKAGE_RE = /\(\s*package:\s*([^()\n]{2,}?)\s*\)/gi;
75870
75934
  var COMPONENT_RE = /\(\s*component:\s*([^()\n]{2,}?)\s*\)/gi;
75871
75935
  var CAUSE_TEXT_MIN = 8;
75872
- var FLAG_RE = /\[([!?])(?:#([a-z][\w-]{0,63})\s+)?\s*((?:\[[^\]\n]*\]|[^\]\n]){8,}?)\s*(?:@\s*([^\s\]]+?)(?::(\d+))?\s*)?\]/g;
75936
+ var FLAG_RE = /\[([!?])(?:#([a-z][\w-]{0,63})\s+)?\s*((?:\[[^\]\n]*\]|[^\]\n])*?)\s*(?:@\s*([^\s\]]+?)(?::(\d+))?\s*)?\]/g;
75873
75937
  var CONSTRAINT_MIN = 8;
75874
75938
  function clauseBefore(sentence, idx) {
75875
75939
  const win = sentence.slice(Math.max(0, idx - 80), idx);
@@ -76013,6 +76077,7 @@ function cleanEmittedTags(tags) {
76013
76077
  }
76014
76078
  function emptyTagDispositions() {
76015
76079
  return {
76080
+ invalidVulnerability: 0,
76016
76081
  oversizeSegments: 0,
76017
76082
  codeSpanNeutralized: 0,
76018
76083
  refuteTooShort: 0,
@@ -76025,6 +76090,7 @@ function emptyTagDispositions() {
76025
76090
  };
76026
76091
  }
76027
76092
  function addTagDispositions(a, b) {
76093
+ a.invalidVulnerability += b.invalidVulnerability;
76028
76094
  a.oversizeSegments += b.oversizeSegments;
76029
76095
  a.codeSpanNeutralized += b.codeSpanNeutralized;
76030
76096
  a.refuteTooShort += b.refuteTooShort;
@@ -76038,12 +76104,31 @@ function addTagDispositions(a, b) {
76038
76104
  function parseInlineTags(text) {
76039
76105
  return parseInlineTagsWithDispositions(text).tags;
76040
76106
  }
76107
+ function* captureSentences(text) {
76108
+ for (const line of text.split(/\n+/)) {
76109
+ if (line.length > 2e4) {
76110
+ yield line;
76111
+ continue;
76112
+ }
76113
+ const flags2 = [...line.matchAll(new RegExp(FLAG_RE.source, FLAG_RE.flags))].map((m) => ({ start: m.index, end: m.index + m[0].length }));
76114
+ let flag = 0;
76115
+ let start2 = 0;
76116
+ for (const boundary of line.matchAll(/(?<=[.!?])\s+(?!\(fix:)/g)) {
76117
+ const index = boundary.index;
76118
+ while (flags2[flag] && flags2[flag].end <= index) flag++;
76119
+ if (flags2[flag] && flags2[flag].start < index) continue;
76120
+ yield line.slice(start2, index);
76121
+ start2 = index + boundary[0].length;
76122
+ }
76123
+ yield line.slice(start2);
76124
+ }
76125
+ }
76041
76126
  function parseInlineTagsWithDispositions(text) {
76042
76127
  const out2 = [];
76043
76128
  const dispositions = emptyTagDispositions();
76044
76129
  const deFenced = text.replace(/```[\s\S]*?```/g, " ");
76045
76130
  let seqNo = -1;
76046
- for (const raw2 of deFenced.split(/(?<=[.!?])\s+(?!\(fix:)|\n+/)) {
76131
+ for (const raw2 of captureSentences(deFenced)) {
76047
76132
  seqNo++;
76048
76133
  if (raw2.length > 2e4) {
76049
76134
  dispositions.oversizeSegments++;
@@ -76269,6 +76354,29 @@ function parseInlineTagsWithDispositions(text) {
76269
76354
  while ((f = FLAG_RE.exec(sentence)) !== null) {
76270
76355
  const flagThread = f[2];
76271
76356
  const raw3 = f[3].trim();
76357
+ if (/^vuln\b/i.test(raw3)) {
76358
+ const typed = /^vuln:\s*([\s\S]*)$/i.exec(raw3);
76359
+ const body2 = typed?.[1]?.trim() ?? "";
76360
+ const parts2 = body2.split(CAUSE_ARROW).map((s) => s.trim());
76361
+ const symptom = f[1] === "!" && parts2.length > 1 && !f[4] ? new RegExp(FLAG_RE.source, "i").exec(`[!${parts2[0]}]`) : null;
76362
+ const statement = f[1] === "?" ? body2 : symptom?.[3]?.trim() ?? parts2[0];
76363
+ if (statement.length < 8 || /^<[^>]*>$/.test(statement)) {
76364
+ dispositions.invalidVulnerability++;
76365
+ continue;
76366
+ }
76367
+ const path2 = (f[4] ?? symptom?.[4])?.trim();
76368
+ const line = f[5] ?? symptom?.[5];
76369
+ out2.push({
76370
+ kind: "vulnerability",
76371
+ statement,
76372
+ assessment: f[1] === "!" ? "asserted" : "suspected",
76373
+ ...f[1] === "!" && parts2.length > 1 ? { vulnerabilityCauses: parts2.slice(1).filter((s) => s.length >= CAUSE_TEXT_MIN) } : {},
76374
+ ...path2 ? { location: { path: path2, ...line ? { line: Number(line) } : {} } } : {},
76375
+ ...flagThread ? { threadId: flagThread } : {},
76376
+ sentence: sfield
76377
+ });
76378
+ continue;
76379
+ }
76272
76380
  if (FLAG_BODY_HANDLE_RE.test(raw3) && !f[4]) {
76273
76381
  dispositions.flagAsHandle++;
76274
76382
  out2.push({ kind: "prior", handle: raw3, sentence: sfield });
@@ -76639,7 +76747,7 @@ function harvestInlineTags(store, text, opts) {
76639
76747
  const parsed = parseInlineTagsWithDispositions(text);
76640
76748
  const tags = parsed.tags;
76641
76749
  plan.dispositions = parsed.dispositions;
76642
- const symptomSeqs = tags.filter((t) => (t.kind === "problem" || t.kind === "todo") && t.statement).map((t) => ({ seq: t.seq ?? -1, statement: t.statement, threadId: t.threadId })).sort((a, b) => a.seq - b.seq);
76750
+ const symptomSeqs = tags.filter((t) => (t.kind === "problem" || t.kind === "todo" || t.kind === "vulnerability") && t.statement).map((t) => ({ seq: t.seq ?? -1, statement: t.statement, threadId: t.threadId, kind: t.kind })).sort((a, b) => a.seq - b.seq);
76643
76751
  const bindSymptom = (seq, threadId) => {
76644
76752
  if (threadId) {
76645
76753
  const hit = symptomSeqs.find((s) => s.threadId === threadId);
@@ -76652,6 +76760,7 @@ function harvestInlineTags(store, text, opts) {
76652
76760
  if (seq == null) return { evidence: "inferred" };
76653
76761
  let best;
76654
76762
  for (const s of symptomSeqs) {
76763
+ if (s.kind === "vulnerability") continue;
76655
76764
  if (s.seq <= seq) best = s;
76656
76765
  else break;
76657
76766
  }
@@ -76666,10 +76775,13 @@ function harvestInlineTags(store, text, opts) {
76666
76775
  return id;
76667
76776
  };
76668
76777
  for (const tag of tags) {
76669
- if (tag.kind === "problem" || tag.kind === "todo") {
76778
+ if (tag.kind === "problem" || tag.kind === "todo" || tag.kind === "vulnerability") {
76670
76779
  plan.problems.push({
76671
76780
  statement: tag.statement,
76672
76781
  kind: tag.kind,
76782
+ ...tag.assessment ? { assessment: tag.assessment } : {},
76783
+ ...tag.vulnerabilityCauses ? { vulnerabilityCauses: tag.vulnerabilityCauses } : {},
76784
+ ...tag.kind === "vulnerability" && tag.seq !== void 0 ? { seq: tag.seq } : {},
76673
76785
  ...tag.location ? { location: tag.location } : {},
76674
76786
  ...tag.threadId ? { threadId: tag.threadId } : {}
76675
76787
  });
@@ -76692,7 +76804,7 @@ function harvestInlineTags(store, text, opts) {
76692
76804
  } else if (tag.threadId && !b.statement) {
76693
76805
  plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, threadId: tag.threadId });
76694
76806
  } else if (b.statement && b.evidence === "witnessed") {
76695
- plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, boundStatement: b.statement });
76807
+ plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, boundStatement: b.statement, ...tag.threadId ? { threadId: tag.threadId } : {} });
76696
76808
  } else {
76697
76809
  plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, ambiguous: true });
76698
76810
  }
@@ -76895,6 +77007,7 @@ function harvestInlineTags(store, text, opts) {
76895
77007
  }
76896
77008
  var CORROBORATABLE_LABELS = /* @__PURE__ */ new Set([
76897
77009
  "Problem",
77010
+ "Vulnerability",
76898
77011
  "Solution",
76899
77012
  "RootCause",
76900
77013
  "Pattern",
@@ -80673,9 +80786,9 @@ function partitionReplayable(queue, channel, isShipped) {
80673
80786
  function retireWitnesses(queue, channel, settledKeys) {
80674
80787
  return queue.filter((w) => !(w.channel === channel && settledKeys.has(w.witnessKey)));
80675
80788
  }
80676
- var WIRE_SHIPPED_LABELS = /* @__PURE__ */ new Set(["Problem", "Solution", "RootCause"]);
80789
+ var WIRE_SHIPPED_LABELS = /* @__PURE__ */ new Set(["Problem", "Vulnerability", "Solution", "RootCause"]);
80677
80790
  var MOTIF_ID_PREFIX = "motif_community_";
80678
- var CANONICAL_ID_RE = /^(?:pat|sol|drc|dcause|dfix|dprob|prob|claim|err)_[0-9a-f]{6,}$/i;
80791
+ var CANONICAL_ID_RE = /^(?:pat|sol|drc|dcause|dfix|dprob|dvuln|prob|claim|err)_[0-9a-f]{6,}$/i;
80679
80792
  function classifyWitnessTarget(nodeId, getNode) {
80680
80793
  if (nodeId.startsWith(MOTIF_ID_PREFIX)) {
80681
80794
  const exemplar = nodeId.slice(MOTIF_ID_PREFIX.length);
@@ -81245,7 +81358,7 @@ function createWatchBreaker(deps) {
81245
81358
  }
81246
81359
 
81247
81360
  // src/engine.ts
81248
- var DAEMON_VERSION = true ? "2.0.15" : "2.0.0-alpha.0";
81361
+ var DAEMON_VERSION = true ? "2.0.16-dev.3343" : "2.0.0-alpha.0";
81249
81362
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
81250
81363
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
81251
81364
  var GIT_OP_MUTE_MS = 4e3;
@@ -82137,6 +82250,9 @@ function createWorkspaceEngine(opts) {
82137
82250
  const harvestTextsInner = async (sessionId, items) => {
82138
82251
  let minted = 0;
82139
82252
  let tagsSeen = 0;
82253
+ let vulnerabilitiesSeen = 0;
82254
+ let vulnerabilitiesMinted = 0;
82255
+ let vulnerabilitiesSuspected = 0;
82140
82256
  let tagsRejected = 0;
82141
82257
  let tagsRestated = 0;
82142
82258
  let resolved = 0;
@@ -82367,10 +82483,73 @@ function createWorkspaceEngine(opts) {
82367
82483
  if ((toolRuns.get(sessionId)?.size ?? 0) > 0) touchedToolTurns++;
82368
82484
  const wf = workingFiles.get(sessionId);
82369
82485
  let inScopeProblemId;
82486
+ const findingIds = /* @__PURE__ */ new Map();
82487
+ const conflictingThreads = /* @__PURE__ */ new Set();
82370
82488
  const threads = sessionThreads.get(sessionId) ?? /* @__PURE__ */ new Map();
82371
82489
  sessionThreads.set(sessionId, threads);
82372
82490
  for (const p of plan.problems) {
82373
82491
  tagsSeen++;
82492
+ if (p.kind === "vulnerability") {
82493
+ vulnerabilitiesSeen++;
82494
+ if (p.assessment === "suspected") vulnerabilitiesSuspected++;
82495
+ let fileText;
82496
+ let anchorReadFailed = false;
82497
+ if (p.location) {
82498
+ try {
82499
+ fileText = readFileSync29(join35(opts.workspaceRoot, p.location.path), "utf8");
82500
+ } catch {
82501
+ anchorReadFailed = true;
82502
+ }
82503
+ }
82504
+ const result = ingestWitnessedVulnerability(store, {
82505
+ statement: p.statement,
82506
+ assessment: p.assessment,
82507
+ target: p.location?.path,
82508
+ line: p.location?.line,
82509
+ fileText,
82510
+ existingId: p.threadId ? threads.get(p.threadId) ?? handleMap[p.threadId]?.id ?? store.getNode(p.threadId)?.id : void 0
82511
+ }, { workspaceId: profile.id, source: sessionId, ts: t, hostHarness: turn.hostHarness });
82512
+ let anchorDisposition = p.location ? "unresolved" : "absent";
82513
+ if (result.rejected) {
82514
+ tagsRejected++;
82515
+ if (p.threadId) conflictingThreads.add(p.threadId);
82516
+ } else {
82517
+ findingIds.set(p.statement, result.id);
82518
+ if (p.threadId) threads.set(p.threadId, result.id);
82519
+ if (result.created || result.corroborated || result.upgraded) minted++;
82520
+ else tagsRestated++;
82521
+ if (result.created) vulnerabilitiesMinted++;
82522
+ if (p.location) {
82523
+ try {
82524
+ const anchored = anchorProblemToWorkingFile(store, result.id, p.location.path, profile.id, t, "witnessed", { line: p.location.line, fileText });
82525
+ anchorDisposition = anchored ? "resolved" : anchorReadFailed ? "error" : "unresolved";
82526
+ if (anchored) deriveContextFromAnchors(store, result.id, t);
82527
+ } catch {
82528
+ anchorDisposition = "error";
82529
+ }
82530
+ }
82531
+ const node2 = store.getNode(result.id);
82532
+ store.updateNode(result.id, { attrs: { ...node2.attrs, anchorDisposition } });
82533
+ if (p.vulnerabilityCauses) triaged += recordVulnerabilityCause(store, result.id, p.vulnerabilityCauses, t);
82534
+ }
82535
+ log.append({
82536
+ kind: "vulnerability.witness",
82537
+ workspaceId: profile.id,
82538
+ ts: t,
82539
+ digest: digest({ kind: "vulnerability.witness", workspaceId: profile.id, sessionId, turnId: turn.uuid, statement: p.statement, assessment: p.assessment, location: p.location }),
82540
+ sessionId,
82541
+ turnId: turn.uuid,
82542
+ harness: turn.hostHarness,
82543
+ findingId: result.id,
82544
+ statement: p.statement,
82545
+ assessment: p.assessment,
82546
+ location: p.location,
82547
+ seq: p.seq,
82548
+ anchorDisposition,
82549
+ disposition: result.rejected ?? (result.created ? "created" : result.upgraded ? "upgraded" : result.corroborated ? "corroborated" : "restated")
82550
+ });
82551
+ continue;
82552
+ }
82374
82553
  const anchorPath = p.location?.path ?? editedTurnFile;
82375
82554
  const anchorProvenance = p.location?.path ? "witnessed" : "edited";
82376
82555
  const hintPath = anchorPath ? void 0 : turnFile ?? wf;
@@ -82465,10 +82644,10 @@ function createWorkspaceEngine(opts) {
82465
82644
  if (r.created || r.corroborated) {
82466
82645
  minted++;
82467
82646
  if (p.kind !== "constraint") {
82468
- sessionLastProblem.set(sessionId, designProblemId(p.statement));
82469
- inScopeProblemId = designProblemId(p.statement);
82647
+ sessionLastProblem.set(sessionId, r.problemId);
82648
+ inScopeProblemId = r.problemId;
82470
82649
  }
82471
- if (p.threadId) threads.set(p.threadId, designProblemId(p.statement));
82650
+ if (p.threadId) threads.set(p.threadId, r.problemId);
82472
82651
  try {
82473
82652
  if (anchorPath) {
82474
82653
  let enclosing = {};
@@ -82505,7 +82684,8 @@ function createWorkspaceEngine(opts) {
82505
82684
  );
82506
82685
  }
82507
82686
  for (const fx of plan.fixes) {
82508
- const pid = fx.problemId ?? (fx.threadId ? threads.get(fx.threadId) : void 0) ?? (fx.boundStatement ? designProblemId(fx.boundStatement) : void 0);
82687
+ if (fx.threadId && conflictingThreads.has(fx.threadId)) continue;
82688
+ const pid = fx.problemId ?? (fx.threadId ? threads.get(fx.threadId) : void 0) ?? (fx.boundStatement ? findingIds.get(fx.boundStatement) ?? designProblemId(fx.boundStatement) : void 0);
82509
82689
  if (!pid) {
82510
82690
  if (fx.inScope) {
82511
82691
  console.warn(
@@ -82517,7 +82697,7 @@ function createWorkspaceEngine(opts) {
82517
82697
  const closed = resolveDesignProblemById(store, pid, fx.fixNote, t);
82518
82698
  if (!closed) {
82519
82699
  const n = store.getNode(pid);
82520
- if (!n || n.label !== "Problem" || n.attrs["resolvedAs"] != null) {
82700
+ if (!n || n.label !== "Problem" && n.label !== "Vulnerability" || n.attrs["resolvedAs"] != null) {
82521
82701
  console.warn(
82522
82702
  `[errata] fix tag went nowhere: ${!n ? `id ${pid.slice(0, 32)} unknown` : n.label !== "Problem" ? `${pid.slice(0, 32)} is a ${n.label}, not a Problem` : "the problem was retracted \u2014 a retraction is not fixable"} \u2014 the fix was NOT recorded`
82523
82703
  );
@@ -82536,10 +82716,23 @@ function createWorkspaceEngine(opts) {
82536
82716
  }
82537
82717
  }
82538
82718
  }
82719
+ const vulnerabilityTriages = /* @__PURE__ */ new Set();
82720
+ for (const tr of plan.triages) {
82721
+ if (tr.threadId && conflictingThreads.has(tr.threadId)) {
82722
+ vulnerabilityTriages.add(tr);
82723
+ continue;
82724
+ }
82725
+ const id = tr.symptomProblemId ?? (tr.threadId ? threads.get(tr.threadId) : void 0) ?? (tr.symptomStatement ? findingIds.get(tr.symptomStatement) : void 0);
82726
+ if (id && store.getNode(id)?.label === "Vulnerability") {
82727
+ vulnerabilityTriages.add(tr);
82728
+ if (tr.evidence === "witnessed" && tr.causeDescription) triaged += recordVulnerabilityCause(store, id, [tr.causeDescription], t);
82729
+ }
82730
+ }
82539
82731
  if (opts.sharedStore && plan.triages.length > 0) {
82540
82732
  const fallbackId = sessionLastProblem.get(sessionId);
82541
82733
  const fallbackStatement = fallbackId ? store.getNode(fallbackId)?.description : void 0;
82542
82734
  for (const tr of plan.triages) {
82735
+ if (vulnerabilityTriages.has(tr)) continue;
82543
82736
  let presentingStatement = tr.symptomStatement;
82544
82737
  let evidence = tr.evidence;
82545
82738
  if (!presentingStatement && tr.threadId) {
@@ -82557,7 +82750,7 @@ function createWorkspaceEngine(opts) {
82557
82750
  evidence = "inferred";
82558
82751
  }
82559
82752
  if (!presentingStatement) continue;
82560
- const presentingId = tr.symptomProblemId ?? designProblemId(presentingStatement);
82753
+ const presentingId = tr.symptomProblemId ?? (tr.threadId ? threads.get(tr.threadId) : void 0) ?? designProblemId(presentingStatement);
82561
82754
  if (tr.causeId === presentingId) continue;
82562
82755
  try {
82563
82756
  recordTriageObservation(
@@ -82615,7 +82808,7 @@ function createWorkspaceEngine(opts) {
82615
82808
  }
82616
82809
  }
82617
82810
  for (const at of plan.attempts) {
82618
- const pid = at.problemId ?? (at.threadId ? threads.get(at.threadId) : void 0) ?? (at.boundStatement ? designProblemId(at.boundStatement) : void 0);
82811
+ const pid = at.problemId ?? (at.threadId ? threads.get(at.threadId) : void 0) ?? (at.boundStatement ? findingIds.get(at.boundStatement) ?? designProblemId(at.boundStatement) : void 0);
82619
82812
  const node2 = pid ? store.getNode(pid) : void 0;
82620
82813
  if (!pid || !node2) continue;
82621
82814
  try {
@@ -82631,7 +82824,7 @@ function createWorkspaceEngine(opts) {
82631
82824
  }
82632
82825
  }
82633
82826
  const bindPid = (tag) => {
82634
- const pid = tag.problemId ?? (tag.threadId ? threads.get(tag.threadId) : void 0) ?? (tag.boundStatement ? designProblemId(tag.boundStatement) : void 0) ?? inScopeProblemId ?? sessionLastProblem.get(sessionId);
82827
+ const pid = tag.problemId ?? (tag.threadId ? threads.get(tag.threadId) : void 0) ?? (tag.boundStatement ? findingIds.get(tag.boundStatement) ?? designProblemId(tag.boundStatement) : void 0) ?? inScopeProblemId ?? sessionLastProblem.get(sessionId);
82635
82828
  return pid && store.getNode(pid) ? pid : void 0;
82636
82829
  };
82637
82830
  const mintCiteEdge = (from, to, type, confidence, extraAttrs) => {
@@ -82785,7 +82978,12 @@ function createWorkspaceEngine(opts) {
82785
82978
  for (const [session, items2] of groups) {
82786
82979
  if (items2.length === 0) continue;
82787
82980
  try {
82788
- const raw2 = await send(items2, session);
82981
+ const wireItems = items2.map(({ nodeId, witnessKey }) => {
82982
+ const node2 = store.getNode(nodeId);
82983
+ const cloudId = node2?.attrs["privateFinding"] ? node2.attrs["cloudNodeId"] : void 0;
82984
+ return { nodeId: typeof cloudId === "string" ? cloudId : nodeId, witnessKey };
82985
+ });
82986
+ const raw2 = await send(wireItems, session);
82789
82987
  const d = {
82790
82988
  recorded: raw2?.recorded ?? 0,
82791
82989
  unmatched: raw2?.unmatched ?? 0,
@@ -82798,8 +82996,8 @@ function createWorkspaceEngine(opts) {
82798
82996
  duplicate += d.duplicate;
82799
82997
  selfGated += d.selfGated;
82800
82998
  const missing = new Set(d.unmatchedIds);
82801
- for (const it of items2) {
82802
- if (missing.has(it.nodeId)) stillUnmatched.push({ channel, ...it, sessionKey: session, counted: isShipped(it.nodeId) });
82999
+ for (const [index, it] of items2.entries()) {
83000
+ if (missing.has(wireItems[index].nodeId)) stillUnmatched.push({ channel, ...it, sessionKey: session, counted: isShipped(it.nodeId) });
82803
83001
  else settled.add(it.witnessKey);
82804
83002
  }
82805
83003
  } catch (err2) {
@@ -82969,6 +83167,10 @@ function createWorkspaceEngine(opts) {
82969
83167
  // but nothing judgeable was cited.
82970
83168
  tagsVerdictNoHandle: dispositions.verdictNoHandle,
82971
83169
  tagsFlagAsHandle: dispositions.flagAsHandle,
83170
+ tagsInvalidVulnerability: dispositions.invalidVulnerability,
83171
+ vulnerabilitiesSeen,
83172
+ vulnerabilitiesMinted,
83173
+ vulnerabilitiesSuspected,
82972
83174
  unresolvedHandles,
82973
83175
  priorEdgesFlagOff,
82974
83176
  priorEdgesNoSource,
@@ -84305,7 +84507,7 @@ function internalSymbolsFor(description, lexiconHas, publicIndex) {
84305
84507
 
84306
84508
  // src/instance-ingest.ts
84307
84509
  init_generalize_graph();
84308
- var INSTANCE_LABELS = ["Problem", "Solution", "RootCause"];
84510
+ var INSTANCE_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
84309
84511
  var INSTANCE_EDGES = ["CAUSED_BY", "SOLVED_BY"];
84310
84512
  var ANCHOR_EDGES = ["OCCURS_IN", "DEPENDS_ON", "PERTAIN_TO", "CONCERNS"];
84311
84513
  var ANCHOR_TARGET_LABELS = ["Language", "Package", "Domain", "Component"];
@@ -84440,6 +84642,10 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84440
84642
  attrs: {
84441
84643
  scope: stripCodebaseScope(n.attrs["scope"]),
84442
84644
  ...n.attrs["kind"] ? { kind: n.attrs["kind"] } : {},
84645
+ ...n.attrs["privateFinding"] ? { privateFinding: true } : {},
84646
+ ...n.attrs["assessment"] ? { assessment: n.attrs["assessment"] } : {},
84647
+ ...n.attrs["sourceClass"] ? { sourceClass: n.attrs["sourceClass"] } : {},
84648
+ ...n.attrs["resolutionWitnessed"] ? { resolutionWitnessed: true, resolvedAt: n.attrs["resolvedAt"] } : {},
84443
84649
  ...n.attrs["resolvedAs"] ? { resolvedAs: n.attrs["resolvedAs"] } : {},
84444
84650
  ...preserved.length > 0 ? { preservedSymbols: preserved } : {},
84445
84651
  ...internal.length > 0 ? { internalSymbols: internal } : {}
@@ -84454,6 +84660,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84454
84660
  const semanticEndpoints = /* @__PURE__ */ new Map();
84455
84661
  const includedByLabel = /* @__PURE__ */ new Map();
84456
84662
  const composition = {
84663
+ privateTargetMissing: 0,
84457
84664
  candidates: 0,
84458
84665
  shipped: 0,
84459
84666
  cloudSourced: 0,
@@ -84499,11 +84706,15 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84499
84706
  if (walked.has(n.id)) continue;
84500
84707
  walked.add(n.id);
84501
84708
  composition.candidates++;
84709
+ if ((n.attrs["privateFinding"] || n.label === "Vulnerability") && !opts.project) {
84710
+ composition.privateTargetMissing++;
84711
+ continue;
84712
+ }
84502
84713
  if (n.attrs["source"] === "cloud") {
84503
84714
  composition.cloudSourced++;
84504
84715
  continue;
84505
84716
  }
84506
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
84717
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
84507
84718
  composition.falsePositive++;
84508
84719
  continue;
84509
84720
  }
@@ -84526,7 +84737,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84526
84737
  composition.terminalRefused++;
84527
84738
  continue;
84528
84739
  }
84529
- if (!noveltyAgainst(n, ref, noveltyOpts).ready) {
84740
+ if (!n.attrs["privateFinding"] && !noveltyAgainst(n, ref, noveltyOpts).ready) {
84530
84741
  composition.notNovel++;
84531
84742
  continue;
84532
84743
  }
@@ -84569,7 +84780,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84569
84780
  }
84570
84781
  if (targets.length === 0) continue;
84571
84782
  const shipped = shippedById.get(sourceId);
84572
- if (shipped) {
84783
+ if (shipped && !shipped.attrs["privateFinding"]) {
84573
84784
  const anchors = (label) => targets.filter((x) => x.t.label === label).map(
84574
84785
  (x) => label === "Language" ? langAnchor(x.t) : label === "Component" ? `component:${x.wireId}` : x.wireId
84575
84786
  ).filter((a) => a != null).sort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.15",
3
+ "version": "2.0.16-dev.3343",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -15122,7 +15122,7 @@ var init_edge_rules = __esm({
15122
15122
  // anchor anything at anything, e.g. at a public `Package`, quietly re-creating
15123
15123
  // the code↔collective bridge the membrane exists to prevent.
15124
15124
  ANCHORED_AT: {
15125
- from: ["Problem", "Solution", "RootCause", "Claim"],
15125
+ from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim"],
15126
15126
  to: ["Symbol"]
15127
15127
  },
15128
15128
  // ── Security spine (v1 taxonomy.ts SECURITY_FORWARD_ONLY commentary) ──
@@ -15167,7 +15167,7 @@ var init_edge_rules = __esm({
15167
15167
  // `Component` joined the target set with OM-agent-anchors: an agent-named
15168
15168
  // component ("React Router") is the same knowledge→named-unit anchor shape as
15169
15169
  // a Tool — the knowledge is ABOUT it, not dependent on it.
15170
- CONCERNS: { from: ["Problem", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15170
+ CONCERNS: { from: ["Problem", "Vulnerability", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15171
15171
  OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15172
15172
  INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15173
15173
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
@@ -24827,7 +24827,8 @@ CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
24827
24827
  -- its table row. Partial, so it maintains a few thousand rows instead of adding
24828
24828
  -- a write to every node write. Its label list is FINGERPRINT_INDEXED_LABELS.
24829
24829
  DROP INDEX IF EXISTS nodes_live_fingerprint;
24830
- CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause');
24830
+ DROP INDEX IF EXISTS nodes_instance_fingerprint;
24831
+ CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause');
24831
24832
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
24832
24833
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
24833
24834
  -- Expression index on the per-node relPath (stored in attrs_json) so the
@@ -24898,7 +24899,7 @@ function rowToEdge(r) {
24898
24899
  attrs: JSON.parse(r.attrs_json)
24899
24900
  };
24900
24901
  }
24901
- var FINGERPRINT_INDEXED_LABELS = ["Problem", "Solution", "RootCause"];
24902
+ var FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
24902
24903
  var SqliteGraphStore = class {
24903
24904
  db;
24904
24905
  // Typed edge lookups build their SQL from the number of types asked for, and
@@ -25576,8 +25577,8 @@ var SqliteGraphStore = class {
25576
25577
  const r2 = this.db.prepare(
25577
25578
  `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
25578
25579
  MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
25579
- FROM nodes INDEXED BY nodes_instance_fingerprint
25580
- WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause')`
25580
+ FROM nodes INDEXED BY nodes_instance_fingerprint_v2
25581
+ WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
25581
25582
  ).get();
25582
25583
  return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
25583
25584
  }
@@ -26181,7 +26182,7 @@ function mergeContextEdge(store2, from, to, type, ts) {
26181
26182
  function deriveContextFromAnchors(store2, problemId, ts, pkgIndex) {
26182
26183
  const report = { languages: [], packages: [] };
26183
26184
  const p = store2.getNode(problemId);
26184
- if (!p || p.label !== "Problem") return report;
26185
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return report;
26185
26186
  const files = store2.outEdges(problemId, ["ANCHORED_AT"]).map((e) => store2.getNode(e.to)).filter((n) => !!n && n.label === "File");
26186
26187
  if (files.length === 0) return report;
26187
26188
  const idx = pkgIndex ?? buildPackageIndex(store2);
@@ -26386,7 +26387,7 @@ function priorsForFile(store2, relPath) {
26386
26387
  for (const e of store2.inEdges(file2.id, ["ANCHORED_AT"])) {
26387
26388
  const n = store2.getNode(e.from);
26388
26389
  if (!n) continue;
26389
- if (n.label === "Problem") {
26390
+ if (n.label === "Problem" || n.label === "Vulnerability") {
26390
26391
  if (seenProblem.has(n.id)) continue;
26391
26392
  seenProblem.add(n.id);
26392
26393
  if (!n.attrs["resolvedAt"]) {
@@ -27654,7 +27655,7 @@ var AGENTS_POINTER_BODY = [
27654
27655
  init_src2();
27655
27656
  init_src();
27656
27657
  function buildSnapshot(opts) {
27657
- const problems = opts.store.findNodesByLabel("Problem").filter((p) => p.attrs["mergedInto"] === void 0 && historyReadPolicy(p, { mode: "passive", atCommit: opts.atCommit }).allow);
27658
+ const problems = [...opts.store.findNodesByLabel("Problem"), ...opts.store.findNodesByLabel("Vulnerability")].filter((p) => p.attrs["mergedInto"] === void 0 && historyReadPolicy(p, { mode: "passive", atCommit: opts.atCommit }).allow);
27658
27659
  const stillOpen = problems.filter((p) => p.attrs["resolvedAt"] == null);
27659
27660
  const allProblems = stillOpen.filter((p) => !isConstraintProblem(p));
27660
27661
  allProblems.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
@@ -27809,11 +27810,11 @@ function sliceForFile(store2, relPath, atCommit) {
27809
27810
  const priors = fp ? {
27810
27811
  openProblems: fp.openProblems.slice(0, 3).map((p) => ({
27811
27812
  id: p.id,
27812
- description: p.description,
27813
+ description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description,
27813
27814
  ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
27814
27815
  })),
27815
27816
  constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
27816
- resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
27817
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description })),
27817
27818
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
27818
27819
  solutionsByProblem: Object.fromEntries(
27819
27820
  [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [