@inerrata-corporation/errata 2.0.20 → 2.0.21-dev.3716

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/errata.mjs CHANGED
@@ -137,6 +137,9 @@ var init_castalia = __esm({
137
137
  "probe-failed"
138
138
  ];
139
139
  ROUTING_REASONS = [
140
+ "trusted-system-public",
141
+ "public-target-staged",
142
+ "source-membrane",
140
143
  /** Harness org (CTF lane, e2e battery, canary) — forced org-private. */
141
144
  "harness-clamp",
142
145
  "witnessed-vulnerability-private",
@@ -18594,6 +18597,11 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18594
18597
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
18595
18598
  -- every edited file of every turn, and a scan parses every Problem's attrs.
18596
18599
  CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
18600
+ -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
18601
+ -- tool response (the enrichment piggyback), so it must not parse every
18602
+ -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
18603
+ -- closed versions too.
18604
+ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_json, '$.enrichmentPending'));
18597
18605
  `;
18598
18606
  FINGERPRINT_INDEXED_LABELS = ["Problem", "Vulnerability", "Solution", "RootCause"];
18599
18607
  NODE_LABELS_CHUNK = 256;
@@ -19194,7 +19202,10 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
19194
19202
  }));
19195
19203
  }
19196
19204
  findAllVersionsByLabel(label, attribute) {
19197
- const rows = attribute ? this.db.prepare("SELECT * FROM nodes WHERE label = ? AND json_extract(attrs_json, ?) = ?").all(label, `$.${attribute.path}`, attribute.value) : this.stmts.findByLabelAll.all(label);
19205
+ if (attribute && !/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(attribute.path)) {
19206
+ throw new Error(`invalid attr path: ${attribute.path}`);
19207
+ }
19208
+ const rows = attribute ? this.db.prepare(`SELECT * FROM nodes WHERE json_extract(attrs_json, '$.${attribute.path}') = ? AND label = ?`).all(attribute.value, label) : this.stmts.findByLabelAll.all(label);
19198
19209
  return rows.map(rowToNode);
19199
19210
  }
19200
19211
  searchNodes(opts) {
@@ -19219,6 +19230,21 @@ CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_js
19219
19230
  const rows = this.db.prepare(sql).all(params);
19220
19231
  return rows.map(rowToNode);
19221
19232
  }
19233
+ searchDescriptionTerms(terms, limit) {
19234
+ if (terms.length === 0) return [];
19235
+ const score2 = terms.map(() => "(description LIKE ?)").join(" + ");
19236
+ const rows = this.db.prepare(
19237
+ `SELECT n.*, h.m AS matched
19238
+ FROM (SELECT rowid AS rid, ${score2} AS m
19239
+ FROM nodes INDEXED BY nodes_live_description
19240
+ WHERE valid_to IS NULL) h
19241
+ JOIN nodes n ON n.rowid = h.rid
19242
+ WHERE h.m > 0
19243
+ ORDER BY h.m DESC, n.page_rank DESC, n.last_updated_at DESC
19244
+ LIMIT ?`
19245
+ ).all(...terms.map((t) => `%${t}%`), Math.max(1, limit));
19246
+ return rows.map((r) => ({ node: rowToNode(r), matched: Number(r.matched) }));
19247
+ }
19222
19248
  typedEdgeStmt(sql) {
19223
19249
  let stmt = this.typedEdgeStmts.get(sql);
19224
19250
  if (!stmt) {
@@ -21681,7 +21707,11 @@ function priorsForFile(store, relPath) {
21681
21707
  const resolvedProblems = [];
21682
21708
  const seenProblem = /* @__PURE__ */ new Set();
21683
21709
  const related = /* @__PURE__ */ new Map();
21684
- for (const e of store.inEdges(file2.id, ["ANCHORED_AT"])) {
21710
+ const anchorEdges = [
21711
+ ...store.inEdges(file2.id, ["ANCHORED_AT"]),
21712
+ ...store.outEdges(file2.id, ["DEFINES"]).flatMap((d) => store.inEdges(d.to, ["ANCHORED_AT"]))
21713
+ ];
21714
+ for (const e of anchorEdges) {
21685
21715
  const n = store.getNode(e.from);
21686
21716
  if (!n) continue;
21687
21717
  if (n.label === "Problem" || n.label === "Vulnerability") {
@@ -21929,12 +21959,21 @@ function resolveDesignProblemByStatement(store, statement, fixNote, t) {
21929
21959
  const problemId = identityId({ kind: "DesignProblem", statement });
21930
21960
  return closeDesignProblem(store, problemId, fixNote, t);
21931
21961
  }
21962
+ function closableByCaller(node2) {
21963
+ if (node2.attrs["source"] !== "cloud") return true;
21964
+ const visibility = node2.attrs["visibility"];
21965
+ return typeof visibility === "string" && MEMBER_VISIBILITIES.has(visibility);
21966
+ }
21967
+ function isVerdictable(node2) {
21968
+ return node2.label === "Problem" && node2.attrs["resolvedAt"] == null && (node2.validTo ?? null) === null && !isConstraintProblem(node2) && node2.attrs["kind"] !== "todo";
21969
+ }
21932
21970
  function resolveDesignProblemById(store, id, fixNote, t) {
21933
21971
  return closeDesignProblem(store, id.trim(), fixNote, t);
21934
21972
  }
21935
21973
  function closeDesignProblem(store, problemId, fixNote, t) {
21936
21974
  const p = store.getNode(problemId);
21937
21975
  if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
21976
+ if (!closableByCaller(p)) return false;
21938
21977
  if (p.attrs["resolvedAs"]) return false;
21939
21978
  if (fixNote?.trim() && !isFragmentFixNote(fixNote)) {
21940
21979
  const sols = store.outEdges(problemId, ["SOLVED_BY"]).map((e) => store.getNode(e.to)).filter((n) => n !== null);
@@ -22007,6 +22046,7 @@ function retractDesignProblemByStatement(store, statement, resolvedAs, reason, t
22007
22046
  function retractDesignProblem(store, problemId, resolvedAs, reason, t) {
22008
22047
  const p = store.getNode(problemId);
22009
22048
  if (!p || p.label !== "Problem" || p.attrs["resolvedAt"]) return false;
22049
+ if (!closableByCaller(p)) return false;
22010
22050
  store.updateNode(problemId, {
22011
22051
  attrs: {
22012
22052
  ...p.attrs,
@@ -22142,7 +22182,7 @@ function priorExposure(store, nodeId, atSeq) {
22142
22182
  if ((n.attrs["evictedCount"] ?? 0) > 0) return "evicted";
22143
22183
  return "unshown";
22144
22184
  }
22145
- var DESIGN_PROMOTE_AT, STATEMENT_ALIAS_CAP, RECURRENCE_DEBOUNCE_MS, PATTERN_DEDUP_COSINE, PATTERN_ALIAS_CAP, DEDUP_STOPWORDS, SAME_ANCHOR_DEDUP_JACCARD, EMPTY_FOLD_REPORT, SAME_ANCHOR_SCOUT_AUTO_COSINE, SAME_ANCHOR_SCOUT_MIN_COSINE, SAME_ANCHOR_NOMINATE_JACCARD, REOPEN_MIN_SOURCES, RECURRENCE_SOURCES_CAP, CITABLE_PRIOR_LABELS, MAX_ANCHOR_FILES, ENCLOSING_SYMBOL_KINDS, ENCLOSING_WALK_DEPTH, FIX_CANDIDATE_ASK_LIMIT;
22185
+ var DESIGN_PROMOTE_AT, STATEMENT_ALIAS_CAP, RECURRENCE_DEBOUNCE_MS, PATTERN_DEDUP_COSINE, PATTERN_ALIAS_CAP, DEDUP_STOPWORDS, SAME_ANCHOR_DEDUP_JACCARD, EMPTY_FOLD_REPORT, SAME_ANCHOR_SCOUT_AUTO_COSINE, SAME_ANCHOR_SCOUT_MIN_COSINE, SAME_ANCHOR_NOMINATE_JACCARD, REOPEN_MIN_SOURCES, RECURRENCE_SOURCES_CAP, CITABLE_PRIOR_LABELS, MAX_ANCHOR_FILES, ENCLOSING_SYMBOL_KINDS, ENCLOSING_WALK_DEPTH, MEMBER_VISIBILITIES, FIX_CANDIDATE_ASK_LIMIT;
22146
22186
  var init_design_problem = __esm({
22147
22187
  "../../packages/local-graph/src/design-problem.ts"() {
22148
22188
  "use strict";
@@ -22219,6 +22259,7 @@ var init_design_problem = __esm({
22219
22259
  "Property"
22220
22260
  ]);
22221
22261
  ENCLOSING_WALK_DEPTH = 8;
22262
+ MEMBER_VISIBILITIES = /* @__PURE__ */ new Set(["project", "team", "org"]);
22222
22263
  FIX_CANDIDATE_ASK_LIMIT = 5;
22223
22264
  }
22224
22265
  });
@@ -25358,6 +25399,7 @@ __export(src_exports2, {
25358
25399
  clearAnsweredRevisits: () => clearAnsweredRevisits,
25359
25400
  clearRevisit: () => clearRevisit,
25360
25401
  clearSettledFixCandidates: () => clearSettledFixCandidates,
25402
+ closableByCaller: () => closableByCaller,
25361
25403
  communityInductionRequests: () => communityInductionRequests,
25362
25404
  communitySeeds: () => communitySeeds,
25363
25405
  compareSemver: () => compareSemver,
@@ -25396,6 +25438,7 @@ __export(src_exports2, {
25396
25438
  isFragmentFixNote: () => isFragmentFixNote,
25397
25439
  isPlaceholderStatement: () => isPlaceholderStatement,
25398
25440
  isUnfilledPlaceholder: () => isUnfilledPlaceholder,
25441
+ isVerdictable: () => isVerdictable,
25399
25442
  learnedConductance: () => learnedConductance,
25400
25443
  linkProblemToLanguages: () => linkProblemToLanguages,
25401
25444
  linkProblemToPackages: () => linkProblemToPackages,
@@ -25923,6 +25966,13 @@ function renderSnapshot(s) {
25923
25966
  lines.push("_Priors that speak to it \u2014 cite one with its `[handle]` if you lean on it:_");
25924
25967
  for (const h of si.hits) {
25925
25968
  lines.push(`- **${h.description}** \u2014 \`${h.id}\``);
25969
+ if (h.verdict === "own") {
25970
+ lines.push(
25971
+ ` - still open \u2014 did this work settle it? fixed \u2192 (fix:[${h.id}]) \xB7 not a real problem \u2192 an \`errata-resolved\` block with \`id: ` + h.id + "`, `disposition: false_positive`, `reason: \u2026` \xB7 still open \u2192 say nothing"
25972
+ );
25973
+ } else if (h.verdict === "foreign") {
25974
+ lines.push(` - open in another membrane \u2014 not yours to close: (confirm:[${h.id}]) it held \xB7 (refute:[${h.id}] one line why)`);
25975
+ }
25926
25976
  }
25927
25977
  }
25928
25978
  if (si.remoteHits?.length) {
@@ -27795,8 +27845,8 @@ var init_config = __esm({
27795
27845
  "use strict";
27796
27846
  init_paths();
27797
27847
  LEGACY_CLOUD_URL = "https://inerrata-gateway.onrender.com";
27798
- DEFAULT_CLOUD_URL = true ? "https://inerrata.dev" : DEV_CLOUD_URL;
27799
- DEFAULT_UPDATE_CHANNEL = true ? "latest" : "dev";
27848
+ DEFAULT_CLOUD_URL = true ? "https://dev.inerrata.dev" : DEV_CLOUD_URL;
27849
+ DEFAULT_UPDATE_CHANNEL = false ? "latest" : "dev";
27800
27850
  }
27801
27851
  });
27802
27852
 
@@ -32524,7 +32574,10 @@ var init_agent_signals = __esm({
32524
32574
  // lean-on corroboration lived, because only corroboration had an inline path. Grammar
32525
32575
  // single-sourced with parseInlineTags' CONFIRM_PREFIX/REFUTE_PREFIX.
32526
32576
  confirm: (ref) => `(confirm:[${ref}])`,
32527
- refute: (ref) => `(refute:[${ref}] one line why)`
32577
+ refute: (ref) => `(refute:[${ref}] one line why)`,
32578
+ // RETRACT — "not a real problem": the errata-resolved block with a disposition
32579
+ // (parseDesignResolutions, turn.ts). No inline form exists; the block is the grammar.
32580
+ retract: (id) => ["```errata-resolved", `id: ${id}`, "disposition: false_positive", "reason: why it was never a real problem", "```"].join("\n")
32528
32581
  };
32529
32582
  TAG_OPENERS = [
32530
32583
  ...new Set(
@@ -34414,6 +34467,19 @@ function buildSymbolLexicon(store) {
34414
34467
  }
34415
34468
  return lex;
34416
34469
  }
34470
+ function codeGraphKey(store) {
34471
+ const files = store.labelFingerprint?.(["File"]);
34472
+ if (files === void 0) return null;
34473
+ return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
34474
+ }
34475
+ function lexiconsFor(store) {
34476
+ const key = codeGraphKey(store);
34477
+ const hit = key !== null ? lexiconCache.get(store) : void 0;
34478
+ if (hit && hit.key === key) return hit;
34479
+ const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
34480
+ if (key !== null) lexiconCache.set(store, { key, ...built });
34481
+ return built;
34482
+ }
34417
34483
  function replaceWithPhrase(text, before, escapedKey, after, phrase2) {
34418
34484
  if (!RE_LEADING_ARTICLE2.test(phrase2)) {
34419
34485
  return text.replace(new RegExp(`${before}${escapedKey}${after}`, "g"), phrase2);
@@ -34579,7 +34645,7 @@ function registerRenderedContributorLabels(labels) {
34579
34645
  if (trimmed.length >= 3) renderedContributorLabels.add(trimmed);
34580
34646
  }
34581
34647
  }
34582
- var SYMBOL_LABELS2, KIND_PHRASE, BUILTIN_TYPE_NAMES, DOC_COMMENT_KINDS, MAX_DOC_CHARS, RE_RESERVED2, escapeRe4, RE_LEADING_ARTICLE2, CONVENTION_FILENAMES, CONVENTION_BASENAME_MIN_FILES, CONVENTION_STEMS, FILE_TOKEN_RE, STEM_TOKEN_RE, SOURCE_EXTENSIONS, TOKEN_RE3, renderedContributorLabels;
34648
+ var SYMBOL_LABELS2, KIND_PHRASE, BUILTIN_TYPE_NAMES, DOC_COMMENT_KINDS, MAX_DOC_CHARS, LEXICON_SYMBOL_LABELS, lexiconCache, RE_RESERVED2, escapeRe4, RE_LEADING_ARTICLE2, CONVENTION_FILENAMES, CONVENTION_BASENAME_MIN_FILES, CONVENTION_STEMS, FILE_TOKEN_RE, STEM_TOKEN_RE, SOURCE_EXTENSIONS, TOKEN_RE3, renderedContributorLabels;
34583
34649
  var init_generalize_graph = __esm({
34584
34650
  "src/generalize-graph.ts"() {
34585
34651
  "use strict";
@@ -34629,6 +34695,8 @@ var init_generalize_graph = __esm({
34629
34695
  ]);
34630
34696
  DOC_COMMENT_KINDS = /* @__PURE__ */ new Set(["docstring", "explanation"]);
34631
34697
  MAX_DOC_CHARS = 600;
34698
+ LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
34699
+ lexiconCache = /* @__PURE__ */ new WeakMap();
34632
34700
  RE_RESERVED2 = /[.*+?^${}()|[\]\\]/g;
34633
34701
  escapeRe4 = (s) => s.replace(RE_RESERVED2, "\\$&");
34634
34702
  RE_LEADING_ARTICLE2 = /^(?:an?|the)\s/i;
@@ -34831,19 +34899,18 @@ function isQuotaError(err2) {
34831
34899
  return status === 403 && /quota/i.test(msg);
34832
34900
  }
34833
34901
  async function dualAugment(opts) {
34834
- const { store, cloud, localHits, seedNode, queryText, limit } = opts;
34902
+ const { store, cloud, localHits, seedNode, queryText, limit, generalizeSoft } = opts;
34835
34903
  const cloudHits = [];
34836
34904
  let seedUsageCount;
34837
34905
  let seedProvenance = "local";
34838
34906
  const calls = [];
34839
34907
  try {
34840
- const lexicon = buildSymbolLexicon(store);
34841
34908
  const hardIds = [.../* @__PURE__ */ new Set([...seedNode ? [seedNode.id] : [], ...localHits.map((h) => h.id)])].filter((nid) => {
34842
34909
  const n = seedNode && nid === seedNode.id ? seedNode : store.getNode(nid);
34843
34910
  return !!n && isHardBridge(n);
34844
34911
  });
34845
34912
  const softText = queryText ?? (seedNode && BRIDGE_LABELS.has(seedNode.label) ? seedNode.description : void 0);
34846
- const softQ = softText ? generalizeSymbols(softText, lexicon, 1) : void 0;
34913
+ const softQ = softText ? await generalizeSoft?.(softText) ?? generalizeSymbols(softText, lexiconsFor(store).symbols, 1) : void 0;
34847
34914
  if (seedNode?.label === "Package") {
34848
34915
  calls.push(
34849
34916
  cloud.packages(seedNode.id).then((pn) => {
@@ -34955,6 +35022,50 @@ var init_dual_augment = __esm({
34955
35022
  }
34956
35023
  });
34957
35024
 
35025
+ // src/daemon-generalize-client.ts
35026
+ function daemonLoopbackUrl(lockPath = globalDaemonLock()) {
35027
+ const url2 = readDaemonLock(lockPath)?.webUiUrl;
35028
+ if (!url2) return null;
35029
+ try {
35030
+ const u = new URL(url2);
35031
+ const isLoopback = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "[::1]";
35032
+ return u.protocol === "http:" && isLoopback ? u.origin : null;
35033
+ } catch {
35034
+ return null;
35035
+ }
35036
+ }
35037
+ function daemonGeneralizer(workspaceRoot, opts = {}) {
35038
+ const baseUrl = opts.baseUrl ?? (() => daemonLoopbackUrl());
35039
+ const fetchFn = opts.fetchFn ?? fetch;
35040
+ const timeoutMs = opts.timeoutMs ?? DAEMON_GENERALIZE_TIMEOUT_MS;
35041
+ return async (text) => {
35042
+ try {
35043
+ const base = baseUrl();
35044
+ if (!base) return void 0;
35045
+ const res = await fetchFn(`${base}/api/generalize`, {
35046
+ method: "POST",
35047
+ headers: { "content-type": "application/json" },
35048
+ body: JSON.stringify({ workspaceRoot, text }),
35049
+ signal: AbortSignal.timeout(timeoutMs)
35050
+ });
35051
+ if (!res.ok) return void 0;
35052
+ const body2 = await res.json();
35053
+ return typeof body2.generalized === "string" ? body2.generalized : void 0;
35054
+ } catch {
35055
+ return void 0;
35056
+ }
35057
+ };
35058
+ }
35059
+ var DAEMON_GENERALIZE_TIMEOUT_MS;
35060
+ var init_daemon_generalize_client = __esm({
35061
+ "src/daemon-generalize-client.ts"() {
35062
+ "use strict";
35063
+ init_lock();
35064
+ init_paths();
35065
+ DAEMON_GENERALIZE_TIMEOUT_MS = 3e3;
35066
+ }
35067
+ });
35068
+
34958
35069
  // ../../packages/indexer/src/parse-cache.ts
34959
35070
  import { createHash as createHash6 } from "node:crypto";
34960
35071
  import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync14, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync11 } from "node:fs";
@@ -43897,7 +44008,7 @@ __export(mcp_exports, {
43897
44008
  import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
43898
44009
  import { resolve as resolve4 } from "node:path";
43899
44010
  function collectEnrichmentPulls(store, opts = {}) {
43900
- const pending = store.findNodesByLabel("Solution").filter((s) => s.attrs["enrichmentPending"] === true).filter((s) => !opts.onlyUnsurfaced || s.attrs["enrichmentSurfaced"] !== true);
44011
+ const pending = store.findLiveNodesByAttr("Solution", "enrichmentPending", 1).filter((s) => s.attrs["enrichmentPending"] === true).filter((s) => !opts.onlyUnsurfaced || s.attrs["enrichmentSurfaced"] !== true);
43901
44012
  const byEpisode = /* @__PURE__ */ new Map();
43902
44013
  for (const s of pending) {
43903
44014
  const ep = s.attrs["episodeId"] ?? s.id;
@@ -44014,20 +44125,8 @@ function searchGraph(store, query, limit) {
44014
44125
  const terms = q.split(/\s+/).filter((t) => t.length >= 2);
44015
44126
  const pool = Math.max(limit * 3, 60);
44016
44127
  const raw2 = /* @__PURE__ */ new Map();
44017
- const collect2 = (nodes) => {
44018
- for (const n of nodes) {
44019
- const e = raw2.get(n.id);
44020
- if (e) e.matched++;
44021
- else raw2.set(n.id, { node: n, matched: 1 });
44022
- }
44023
- };
44024
- if (terms.length <= 1) {
44025
- collect2(store.searchNodes({ descriptionLike: q, limit: pool }));
44026
- } else {
44027
- for (const term of terms) {
44028
- collect2(store.searchNodes({ descriptionLike: term, limit: 200 }));
44029
- }
44030
- }
44128
+ const scored = !q ? store.searchNodes({ limit: pool }).map((node2) => ({ node: node2, matched: 1 })) : terms.length <= 1 ? store.searchDescriptionTerms([q], pool) : store.searchDescriptionTerms(terms, Math.max(pool, 600));
44129
+ for (const hit of scored) raw2.set(hit.node.id, hit);
44031
44130
  const merged = /* @__PURE__ */ new Map();
44032
44131
  const add = (node2, matched, via) => {
44033
44132
  const e = merged.get(node2.id);
@@ -44327,6 +44426,7 @@ async function runMcpServer(workspaceRoot, options = {}) {
44327
44426
  const store = openGraphStore({ path: paths.castalia });
44328
44427
  const handle2 = createMcpHandler(store, {
44329
44428
  ...buildToolContext(workspaceRoot),
44429
+ generalizeSoft: daemonGeneralizer(workspaceRoot),
44330
44430
  sharedDbPath: sharedStorePath()
44331
44431
  }, options);
44332
44432
  process.stdin.setEncoding("utf8");
@@ -44372,6 +44472,7 @@ var init_mcp = __esm({
44372
44472
  init_src8();
44373
44473
  init_dual_burst();
44374
44474
  init_paths();
44475
+ init_daemon_generalize_client();
44375
44476
  init_webui();
44376
44477
  init_reconcile();
44377
44478
  init_agent_signals();
@@ -44447,6 +44548,7 @@ var init_mcp = __esm({
44447
44548
  const dual = await dualAugment({
44448
44549
  store,
44449
44550
  cloud: ctx.cloud,
44551
+ ...ctx.generalizeSoft ? { generalizeSoft: ctx.generalizeSoft } : {},
44450
44552
  localHits,
44451
44553
  queryText: query,
44452
44554
  limit
@@ -44626,6 +44728,7 @@ var init_mcp = __esm({
44626
44728
  const dual = await dualAugment({
44627
44729
  store,
44628
44730
  cloud: ctx.cloud,
44731
+ ...ctx.generalizeSoft ? { generalizeSoft: ctx.generalizeSoft } : {},
44629
44732
  localHits,
44630
44733
  seedNode,
44631
44734
  limit: args2["limit"] != null ? Number(args2["limit"]) : 20
@@ -45197,6 +45300,7 @@ var init_mcp = __esm({
45197
45300
  const dual = await dualAugment({
45198
45301
  store,
45199
45302
  cloud: ctx.cloud,
45303
+ ...ctx.generalizeSoft ? { generalizeSoft: ctx.generalizeSoft } : {},
45200
45304
  localHits,
45201
45305
  seedNode: node2,
45202
45306
  limit
@@ -53491,6 +53595,7 @@ __export(webui_exports, {
53491
53595
  asFailedPostToolUse: () => asFailedPostToolUse,
53492
53596
  buildWebUi: () => buildWebUi,
53493
53597
  decideContextInject: () => decideContextInject,
53598
+ editVerdict: () => editVerdict,
53494
53599
  errorRecallTerms: () => errorRecallTerms,
53495
53600
  fileRecall: () => fileRecall,
53496
53601
  fileUriToFsPath: () => fileUriToFsPath,
@@ -53601,6 +53706,26 @@ ${lines.join("\n")}
53601
53706
  card
53602
53707
  };
53603
53708
  }
53709
+ function editVerdict(store, relPath) {
53710
+ const priors = priorsForFile(store, relPath);
53711
+ if (!priors || priors.openProblems.length === 0) return null;
53712
+ const ranked = rankByRelevance(store, priors.file.id, priors.openProblems.map((n) => n.id));
53713
+ const rank = new Map(ranked.map((r, i2) => [r.id, i2]));
53714
+ const asked = [...priors.openProblems].sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0)).slice(0, 3);
53715
+ const lines = asked.map(
53716
+ (p) => closableByCaller(p) ? recallLine(p, `fixed: ${TAG_EXAMPLE.fix(p.id)} \xB7 not a real problem: retract it (block below) \xB7 still open: say nothing`) : recallLine(p, `another membrane's \u2014 not yours to close: ${TAG_EXAMPLE.confirm(p.id)} it held \xB7 ${TAG_EXAMPLE.refute(p.id)}`)
53717
+ );
53718
+ const retractable = asked.find((p) => closableByCaller(p));
53719
+ const shown = asked.map(deliveredPrior);
53720
+ const headline = `verdict asked: ${plural(shown.length, "open problem")} on ${relPath.split("/").slice(-2).join("/")}`;
53721
+ return {
53722
+ context: `errata \u2014 you just edited ${relPath}; ${plural(priors.openProblems.length, "open problem")} recorded on it. Give each one below a verdict in your reply:
53723
+ ${lines.join("\n")}
53724
+ ` + (retractable ? `Retract (not a real problem):
53725
+ ${TAG_EXAMPLE.retract(retractable.id)}` : ""),
53726
+ card: deliveredCard(store, headline, shown) ?? { headline, items: [] }
53727
+ };
53728
+ }
53604
53729
  function workspacePackageIds(store) {
53605
53730
  const hit = wsPackagesCache.get(store);
53606
53731
  const now = Date.now();
@@ -53671,6 +53796,7 @@ function buildWebUi(deps) {
53671
53796
  const B = deps.basePath ?? "";
53672
53797
  const lastInjectedHash = /* @__PURE__ */ new Map();
53673
53798
  const fileRecallSeen = /* @__PURE__ */ new Map();
53799
+ const editVerdictAsked = /* @__PURE__ */ new Map();
53674
53800
  const lastSubagentInjectAt = /* @__PURE__ */ new Map();
53675
53801
  const noteSessionFrom = (headerOrigin, body2, ended = false) => {
53676
53802
  const { sessionId, agentId } = hookAudience(body2);
@@ -53935,6 +54061,27 @@ function buildWebUi(deps) {
53935
54061
  }
53936
54062
  }
53937
54063
  }
54064
+ if (event === "PostToolUse" && !recall && rel && (tool === "Edit" || tool === "MultiEdit" || tool === "Write" || tool === "NotebookEdit")) {
54065
+ const sid = hookAudience(body2).contextKey;
54066
+ let asked = editVerdictAsked.get(sid);
54067
+ if (!asked) {
54068
+ asked = /* @__PURE__ */ new Set();
54069
+ editVerdictAsked.set(sid, asked);
54070
+ }
54071
+ if (!asked.has(rel)) {
54072
+ const verdict = editVerdict(deps.store, rel);
54073
+ if (verdict) {
54074
+ asked.add(rel);
54075
+ let seen = fileRecallSeen.get(sid);
54076
+ if (!seen) {
54077
+ seen = /* @__PURE__ */ new Set();
54078
+ fileRecallSeen.set(sid, seen);
54079
+ }
54080
+ seen.add(rel);
54081
+ recall = verdict;
54082
+ }
54083
+ }
54084
+ }
53938
54085
  if (event === "PostToolUse" && !recall && rel && (tool === "Read" || tool === "Edit" || tool === "Write" || tool === "NotebookEdit")) {
53939
54086
  const sid = hookAudience(body2).contextKey;
53940
54087
  let seen = fileRecallSeen.get(sid);
@@ -82706,7 +82853,7 @@ function createWatchBreaker(deps) {
82706
82853
  }
82707
82854
 
82708
82855
  // src/engine.ts
82709
- var DAEMON_VERSION = true ? "2.0.20" : "2.0.0-alpha.0";
82856
+ var DAEMON_VERSION = true ? "2.0.21-dev.3716" : "2.0.0-alpha.0";
82710
82857
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
82711
82858
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
82712
82859
  var GIT_OP_MUTE_MS = 4e3;
@@ -83167,7 +83314,8 @@ function createWorkspaceEngine(opts) {
83167
83314
  id: h.node.id,
83168
83315
  label: h.node.label,
83169
83316
  description: h.node.description,
83170
- coverage: h.coverage
83317
+ coverage: h.coverage,
83318
+ ...isVerdictable(h.node) ? { verdict: closableByCaller(h.node) ? "own" : "foreign" } : {}
83171
83319
  })),
83172
83320
  ...collective && collective.hits.length > 0 ? {
83173
83321
  remoteHits: collective.hits.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description }))
@@ -84176,7 +84324,11 @@ function createWorkspaceEngine(opts) {
84176
84324
  const closed = resolveDesignProblemById(store, pid, fx.fixNote, t);
84177
84325
  if (!closed) {
84178
84326
  const n = store.getNode(pid);
84179
- if (!n || n.label !== "Problem" && n.label !== "Vulnerability" || n.attrs["resolvedAs"] != null) {
84327
+ if (n && n.attrs["resolvedAt"] == null && !closableByCaller(n)) {
84328
+ console.warn(
84329
+ `[errata] fix tag not applied: ${pid.slice(0, 32)} belongs to a membrane you are not in \u2014 a verdict closes only your own problems; cite it with (confirm:[id]) or (refute:[id] why) instead`
84330
+ );
84331
+ } else if (!n || n.label !== "Problem" && n.label !== "Vulnerability" || n.attrs["resolvedAs"] != null) {
84180
84332
  console.warn(
84181
84333
  `[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`
84182
84334
  );
@@ -86732,21 +86884,7 @@ function liveBackoffMs(failures) {
86732
86884
 
86733
86885
  // src/drain-build.ts
86734
86886
  init_generalize_graph();
86735
- var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
86736
- function codeGraphKey(store) {
86737
- const files = store.labelFingerprint?.(["File"]);
86738
- if (files === void 0) return null;
86739
- return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
86740
- }
86741
- var lexiconCache = /* @__PURE__ */ new WeakMap();
86742
- function lexiconsFor(store) {
86743
- const key = codeGraphKey(store);
86744
- const hit = key !== null ? lexiconCache.get(store) : void 0;
86745
- if (hit && hit.key === key) return hit;
86746
- const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
86747
- if (key !== null) lexiconCache.set(store, { key, ...built });
86748
- return built;
86749
- }
86887
+ init_generalize_graph();
86750
86888
  function buildDrainContext(store, input) {
86751
86889
  return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
86752
86890
  ...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
@@ -86881,6 +87019,98 @@ var ConsolidateWorker = class {
86881
87019
  }
86882
87020
  };
86883
87021
 
87022
+ // src/generalize-worker-client.ts
87023
+ import { Worker as Worker3 } from "node:worker_threads";
87024
+ var GENERALIZE_WORKER_IDLE_MS = 30 * 6e4;
87025
+ var GeneralizeWorker = class {
87026
+ worker = null;
87027
+ ready = null;
87028
+ pending = /* @__PURE__ */ new Map();
87029
+ nextId = 1;
87030
+ stopped = false;
87031
+ idleTimer = null;
87032
+ async generalize(castaliaPath, text) {
87033
+ if (this.stopped) throw new Error("generalize worker stopped");
87034
+ this.ensureWorker();
87035
+ this.touch();
87036
+ await this.ready;
87037
+ const id = this.nextId++;
87038
+ return new Promise((resolve10, reject) => {
87039
+ this.pending.set(id, { resolve: resolve10, reject });
87040
+ const req = { id, castaliaPath, text };
87041
+ this.worker.postMessage(req);
87042
+ });
87043
+ }
87044
+ touch() {
87045
+ if (this.idleTimer) clearTimeout(this.idleTimer);
87046
+ this.idleTimer = setTimeout(() => {
87047
+ if (this.pending.size > 0) return this.touch();
87048
+ const w = this.worker;
87049
+ this.worker = null;
87050
+ this.ready = null;
87051
+ void w?.terminate().catch(() => {
87052
+ });
87053
+ }, GENERALIZE_WORKER_IDLE_MS);
87054
+ this.idleTimer.unref?.();
87055
+ }
87056
+ ensureWorker() {
87057
+ if (this.worker) return;
87058
+ const worker = new Worker3(new URL("./generalize-worker.mjs", import.meta.url), {
87059
+ workerData: {},
87060
+ execArgv: process.execArgv,
87061
+ resourceLimits: workerResourceLimits()
87062
+ });
87063
+ noteIsolateSpawned();
87064
+ this.worker = worker;
87065
+ this.ready = new Promise((resolve10, reject) => {
87066
+ const onReady = (m) => {
87067
+ if (m?.ready) {
87068
+ worker.off("message", onReady);
87069
+ worker.off("error", onErr);
87070
+ resolve10();
87071
+ }
87072
+ };
87073
+ const onErr = (err2) => {
87074
+ worker.off("message", onReady);
87075
+ reject(err2);
87076
+ };
87077
+ worker.once("error", onErr);
87078
+ worker.on("message", onReady);
87079
+ });
87080
+ worker.on("message", (m) => {
87081
+ if (typeof m?.id !== "number") return;
87082
+ const p = this.pending.get(m.id);
87083
+ if (!p) return;
87084
+ this.pending.delete(m.id);
87085
+ if (m.ok && typeof m.result === "string") p.resolve(m.result);
87086
+ else p.reject(new Error(m.error ?? "generalization failed"));
87087
+ });
87088
+ const onGone = (err2) => {
87089
+ const e = err2 instanceof Error ? err2 : new Error("generalize worker exited");
87090
+ for (const [, p] of this.pending) p.reject(e);
87091
+ this.pending.clear();
87092
+ if (this.worker === worker) {
87093
+ this.worker = null;
87094
+ this.ready = null;
87095
+ }
87096
+ };
87097
+ worker.on("error", onGone);
87098
+ worker.on("exit", () => {
87099
+ noteIsolateExited();
87100
+ onGone();
87101
+ });
87102
+ }
87103
+ async stop() {
87104
+ this.stopped = true;
87105
+ if (this.idleTimer) clearTimeout(this.idleTimer);
87106
+ if (this.worker) {
87107
+ await this.worker.terminate().catch(() => {
87108
+ });
87109
+ this.worker = null;
87110
+ }
87111
+ }
87112
+ };
87113
+
86884
87114
  // src/prepare-store.ts
86885
87115
  init_src5();
86886
87116
  async function prepareStore(path2, root, workspaceId2) {
@@ -89836,6 +90066,7 @@ async function startMultiDaemon(opts = {}) {
89836
90066
  const sharedStore = openGraphStore({ path: sharedDbPath });
89837
90067
  const bundledRun = import.meta.url.endsWith(".mjs");
89838
90068
  const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
90069
+ const generalizeWorker = bundledRun ? new GeneralizeWorker() : null;
89839
90070
  startLoopLagMonitor();
89840
90071
  let baseUrl = "";
89841
90072
  const records = [];
@@ -91523,6 +91754,7 @@ async function startMultiDaemon(opts = {}) {
91523
91754
  await Promise.allSettled(attaching.values());
91524
91755
  for (const r of records) await r.engine.stop();
91525
91756
  if (consolidateWorker) await consolidateWorker.stop();
91757
+ if (generalizeWorker) await generalizeWorker.stop();
91526
91758
  try {
91527
91759
  sharedStore.close();
91528
91760
  } catch {
@@ -91551,6 +91783,33 @@ async function startMultiDaemon(opts = {}) {
91551
91783
  }
91552
91784
  });
91553
91785
  app.get("/api/sync", (c) => c.json(flushSnapshot()));
91786
+ const GENERALIZE_MAX_CHARS = 8e3;
91787
+ app.post("/api/generalize", async (c) => {
91788
+ const host = (c.req.header("host") ?? "").toLowerCase();
91789
+ if (!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(host)) return c.json({ error: "forbidden" }, 403);
91790
+ if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) {
91791
+ return c.json({ error: "json required" }, 415);
91792
+ }
91793
+ let body2;
91794
+ try {
91795
+ body2 = await c.req.json();
91796
+ } catch {
91797
+ return c.json({ error: "bad json" }, 400);
91798
+ }
91799
+ if (typeof body2.workspaceRoot !== "string" || typeof body2.text !== "string") {
91800
+ return c.json({ error: "workspaceRoot and text required" }, 400);
91801
+ }
91802
+ if (body2.text.length > GENERALIZE_MAX_CHARS) return c.json({ error: "text too long" }, 413);
91803
+ const entry = workspaceEntryForPath(pathResolve(body2.workspaceRoot));
91804
+ if (!entry) return c.json({ error: "unregistered workspace" }, 404);
91805
+ if (!generalizeWorker) return c.json({ error: "no worker" }, 503);
91806
+ try {
91807
+ const generalized = await generalizeWorker.generalize(workspacePaths(entry.path).castalia, body2.text);
91808
+ return c.json({ generalized });
91809
+ } catch (err2) {
91810
+ return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 503);
91811
+ }
91812
+ });
91554
91813
  app.post("/api/tick", async (c) => {
91555
91814
  try {
91556
91815
  return c.json({ reports: Object.fromEntries(await daemon.tickAll()) });
@@ -94099,7 +94358,7 @@ async function cmdInit() {
94099
94358
  }
94100
94359
  console.log("");
94101
94360
  console.log("connecting your cloud account...");
94102
- await cmdLoginOAuth(cfgNow);
94361
+ await cmdLoginOAuth(cfgNow, false, false);
94103
94362
  cfgNow = loadConfig();
94104
94363
  if (!hasCloudCredential(cfgNow)) {
94105
94364
  console.error("init requires login \u2014 re-run `errata init`, or `errata init --no-login` to stay unlinked.");
@@ -94742,7 +95001,7 @@ async function warnIfApprovalUnreachable(url2) {
94742
95001
  } catch {
94743
95002
  }
94744
95003
  }
94745
- async function cmdLoginOAuth(cfg, useBrowserLoopback = false) {
95004
+ async function cmdLoginOAuth(cfg, useBrowserLoopback = false, linkAfterLogin = true) {
94746
95005
  const inspection = await inspectCloudEndpoint(cfg.cloudUrl);
94747
95006
  try {
94748
95007
  assertCloudEndpointAllowed(cfg.cloudUrl, inspection);
@@ -94779,7 +95038,7 @@ async function cmdLoginOAuth(cfg, useBrowserLoopback = false) {
94779
95038
  nextCfg.email = me.handle;
94780
95039
  saveConfig(nextCfg);
94781
95040
  console.log(`logged in as ${me.handle} (${me.tier}) \u2014 tokens stored at ${globalConfigPath()}`);
94782
- await linkRegisteredWorkspaces(nextCfg);
95041
+ if (linkAfterLogin) await linkRegisteredWorkspaces(nextCfg);
94783
95042
  } catch (err2) {
94784
95043
  console.error(`signed in, but identity check failed: ${err2 instanceof Error ? err2.message : err2}`);
94785
95044
  console.error(` credentials were not stored; try again or paste a key: errata login --token <key>`);