@inerrata-corporation/errata 2.0.16 → 2.0.17-dev.3369

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`) ──
@@ -18274,7 +18281,8 @@ CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
18274
18281
  -- its table row. Partial, so it maintains a few thousand rows instead of adding
18275
18282
  -- a write to every node write. Its label list is FINGERPRINT_INDEXED_LABELS.
18276
18283
  DROP INDEX IF EXISTS nodes_live_fingerprint;
18277
- 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');
18284
+ DROP INDEX IF EXISTS nodes_instance_fingerprint;
18285
+ 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');
18278
18286
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
18279
18287
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
18280
18288
  -- Expression index on the per-node relPath (stored in attrs_json) so the
@@ -18288,7 +18296,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18288
18296
  -- every edited file of every turn, and a scan parses every Problem's attrs.
18289
18297
  CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
18290
18298
  `;
18291
- FINGERPRINT_INDEXED_LABELS = ["Problem", "Solution", "RootCause"];
18299
+ FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
18292
18300
  SqliteGraphStore = class {
18293
18301
  db;
18294
18302
  // Typed edge lookups build their SQL from the number of types asked for, and
@@ -18966,8 +18974,8 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
18966
18974
  const r2 = this.db.prepare(
18967
18975
  `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
18968
18976
  MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
18969
- FROM nodes INDEXED BY nodes_instance_fingerprint
18970
- WHERE valid_to IS NULL AND label IN ('Problem','Solution','RootCause')`
18977
+ FROM nodes INDEXED BY nodes_instance_fingerprint_v2
18978
+ WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
18971
18979
  ).get();
18972
18980
  return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
18973
18981
  }
@@ -20263,7 +20271,7 @@ function mergeContextEdge(store, from, to, type, ts) {
20263
20271
  function deriveContextFromAnchors(store, problemId, ts, pkgIndex) {
20264
20272
  const report = { languages: [], packages: [] };
20265
20273
  const p = store.getNode(problemId);
20266
- if (!p || p.label !== "Problem") return report;
20274
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return report;
20267
20275
  const files = store.outEdges(problemId, ["ANCHORED_AT"]).map((e) => store.getNode(e.to)).filter((n) => !!n && n.label === "File");
20268
20276
  if (files.length === 0) return report;
20269
20277
  const idx = pkgIndex ?? buildPackageIndex(store);
@@ -20901,6 +20909,81 @@ function ingestDesignProblem(store, flag, opts) {
20901
20909
  ...reopenedFromRegression ? { reopenedFromRegression } : {}
20902
20910
  };
20903
20911
  }
20912
+ function ingestWitnessedVulnerability(store, flag, opts) {
20913
+ const statement = flag.statement.trim();
20914
+ if (isPlaceholderStatement(statement)) return { id: "", created: false, corroborated: false, upgraded: false, rejected: "statement" };
20915
+ const target = flag.target?.trim().replace(/\\/g, "/").replace(/^\.\//, "") ?? "";
20916
+ const file2 = target ? resolveFileNode(store, target, opts.workspaceId) : null;
20917
+ const symbol2 = file2 && flag.line !== void 0 && flag.fileText !== void 0 ? resolveEnclosingSymbol(store, file2.id, flag.line, flag.fileText) : null;
20918
+ const identityTarget = symbol2 ? String(symbol2.attrs["logicalId"] ?? `${target}#${symbol2.attrs["qname"] ?? symbol2.description}`) : target;
20919
+ const compatibleTarget = (n) => !symbol2 || !n.attrs["identityTarget"] || n.attrs["identityTarget"] === target || n.attrs["identityTarget"] === identityTarget;
20920
+ const bound = flag.existingId ? store.getNode(flag.existingId) : null;
20921
+ if (flag.existingId && (!bound || bound.label !== "Vulnerability" || bound.attrs["workspaceId"] !== opts.workspaceId || target && bound.attrs["affectedTarget"] && bound.attrs["affectedTarget"] !== target || !compatibleTarget(bound))) {
20922
+ return { id: "", created: false, corroborated: false, upgraded: false, rejected: "binding-conflict" };
20923
+ }
20924
+ const statementKey = identityId({ kind: "DesignProblem", statement });
20925
+ 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));
20926
+ const id = bound?.id ?? restated?.id ?? identityId({ kind: "WitnessedVulnerability", workspaceId: opts.workspaceId, target: identityTarget, statement });
20927
+ const existing = bound ?? store.getNode(id);
20928
+ const sources = existing?.attrs["sources"] ?? [];
20929
+ const corroborated = !!existing && !sources.includes(opts.source);
20930
+ const upgraded = !!existing && existing.attrs["assessment"] !== "asserted" && flag.assessment === "asserted";
20931
+ const assessment = existing?.attrs["assessment"] === "asserted" ? "asserted" : flag.assessment;
20932
+ if (!existing) {
20933
+ store.mergeNode(buildNode(store, id, "Vulnerability", statement, opts.ts, {
20934
+ source: "convo",
20935
+ sourceClass: "agent-observation",
20936
+ privateFinding: id,
20937
+ workspaceId: opts.workspaceId,
20938
+ affectedTarget: target,
20939
+ identityTarget,
20940
+ assessment,
20941
+ anchorDisposition: target ? "unresolved" : "absent",
20942
+ provisional: true,
20943
+ sources: [opts.source],
20944
+ corroborations: 0,
20945
+ ...opts.hostHarness ? { hostHarness: opts.hostHarness } : {}
20946
+ }));
20947
+ } else if (corroborated || upgraded || !existing.attrs["affectedTarget"] && target || symbol2 && existing.attrs["identityTarget"] !== identityTarget) {
20948
+ store.updateNode(id, {
20949
+ attrs: {
20950
+ ...existing.attrs,
20951
+ assessment,
20952
+ ...upgraded && existing.description !== statement ? { statementAliases: [...existing.attrs["statementAliases"] ?? [], existing.description].slice(-5) } : {},
20953
+ affectedTarget: existing.attrs["affectedTarget"] || target,
20954
+ ...symbol2 ? { identityTarget } : {},
20955
+ sources: corroborated ? [...sources, opts.source] : sources,
20956
+ corroborations: (Number(existing.attrs["corroborations"]) || 0) + (corroborated ? 1 : 0)
20957
+ },
20958
+ // Assessment strengthens on evidence, but does not certify exploitability.
20959
+ ...upgraded ? { description: statement } : {},
20960
+ ...corroborated ? { cumulativeHits: existing.cumulativeHits + 1 } : {},
20961
+ ...reinforcedSeqAttrs(store, "Vulnerability"),
20962
+ lastUpdatedAt: opts.ts
20963
+ });
20964
+ }
20965
+ return { id, created: !existing, corroborated, upgraded };
20966
+ }
20967
+ function recordVulnerabilityCause(store, findingId, causes, ts) {
20968
+ const finding = store.getNode(findingId);
20969
+ if (finding?.label !== "Vulnerability") return 0;
20970
+ let from = findingId;
20971
+ let count = 0;
20972
+ for (const description of causes) {
20973
+ if (isPlaceholderStatement(description)) continue;
20974
+ const id = genericCanonicalId("dcause", { findingId, description: description.trim() });
20975
+ if (!store.getNode(id)) store.mergeNode(buildNode(store, id, "RootCause", description.trim(), ts, {
20976
+ source: "convo",
20977
+ privateFinding: findingId,
20978
+ sources: finding.attrs["sources"],
20979
+ assessment: finding.attrs["assessment"]
20980
+ }));
20981
+ mergeEdge(store, from, id, "CAUSED_BY", ts);
20982
+ from = id;
20983
+ count++;
20984
+ }
20985
+ return count;
20986
+ }
20904
20987
  function canonicalizePatternText(name2) {
20905
20988
  const stripped = name2.replace(/^\s*pattern(?:\s*[×x]\s*\d+)?\s*(?:\(exemplar\))?\s*[:—–-]\s*/i, "").replace(/\s+/g, " ").trim();
20906
20989
  return stripped.length > 0 ? stripped : name2.replace(/\s+/g, " ").trim();
@@ -21211,6 +21294,7 @@ function findReusableSolution(store, fixText) {
21211
21294
  const key = solutionTextKey(fixText);
21212
21295
  if (!key) return null;
21213
21296
  for (const s of store.findNodesByLabel("Solution")) {
21297
+ if (s.attrs["privateFinding"]) continue;
21214
21298
  if (s.validTo != null) continue;
21215
21299
  if (isAutoMinted(s)) continue;
21216
21300
  if (solutionTextKey(s.description ?? "") === key) return s;
@@ -21235,7 +21319,7 @@ function priorsForFile(store, relPath) {
21235
21319
  for (const e of store.inEdges(file2.id, ["ANCHORED_AT"])) {
21236
21320
  const n = store.getNode(e.from);
21237
21321
  if (!n) continue;
21238
- if (n.label === "Problem") {
21322
+ if (n.label === "Problem" || n.label === "Vulnerability") {
21239
21323
  if (seenProblem.has(n.id)) continue;
21240
21324
  seenProblem.add(n.id);
21241
21325
  if (!n.attrs["resolvedAt"]) {
@@ -21305,7 +21389,8 @@ function solutionForProblem(store, problemId) {
21305
21389
  }
21306
21390
  function anchorProblemToWorkingFile(store, problemId, relPath, workspaceId2, ts, provenance = "edited", opts = {}) {
21307
21391
  const p = store.getNode(problemId);
21308
- if (!p || p.label !== "Problem") return false;
21392
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
21393
+ if (p.label === "Vulnerability" && provenance !== "witnessed") return false;
21309
21394
  if (!isAnchorableCodePath(relPath)) return false;
21310
21395
  const file2 = resolveFileNode(store, relPath, workspaceId2);
21311
21396
  if (!file2) return false;
@@ -21484,18 +21569,22 @@ function resolveDesignProblemById(store, id, fixNote, t) {
21484
21569
  }
21485
21570
  function closeDesignProblem(store, problemId, fixNote, t) {
21486
21571
  const p = store.getNode(problemId);
21487
- if (!p || p.label !== "Problem") return false;
21572
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
21488
21573
  if (p.attrs["resolvedAs"]) return false;
21489
21574
  if (fixNote?.trim() && !isFragmentFixNote(fixNote)) {
21490
21575
  const sols = store.outEdges(problemId, ["SOLVED_BY"]).map((e) => store.getNode(e.to)).filter((n) => n !== null);
21491
21576
  if (sols.every((s) => isAutoMinted(s))) {
21492
- const reuse = findReusableSolution(store, fixNote);
21577
+ const reuse = p.label === "Vulnerability" ? null : findReusableSolution(store, fixNote);
21493
21578
  if (reuse) {
21494
21579
  mergeEdge(store, problemId, reuse.id, "SOLVED_BY", t);
21495
21580
  } else {
21496
21581
  const solId = `dfix_${digest({ problemId, fix: fixNote })}`.slice(0, 56);
21497
21582
  store.mergeNode(
21498
- buildNode(store, solId, "Solution", fixNote.trim(), t, { source: "convo", provisional: false })
21583
+ buildNode(store, solId, "Solution", fixNote.trim(), t, {
21584
+ source: "convo",
21585
+ provisional: false,
21586
+ ...p.label === "Vulnerability" ? { privateFinding: problemId, sources: p.attrs["sources"] } : {}
21587
+ })
21499
21588
  );
21500
21589
  mergeEdge(store, problemId, solId, "SOLVED_BY", t);
21501
21590
  }
@@ -21509,6 +21598,7 @@ function closeDesignProblem(store, problemId, fixNote, t) {
21509
21598
  // mints no Solution, and a legacy placeholder beside it reads as
21510
21599
  // all-heuristic either way.
21511
21600
  attrs: { ...p.attrs, provisional: false, resolvedAt: t, resolutionWitnessed: true },
21601
+ ...reinforcedSeqAttrs(store, p.label),
21512
21602
  lastUpdatedAt: t
21513
21603
  });
21514
21604
  return true;
@@ -23860,7 +23950,7 @@ function detectLocalCommunities(store, opts = {}) {
23860
23950
  const ids = /* @__PURE__ */ new Set();
23861
23951
  for (const label of JOINT_COMMUNITY_LABELS) {
23862
23952
  for (const n of store.findNodesByLabel(label)) {
23863
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
23953
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
23864
23954
  ids.add(n.id);
23865
23955
  }
23866
23956
  }
@@ -23997,6 +24087,7 @@ var init_community2 = __esm({
23997
24087
  init_src4();
23998
24088
  JOINT_COMMUNITY_LABELS = [
23999
24089
  "Problem",
24090
+ "Vulnerability",
24000
24091
  "Solution",
24001
24092
  "RootCause",
24002
24093
  "Pattern"
@@ -24858,6 +24949,7 @@ __export(src_exports2, {
24858
24949
  induceAbstractions: () => induceAbstractions,
24859
24950
  induceTriage: () => induceTriage,
24860
24951
  ingestDesignProblem: () => ingestDesignProblem,
24952
+ ingestWitnessedVulnerability: () => ingestWitnessedVulnerability,
24861
24953
  isConstraintProblem: () => isConstraintProblem,
24862
24954
  isFragmentFixNote: () => isFragmentFixNote,
24863
24955
  isPlaceholderStatement: () => isPlaceholderStatement,
@@ -24905,6 +24997,7 @@ __export(src_exports2, {
24905
24997
  recordMisreadPrior: () => recordMisreadPrior,
24906
24998
  recordRenderLedger: () => recordRenderLedger,
24907
24999
  recordTriageObservation: () => recordTriageObservation,
25000
+ recordVulnerabilityCause: () => recordVulnerabilityCause,
24908
25001
  reinforceSameAnchorProblem: () => reinforceSameAnchorProblem,
24909
25002
  relateClaims: () => relateClaims,
24910
25003
  reopenAutoClosedProblems: () => reopenAutoClosedProblems,
@@ -24974,7 +25067,7 @@ function isPassiveInjectable(node2, atCommit) {
24974
25067
  return false;
24975
25068
  }
24976
25069
  function buildSnapshot(opts) {
24977
- const problems = opts.store.findNodesByLabel("Problem").filter((p) => p.attrs["mergedInto"] === void 0 && historyReadPolicy(p, { mode: "passive", atCommit: opts.atCommit }).allow);
25070
+ 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);
24978
25071
  const stillOpen = problems.filter((p) => p.attrs["resolvedAt"] == null);
24979
25072
  const allProblems = stillOpen.filter((p) => !isConstraintProblem(p));
24980
25073
  allProblems.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
@@ -25129,11 +25222,11 @@ function sliceForFile(store, relPath, atCommit) {
25129
25222
  const priors = fp ? {
25130
25223
  openProblems: fp.openProblems.slice(0, 3).map((p) => ({
25131
25224
  id: p.id,
25132
- description: p.description,
25225
+ description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description,
25133
25226
  ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
25134
25227
  })),
25135
25228
  constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
25136
- resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
25229
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.label === "Vulnerability" ? `Vulnerability (${p.attrs["assessment"] ?? "unassessed"}): ${p.description}` : p.description })),
25137
25230
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
25138
25231
  solutionsByProblem: Object.fromEntries(
25139
25232
  [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [
@@ -25251,7 +25344,8 @@ function renderSnapshot(s) {
25251
25344
  if (s.recentProblems.length > 0) {
25252
25345
  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):_");
25253
25346
  for (const r of s.recentProblems) {
25254
- lines.push(`- **${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25347
+ const badge = r.node.label === "Vulnerability" ? `Vulnerability (${r.node.attrs["assessment"] ?? "unassessed"}): ` : "";
25348
+ lines.push(`- **${badge}${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25255
25349
  const hits = r.node.cumulativeHits ?? 0;
25256
25350
  const surprise = (r.node.cumulativeSurprise ?? 0).toFixed(2);
25257
25351
  lines.push(` - observed ${hits}\xD7 \xB7 cumulativeSurprise ${surprise}`);
@@ -25292,7 +25386,8 @@ function renderSnapshot(s) {
25292
25386
  if (s.recentResolved.length > 0) {
25293
25387
  lines.push("_Recently resolved \u2014 solved here; jumping-off points, not live defects:_");
25294
25388
  for (const r of s.recentResolved) {
25295
- lines.push(`- \u2713 **${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25389
+ const badge = r.node.label === "Vulnerability" ? `Vulnerability (${r.node.attrs["assessment"] ?? "unassessed"}): ` : "";
25390
+ lines.push(`- \u2713 **${badge}${r.node.description}** \u2014 \`${r.node.id}\`${tagOf(r.node)}`);
25296
25391
  if (r.solution) {
25297
25392
  lines.push(` - solved: ${r.solution.description.slice(0, 160)} \u2014 \`${r.solution.id}\`${tagOf(r.solution)}`);
25298
25393
  }
@@ -27152,8 +27247,8 @@ var init_config = __esm({
27152
27247
  "use strict";
27153
27248
  init_paths();
27154
27249
  LEGACY_CLOUD_URL = "https://inerrata-gateway.onrender.com";
27155
- DEFAULT_CLOUD_URL = true ? "https://inerrata.dev" : DEV_CLOUD_URL;
27156
- DEFAULT_UPDATE_CHANNEL = true ? "latest" : "dev";
27250
+ DEFAULT_CLOUD_URL = true ? "https://dev.inerrata.dev" : DEV_CLOUD_URL;
27251
+ DEFAULT_UPDATE_CHANNEL = false ? "latest" : "dev";
27157
27252
  }
27158
27253
  });
27159
27254
 
@@ -31713,7 +31808,7 @@ var init_src8 = __esm({
31713
31808
 
31714
31809
  // src/agent-signals.ts
31715
31810
  function buildFileRecallInstruction(referent = "\u2026") {
31716
- 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.`;
31811
+ 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.`;
31717
31812
  }
31718
31813
  function triageBullet(ref) {
31719
31814
  return [
@@ -31749,15 +31844,15 @@ function attemptBullet() {
31749
31844
  ].join("\n");
31750
31845
  }
31751
31846
  function buildAgentInstruction(opts = {}) {
31752
- const signals = opts.signals ?? ["prior", "problem", "domain", "fix", "constraint", "triage", "attempt", "link"];
31847
+ const signals = opts.signals ?? ["prior", "problem", "vulnerability", "domain", "fix", "constraint", "triage", "attempt", "link"];
31753
31848
  const ref = opts.referent ?? "its-handle";
31754
31849
  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);
31755
- 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:`;
31756
- 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).";
31850
+ 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:";
31851
+ 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).";
31757
31852
  return [
31758
31853
  head2,
31759
31854
  ...signals.map(
31760
- (s) => s === "triage" ? triageBullet(ref) : s === "attempt" ? attemptBullet() : s === "link" ? linkBullet(ref) : s === "verdict" ? verdictBullet(ref) : ` \u2022 ${GLOSS[s]} \u2192 ${token(s)}`
31855
+ (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)}`
31761
31856
  ),
31762
31857
  tail
31763
31858
  ].join("\n");
@@ -31769,6 +31864,9 @@ var init_agent_signals = __esm({
31769
31864
  TAG_EXAMPLE = {
31770
31865
  prior: (ref) => `([${ref}])`,
31771
31866
  problem: () => `[!one line on what's wrong]`,
31867
+ vulnerability: () => `[!vuln: who can do what; observed basis @ path:line]`,
31868
+ suspectedVulnerability: () => `[?vuln: consequence; missing check]`,
31869
+ vulnerabilityThread: () => `[!#v vuln: finding here]`,
31772
31870
  fix: (ref) => `(fix:[${ref}])`,
31773
31871
  // CONSTRAINT — the native capture path for a DIFFUSE problem (a constraint/tension/
31774
31872
  // conflict that shaped a decision). Free text, attaches to the decision sentence the
@@ -31789,7 +31887,7 @@ var init_agent_signals = __esm({
31789
31887
  // tags never need one — the id is for diagnoses that unfold over distance.
31790
31888
  problemThread: () => `[!#slug one line on what's wrong]`,
31791
31889
  causeThread: () => `(cause:#slug the mechanism)`,
31792
- fixThread: () => `(fix:#slug what you changed)`,
31890
+ fixThread: (thread = "slug") => `(fix:#${thread} what you changed)`,
31793
31891
  // ATTEMPT arc — what's being tried and what didn't work. A failed attempt is a
31794
31892
  // witnessed NEGATIVE result: it saves the next agent the same dead end.
31795
31893
  tried: () => `(tried: the approach you're attempting)`,
@@ -31831,8 +31929,9 @@ var init_agent_signals = __esm({
31831
31929
  // problems, and narrated them as progress (a pick-best-before-scoring bug lost).
31832
31930
  // Every example here was diagnosis-framed, so nothing contradicted that sorting.
31833
31931
  // Kept to 29 chars: the instruction sits at the 2600 passive-budget ceiling.
31834
- 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]",
31835
- 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",
31932
+ problem: "any bug or finding, even in code you just wrote; name affected code: [!what's wrong @ path:42]",
31933
+ vulnerability: "unauthorized actions, data exposure, or execution/disruption an attacker can trigger",
31934
+ 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",
31836
31935
  constraint: "a design decision forced by conflicting requirements or constraints \u2014 tag the tension, especially when your fix makes it 'only look' resolved",
31837
31936
  triage: "you diagnosed a cause \u2014 fold the chain into the flag with <- (caused-by), deepest sure mechanism rightmost",
31838
31937
  attempt: "attempting an approach / an attempt failed \u2014 a failed attempt is knowledge, it rules out a path",
@@ -31854,7 +31953,7 @@ var init_agent_signals = __esm({
31854
31953
  // Sim note (2026-08-02 shrink): domain emission held 4/4 with just the trigger
31855
31954
  // clause; the pattern convergence hook now rides here too (it fell 0/4 when it
31856
31955
  // only appeared in the link bullet's tail).
31857
- 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",
31956
+ 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",
31858
31957
  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",
31859
31958
  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"
31860
31959
  };
@@ -32095,7 +32194,7 @@ function readContextCard(root, hash2) {
32095
32194
  function openProblems(store, now = Date.now()) {
32096
32195
  const hit = openProblemsCache.get(store);
32097
32196
  if (hit && now - hit.at < OPEN_PROBLEMS_TTL_MS) return hit.nodes;
32098
- const nodes = store.findNodesByLabel("Problem").filter((p) => live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p));
32197
+ const nodes = [...store.findNodesByLabel("Problem"), ...store.findNodesByLabel("Vulnerability")].filter((p) => live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p));
32099
32198
  openProblemsCache.set(store, { at: now, nodes });
32100
32199
  return nodes;
32101
32200
  }
@@ -43482,47 +43581,9 @@ var init_mcp = __esm({
43482
43581
  init_reconcile();
43483
43582
  init_agent_signals();
43484
43583
  INSTRUCTIONS = [
43485
- // KEEP THIS UNDER THE CLIENT'S ~2 KB instruction-injection budget (the client
43486
- // truncates `instructions` on connect, ~2008 chars observed). Capture no longer
43487
- // depends on a standing emit-instruction block: the daemon derives problems/fixes
43488
- // from your WORK deterministically, and the one thing it asks of the agent — a
43489
- // one-line root cause at a fix — rides INLINE on the next tool response (the ⚡
43490
- // errata line; see the dispatch's enrichment piggyback) instead of a fenced
43491
- // protocol the agent has to remember. So this stays short: tool-routing steering
43492
- // plus a single line about the inline nudge. Per-tool routing lives in each tool's
43493
- // own `description` (delivered via tools/list). Don't re-add a tool table here.
43494
- "errata is a LIVE, bi-temporal code graph of this workspace, kept current by a",
43495
- "file watcher \u2014 a SEPARATE DIAGNOSTIC AXIS to run ALONGSIDE grep/glob/read, not",
43496
- "instead of them. For STRUCTURAL questions (callers of callers, blast-radius,",
43497
- "rename lineage) prefer it: faster, more complete, and it reaches relationships",
43498
- "grep can't. Each tool's description says when to reach for it. Fall back to",
43499
- 'grep/read when a tool returns found:false (ESPECIALLY reason:"unresolved" \u2014 the',
43500
- "name was not recognized, NOT an empty answer; an empty list like callers:[] is a",
43501
- 'confident "none" \u2014 trust it), when you need EXACT bytes before a high-stakes',
43502
- "edit, or for an unindexed file/language. Always confirm exact bytes with a read",
43503
- "before editing.",
43504
- "",
43505
- "errata is a cloud knowledge substrate \u2014 AND ALSO this workspace's code graph.",
43506
- "The problems you hit and the fixes you make become shared knowledge other agents",
43507
- "recall. That capture is automatic: derived from your edits/commands/commits \u2014 no",
43508
- "emit needed. Just after you resolve something a tool result may carry a one-line",
43509
- "\u26A1 errata note asking the ROOT CAUSE; answer via errata.annotate_resolution when",
43510
- "you know it, skip it otherwise.",
43511
- "",
43512
- "Two things it CAN'T derive \u2014 only you witness them, and this is a CAPTURE",
43513
- "PROTOCOL, not optional formatting: a problem or fix you state only in prose, a",
43514
- "list, or your own numbering is SILENTLY LOST \u2014 the tag is the ONLY thing recorded.",
43515
- "So the moment you state one, wrap it inline (no fence, no extra call):",
43516
- // Signal tokens come from the single source (agent-signals.ts) so this always-on
43517
- // block can't teach a grammar the parser rejects. Referent is "its-id" here — the
43518
- // raw MCP channel has no primed handles, so the agent cites a tool-result id.
43519
- ` \u2022 ${TAG_EXAMPLE.problem()} \u2014 EVERY problem you notice or report ([?\u2026] = TODO)`,
43520
- ` \u2022 ${TAG_EXAMPLE.fix("its-id")} \u2014 your edit fixed a problem errata showed you`,
43521
- "Tag every one you state \u2014 we filter downstream, so don't self-censor or batch.",
43522
- "",
43523
- "Graph-as-files: the graph is ALSO a virtual FS \u2014 Read (or `cat`)",
43524
- ".errata/g/<verb>/<seed> to run a query as a file read (e.g. .errata/g/burst/",
43525
- "<symbol>, /why/<sym>, /trace/<a>..<b>). Read .errata/g/index for the vocabulary."
43584
+ "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.",
43585
+ buildAgentInstruction({ signals: ["prior", "problem", "vulnerability", "fix"], referent: "its-id" }),
43586
+ "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."
43526
43587
  ].join("\n");
43527
43588
  AUDIT_RUN_THRESHOLD = 5;
43528
43589
  AUDIT_NUDGE_COOLDOWN = 12;
@@ -44114,7 +44175,7 @@ var init_mcp = __esm({
44114
44175
  },
44115
44176
  {
44116
44177
  name: "errata.problems",
44117
- 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.",
44178
+ 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.",
44118
44179
  inputSchema: {
44119
44180
  type: "object",
44120
44181
  properties: {
@@ -44126,13 +44187,15 @@ var init_mcp = __esm({
44126
44187
  const want = String(args2["status"] ?? "open");
44127
44188
  const limit = Math.max(1, Math.min(200, Number(args2["limit"] ?? 50)));
44128
44189
  const RETRACTIONS = /* @__PURE__ */ new Set(["false_positive", "invalid", "duplicate"]);
44129
- const rows = store.findAllVersionsByLabel("Problem").map((p) => {
44190
+ const rows = [...store.findAllVersionsByLabel("Problem"), ...store.findAllVersionsByLabel("Vulnerability")].map((p) => {
44130
44191
  const ra = p.attrs["resolvedAs"];
44131
44192
  const status = ra && RETRACTIONS.has(ra) ? ra : p.attrs["resolvedAt"] != null ? "resolved" : isConstraintProblem(p) ? "constraint" : "open";
44132
44193
  const anchorEdge = store.outEdges(p.id, ["ANCHORED_AT"])[0];
44133
44194
  const anchor = anchorEdge ? store.getNode(anchorEdge.to)?.description : void 0;
44134
44195
  return {
44135
44196
  id: p.id,
44197
+ label: p.label,
44198
+ ...p.label === "Vulnerability" ? { assessment: p.attrs["assessment"] } : {},
44136
44199
  problem: p.description,
44137
44200
  status,
44138
44201
  createdAt: p.createdAt,
@@ -52697,7 +52760,8 @@ function errorRecallTerms(errorText) {
52697
52760
  }
52698
52761
  function recallLine(n, token) {
52699
52762
  const desc = (n.description ?? "").replace(/\s+/g, " ").slice(0, 120);
52700
- return `- [${n.label}] ${desc} \u2192 ${token}`;
52763
+ const label = n.label === "Vulnerability" ? `Vulnerability; ${n.attrs["assessment"] ?? "unassessed"}` : n.label;
52764
+ return `- [${label}] ${desc} \u2192 ${token}`;
52701
52765
  }
52702
52766
  function recallForFile(store, relPath) {
52703
52767
  return fileRecall(store, relPath)?.context ?? null;
@@ -52718,7 +52782,7 @@ function fileRecall(store, relPath) {
52718
52782
  const shownRelated = priors.related.slice(0, 4);
52719
52783
  for (const p of shownProblems) {
52720
52784
  const hasCause = store.outEdges(p.id, ["CAUSED_BY"]).some((e) => store.getNode(e.to)?.label === "RootCause");
52721
- const causeCue = hasCause ? "" : ` \xB7 ${TAG_EXAMPLE.triageMint()} to diagnose its cause`;
52785
+ const causeCue = hasCause ? "" : ` \xB7 ${p.label === "Vulnerability" ? `(cause:#${p.id} the mechanism)` : TAG_EXAMPLE.triageMint()} to diagnose its cause`;
52722
52786
  lines.push(
52723
52787
  recallLine(p, `${TAG_EXAMPLE.fix(p.id)} if you resolved it \xB7 ${TAG_EXAMPLE.prior(p.id)} to cite${causeCue}`)
52724
52788
  );
@@ -75988,7 +76052,7 @@ init_src5();
75988
76052
  import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "node:fs";
75989
76053
  var NEG = /n['']t|\b(?:not|never|no|none|neither|nor|unrelated|irrelevant|would|might|could|if)\b/i;
75990
76054
  var CITE_GROUP_RE = /\(((?:[^()\n]|\([^()\n]*\)){1,200})\)/g;
75991
- var HANDLE_RE = /\[([a-z0-9][\w-]{0,80})\]|((?:pat|sol|drc|dcause|dfix|dprob|prob|claim|err)_[0-9a-f]{6,})/gi;
76055
+ var HANDLE_RE = /\[([a-z0-9][\w-]{0,80})\]|((?:pat|sol|drc|dcause|dfix|dprob|dvuln|prob|claim|err)_[0-9a-f]{6,})/gi;
75992
76056
  var PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
75993
76057
  "handle",
75994
76058
  "its-handle",
@@ -76003,7 +76067,7 @@ var PLACEHOLDER_HANDLES = /* @__PURE__ */ new Set([
76003
76067
  "b"
76004
76068
  ]);
76005
76069
  var isPlaceholderHandle = (h) => PLACEHOLDER_HANDLES.has(h.toLowerCase());
76006
- 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;
76070
+ 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;
76007
76071
  var FIX_PREFIX = /^\s*fix:(?:#([a-z][\w-]{0,63}))?\s*/i;
76008
76072
  var CONSTRAINT_RE = /\(\s*constraint:\s*([^()\n]{8,}?)\s*\)/gi;
76009
76073
  var CAUSE_PREFIX = /^\s*cause:(?:#([a-z][\w-]{0,63}))?\s*/i;
@@ -76028,7 +76092,7 @@ var DOMAIN_RE = /\(\s*domain:\s*([^()\n]{3,}?)\s*\)/gi;
76028
76092
  var PACKAGE_RE = /\(\s*package:\s*([^()\n]{2,}?)\s*\)/gi;
76029
76093
  var COMPONENT_RE = /\(\s*component:\s*([^()\n]{2,}?)\s*\)/gi;
76030
76094
  var CAUSE_TEXT_MIN = 8;
76031
- var FLAG_RE = /\[([!?])(?:#([a-z][\w-]{0,63})\s+)?\s*((?:\[[^\]\n]*\]|[^\]\n]){8,}?)\s*(?:@\s*([^\s\]]+?)(?::(\d+))?\s*)?\]/g;
76095
+ var FLAG_RE = /\[([!?])(?:#([a-z][\w-]{0,63})\s+)?\s*((?:\[[^\]\n]*\]|[^\]\n])*?)\s*(?:@\s*([^\s\]]+?)(?::(\d+))?\s*)?\]/g;
76032
76096
  var CONSTRAINT_MIN = 8;
76033
76097
  function clauseBefore(sentence, idx) {
76034
76098
  const win = sentence.slice(Math.max(0, idx - 80), idx);
@@ -76172,6 +76236,7 @@ function cleanEmittedTags(tags) {
76172
76236
  }
76173
76237
  function emptyTagDispositions() {
76174
76238
  return {
76239
+ invalidVulnerability: 0,
76175
76240
  oversizeSegments: 0,
76176
76241
  codeSpanNeutralized: 0,
76177
76242
  refuteTooShort: 0,
@@ -76184,6 +76249,7 @@ function emptyTagDispositions() {
76184
76249
  };
76185
76250
  }
76186
76251
  function addTagDispositions(a, b) {
76252
+ a.invalidVulnerability += b.invalidVulnerability;
76187
76253
  a.oversizeSegments += b.oversizeSegments;
76188
76254
  a.codeSpanNeutralized += b.codeSpanNeutralized;
76189
76255
  a.refuteTooShort += b.refuteTooShort;
@@ -76197,12 +76263,31 @@ function addTagDispositions(a, b) {
76197
76263
  function parseInlineTags(text) {
76198
76264
  return parseInlineTagsWithDispositions(text).tags;
76199
76265
  }
76266
+ function* captureSentences(text) {
76267
+ for (const line of text.split(/\n+/)) {
76268
+ if (line.length > 2e4) {
76269
+ yield line;
76270
+ continue;
76271
+ }
76272
+ const flags2 = [...line.matchAll(new RegExp(FLAG_RE.source, FLAG_RE.flags))].map((m) => ({ start: m.index, end: m.index + m[0].length }));
76273
+ let flag = 0;
76274
+ let start2 = 0;
76275
+ for (const boundary of line.matchAll(/(?<=[.!?])\s+(?!\(fix:)/g)) {
76276
+ const index = boundary.index;
76277
+ while (flags2[flag] && flags2[flag].end <= index) flag++;
76278
+ if (flags2[flag] && flags2[flag].start < index) continue;
76279
+ yield line.slice(start2, index);
76280
+ start2 = index + boundary[0].length;
76281
+ }
76282
+ yield line.slice(start2);
76283
+ }
76284
+ }
76200
76285
  function parseInlineTagsWithDispositions(text) {
76201
76286
  const out2 = [];
76202
76287
  const dispositions = emptyTagDispositions();
76203
76288
  const deFenced = text.replace(/```[\s\S]*?```/g, " ");
76204
76289
  let seqNo = -1;
76205
- for (const raw2 of deFenced.split(/(?<=[.!?])\s+(?!\(fix:)|\n+/)) {
76290
+ for (const raw2 of captureSentences(deFenced)) {
76206
76291
  seqNo++;
76207
76292
  if (raw2.length > 2e4) {
76208
76293
  dispositions.oversizeSegments++;
@@ -76428,6 +76513,29 @@ function parseInlineTagsWithDispositions(text) {
76428
76513
  while ((f = FLAG_RE.exec(sentence)) !== null) {
76429
76514
  const flagThread = f[2];
76430
76515
  const raw3 = f[3].trim();
76516
+ if (/^vuln\b/i.test(raw3)) {
76517
+ const typed = /^vuln:\s*([\s\S]*)$/i.exec(raw3);
76518
+ const body2 = typed?.[1]?.trim() ?? "";
76519
+ const parts2 = body2.split(CAUSE_ARROW).map((s) => s.trim());
76520
+ const symptom = f[1] === "!" && parts2.length > 1 && !f[4] ? new RegExp(FLAG_RE.source, "i").exec(`[!${parts2[0]}]`) : null;
76521
+ const statement = f[1] === "?" ? body2 : symptom?.[3]?.trim() ?? parts2[0];
76522
+ if (statement.length < 8 || /^<[^>]*>$/.test(statement)) {
76523
+ dispositions.invalidVulnerability++;
76524
+ continue;
76525
+ }
76526
+ const path2 = (f[4] ?? symptom?.[4])?.trim();
76527
+ const line = f[5] ?? symptom?.[5];
76528
+ out2.push({
76529
+ kind: "vulnerability",
76530
+ statement,
76531
+ assessment: f[1] === "!" ? "asserted" : "suspected",
76532
+ ...f[1] === "!" && parts2.length > 1 ? { vulnerabilityCauses: parts2.slice(1).filter((s) => s.length >= CAUSE_TEXT_MIN) } : {},
76533
+ ...path2 ? { location: { path: path2, ...line ? { line: Number(line) } : {} } } : {},
76534
+ ...flagThread ? { threadId: flagThread } : {},
76535
+ sentence: sfield
76536
+ });
76537
+ continue;
76538
+ }
76431
76539
  if (FLAG_BODY_HANDLE_RE.test(raw3) && !f[4]) {
76432
76540
  dispositions.flagAsHandle++;
76433
76541
  out2.push({ kind: "prior", handle: raw3, sentence: sfield });
@@ -76798,7 +76906,7 @@ function harvestInlineTags(store, text, opts) {
76798
76906
  const parsed = parseInlineTagsWithDispositions(text);
76799
76907
  const tags = parsed.tags;
76800
76908
  plan.dispositions = parsed.dispositions;
76801
- 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);
76909
+ 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);
76802
76910
  const bindSymptom = (seq, threadId) => {
76803
76911
  if (threadId) {
76804
76912
  const hit = symptomSeqs.find((s) => s.threadId === threadId);
@@ -76811,6 +76919,7 @@ function harvestInlineTags(store, text, opts) {
76811
76919
  if (seq == null) return { evidence: "inferred" };
76812
76920
  let best;
76813
76921
  for (const s of symptomSeqs) {
76922
+ if (s.kind === "vulnerability") continue;
76814
76923
  if (s.seq <= seq) best = s;
76815
76924
  else break;
76816
76925
  }
@@ -76825,10 +76934,13 @@ function harvestInlineTags(store, text, opts) {
76825
76934
  return id;
76826
76935
  };
76827
76936
  for (const tag of tags) {
76828
- if (tag.kind === "problem" || tag.kind === "todo") {
76937
+ if (tag.kind === "problem" || tag.kind === "todo" || tag.kind === "vulnerability") {
76829
76938
  plan.problems.push({
76830
76939
  statement: tag.statement,
76831
76940
  kind: tag.kind,
76941
+ ...tag.assessment ? { assessment: tag.assessment } : {},
76942
+ ...tag.vulnerabilityCauses ? { vulnerabilityCauses: tag.vulnerabilityCauses } : {},
76943
+ ...tag.kind === "vulnerability" && tag.seq !== void 0 ? { seq: tag.seq } : {},
76832
76944
  ...tag.location ? { location: tag.location } : {},
76833
76945
  ...tag.threadId ? { threadId: tag.threadId } : {}
76834
76946
  });
@@ -76851,7 +76963,7 @@ function harvestInlineTags(store, text, opts) {
76851
76963
  } else if (tag.threadId && !b.statement) {
76852
76964
  plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, threadId: tag.threadId });
76853
76965
  } else if (b.statement && b.evidence === "witnessed") {
76854
- plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, boundStatement: b.statement });
76966
+ plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, boundStatement: b.statement, ...tag.threadId ? { threadId: tag.threadId } : {} });
76855
76967
  } else {
76856
76968
  plan.fixes.push({ inScope: true, fixNote: tag.fixRationale, ambiguous: true });
76857
76969
  }
@@ -77054,6 +77166,7 @@ function harvestInlineTags(store, text, opts) {
77054
77166
  }
77055
77167
  var CORROBORATABLE_LABELS = /* @__PURE__ */ new Set([
77056
77168
  "Problem",
77169
+ "Vulnerability",
77057
77170
  "Solution",
77058
77171
  "RootCause",
77059
77172
  "Pattern",
@@ -80832,9 +80945,9 @@ function partitionReplayable(queue, channel, isShipped) {
80832
80945
  function retireWitnesses(queue, channel, settledKeys) {
80833
80946
  return queue.filter((w) => !(w.channel === channel && settledKeys.has(w.witnessKey)));
80834
80947
  }
80835
- var WIRE_SHIPPED_LABELS = /* @__PURE__ */ new Set(["Problem", "Solution", "RootCause"]);
80948
+ var WIRE_SHIPPED_LABELS = /* @__PURE__ */ new Set(["Problem", "Vulnerability", "Solution", "RootCause"]);
80836
80949
  var MOTIF_ID_PREFIX = "motif_community_";
80837
- var CANONICAL_ID_RE = /^(?:pat|sol|drc|dcause|dfix|dprob|prob|claim|err)_[0-9a-f]{6,}$/i;
80950
+ var CANONICAL_ID_RE = /^(?:pat|sol|drc|dcause|dfix|dprob|dvuln|prob|claim|err)_[0-9a-f]{6,}$/i;
80838
80951
  function classifyWitnessTarget(nodeId, getNode) {
80839
80952
  if (nodeId.startsWith(MOTIF_ID_PREFIX)) {
80840
80953
  const exemplar = nodeId.slice(MOTIF_ID_PREFIX.length);
@@ -81404,7 +81517,7 @@ function createWatchBreaker(deps) {
81404
81517
  }
81405
81518
 
81406
81519
  // src/engine.ts
81407
- var DAEMON_VERSION = true ? "2.0.16" : "2.0.0-alpha.0";
81520
+ var DAEMON_VERSION = true ? "2.0.17-dev.3369" : "2.0.0-alpha.0";
81408
81521
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
81409
81522
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
81410
81523
  var GIT_OP_MUTE_MS = 4e3;
@@ -82296,6 +82409,9 @@ function createWorkspaceEngine(opts) {
82296
82409
  const harvestTextsInner = async (sessionId, items) => {
82297
82410
  let minted = 0;
82298
82411
  let tagsSeen = 0;
82412
+ let vulnerabilitiesSeen = 0;
82413
+ let vulnerabilitiesMinted = 0;
82414
+ let vulnerabilitiesSuspected = 0;
82299
82415
  let tagsRejected = 0;
82300
82416
  let tagsRestated = 0;
82301
82417
  let resolved = 0;
@@ -82526,10 +82642,73 @@ function createWorkspaceEngine(opts) {
82526
82642
  if ((toolRuns.get(sessionId)?.size ?? 0) > 0) touchedToolTurns++;
82527
82643
  const wf = workingFiles.get(sessionId);
82528
82644
  let inScopeProblemId;
82645
+ const findingIds = /* @__PURE__ */ new Map();
82646
+ const conflictingThreads = /* @__PURE__ */ new Set();
82529
82647
  const threads = sessionThreads.get(sessionId) ?? /* @__PURE__ */ new Map();
82530
82648
  sessionThreads.set(sessionId, threads);
82531
82649
  for (const p of plan.problems) {
82532
82650
  tagsSeen++;
82651
+ if (p.kind === "vulnerability") {
82652
+ vulnerabilitiesSeen++;
82653
+ if (p.assessment === "suspected") vulnerabilitiesSuspected++;
82654
+ let fileText;
82655
+ let anchorReadFailed = false;
82656
+ if (p.location) {
82657
+ try {
82658
+ fileText = readFileSync31(join35(opts.workspaceRoot, p.location.path), "utf8");
82659
+ } catch {
82660
+ anchorReadFailed = true;
82661
+ }
82662
+ }
82663
+ const result = ingestWitnessedVulnerability(store, {
82664
+ statement: p.statement,
82665
+ assessment: p.assessment,
82666
+ target: p.location?.path,
82667
+ line: p.location?.line,
82668
+ fileText,
82669
+ existingId: p.threadId ? threads.get(p.threadId) ?? handleMap[p.threadId]?.id ?? store.getNode(p.threadId)?.id : void 0
82670
+ }, { workspaceId: profile.id, source: sessionId, ts: t, hostHarness: turn.hostHarness });
82671
+ let anchorDisposition = p.location ? "unresolved" : "absent";
82672
+ if (result.rejected) {
82673
+ tagsRejected++;
82674
+ if (p.threadId) conflictingThreads.add(p.threadId);
82675
+ } else {
82676
+ findingIds.set(p.statement, result.id);
82677
+ if (p.threadId) threads.set(p.threadId, result.id);
82678
+ if (result.created || result.corroborated || result.upgraded) minted++;
82679
+ else tagsRestated++;
82680
+ if (result.created) vulnerabilitiesMinted++;
82681
+ if (p.location) {
82682
+ try {
82683
+ const anchored = anchorProblemToWorkingFile(store, result.id, p.location.path, profile.id, t, "witnessed", { line: p.location.line, fileText });
82684
+ anchorDisposition = anchored ? "resolved" : anchorReadFailed ? "error" : "unresolved";
82685
+ if (anchored) deriveContextFromAnchors(store, result.id, t);
82686
+ } catch {
82687
+ anchorDisposition = "error";
82688
+ }
82689
+ }
82690
+ const node2 = store.getNode(result.id);
82691
+ store.updateNode(result.id, { attrs: { ...node2.attrs, anchorDisposition } });
82692
+ if (p.vulnerabilityCauses) triaged += recordVulnerabilityCause(store, result.id, p.vulnerabilityCauses, t);
82693
+ }
82694
+ log.append({
82695
+ kind: "vulnerability.witness",
82696
+ workspaceId: profile.id,
82697
+ ts: t,
82698
+ digest: digest({ kind: "vulnerability.witness", workspaceId: profile.id, sessionId, turnId: turn.uuid, statement: p.statement, assessment: p.assessment, location: p.location }),
82699
+ sessionId,
82700
+ turnId: turn.uuid,
82701
+ harness: turn.hostHarness,
82702
+ findingId: result.id,
82703
+ statement: p.statement,
82704
+ assessment: p.assessment,
82705
+ location: p.location,
82706
+ seq: p.seq,
82707
+ anchorDisposition,
82708
+ disposition: result.rejected ?? (result.created ? "created" : result.upgraded ? "upgraded" : result.corroborated ? "corroborated" : "restated")
82709
+ });
82710
+ continue;
82711
+ }
82533
82712
  const anchorPath = p.location?.path ?? editedTurnFile;
82534
82713
  const anchorProvenance = p.location?.path ? "witnessed" : "edited";
82535
82714
  const hintPath = anchorPath ? void 0 : turnFile ?? wf;
@@ -82624,10 +82803,10 @@ function createWorkspaceEngine(opts) {
82624
82803
  if (r.created || r.corroborated) {
82625
82804
  minted++;
82626
82805
  if (p.kind !== "constraint") {
82627
- sessionLastProblem.set(sessionId, designProblemId(p.statement));
82628
- inScopeProblemId = designProblemId(p.statement);
82806
+ sessionLastProblem.set(sessionId, r.problemId);
82807
+ inScopeProblemId = r.problemId;
82629
82808
  }
82630
- if (p.threadId) threads.set(p.threadId, designProblemId(p.statement));
82809
+ if (p.threadId) threads.set(p.threadId, r.problemId);
82631
82810
  try {
82632
82811
  if (anchorPath) {
82633
82812
  let enclosing = {};
@@ -82664,7 +82843,8 @@ function createWorkspaceEngine(opts) {
82664
82843
  );
82665
82844
  }
82666
82845
  for (const fx of plan.fixes) {
82667
- const pid = fx.problemId ?? (fx.threadId ? threads.get(fx.threadId) : void 0) ?? (fx.boundStatement ? designProblemId(fx.boundStatement) : void 0);
82846
+ if (fx.threadId && conflictingThreads.has(fx.threadId)) continue;
82847
+ const pid = fx.problemId ?? (fx.threadId ? threads.get(fx.threadId) : void 0) ?? (fx.boundStatement ? findingIds.get(fx.boundStatement) ?? designProblemId(fx.boundStatement) : void 0);
82668
82848
  if (!pid) {
82669
82849
  if (fx.inScope) {
82670
82850
  console.warn(
@@ -82676,7 +82856,7 @@ function createWorkspaceEngine(opts) {
82676
82856
  const closed = resolveDesignProblemById(store, pid, fx.fixNote, t);
82677
82857
  if (!closed) {
82678
82858
  const n = store.getNode(pid);
82679
- if (!n || n.label !== "Problem" || n.attrs["resolvedAs"] != null) {
82859
+ if (!n || n.label !== "Problem" && n.label !== "Vulnerability" || n.attrs["resolvedAs"] != null) {
82680
82860
  console.warn(
82681
82861
  `[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`
82682
82862
  );
@@ -82695,10 +82875,23 @@ function createWorkspaceEngine(opts) {
82695
82875
  }
82696
82876
  }
82697
82877
  }
82878
+ const vulnerabilityTriages = /* @__PURE__ */ new Set();
82879
+ for (const tr of plan.triages) {
82880
+ if (tr.threadId && conflictingThreads.has(tr.threadId)) {
82881
+ vulnerabilityTriages.add(tr);
82882
+ continue;
82883
+ }
82884
+ const id = tr.symptomProblemId ?? (tr.threadId ? threads.get(tr.threadId) : void 0) ?? (tr.symptomStatement ? findingIds.get(tr.symptomStatement) : void 0);
82885
+ if (id && store.getNode(id)?.label === "Vulnerability") {
82886
+ vulnerabilityTriages.add(tr);
82887
+ if (tr.evidence === "witnessed" && tr.causeDescription) triaged += recordVulnerabilityCause(store, id, [tr.causeDescription], t);
82888
+ }
82889
+ }
82698
82890
  if (opts.sharedStore && plan.triages.length > 0) {
82699
82891
  const fallbackId = sessionLastProblem.get(sessionId);
82700
82892
  const fallbackStatement = fallbackId ? store.getNode(fallbackId)?.description : void 0;
82701
82893
  for (const tr of plan.triages) {
82894
+ if (vulnerabilityTriages.has(tr)) continue;
82702
82895
  let presentingStatement = tr.symptomStatement;
82703
82896
  let evidence = tr.evidence;
82704
82897
  if (!presentingStatement && tr.threadId) {
@@ -82716,7 +82909,7 @@ function createWorkspaceEngine(opts) {
82716
82909
  evidence = "inferred";
82717
82910
  }
82718
82911
  if (!presentingStatement) continue;
82719
- const presentingId = tr.symptomProblemId ?? designProblemId(presentingStatement);
82912
+ const presentingId = tr.symptomProblemId ?? (tr.threadId ? threads.get(tr.threadId) : void 0) ?? designProblemId(presentingStatement);
82720
82913
  if (tr.causeId === presentingId) continue;
82721
82914
  try {
82722
82915
  recordTriageObservation(
@@ -82774,7 +82967,7 @@ function createWorkspaceEngine(opts) {
82774
82967
  }
82775
82968
  }
82776
82969
  for (const at of plan.attempts) {
82777
- const pid = at.problemId ?? (at.threadId ? threads.get(at.threadId) : void 0) ?? (at.boundStatement ? designProblemId(at.boundStatement) : void 0);
82970
+ const pid = at.problemId ?? (at.threadId ? threads.get(at.threadId) : void 0) ?? (at.boundStatement ? findingIds.get(at.boundStatement) ?? designProblemId(at.boundStatement) : void 0);
82778
82971
  const node2 = pid ? store.getNode(pid) : void 0;
82779
82972
  if (!pid || !node2) continue;
82780
82973
  try {
@@ -82790,7 +82983,7 @@ function createWorkspaceEngine(opts) {
82790
82983
  }
82791
82984
  }
82792
82985
  const bindPid = (tag) => {
82793
- const pid = tag.problemId ?? (tag.threadId ? threads.get(tag.threadId) : void 0) ?? (tag.boundStatement ? designProblemId(tag.boundStatement) : void 0) ?? inScopeProblemId ?? sessionLastProblem.get(sessionId);
82986
+ 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);
82794
82987
  return pid && store.getNode(pid) ? pid : void 0;
82795
82988
  };
82796
82989
  const mintCiteEdge = (from, to, type, confidence, extraAttrs) => {
@@ -82944,7 +83137,12 @@ function createWorkspaceEngine(opts) {
82944
83137
  for (const [session, items2] of groups) {
82945
83138
  if (items2.length === 0) continue;
82946
83139
  try {
82947
- const raw2 = await send(items2, session);
83140
+ const wireItems = items2.map(({ nodeId, witnessKey }) => {
83141
+ const node2 = store.getNode(nodeId);
83142
+ const cloudId = node2?.attrs["privateFinding"] ? node2.attrs["cloudNodeId"] : void 0;
83143
+ return { nodeId: typeof cloudId === "string" ? cloudId : nodeId, witnessKey };
83144
+ });
83145
+ const raw2 = await send(wireItems, session);
82948
83146
  const d = {
82949
83147
  recorded: raw2?.recorded ?? 0,
82950
83148
  unmatched: raw2?.unmatched ?? 0,
@@ -82957,8 +83155,8 @@ function createWorkspaceEngine(opts) {
82957
83155
  duplicate += d.duplicate;
82958
83156
  selfGated += d.selfGated;
82959
83157
  const missing = new Set(d.unmatchedIds);
82960
- for (const it of items2) {
82961
- if (missing.has(it.nodeId)) stillUnmatched.push({ channel, ...it, sessionKey: session, counted: isShipped(it.nodeId) });
83158
+ for (const [index, it] of items2.entries()) {
83159
+ if (missing.has(wireItems[index].nodeId)) stillUnmatched.push({ channel, ...it, sessionKey: session, counted: isShipped(it.nodeId) });
82962
83160
  else settled.add(it.witnessKey);
82963
83161
  }
82964
83162
  } catch (err2) {
@@ -83128,6 +83326,10 @@ function createWorkspaceEngine(opts) {
83128
83326
  // but nothing judgeable was cited.
83129
83327
  tagsVerdictNoHandle: dispositions.verdictNoHandle,
83130
83328
  tagsFlagAsHandle: dispositions.flagAsHandle,
83329
+ tagsInvalidVulnerability: dispositions.invalidVulnerability,
83330
+ vulnerabilitiesSeen,
83331
+ vulnerabilitiesMinted,
83332
+ vulnerabilitiesSuspected,
83131
83333
  unresolvedHandles,
83132
83334
  priorEdgesFlagOff,
83133
83335
  priorEdgesNoSource,
@@ -84465,7 +84667,7 @@ function internalSymbolsFor(description, lexiconHas, publicIndex) {
84465
84667
 
84466
84668
  // src/instance-ingest.ts
84467
84669
  init_generalize_graph();
84468
- var INSTANCE_LABELS = ["Problem", "Solution", "RootCause"];
84670
+ var INSTANCE_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
84469
84671
  var INSTANCE_EDGES = ["CAUSED_BY", "SOLVED_BY"];
84470
84672
  var ANCHOR_EDGES = ["OCCURS_IN", "DEPENDS_ON", "PERTAIN_TO", "CONCERNS"];
84471
84673
  var ANCHOR_TARGET_LABELS = ["Language", "Package", "Domain", "Component"];
@@ -84600,6 +84802,10 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84600
84802
  attrs: {
84601
84803
  scope: stripCodebaseScope(n.attrs["scope"]),
84602
84804
  ...n.attrs["kind"] ? { kind: n.attrs["kind"] } : {},
84805
+ ...n.attrs["privateFinding"] ? { privateFinding: true } : {},
84806
+ ...n.attrs["assessment"] ? { assessment: n.attrs["assessment"] } : {},
84807
+ ...n.attrs["sourceClass"] ? { sourceClass: n.attrs["sourceClass"] } : {},
84808
+ ...n.attrs["resolutionWitnessed"] ? { resolutionWitnessed: true, resolvedAt: n.attrs["resolvedAt"] } : {},
84603
84809
  ...n.attrs["resolvedAs"] ? { resolvedAs: n.attrs["resolvedAs"] } : {},
84604
84810
  ...preserved.length > 0 ? { preservedSymbols: preserved } : {},
84605
84811
  ...internal.length > 0 ? { internalSymbols: internal } : {}
@@ -84614,6 +84820,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84614
84820
  const semanticEndpoints = /* @__PURE__ */ new Map();
84615
84821
  const includedByLabel = /* @__PURE__ */ new Map();
84616
84822
  const composition = {
84823
+ privateTargetMissing: 0,
84617
84824
  candidates: 0,
84618
84825
  shipped: 0,
84619
84826
  cloudSourced: 0,
@@ -84659,11 +84866,15 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84659
84866
  if (walked.has(n.id)) continue;
84660
84867
  walked.add(n.id);
84661
84868
  composition.candidates++;
84869
+ if ((n.attrs["privateFinding"] || n.label === "Vulnerability") && !opts.project) {
84870
+ composition.privateTargetMissing++;
84871
+ continue;
84872
+ }
84662
84873
  if (n.attrs["source"] === "cloud") {
84663
84874
  composition.cloudSourced++;
84664
84875
  continue;
84665
84876
  }
84666
- if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
84877
+ if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) {
84667
84878
  composition.falsePositive++;
84668
84879
  continue;
84669
84880
  }
@@ -84686,7 +84897,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84686
84897
  composition.terminalRefused++;
84687
84898
  continue;
84688
84899
  }
84689
- if (!noveltyAgainst(n, ref, noveltyOpts).ready) {
84900
+ if (!n.attrs["privateFinding"] && !noveltyAgainst(n, ref, noveltyOpts).ready) {
84690
84901
  composition.notNovel++;
84691
84902
  continue;
84692
84903
  }
@@ -84729,7 +84940,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
84729
84940
  }
84730
84941
  if (targets.length === 0) continue;
84731
84942
  const shipped = shippedById.get(sourceId);
84732
- if (shipped) {
84943
+ if (shipped && !shipped.attrs["privateFinding"]) {
84733
84944
  const anchors = (label) => targets.filter((x) => x.t.label === label).map(
84734
84945
  (x) => label === "Language" ? langAnchor(x.t) : label === "Component" ? `component:${x.wireId}` : x.wireId
84735
84946
  ).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.16",
3
+ "version": "2.0.17-dev.3369",
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) => [