memorix 1.2.0 → 1.2.1

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/dist/index.js CHANGED
@@ -9062,6 +9062,36 @@ var init_session_store = __esm({
9062
9062
  }
9063
9063
  });
9064
9064
 
9065
+ // src/memory/graph-scope.ts
9066
+ function projectGraphEntityNames(observations2) {
9067
+ const entityNames = /* @__PURE__ */ new Set();
9068
+ for (const observation of observations2) {
9069
+ if ((observation.status ?? "active") !== "active") continue;
9070
+ const entityName = observation.entityName?.trim();
9071
+ if (entityName) entityNames.add(entityName);
9072
+ for (const relatedEntity of observation.relatedEntities ?? []) {
9073
+ const name = relatedEntity.trim();
9074
+ if (name) entityNames.add(name);
9075
+ }
9076
+ }
9077
+ return entityNames;
9078
+ }
9079
+ function scopeKnowledgeGraphToProject(graph, observations2) {
9080
+ const entityNames = projectGraphEntityNames(observations2);
9081
+ const entities = graph.entities.filter((entity) => entityNames.has(entity.name));
9082
+ const visibleNames = new Set(entities.map((entity) => entity.name));
9083
+ const relations = graph.relations.filter(
9084
+ (relation) => visibleNames.has(relation.from) && visibleNames.has(relation.to)
9085
+ );
9086
+ return { entities, relations, entityNames };
9087
+ }
9088
+ var init_graph_scope = __esm({
9089
+ "src/memory/graph-scope.ts"() {
9090
+ "use strict";
9091
+ init_esm_shims();
9092
+ }
9093
+ });
9094
+
9065
9095
  // src/memory/disclosure-policy.ts
9066
9096
  function resolveEvidenceBasis(fields) {
9067
9097
  if (fields.commitHash || fields.source === "git" || fields.sourceDetail === "git-ingest") {
@@ -12174,6 +12204,7 @@ __export(claims_exports, {
12174
12204
  buildClaimIdentity: () => buildClaimIdentity,
12175
12205
  deriveLowRiskClaimsFromObservation: () => deriveLowRiskClaimsFromObservation,
12176
12206
  requalifyClaimsForCodeState: () => requalifyClaimsForCodeState,
12207
+ reviewClaim: () => reviewClaim,
12177
12208
  selectClaimsForTask: () => selectClaimsForTask,
12178
12209
  supersedeClaim: () => supersedeClaim,
12179
12210
  writeClaim: () => writeClaim
@@ -12360,6 +12391,39 @@ function writeClaim(store, input) {
12360
12391
  };
12361
12392
  });
12362
12393
  }
12394
+ function reviewClaim(store, input) {
12395
+ if (input.reviewState !== "approved" && input.reviewState !== "rejected") {
12396
+ throw new Error("A claim review must approve or reject the claim");
12397
+ }
12398
+ const detail = compactText(input.detail, "review detail");
12399
+ return store.transaction(() => {
12400
+ const claim = store.getClaim(input.claimId);
12401
+ if (!claim) throw new Error("Claim was not found");
12402
+ if (input.reviewState === "approved" && claim.status === "superseded") {
12403
+ throw new Error("A superseded claim cannot be approved");
12404
+ }
12405
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
12406
+ const nextStatus = input.reviewState === "rejected" ? "unknown" : claim.status === "unknown" && claim.reviewState === "rejected" ? "active" : claim.status;
12407
+ const updated = {
12408
+ ...claim,
12409
+ status: nextStatus,
12410
+ reviewState: input.reviewState,
12411
+ updatedAt: now3
12412
+ };
12413
+ store.updateClaim(updated);
12414
+ store.recordEvent({
12415
+ projectId: claim.projectId,
12416
+ claimId: claim.id,
12417
+ kind: "reviewed",
12418
+ fromStatus: claim.status,
12419
+ toStatus: updated.status,
12420
+ detail: "Review: " + claim.reviewState + " -> " + input.reviewState + ". " + detail,
12421
+ createdAt: now3
12422
+ });
12423
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now3);
12424
+ return store.getClaim(claim.id) ?? updated;
12425
+ });
12426
+ }
12363
12427
  function supersedeClaim(store, input) {
12364
12428
  const evidence = validateEvidence(input.evidence);
12365
12429
  return store.transaction(() => {
@@ -12448,7 +12512,7 @@ function deriveLowRiskClaimsFromObservation(store, observation, codeStore) {
12448
12512
  scope: "project",
12449
12513
  confidence,
12450
12514
  observedAt: observation.updatedAt ?? observation.createdAt,
12451
- reviewState: "approved",
12515
+ reviewState: isGit ? "approved" : "needs-review",
12452
12516
  origin: isGit ? "git" : "derived",
12453
12517
  evidence
12454
12518
  });
@@ -13324,148 +13388,492 @@ var init_workflow_store = __esm({
13324
13388
  }
13325
13389
  });
13326
13390
 
13327
- // src/knowledge/workflows.ts
13328
- var workflows_exports = {};
13329
- __export(workflows_exports, {
13330
- WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
13331
- applyWorkflowAdapter: () => applyWorkflowAdapter,
13332
- importWindsurfWorkflows: () => importWindsurfWorkflows,
13333
- parseWorkflowMarkdown: () => parseWorkflowMarkdown,
13334
- previewWorkflowAdapter: () => previewWorkflowAdapter,
13335
- recordWorkflowRun: () => recordWorkflowRun,
13336
- renderWorkflowMarkdown: () => renderWorkflowMarkdown,
13337
- selectWorkflows: () => selectWorkflows,
13338
- selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
13339
- syncCanonicalWorkflows: () => syncCanonicalWorkflows,
13340
- writeCanonicalWorkflow: () => writeCanonicalWorkflow
13391
+ // src/codegraph/task-lens.ts
13392
+ var task_lens_exports = {};
13393
+ __export(task_lens_exports, {
13394
+ containsTaskKeyword: () => containsTaskKeyword,
13395
+ lensPathCandidates: () => lensPathCandidates,
13396
+ lensVerificationHints: () => lensVerificationHints,
13397
+ rankLensPaths: () => rankLensPaths,
13398
+ rankLensSources: () => rankLensSources,
13399
+ resolveTaskLens: () => resolveTaskLens,
13400
+ scoreLensSource: () => scoreLensSource,
13401
+ shouldShowLensSource: () => shouldShowLensSource
13341
13402
  });
13342
- import { createHash as createHash12 } from "crypto";
13343
- import { promises as fs14 } from "fs";
13344
- import path17 from "path";
13345
- import matter11 from "gray-matter";
13346
- function hash3(value) {
13347
- return createHash12("sha256").update(value).digest("hex");
13348
- }
13349
- function now2() {
13350
- return (/* @__PURE__ */ new Date()).toISOString();
13351
- }
13352
- function slug(value) {
13353
- const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
13354
- return normalized || "workflow";
13403
+ function normalizePath2(path25) {
13404
+ return path25.replace(/\\/g, "/");
13355
13405
  }
13356
- function titleFromName(name) {
13357
- return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
13406
+ function tokenize2(text) {
13407
+ return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
13358
13408
  }
13359
- function requiredText(data, key) {
13360
- const value = data[key];
13361
- if (typeof value !== "string" || !value.trim()) {
13362
- throw new Error("workflow frontmatter requires " + key);
13409
+ function containsTaskKeyword(text, keyword) {
13410
+ const matcher = /^[a-z0-9_-]+$/i.test(keyword) ? new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=[^a-z0-9_-]|$)`, "gi") : new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
13411
+ let match;
13412
+ while ((match = matcher.exec(text)) !== null) {
13413
+ const keywordIndex = match.index + (match[1]?.length ?? 0);
13414
+ if (!isNegatedTaskKeyword(text, keywordIndex)) return true;
13363
13415
  }
13364
- return value.trim();
13416
+ return false;
13365
13417
  }
13366
- function optionalText4(data, key) {
13367
- const value = data[key];
13368
- if (value === void 0 || value === null || value === "") return void 0;
13369
- if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
13370
- return value.trim() || void 0;
13418
+ function isNegatedTaskKeyword(text, keywordIndex) {
13419
+ const boundary = Math.max(
13420
+ text.lastIndexOf(".", keywordIndex - 1),
13421
+ text.lastIndexOf("!", keywordIndex - 1),
13422
+ text.lastIndexOf("?", keywordIndex - 1),
13423
+ text.lastIndexOf(";", keywordIndex - 1),
13424
+ text.lastIndexOf("\n", keywordIndex - 1),
13425
+ text.lastIndexOf("\u3002", keywordIndex - 1),
13426
+ text.lastIndexOf("\uFF01", keywordIndex - 1),
13427
+ text.lastIndexOf("\uFF1F", keywordIndex - 1),
13428
+ text.lastIndexOf("\uFF1B", keywordIndex - 1)
13429
+ );
13430
+ let prefix = text.slice(boundary + 1, keywordIndex).toLowerCase();
13431
+ const contrast = /(?:,|\uff0c)\s*(?:but|however|instead|yet|\u4f46\u662f|\u4f46|\u800c\u662f)\s*/gi;
13432
+ let contrastMatch;
13433
+ let contrastEnd = -1;
13434
+ while ((contrastMatch = contrast.exec(prefix)) !== null) contrastEnd = contrast.lastIndex;
13435
+ if (contrastEnd >= 0) prefix = prefix.slice(contrastEnd);
13436
+ if (/\b(?:do|does|did|should|must|can|could|will|would|may|might)\s+not\b/.test(prefix)) return true;
13437
+ if (/\b(?:don't|dont|never|without|avoid|skip)\b/.test(prefix)) return true;
13438
+ if (/(?:^|[\s,])no\s+(?:[a-z0-9_-]+\s*){0,4}$/i.test(prefix)) return true;
13439
+ const chineseClauseStart = Math.max(prefix.lastIndexOf(","), prefix.lastIndexOf("\uFF0C"), prefix.lastIndexOf("\u3001"));
13440
+ const chineseClause = prefix.slice(chineseClauseStart + 1);
13441
+ return /(?:\u4e0d\u8981|\u4e0d\u5e94|\u4e0d\u53ef|\u4e0d\u80fd|\u4e0d\u4f1a|\u7981\u6b62|\u52ff|\u4e0d)\s*(?:\u7acb\u5373|\u76f4\u63a5|\u518d|\u73b0\u5728|\u64c5\u81ea|\u5148)?\s*$/.test(chineseClause);
13371
13442
  }
13372
- function optionalStringArray(data, key) {
13373
- const value = data[key];
13374
- if (value === void 0 || value === null) return [];
13375
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
13376
- throw new Error("workflow frontmatter field " + key + " must be a string array");
13443
+ function resolveTaskLens(task) {
13444
+ const normalized = (task ?? "").toLowerCase();
13445
+ if (!normalized.trim()) return LENSES.general;
13446
+ let best = {
13447
+ id: "general",
13448
+ score: 0,
13449
+ priority: Number.MAX_SAFE_INTEGER
13450
+ };
13451
+ for (const id of LENS_PRIORITY) {
13452
+ const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsTaskKeyword(normalized, keyword) ? 1 : 0), 0);
13453
+ const priority = LENS_PRIORITY.indexOf(id);
13454
+ if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
13455
+ best = { id, score, priority };
13456
+ }
13377
13457
  }
13378
- return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
13458
+ return best.score > 0 ? LENSES[best.id] : LENSES.general;
13379
13459
  }
13380
- function optionalNumber(data, key, fallback) {
13381
- const value = data[key];
13382
- if (value === void 0 || value === null || value === "") return fallback;
13383
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
13384
- throw new Error("workflow frontmatter field " + key + " must be a positive integer");
13460
+ function pathKindScore(path25, lens) {
13461
+ const normalized = normalizePath2(path25).toLowerCase();
13462
+ const name = normalized.split("/").pop() ?? normalized;
13463
+ const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
13464
+ const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
13465
+ const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
13466
+ const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
13467
+ const isChangelog = name === "changelog.md" || name === "changes.md";
13468
+ const isWorkflow = normalized.startsWith(".github/workflows/");
13469
+ switch (lens.id) {
13470
+ case "bugfix":
13471
+ return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
13472
+ case "test":
13473
+ return (isTest ? 90 : 0) + (isSource ? 45 : 0);
13474
+ case "release":
13475
+ return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
13476
+ case "onboarding":
13477
+ return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
13478
+ case "docs":
13479
+ return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
13480
+ case "refactor":
13481
+ return (isSource ? 70 : 0) + (isTest ? 65 : 0);
13482
+ case "feature":
13483
+ return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
13484
+ default:
13485
+ if (isSource || isTest) return 60;
13486
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
13487
+ if (inDocs) return 20;
13488
+ return 0;
13385
13489
  }
13386
- return value;
13387
13490
  }
13388
- function optionalStatus(data) {
13389
- const value = data.status;
13390
- if (value === void 0 || value === null || value === "") return "draft";
13391
- if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
13392
- throw new Error("workflow frontmatter field status is invalid");
13393
- }
13394
- return value;
13491
+ function tokenScore(text, tokens) {
13492
+ if (tokens.length === 0) return 0;
13493
+ const normalized = text.toLowerCase();
13494
+ return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
13395
13495
  }
13396
- function normalizeAgents(values) {
13397
- const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
13398
- if (unknown.length > 0) {
13399
- throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
13400
- }
13401
- return values;
13496
+ function sourceTaskMatchScore(source, task) {
13497
+ const tokens = tokenize2(task ?? "");
13498
+ if (tokens.length === 0) return 0;
13499
+ const text = [
13500
+ source.title,
13501
+ source.path ?? "",
13502
+ source.symbol ?? ""
13503
+ ].join("\n").toLowerCase();
13504
+ const matches = tokens.filter((token) => text.includes(token)).length;
13505
+ if (matches >= 2) return 40;
13506
+ if (matches === 1) return 24;
13507
+ return 0;
13402
13508
  }
13403
- function phaseFromValue(value, index) {
13404
- if (!value || typeof value !== "object" || Array.isArray(value)) {
13405
- throw new Error("workflow frontmatter phases must contain objects");
13406
- }
13407
- const data = value;
13408
- const title = requiredText(data, "title");
13409
- const phaseId = optionalText4(data, "id") ?? slug(title) + "-" + (index + 1);
13410
- return {
13411
- id: phaseId,
13412
- title,
13413
- instructions: optionalText4(data, "instructions") ?? "",
13414
- branches: optionalStringArray(data, "branches"),
13415
- expectedOutputs: optionalStringArray(data, "expectedOutputs"),
13416
- verificationGates: optionalStringArray(data, "verificationGates")
13417
- };
13509
+ function fallbackPathRank(path25) {
13510
+ const normalized = normalizePath2(path25);
13511
+ if (normalized.startsWith("src/")) return 0;
13512
+ if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
13513
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
13514
+ if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
13515
+ return 4;
13418
13516
  }
13419
- function phasesFromBody(body) {
13420
- const headers = [...body.matchAll(/^##\s+(.+?)\s*$/gm)];
13421
- if (headers.length === 0) {
13422
- const instructions = body.trim();
13423
- return instructions ? [{
13424
- id: "execute",
13425
- title: "Execute",
13426
- instructions,
13427
- branches: [],
13428
- expectedOutputs: [],
13429
- verificationGates: []
13430
- }] : [];
13431
- }
13432
- return headers.map((match, index) => {
13433
- const title = match[1].trim();
13434
- const start = (match.index ?? 0) + match[0].length;
13435
- const end = index + 1 < headers.length ? headers[index + 1].index ?? body.length : body.length;
13436
- return {
13437
- id: slug(title) + "-" + (index + 1),
13438
- title,
13439
- instructions: body.slice(start, end).trim(),
13440
- branches: [],
13441
- expectedOutputs: [],
13442
- verificationGates: []
13443
- };
13444
- });
13517
+ function rankLensPaths(paths, lens, task) {
13518
+ const tokens = tokenize2(task ?? "");
13519
+ return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
13520
+ path: path25,
13521
+ index,
13522
+ score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
13523
+ fallback: fallbackPathRank(path25)
13524
+ })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
13445
13525
  }
13446
- function normalizeSourcePath(sourcePath) {
13447
- const normalized = sourcePath.replace(/\\/g, "/");
13448
- if (!normalized.startsWith("workflows/") || !normalized.toLowerCase().endsWith(".md") || normalized.includes("../") || path17.posix.normalize(normalized) !== normalized) {
13449
- throw new Error("workflow source path must be a safe workflows/*.md path");
13526
+ function lensPathCandidates(lens) {
13527
+ switch (lens.id) {
13528
+ case "release":
13529
+ return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
13530
+ case "onboarding":
13531
+ return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
13532
+ case "docs":
13533
+ return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
13534
+ case "test":
13535
+ case "bugfix":
13536
+ return ["tests", "test"];
13537
+ default:
13538
+ return [];
13450
13539
  }
13451
- return normalized;
13452
13540
  }
13453
- function sourceHashFor(workflow) {
13454
- return hash3(JSON.stringify({
13455
- id: workflow.id,
13456
- title: workflow.title,
13457
- description: workflow.description,
13458
- status: workflow.status,
13459
- version: workflow.version,
13460
- taskLenses: workflow.taskLenses,
13461
- triggers: workflow.triggers,
13462
- assumptions: workflow.assumptions,
13463
- requiredContext: workflow.requiredContext,
13464
- guardrails: workflow.guardrails,
13465
- allowedTools: workflow.allowedTools,
13466
- phases: workflow.phases,
13467
- verificationGates: workflow.verificationGates,
13468
- claimIds: workflow.claimIds,
13541
+ function scoreLensSource(source, lens, task) {
13542
+ const tokens = tokenize2(task ?? "");
13543
+ const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
13544
+ return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
13545
+ }
13546
+ function rankLensSources(sources, lens, task) {
13547
+ return [...sources].map((source, index) => ({
13548
+ source,
13549
+ index,
13550
+ score: scoreLensSource(source, lens, task)
13551
+ })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
13552
+ }
13553
+ function lensVerificationHints(lens) {
13554
+ switch (lens.id) {
13555
+ case "bugfix":
13556
+ return [
13557
+ "run the smallest failing test or repro first",
13558
+ "inspect the changed code path before trusting old memory"
13559
+ ];
13560
+ case "test":
13561
+ return [
13562
+ "run the exact focused test file or test name first",
13563
+ "inspect fixtures and harness setup before changing assertions"
13564
+ ];
13565
+ case "release":
13566
+ return [
13567
+ "run build, tests, package smoke, and publish dry-run where available",
13568
+ "verify package metadata, changelog, and Git state before publishing"
13569
+ ];
13570
+ case "onboarding":
13571
+ return [
13572
+ "read the docs/start files first, then inspect only the code paths needed for the task",
13573
+ "treat old implementation memories as leads until current code confirms them"
13574
+ ];
13575
+ case "refactor":
13576
+ return [
13577
+ "inspect call sites and tests before editing shared code",
13578
+ "run the narrow affected test plus one regression smoke"
13579
+ ];
13580
+ case "docs":
13581
+ return [
13582
+ "check headings, links, commands, and examples against current code",
13583
+ "run the smallest docs or package smoke available"
13584
+ ];
13585
+ case "feature":
13586
+ return [
13587
+ "inspect the closest existing implementation pattern before adding new code",
13588
+ "run focused tests plus one user-flow smoke after changes"
13589
+ ];
13590
+ default:
13591
+ return [
13592
+ "inspect the Start here files before editing",
13593
+ "run the smallest relevant test or smoke command after changes"
13594
+ ];
13595
+ }
13596
+ }
13597
+ function shouldShowLensSource(source, lens, task) {
13598
+ if (lens.id === "general") return true;
13599
+ const score = scoreLensSource(source, lens, task);
13600
+ if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13601
+ return score > 0;
13602
+ }
13603
+ var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
13604
+ var init_task_lens = __esm({
13605
+ "src/codegraph/task-lens.ts"() {
13606
+ "use strict";
13607
+ init_esm_shims();
13608
+ STOP_WORDS = /* @__PURE__ */ new Set([
13609
+ "a",
13610
+ "an",
13611
+ "and",
13612
+ "as",
13613
+ "for",
13614
+ "in",
13615
+ "into",
13616
+ "of",
13617
+ "on",
13618
+ "onto",
13619
+ "the",
13620
+ "this",
13621
+ "that",
13622
+ "to",
13623
+ "with",
13624
+ "work",
13625
+ "project",
13626
+ "continue",
13627
+ "\u7EE7\u7EED",
13628
+ "\u9879\u76EE"
13629
+ ]);
13630
+ LENSES = {
13631
+ bugfix: {
13632
+ id: "bugfix",
13633
+ description: "debug the failure with current code and the smallest repro first",
13634
+ sourceLimit: 6,
13635
+ cautionLimit: 4,
13636
+ hideUnrelatedCautionDetails: true,
13637
+ hideUnrelatedReliableDetails: false
13638
+ },
13639
+ feature: {
13640
+ id: "feature",
13641
+ description: "build a scoped feature from nearby source, types, and user flow",
13642
+ sourceLimit: 6,
13643
+ cautionLimit: 3,
13644
+ hideUnrelatedCautionDetails: true,
13645
+ hideUnrelatedReliableDetails: false
13646
+ },
13647
+ release: {
13648
+ id: "release",
13649
+ description: "prepare a release using current metadata, changelog, build, and package checks",
13650
+ sourceLimit: 4,
13651
+ cautionLimit: 2,
13652
+ hideUnrelatedCautionDetails: true,
13653
+ hideUnrelatedReliableDetails: true
13654
+ },
13655
+ onboarding: {
13656
+ id: "onboarding",
13657
+ description: "understand the project shape before trusting old implementation details",
13658
+ sourceLimit: 4,
13659
+ cautionLimit: 2,
13660
+ hideUnrelatedCautionDetails: true,
13661
+ hideUnrelatedReliableDetails: true
13662
+ },
13663
+ refactor: {
13664
+ id: "refactor",
13665
+ description: "change structure carefully by reading shared code, call sites, and tests",
13666
+ sourceLimit: 6,
13667
+ cautionLimit: 4,
13668
+ hideUnrelatedCautionDetails: true,
13669
+ hideUnrelatedReliableDetails: false
13670
+ },
13671
+ docs: {
13672
+ id: "docs",
13673
+ description: "update documentation against current code and public entry points",
13674
+ sourceLimit: 5,
13675
+ cautionLimit: 2,
13676
+ hideUnrelatedCautionDetails: true,
13677
+ hideUnrelatedReliableDetails: true
13678
+ },
13679
+ test: {
13680
+ id: "test",
13681
+ description: "work from tests, fixtures, harnesses, and the related source files",
13682
+ sourceLimit: 6,
13683
+ cautionLimit: 3,
13684
+ hideUnrelatedCautionDetails: true,
13685
+ hideUnrelatedReliableDetails: false
13686
+ },
13687
+ general: {
13688
+ id: "general",
13689
+ description: "balanced project handoff with current facts, code memory, and verification hints",
13690
+ sourceLimit: 8,
13691
+ cautionLimit: 5,
13692
+ hideUnrelatedCautionDetails: false,
13693
+ hideUnrelatedReliableDetails: false
13694
+ }
13695
+ };
13696
+ KEYWORDS = {
13697
+ bugfix: [
13698
+ "bug",
13699
+ "crash",
13700
+ "incident",
13701
+ "debug",
13702
+ "error",
13703
+ "fail",
13704
+ "failing",
13705
+ "fix",
13706
+ "issue",
13707
+ "regression",
13708
+ "repro",
13709
+ "\u62A5\u9519",
13710
+ "\u5D29\u6E83",
13711
+ "\u6545\u969C",
13712
+ "\u5931\u8D25",
13713
+ "\u4FEE\u590D",
13714
+ "\u95EE\u9898"
13715
+ ],
13716
+ feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
13717
+ release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
13718
+ onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
13719
+ refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
13720
+ docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
13721
+ test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
13722
+ };
13723
+ LENS_PRIORITY = [
13724
+ "bugfix",
13725
+ "release",
13726
+ "test",
13727
+ "refactor",
13728
+ "feature",
13729
+ "docs",
13730
+ "onboarding"
13731
+ ];
13732
+ }
13733
+ });
13734
+
13735
+ // src/knowledge/workflows.ts
13736
+ var workflows_exports = {};
13737
+ __export(workflows_exports, {
13738
+ WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
13739
+ applyWorkflowAdapter: () => applyWorkflowAdapter,
13740
+ importWindsurfWorkflows: () => importWindsurfWorkflows,
13741
+ parseWorkflowMarkdown: () => parseWorkflowMarkdown,
13742
+ previewWorkflowAdapter: () => previewWorkflowAdapter,
13743
+ recordWorkflowRun: () => recordWorkflowRun,
13744
+ renderWorkflowMarkdown: () => renderWorkflowMarkdown,
13745
+ selectWorkflows: () => selectWorkflows,
13746
+ selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
13747
+ syncCanonicalWorkflows: () => syncCanonicalWorkflows,
13748
+ writeCanonicalWorkflow: () => writeCanonicalWorkflow
13749
+ });
13750
+ import { createHash as createHash12 } from "crypto";
13751
+ import { promises as fs14 } from "fs";
13752
+ import path17 from "path";
13753
+ import matter11 from "gray-matter";
13754
+ function hash3(value) {
13755
+ return createHash12("sha256").update(value).digest("hex");
13756
+ }
13757
+ function now2() {
13758
+ return (/* @__PURE__ */ new Date()).toISOString();
13759
+ }
13760
+ function slug(value) {
13761
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
13762
+ return normalized || "workflow";
13763
+ }
13764
+ function titleFromName(name) {
13765
+ return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
13766
+ }
13767
+ function requiredText(data, key) {
13768
+ const value = data[key];
13769
+ if (typeof value !== "string" || !value.trim()) {
13770
+ throw new Error("workflow frontmatter requires " + key);
13771
+ }
13772
+ return value.trim();
13773
+ }
13774
+ function optionalText4(data, key) {
13775
+ const value = data[key];
13776
+ if (value === void 0 || value === null || value === "") return void 0;
13777
+ if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
13778
+ return value.trim() || void 0;
13779
+ }
13780
+ function optionalStringArray(data, key) {
13781
+ const value = data[key];
13782
+ if (value === void 0 || value === null) return [];
13783
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
13784
+ throw new Error("workflow frontmatter field " + key + " must be a string array");
13785
+ }
13786
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
13787
+ }
13788
+ function optionalNumber(data, key, fallback) {
13789
+ const value = data[key];
13790
+ if (value === void 0 || value === null || value === "") return fallback;
13791
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
13792
+ throw new Error("workflow frontmatter field " + key + " must be a positive integer");
13793
+ }
13794
+ return value;
13795
+ }
13796
+ function optionalStatus(data) {
13797
+ const value = data.status;
13798
+ if (value === void 0 || value === null || value === "") return "draft";
13799
+ if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
13800
+ throw new Error("workflow frontmatter field status is invalid");
13801
+ }
13802
+ return value;
13803
+ }
13804
+ function normalizeAgents(values) {
13805
+ const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
13806
+ if (unknown.length > 0) {
13807
+ throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
13808
+ }
13809
+ return values;
13810
+ }
13811
+ function phaseFromValue(value, index) {
13812
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13813
+ throw new Error("workflow frontmatter phases must contain objects");
13814
+ }
13815
+ const data = value;
13816
+ const title = requiredText(data, "title");
13817
+ const phaseId = optionalText4(data, "id") ?? slug(title) + "-" + (index + 1);
13818
+ return {
13819
+ id: phaseId,
13820
+ title,
13821
+ instructions: optionalText4(data, "instructions") ?? "",
13822
+ branches: optionalStringArray(data, "branches"),
13823
+ expectedOutputs: optionalStringArray(data, "expectedOutputs"),
13824
+ verificationGates: optionalStringArray(data, "verificationGates")
13825
+ };
13826
+ }
13827
+ function phasesFromBody(body) {
13828
+ const headers = [...body.matchAll(/^##\s+(.+?)\s*$/gm)];
13829
+ if (headers.length === 0) {
13830
+ const instructions = body.trim();
13831
+ return instructions ? [{
13832
+ id: "execute",
13833
+ title: "Execute",
13834
+ instructions,
13835
+ branches: [],
13836
+ expectedOutputs: [],
13837
+ verificationGates: []
13838
+ }] : [];
13839
+ }
13840
+ return headers.map((match, index) => {
13841
+ const title = match[1].trim();
13842
+ const start = (match.index ?? 0) + match[0].length;
13843
+ const end = index + 1 < headers.length ? headers[index + 1].index ?? body.length : body.length;
13844
+ return {
13845
+ id: slug(title) + "-" + (index + 1),
13846
+ title,
13847
+ instructions: body.slice(start, end).trim(),
13848
+ branches: [],
13849
+ expectedOutputs: [],
13850
+ verificationGates: []
13851
+ };
13852
+ });
13853
+ }
13854
+ function normalizeSourcePath(sourcePath) {
13855
+ const normalized = sourcePath.replace(/\\/g, "/");
13856
+ if (!normalized.startsWith("workflows/") || !normalized.toLowerCase().endsWith(".md") || normalized.includes("../") || path17.posix.normalize(normalized) !== normalized) {
13857
+ throw new Error("workflow source path must be a safe workflows/*.md path");
13858
+ }
13859
+ return normalized;
13860
+ }
13861
+ function sourceHashFor(workflow) {
13862
+ return hash3(JSON.stringify({
13863
+ id: workflow.id,
13864
+ title: workflow.title,
13865
+ description: workflow.description,
13866
+ status: workflow.status,
13867
+ version: workflow.version,
13868
+ taskLenses: workflow.taskLenses,
13869
+ triggers: workflow.triggers,
13870
+ assumptions: workflow.assumptions,
13871
+ requiredContext: workflow.requiredContext,
13872
+ guardrails: workflow.guardrails,
13873
+ allowedTools: workflow.allowedTools,
13874
+ phases: workflow.phases,
13875
+ verificationGates: workflow.verificationGates,
13876
+ claimIds: workflow.claimIds,
13469
13877
  evidenceRefs: workflow.evidenceRefs,
13470
13878
  codeRefs: workflow.codeRefs,
13471
13879
  compatibleAgents: workflow.compatibleAgents,
@@ -13646,20 +14054,27 @@ function taskScore(workflow, task) {
13646
14054
  const text = task.toLowerCase();
13647
14055
  let score = 0;
13648
14056
  const reasons = [];
14057
+ const matchedLenses = /* @__PURE__ */ new Set();
14058
+ const matchedTriggers = /* @__PURE__ */ new Set();
13649
14059
  for (const lens of workflow.taskLenses) {
13650
14060
  const terms = LENS_TERMS[lens.toLowerCase()] ?? [lens.toLowerCase()];
13651
- if (terms.some((term) => text.includes(term))) {
14061
+ if (terms.some((term) => containsTaskKeyword(task, term))) {
13652
14062
  score += 20;
13653
14063
  reasons.push("matches " + lens + " workflow");
14064
+ matchedLenses.add(lens.toLowerCase());
13654
14065
  }
13655
14066
  }
13656
14067
  for (const trigger of workflow.triggers) {
13657
14068
  const term = trigger.toLowerCase().trim();
13658
- if (term.length >= 2 && text.includes(term)) {
14069
+ if (term.length >= 2 && containsTaskKeyword(task, term)) {
13659
14070
  score += 8;
13660
14071
  reasons.push('matches trigger "' + trigger + '"');
14072
+ matchedTriggers.add(term);
13661
14073
  }
13662
14074
  }
14075
+ const hasSpecificLens = workflow.taskLenses.some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens.toLowerCase()));
14076
+ const hasSpecificMatch = [...matchedLenses].some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens)) || [...matchedTriggers].some((trigger) => !GENERIC_WORKFLOW_TERMS.has(trigger));
14077
+ if (hasSpecificLens && !hasSpecificMatch) return { score: 0, reasons: [] };
13663
14078
  return { score, reasons: [...new Set(reasons)] };
13664
14079
  }
13665
14080
  function selectWorkflows(input) {
@@ -13677,9 +14092,28 @@ function selectWorkflows(input) {
13677
14092
  return selected.slice(0, Math.max(1, Math.min(input.limit ?? 2, 2)));
13678
14093
  }
13679
14094
  function importedWorkflowSpec(input) {
14095
+ let sourceMatter;
14096
+ try {
14097
+ sourceMatter = matter11(input.raw);
14098
+ } catch (error) {
14099
+ throw new Error("Malformed Windsurf workflow Markdown: " + (error instanceof Error ? error.message : String(error)));
14100
+ }
14101
+ if (sourceMatter.data.id !== void 0) {
14102
+ const id = requiredText(sourceMatter.data, "id");
14103
+ const sourcePath2 = "workflows/" + slug(id) + ".md";
14104
+ const canonical = parsedWorkflow(sourceMatter.data, sourceMatter.content, {
14105
+ workspaceId: input.workspace.id,
14106
+ sourcePath: sourcePath2,
14107
+ contentHash: hash3(input.raw)
14108
+ });
14109
+ return materializeWorkflow({
14110
+ ...canonical,
14111
+ importedFrom: input.sourcePath.replace(/\\/g, "/")
14112
+ });
14113
+ }
13680
14114
  const entry = new WorkflowSyncer().parseWindsurfWorkflow(input.sourceName, input.raw);
13681
14115
  const content = entry.content.trim() || "## Execute\n\nFollow the imported workflow.";
13682
- const lenses = inferTaskLenses(entry.name + "\n" + entry.description + "\n" + content);
14116
+ const lenses = inferTaskLenses(entry.name + "\n" + entry.description);
13683
14117
  const createdAt = now2();
13684
14118
  const sourcePath = "workflows/" + slug(entry.name) + ".md";
13685
14119
  const spec = {
@@ -13900,7 +14334,7 @@ async function selectWorkspaceWorkflows(input) {
13900
14334
  errors: synced.errors
13901
14335
  };
13902
14336
  }
13903
- var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS;
14337
+ var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS, GENERIC_WORKFLOW_LENSES, GENERIC_WORKFLOW_TERMS;
13904
14338
  var init_workflows = __esm({
13905
14339
  "src/knowledge/workflows.ts"() {
13906
14340
  "use strict";
@@ -13909,6 +14343,7 @@ var init_workflows = __esm({
13909
14343
  init_workflow_sync();
13910
14344
  init_workspace();
13911
14345
  init_workflow_store();
14346
+ init_task_lens();
13912
14347
  KNOWN_AGENTS = [
13913
14348
  "windsurf",
13914
14349
  "cursor",
@@ -13940,6 +14375,8 @@ var init_workflows = __esm({
13940
14375
  refactor: ["refactor", "cleanup", "restructure", "\u91CD\u6784", "\u6574\u7406"],
13941
14376
  test: ["test", "verify", "smoke", "\u6D4B\u8BD5", "\u9A8C\u8BC1"]
13942
14377
  };
14378
+ GENERIC_WORKFLOW_LENSES = /* @__PURE__ */ new Set(["review", "test"]);
14379
+ GENERIC_WORKFLOW_TERMS = /* @__PURE__ */ new Set(["review", "audit", "test", "verify", "smoke"]);
13943
14380
  }
13944
14381
  });
13945
14382
 
@@ -15124,7 +15561,8 @@ var init_workset = __esm({
15124
15561
  // src/codegraph/current-facts.ts
15125
15562
  var current_facts_exports = {};
15126
15563
  __export(current_facts_exports, {
15127
- collectCurrentProjectFacts: () => collectCurrentProjectFacts
15564
+ collectCurrentProjectFacts: () => collectCurrentProjectFacts,
15565
+ formatGitFact: () => formatGitFact
15128
15566
  });
15129
15567
  import { execFileSync } from "child_process";
15130
15568
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
@@ -15169,11 +15607,16 @@ function runGit2(rootPath, args) {
15169
15607
  }
15170
15608
  }
15171
15609
  function readGitFacts(rootPath) {
15172
- const branch = runGit2(rootPath, ["branch", "--show-current"]);
15610
+ const available = runGit2(rootPath, ["rev-parse", "--is-inside-work-tree"]) === "true";
15611
+ if (!available) {
15612
+ return { available: false, dirty: false, detached: false };
15613
+ }
15614
+ const branch = runGit2(rootPath, ["symbolic-ref", "--quiet", "--short", "HEAD"]) ?? runGit2(rootPath, ["branch", "--show-current"]);
15173
15615
  const commit = runGit2(rootPath, ["rev-parse", "--short", "HEAD"]);
15174
15616
  const latestCommit = runGit2(rootPath, ["log", "-1", "--pretty=%s"]);
15175
15617
  const dirty = Boolean(runGit2(rootPath, ["status", "--porcelain"]));
15176
15618
  return {
15619
+ available: true,
15177
15620
  ...branch ? { branch } : {},
15178
15621
  ...commit ? { commit } : {},
15179
15622
  ...latestCommit ? { latestCommit } : {},
@@ -15181,6 +15624,15 @@ function readGitFacts(rootPath) {
15181
15624
  detached: !branch
15182
15625
  };
15183
15626
  }
15627
+ function formatGitFact(git) {
15628
+ if (!git.available) return "Git: unavailable";
15629
+ const parts = [];
15630
+ if (git.detached) parts.push("detached HEAD");
15631
+ else if (git.branch) parts.push("branch " + git.branch);
15632
+ if (git.commit) parts.push("commit " + git.commit);
15633
+ parts.push(git.dirty ? "dirty worktree" : "clean worktree");
15634
+ return "Git: " + parts.join(", ");
15635
+ }
15184
15636
  function parseProgressNote(content) {
15185
15637
  const lastUpdated = content.match(/Last updated\*\*:\s*(\d{4}-\d{2}-\d{2})/i) ?? content.match(/Last updated:\s*(\d{4}-\d{2}-\d{2})/i);
15186
15638
  const branchHint = content.match(/Branch\*\*:\s*([^\r\n]+)/i) ?? content.match(/Branch:\s*([^\r\n]+)/i);
@@ -15682,485 +16134,172 @@ var init_external_provider = __esm({
15682
16134
  ...typeof exitCode === "number" ? { exitCode } : {},
15683
16135
  ...timedOut ? { timedOut } : {},
15684
16136
  ...outputLimited ? { outputLimited } : {},
15685
- ...spawnError ? { error: spawnError } : {}
15686
- });
15687
- });
15688
- });
15689
- }
15690
- };
15691
- }
15692
- });
15693
-
15694
- // src/codegraph/freshness.ts
15695
- function evaluateCodeRefFreshness(ref, file, symbol) {
15696
- if (!file) {
15697
- return { status: "stale", reason: "referenced file is no longer indexed" };
15698
- }
15699
- if (ref.symbolId && !symbol) {
15700
- return { status: "stale", reason: "referenced symbol is no longer indexed" };
15701
- }
15702
- if (ref.capturedSymbolHash && symbol?.contentHash) {
15703
- if (symbol.contentHash !== ref.capturedSymbolHash) {
15704
- return { status: "stale", reason: "referenced symbol content changed" };
15705
- }
15706
- return { status: "current", reason: "referenced symbol content still matches" };
15707
- }
15708
- if (ref.capturedFileHash && file.contentHash !== ref.capturedFileHash) {
15709
- return { status: "suspect", reason: "referenced file changed since capture" };
15710
- }
15711
- return { status: "current", reason: "referenced file still matches" };
15712
- }
15713
- var init_freshness2 = __esm({
15714
- "src/codegraph/freshness.ts"() {
15715
- "use strict";
15716
- init_esm_shims();
15717
- }
15718
- });
15719
-
15720
- // src/codegraph/project-context.ts
15721
- function uniq(items) {
15722
- return [...new Set(items)];
15723
- }
15724
- function activeObservations(observations2, projectId) {
15725
- return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
15726
- }
15727
- function countLanguages(files) {
15728
- const counts = /* @__PURE__ */ new Map();
15729
- for (const file of files) {
15730
- const language = file.language ?? "unknown";
15731
- counts.set(language, (counts.get(language) ?? 0) + 1);
15732
- }
15733
- return [...counts.entries()].map(([language, files2]) => ({ language, files: files2 })).sort((a, b) => a.language.localeCompare(b.language));
15734
- }
15735
- function suggestedReadRank(path25) {
15736
- const normalized = path25.replace(/\\/g, "/");
15737
- if (normalized.startsWith("src/")) return 0;
15738
- if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 0;
15739
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
15740
- return 3;
15741
- }
15742
- function compactSuggestedReads(paths, limit = 8, exclude) {
15743
- return uniq(paths).filter((path25) => !isCodeGraphExcludedPath(path25, exclude)).sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b)).slice(0, limit);
15744
- }
15745
- function collectGraph(store, projectId, observations2, exclude) {
15746
- const files = store.listFiles(projectId);
15747
- const symbols = store.listReferencedSymbols(projectId);
15748
- const filesById = new Map(files.map((file) => [file.id, file]));
15749
- const symbolsById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
15750
- const observationsById = new Map(observations2.map((obs) => [obs.id, obs]));
15751
- const activeObservationIds = new Set(observations2.map((obs) => obs.id));
15752
- const refs = store.listProjectObservationRefs(projectId).filter((ref) => activeObservationIds.has(ref.observationId));
15753
- const freshness = {
15754
- current: 0,
15755
- suspect: 0,
15756
- stale: 0,
15757
- unbound: 0
15758
- };
15759
- const sources = [];
15760
- const suggestedReads = [];
15761
- for (const ref of refs) {
15762
- const file = ref.fileId ? filesById.get(ref.fileId) : void 0;
15763
- const symbol = ref.symbolId ? symbolsById.get(ref.symbolId) : void 0;
15764
- const result = evaluateCodeRefFreshness(ref, file, symbol);
15765
- freshness[result.status] += 1;
15766
- const observation = observationsById.get(ref.observationId);
15767
- if (!observation) continue;
15768
- const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
15769
- if (result.status === "current" && file && !excluded) suggestedReads.push(file.path);
15770
- if (excluded) continue;
15771
- sources.push({
15772
- observationId: observation.id,
15773
- title: observation.title,
15774
- type: observation.type,
15775
- ...file ? { path: file.path } : {},
15776
- ...symbol ? { symbol: symbol.name } : {},
15777
- status: result.status,
15778
- reason: result.reason
15779
- });
15780
- }
15781
- if (files.length === 0) {
15782
- for (const observation of observations2) {
15783
- for (const path25 of observation.filesModified ?? []) {
15784
- if (!path25 || isCodeGraphExcludedPath(path25, exclude)) continue;
15785
- suggestedReads.push(path25);
15786
- sources.push({
15787
- observationId: observation.id,
15788
- title: observation.title,
15789
- type: observation.type,
15790
- path: path25,
15791
- status: "unbound",
15792
- reason: "Recorded file hint; Code Memory refresh is pending."
16137
+ ...spawnError ? { error: spawnError } : {}
16138
+ });
16139
+ });
15793
16140
  });
15794
- freshness.unbound++;
15795
16141
  }
16142
+ };
16143
+ }
16144
+ });
16145
+
16146
+ // src/codegraph/freshness.ts
16147
+ function evaluateCodeRefFreshness(ref, file, symbol) {
16148
+ if (!file) {
16149
+ return { status: "stale", reason: "referenced file is no longer indexed" };
16150
+ }
16151
+ if (ref.symbolId && !symbol) {
16152
+ return { status: "stale", reason: "referenced symbol is no longer indexed" };
16153
+ }
16154
+ if (ref.capturedSymbolHash && symbol?.contentHash) {
16155
+ if (symbol.contentHash !== ref.capturedSymbolHash) {
16156
+ return { status: "stale", reason: "referenced symbol content changed" };
15796
16157
  }
16158
+ return { status: "current", reason: "referenced symbol content still matches" };
15797
16159
  }
15798
- return {
15799
- files,
15800
- symbols,
15801
- refs,
15802
- freshness,
15803
- sources,
15804
- suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
15805
- };
15806
- }
15807
- function overviewFromGraph(input) {
15808
- const status = input.store.status(input.project.id);
15809
- return {
15810
- project: input.project,
15811
- code: {
15812
- provider: status.provider,
15813
- files: status.files,
15814
- symbols: status.symbols,
15815
- edges: status.edges,
15816
- refs: status.refs,
15817
- ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
15818
- languages: countLanguages(input.graph.files),
15819
- ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
15820
- },
15821
- memory: {
15822
- total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
15823
- active: input.active.length
15824
- },
15825
- freshness: input.graph.freshness,
15826
- suggestedReads: input.graph.suggestedReads
15827
- };
15828
- }
15829
- function buildProjectContextExplain(input) {
15830
- const active = activeObservations(input.observations, input.project.id);
15831
- const graph = collectGraph(input.store, input.project.id, active, input.exclude);
15832
- const overview = overviewFromGraph({
15833
- project: input.project,
15834
- store: input.store,
15835
- observations: input.observations,
15836
- active,
15837
- graph
15838
- });
15839
- return {
15840
- project: input.project,
15841
- sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
15842
- overview
15843
- };
16160
+ if (ref.capturedFileHash && file.contentHash !== ref.capturedFileHash) {
16161
+ return { status: "suspect", reason: "referenced file changed since capture" };
16162
+ }
16163
+ return { status: "current", reason: "referenced file still matches" };
15844
16164
  }
15845
- var init_project_context = __esm({
15846
- "src/codegraph/project-context.ts"() {
16165
+ var init_freshness2 = __esm({
16166
+ "src/codegraph/freshness.ts"() {
15847
16167
  "use strict";
15848
16168
  init_esm_shims();
15849
- init_freshness2();
15850
- init_exclude();
15851
16169
  }
15852
16170
  });
15853
16171
 
15854
- // src/codegraph/task-lens.ts
15855
- var task_lens_exports = {};
15856
- __export(task_lens_exports, {
15857
- lensPathCandidates: () => lensPathCandidates,
15858
- lensVerificationHints: () => lensVerificationHints,
15859
- rankLensPaths: () => rankLensPaths,
15860
- rankLensSources: () => rankLensSources,
15861
- resolveTaskLens: () => resolveTaskLens,
15862
- scoreLensSource: () => scoreLensSource,
15863
- shouldShowLensSource: () => shouldShowLensSource
15864
- });
15865
- function normalizePath2(path25) {
15866
- return path25.replace(/\\/g, "/");
15867
- }
15868
- function tokenize2(text) {
15869
- return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
15870
- }
15871
- function containsKeyword(text, keyword) {
15872
- if (/^[a-z0-9_-]+$/i.test(keyword)) {
15873
- return new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9_-]|$)`, "i").test(text);
15874
- }
15875
- return text.includes(keyword);
16172
+ // src/codegraph/project-context.ts
16173
+ function uniq(items) {
16174
+ return [...new Set(items)];
15876
16175
  }
15877
- function resolveTaskLens(task) {
15878
- const normalized = (task ?? "").toLowerCase();
15879
- if (!normalized.trim()) return LENSES.general;
15880
- let best = {
15881
- id: "general",
15882
- score: 0,
15883
- priority: Number.MAX_SAFE_INTEGER
15884
- };
15885
- for (const id of LENS_PRIORITY) {
15886
- const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsKeyword(normalized, keyword) ? 1 : 0), 0);
15887
- const priority = LENS_PRIORITY.indexOf(id);
15888
- if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
15889
- best = { id, score, priority };
15890
- }
15891
- }
15892
- return best.score > 0 ? LENSES[best.id] : LENSES.general;
16176
+ function activeObservations(observations2, projectId) {
16177
+ return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
15893
16178
  }
15894
- function pathKindScore(path25, lens) {
15895
- const normalized = normalizePath2(path25).toLowerCase();
15896
- const name = normalized.split("/").pop() ?? normalized;
15897
- const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
15898
- const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
15899
- const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
15900
- const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
15901
- const isChangelog = name === "changelog.md" || name === "changes.md";
15902
- const isWorkflow = normalized.startsWith(".github/workflows/");
15903
- switch (lens.id) {
15904
- case "bugfix":
15905
- return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
15906
- case "test":
15907
- return (isTest ? 90 : 0) + (isSource ? 45 : 0);
15908
- case "release":
15909
- return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
15910
- case "onboarding":
15911
- return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
15912
- case "docs":
15913
- return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
15914
- case "refactor":
15915
- return (isSource ? 70 : 0) + (isTest ? 65 : 0);
15916
- case "feature":
15917
- return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
15918
- default:
15919
- if (isSource || isTest) return 60;
15920
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
15921
- if (inDocs) return 20;
15922
- return 0;
16179
+ function countLanguages(files) {
16180
+ const counts = /* @__PURE__ */ new Map();
16181
+ for (const file of files) {
16182
+ const language = file.language ?? "unknown";
16183
+ counts.set(language, (counts.get(language) ?? 0) + 1);
15923
16184
  }
16185
+ return [...counts.entries()].map(([language, files2]) => ({ language, files: files2 })).sort((a, b) => a.language.localeCompare(b.language));
15924
16186
  }
15925
- function tokenScore(text, tokens) {
15926
- if (tokens.length === 0) return 0;
15927
- const normalized = text.toLowerCase();
15928
- return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
15929
- }
15930
- function sourceTaskMatchScore(source, task) {
15931
- const tokens = tokenize2(task ?? "");
15932
- if (tokens.length === 0) return 0;
15933
- const text = [
15934
- source.title,
15935
- source.path ?? "",
15936
- source.symbol ?? ""
15937
- ].join("\n").toLowerCase();
15938
- const matches = tokens.filter((token) => text.includes(token)).length;
15939
- if (matches >= 2) return 40;
15940
- if (matches === 1) return 24;
15941
- return 0;
15942
- }
15943
- function fallbackPathRank(path25) {
15944
- const normalized = normalizePath2(path25);
16187
+ function suggestedReadRank(path25) {
16188
+ const normalized = path25.replace(/\\/g, "/");
15945
16189
  if (normalized.startsWith("src/")) return 0;
15946
- if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
16190
+ if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 0;
15947
16191
  if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
15948
- if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
15949
- return 4;
15950
- }
15951
- function rankLensPaths(paths, lens, task) {
15952
- const tokens = tokenize2(task ?? "");
15953
- return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
15954
- path: path25,
15955
- index,
15956
- score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
15957
- fallback: fallbackPathRank(path25)
15958
- })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
15959
- }
15960
- function lensPathCandidates(lens) {
15961
- switch (lens.id) {
15962
- case "release":
15963
- return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
15964
- case "onboarding":
15965
- return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
15966
- case "docs":
15967
- return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
15968
- case "test":
15969
- case "bugfix":
15970
- return ["tests", "test"];
15971
- default:
15972
- return [];
15973
- }
15974
- }
15975
- function scoreLensSource(source, lens, task) {
15976
- const tokens = tokenize2(task ?? "");
15977
- const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
15978
- return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
16192
+ return 3;
15979
16193
  }
15980
- function rankLensSources(sources, lens, task) {
15981
- return [...sources].map((source, index) => ({
15982
- source,
15983
- index,
15984
- score: scoreLensSource(source, lens, task)
15985
- })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
16194
+ function compactSuggestedReads(paths, limit = 8, exclude) {
16195
+ return uniq(paths).filter((path25) => !isCodeGraphExcludedPath(path25, exclude)).sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b)).slice(0, limit);
15986
16196
  }
15987
- function lensVerificationHints(lens) {
15988
- switch (lens.id) {
15989
- case "bugfix":
15990
- return [
15991
- "run the smallest failing test or repro first",
15992
- "inspect the changed code path before trusting old memory"
15993
- ];
15994
- case "test":
15995
- return [
15996
- "run the exact focused test file or test name first",
15997
- "inspect fixtures and harness setup before changing assertions"
15998
- ];
15999
- case "release":
16000
- return [
16001
- "run build, tests, package smoke, and publish dry-run where available",
16002
- "verify package metadata, changelog, and Git state before publishing"
16003
- ];
16004
- case "onboarding":
16005
- return [
16006
- "read the docs/start files first, then inspect only the code paths needed for the task",
16007
- "treat old implementation memories as leads until current code confirms them"
16008
- ];
16009
- case "refactor":
16010
- return [
16011
- "inspect call sites and tests before editing shared code",
16012
- "run the narrow affected test plus one regression smoke"
16013
- ];
16014
- case "docs":
16015
- return [
16016
- "check headings, links, commands, and examples against current code",
16017
- "run the smallest docs or package smoke available"
16018
- ];
16019
- case "feature":
16020
- return [
16021
- "inspect the closest existing implementation pattern before adding new code",
16022
- "run focused tests plus one user-flow smoke after changes"
16023
- ];
16024
- default:
16025
- return [
16026
- "inspect the Start here files before editing",
16027
- "run the smallest relevant test or smoke command after changes"
16028
- ];
16197
+ function collectGraph(store, projectId, observations2, exclude) {
16198
+ const files = store.listFiles(projectId);
16199
+ const symbols = store.listReferencedSymbols(projectId);
16200
+ const filesById = new Map(files.map((file) => [file.id, file]));
16201
+ const symbolsById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
16202
+ const observationsById = new Map(observations2.map((obs) => [obs.id, obs]));
16203
+ const activeObservationIds = new Set(observations2.map((obs) => obs.id));
16204
+ const refs = store.listProjectObservationRefs(projectId).filter((ref) => activeObservationIds.has(ref.observationId));
16205
+ const freshness = {
16206
+ current: 0,
16207
+ suspect: 0,
16208
+ stale: 0,
16209
+ unbound: 0
16210
+ };
16211
+ const sources = [];
16212
+ const suggestedReads = [];
16213
+ for (const ref of refs) {
16214
+ const file = ref.fileId ? filesById.get(ref.fileId) : void 0;
16215
+ const symbol = ref.symbolId ? symbolsById.get(ref.symbolId) : void 0;
16216
+ const result = evaluateCodeRefFreshness(ref, file, symbol);
16217
+ freshness[result.status] += 1;
16218
+ const observation = observationsById.get(ref.observationId);
16219
+ if (!observation) continue;
16220
+ const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
16221
+ if (result.status === "current" && file && !excluded) suggestedReads.push(file.path);
16222
+ if (excluded) continue;
16223
+ sources.push({
16224
+ observationId: observation.id,
16225
+ title: observation.title,
16226
+ type: observation.type,
16227
+ ...file ? { path: file.path } : {},
16228
+ ...symbol ? { symbol: symbol.name } : {},
16229
+ status: result.status,
16230
+ reason: result.reason
16231
+ });
16232
+ }
16233
+ if (files.length === 0) {
16234
+ for (const observation of observations2) {
16235
+ for (const path25 of observation.filesModified ?? []) {
16236
+ if (!path25 || isCodeGraphExcludedPath(path25, exclude)) continue;
16237
+ suggestedReads.push(path25);
16238
+ sources.push({
16239
+ observationId: observation.id,
16240
+ title: observation.title,
16241
+ type: observation.type,
16242
+ path: path25,
16243
+ status: "unbound",
16244
+ reason: "Recorded file hint; Code Memory refresh is pending."
16245
+ });
16246
+ freshness.unbound++;
16247
+ }
16248
+ }
16029
16249
  }
16250
+ return {
16251
+ files,
16252
+ symbols,
16253
+ refs,
16254
+ freshness,
16255
+ sources,
16256
+ suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
16257
+ };
16030
16258
  }
16031
- function shouldShowLensSource(source, lens, task) {
16032
- if (lens.id === "general") return true;
16033
- const score = scoreLensSource(source, lens, task);
16034
- if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
16035
- return score > 0;
16259
+ function overviewFromGraph(input) {
16260
+ const status = input.store.status(input.project.id);
16261
+ return {
16262
+ project: input.project,
16263
+ code: {
16264
+ provider: status.provider,
16265
+ files: status.files,
16266
+ symbols: status.symbols,
16267
+ edges: status.edges,
16268
+ refs: status.refs,
16269
+ ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
16270
+ languages: countLanguages(input.graph.files),
16271
+ ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
16272
+ },
16273
+ memory: {
16274
+ total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
16275
+ active: input.active.length
16276
+ },
16277
+ freshness: input.graph.freshness,
16278
+ suggestedReads: input.graph.suggestedReads
16279
+ };
16036
16280
  }
16037
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
16038
- var init_task_lens = __esm({
16039
- "src/codegraph/task-lens.ts"() {
16281
+ function buildProjectContextExplain(input) {
16282
+ const active = activeObservations(input.observations, input.project.id);
16283
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
16284
+ const overview = overviewFromGraph({
16285
+ project: input.project,
16286
+ store: input.store,
16287
+ observations: input.observations,
16288
+ active,
16289
+ graph
16290
+ });
16291
+ return {
16292
+ project: input.project,
16293
+ sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
16294
+ overview
16295
+ };
16296
+ }
16297
+ var init_project_context = __esm({
16298
+ "src/codegraph/project-context.ts"() {
16040
16299
  "use strict";
16041
16300
  init_esm_shims();
16042
- STOP_WORDS = /* @__PURE__ */ new Set([
16043
- "a",
16044
- "an",
16045
- "and",
16046
- "as",
16047
- "for",
16048
- "in",
16049
- "into",
16050
- "of",
16051
- "on",
16052
- "onto",
16053
- "the",
16054
- "this",
16055
- "that",
16056
- "to",
16057
- "with",
16058
- "work",
16059
- "project",
16060
- "continue",
16061
- "\u7EE7\u7EED",
16062
- "\u9879\u76EE"
16063
- ]);
16064
- LENSES = {
16065
- bugfix: {
16066
- id: "bugfix",
16067
- description: "debug the failure with current code and the smallest repro first",
16068
- sourceLimit: 6,
16069
- cautionLimit: 4,
16070
- hideUnrelatedCautionDetails: true,
16071
- hideUnrelatedReliableDetails: false
16072
- },
16073
- feature: {
16074
- id: "feature",
16075
- description: "build a scoped feature from nearby source, types, and user flow",
16076
- sourceLimit: 6,
16077
- cautionLimit: 3,
16078
- hideUnrelatedCautionDetails: true,
16079
- hideUnrelatedReliableDetails: false
16080
- },
16081
- release: {
16082
- id: "release",
16083
- description: "prepare a release using current metadata, changelog, build, and package checks",
16084
- sourceLimit: 4,
16085
- cautionLimit: 2,
16086
- hideUnrelatedCautionDetails: true,
16087
- hideUnrelatedReliableDetails: true
16088
- },
16089
- onboarding: {
16090
- id: "onboarding",
16091
- description: "understand the project shape before trusting old implementation details",
16092
- sourceLimit: 4,
16093
- cautionLimit: 2,
16094
- hideUnrelatedCautionDetails: true,
16095
- hideUnrelatedReliableDetails: true
16096
- },
16097
- refactor: {
16098
- id: "refactor",
16099
- description: "change structure carefully by reading shared code, call sites, and tests",
16100
- sourceLimit: 6,
16101
- cautionLimit: 4,
16102
- hideUnrelatedCautionDetails: true,
16103
- hideUnrelatedReliableDetails: false
16104
- },
16105
- docs: {
16106
- id: "docs",
16107
- description: "update documentation against current code and public entry points",
16108
- sourceLimit: 5,
16109
- cautionLimit: 2,
16110
- hideUnrelatedCautionDetails: true,
16111
- hideUnrelatedReliableDetails: true
16112
- },
16113
- test: {
16114
- id: "test",
16115
- description: "work from tests, fixtures, harnesses, and the related source files",
16116
- sourceLimit: 6,
16117
- cautionLimit: 3,
16118
- hideUnrelatedCautionDetails: true,
16119
- hideUnrelatedReliableDetails: false
16120
- },
16121
- general: {
16122
- id: "general",
16123
- description: "balanced project handoff with current facts, code memory, and verification hints",
16124
- sourceLimit: 8,
16125
- cautionLimit: 5,
16126
- hideUnrelatedCautionDetails: false,
16127
- hideUnrelatedReliableDetails: false
16128
- }
16129
- };
16130
- KEYWORDS = {
16131
- bugfix: [
16132
- "bug",
16133
- "crash",
16134
- "debug",
16135
- "error",
16136
- "fail",
16137
- "failing",
16138
- "fix",
16139
- "issue",
16140
- "regression",
16141
- "repro",
16142
- "\u62A5\u9519",
16143
- "\u5D29\u6E83",
16144
- "\u5931\u8D25",
16145
- "\u4FEE\u590D",
16146
- "\u95EE\u9898"
16147
- ],
16148
- feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
16149
- release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
16150
- onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
16151
- refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
16152
- docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
16153
- test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
16154
- };
16155
- LENS_PRIORITY = [
16156
- "bugfix",
16157
- "release",
16158
- "test",
16159
- "refactor",
16160
- "feature",
16161
- "docs",
16162
- "onboarding"
16163
- ];
16301
+ init_freshness2();
16302
+ init_exclude();
16164
16303
  }
16165
16304
  });
16166
16305
 
@@ -16459,15 +16598,7 @@ function formatCurrentFactsLines(facts) {
16459
16598
  if (facts.latestChangelog) {
16460
16599
  lines.push(`- Latest changelog: ${facts.latestChangelog.version}${facts.latestChangelog.date ? ` (${facts.latestChangelog.date})` : ""}`);
16461
16600
  }
16462
- const gitParts = [];
16463
- if (facts.git.detached) {
16464
- gitParts.push("detached HEAD");
16465
- } else if (facts.git.branch) {
16466
- gitParts.push(`branch ${facts.git.branch}`);
16467
- }
16468
- if (facts.git.commit) gitParts.push(`commit ${facts.git.commit}`);
16469
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16470
- lines.push(`- Git: ${gitParts.join(", ")}`);
16601
+ lines.push("- " + formatGitFact(facts.git));
16471
16602
  if (facts.git.latestCommit) lines.push(`- Latest commit: ${facts.git.latestCommit}`);
16472
16603
  lines.push("- Current facts above outrank progress/dev-log files when they conflict.");
16473
16604
  if (facts.staleNotes.length > 0) {
@@ -16489,11 +16620,7 @@ function worksetFactLines(facts) {
16489
16620
  if (facts.latestChangelog) {
16490
16621
  lines.push("Latest changelog: " + facts.latestChangelog.version + (facts.latestChangelog.date ? " (" + facts.latestChangelog.date + ")" : ""));
16491
16622
  }
16492
- const gitParts = [];
16493
- if (facts.git.branch) gitParts.push("branch " + facts.git.branch);
16494
- if (facts.git.commit) gitParts.push("commit " + facts.git.commit);
16495
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16496
- lines.push("Git: " + gitParts.join(", "));
16623
+ lines.push(formatGitFact(facts.git));
16497
16624
  for (const note of facts.staleNotes.slice(0, 1)) {
16498
16625
  lines.push(
16499
16626
  "Historical note: " + note.path + (note.branchHint ? " (branch hint " + note.branchHint + "; " + note.reason + ")" : " (" + note.reason + ")")
@@ -18975,13 +19102,8 @@ function prepareDashboardConfig(projectRoot) {
18975
19102
  }
18976
19103
  }
18977
19104
  function computeProjectGraphCounts(allEntities, allRelations, projectObs) {
18978
- const entityNames = new Set(
18979
- projectObs.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
18980
- );
18981
- const entities = allEntities.filter((e) => entityNames.has(e.name));
18982
- const entityNameSet = new Set(entities.map((e) => e.name));
18983
- const relations = allRelations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
18984
- return { entities: entities.length, relations: relations.length, entityNames };
19105
+ const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
19106
+ return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
18985
19107
  }
18986
19108
  async function handleApi(req, res, dataDir, projectId, projectName, baseDir, projectRoot, projectResolved, mode = "standalone", port = 3210) {
18987
19109
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -19054,13 +19176,8 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19054
19176
  const gStore = getGraphStore();
19055
19177
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
19056
19178
  const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19057
- const projectEntityNames = new Set(
19058
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19059
- );
19060
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
19061
- const entityNameSet = new Set(entities.map((e) => e.name));
19062
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
19063
- sendJson(res, { entities, relations });
19179
+ const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
19180
+ sendJson(res, { entities: scoped.entities, relations: scoped.relations });
19064
19181
  break;
19065
19182
  }
19066
19183
  case "/observations": {
@@ -19252,19 +19369,13 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19252
19369
  const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19253
19370
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19254
19371
  const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
19255
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19256
- const projectEntityNames = new Set(
19257
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19258
- );
19259
- const scopedEntities = fullGraph.entities.filter((e) => projectEntityNames.has(e.name));
19260
- const scopedEntityNameSet = new Set(scopedEntities.map((e) => e.name));
19261
- const scopedRelations = fullGraph.relations.filter((r) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
19372
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
19262
19373
  const graph = generateKnowledgeGraph2({
19263
19374
  projectId: effectiveProjectId,
19264
19375
  observations: allObs,
19265
19376
  miniSkills: skills,
19266
- graphEntities: scopedEntities,
19267
- graphRelations: scopedRelations
19377
+ graphEntities: scoped.entities,
19378
+ graphRelations: scoped.relations
19268
19379
  });
19269
19380
  sendJson(res, graph);
19270
19381
  break;
@@ -19486,16 +19597,11 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19486
19597
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19487
19598
  const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19488
19599
  const nextId2 = await getObservationStore().loadIdCounter();
19489
- const exportEntityNames = new Set(
19490
- observations2.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
19491
- );
19492
- const exportEntities = fullGraph.entities.filter((e) => exportEntityNames.has(e.name));
19493
- const exportEntitySet = new Set(exportEntities.map((e) => e.name));
19494
- const exportRelations = fullGraph.relations.filter((r) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
19600
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations2);
19495
19601
  const exportData = {
19496
19602
  project: { id: effectiveProjectId, name: effectiveProjectName },
19497
19603
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
19498
- graph: { entities: exportEntities, relations: exportRelations },
19604
+ graph: { entities: scoped.entities, relations: scoped.relations },
19499
19605
  observations: observations2,
19500
19606
  nextId: nextId2
19501
19607
  };
@@ -19812,6 +19918,7 @@ var init_server = __esm({
19812
19918
  init_dotenv_loader();
19813
19919
  init_yaml_loader();
19814
19920
  init_yaml_loader();
19921
+ init_graph_scope();
19815
19922
  MIME_TYPES = {
19816
19923
  ".html": "text/html; charset=utf-8",
19817
19924
  ".css": "text/css; charset=utf-8",
@@ -22528,6 +22635,24 @@ async function createAutoRelations(obs, extracted, graphManager) {
22528
22635
  const relationType = inferRelationType(obs);
22529
22636
  const relations = [];
22530
22637
  const selfName = obs.entityName.toLowerCase();
22638
+ const explicitRelated = [...new Set((obs.relatedEntities ?? []).map((name) => name.trim()).filter((name) => name && name.toLowerCase() !== selfName))];
22639
+ if (explicitRelated.length > 0) {
22640
+ await graphManager.createEntities(explicitRelated.map((name) => ({
22641
+ name,
22642
+ entityType: "related",
22643
+ observations: []
22644
+ })));
22645
+ for (const name of explicitRelated) {
22646
+ const matchedEntity = graphManager.findEntityByName(name);
22647
+ if (matchedEntity) {
22648
+ relations.push({
22649
+ from: obs.entityName,
22650
+ to: matchedEntity.name,
22651
+ relationType: "related_entity"
22652
+ });
22653
+ }
22654
+ }
22655
+ }
22531
22656
  const candidates = [
22532
22657
  ...extracted.identifiers,
22533
22658
  ...extracted.files.map((f) => f.split("/").pop()?.replace(/\.\w+$/, "") ?? ""),
@@ -22569,6 +22694,7 @@ async function createAutoRelations(obs, extracted, graphManager) {
22569
22694
 
22570
22695
  // src/server.ts
22571
22696
  init_entity_extractor();
22697
+ init_graph_scope();
22572
22698
 
22573
22699
  // src/compact/engine.ts
22574
22700
  init_esm_shims();
@@ -26254,7 +26380,7 @@ The path should point to a directory containing a .git folder.`
26254
26380
  };
26255
26381
  const server = existingServer ?? new McpServer({
26256
26382
  name: "memorix",
26257
- version: true ? "1.2.0" : "1.0.1"
26383
+ version: true ? "1.2.1" : "1.0.1"
26258
26384
  });
26259
26385
  const originalRegisterTool = server.registerTool.bind(server);
26260
26386
  server.registerTool = ((name, ...args) => {
@@ -26986,7 +27112,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26986
27112
  { getResolvedConfig: getResolvedConfig2 },
26987
27113
  { getExternalCodeGraphContext: getExternalCodeGraphContext2 },
26988
27114
  { getObservationStore: getObservationStore2 },
26989
- { collectCurrentProjectFacts: collectCurrentProjectFacts2 },
27115
+ { collectCurrentProjectFacts: collectCurrentProjectFacts2, formatGitFact: formatGitFact2 },
26990
27116
  { resolveTaskLens: resolveTaskLens2 }
26991
27117
  ] = await Promise.all([
26992
27118
  Promise.resolve().then(() => (init_store(), store_exports)),
@@ -27027,7 +27153,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27027
27153
  if (currentFacts.latestChangelog) {
27028
27154
  worksetFacts.push("Latest changelog: " + currentFacts.latestChangelog.version + (currentFacts.latestChangelog.date ? " (" + currentFacts.latestChangelog.date + ")" : ""));
27029
27155
  }
27030
- worksetFacts.push("Git: " + (currentFacts.git.branch ? "branch " + currentFacts.git.branch + ", " : "") + (currentFacts.git.dirty ? "dirty worktree" : "clean worktree"));
27156
+ worksetFacts.push(formatGitFact2(currentFacts.git));
27031
27157
  const codeState = snapshot ? "- Code state: " + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : "Git unavailable") + ", " + snapshot.worktreeState + " worktree, epoch " + snapshot.sourceEpoch : "- Code state: no completed snapshot yet";
27032
27158
  const pack = await attachTaskWorkset2({
27033
27159
  pack: basePack,
@@ -27059,11 +27185,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27059
27185
  "memorix_knowledge",
27060
27186
  {
27061
27187
  title: "Knowledge Workspace",
27062
- description: "Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, inspect status, compile source-backed proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.",
27188
+ description: "Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, review source-backed claims, compile proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.",
27063
27189
  inputSchema: {
27064
27190
  action: z2.enum([
27065
27191
  "workspace_init",
27066
27192
  "status",
27193
+ "claim_list",
27194
+ "claim_review",
27067
27195
  "compile",
27068
27196
  "lint",
27069
27197
  "proposal_apply",
@@ -27078,6 +27206,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27078
27206
  path: z2.string().optional().describe("Explicit versioned workspace path, used only by workspace_init"),
27079
27207
  proposalId: z2.string().optional().describe("Pending proposal id for proposal_apply"),
27080
27208
  allowManualOverwrite: z2.boolean().optional().default(false).describe("Explicitly allow proposal_apply to replace a manually edited page"),
27209
+ claimId: z2.string().optional().describe("Source-backed claim id for claim_review"),
27210
+ claimReviewState: z2.enum(["approved", "rejected"]).optional().describe("Deliberate review verdict for claim_review"),
27211
+ reviewDetail: z2.string().max(2e3).optional().describe("Evidence check performed before approving or rejecting a claim"),
27081
27212
  workflowId: z2.string().optional().describe("Canonical workflow id for workflow preview, apply, or run"),
27082
27213
  agent: z2.string().optional().describe("Target agent for a workflow adapter"),
27083
27214
  task: z2.string().optional().describe("Task text for workflow selection or a workflow run"),
@@ -27094,6 +27225,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27094
27225
  path: workspacePath,
27095
27226
  proposalId,
27096
27227
  allowManualOverwrite,
27228
+ claimId,
27229
+ claimReviewState,
27230
+ reviewDetail,
27097
27231
  workflowId,
27098
27232
  agent,
27099
27233
  task,
@@ -27116,6 +27250,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27116
27250
  };
27117
27251
  const [
27118
27252
  { ClaimStore: ClaimStore2 },
27253
+ { reviewClaim: reviewClaim2 },
27119
27254
  { CodeGraphStore: CodeGraphStore2 },
27120
27255
  { initializeKnowledgeWorkspace: initializeKnowledgeWorkspace2, loadKnowledgeWorkspace: loadKnowledgeWorkspace2 },
27121
27256
  { KnowledgeWorkspaceStore: KnowledgeWorkspaceStore2 },
@@ -27131,6 +27266,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27131
27266
  }
27132
27267
  ] = await Promise.all([
27133
27268
  Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
27269
+ Promise.resolve().then(() => (init_claims(), claims_exports)),
27134
27270
  Promise.resolve().then(() => (init_store(), store_exports)),
27135
27271
  Promise.resolve().then(() => (init_workspace(), workspace_exports)),
27136
27272
  Promise.resolve().then(() => (init_workspace_store(), workspace_store_exports)),
@@ -27180,10 +27316,52 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27180
27316
  targetPath: proposal.targetPath,
27181
27317
  reason: proposal.reason,
27182
27318
  createdAt: proposal.createdAt
27183
- }))
27319
+ })),
27320
+ reviewableClaims: claims.listClaims(project.id, { limit: 100 }).filter((claim) => claim.reviewState === "needs-review").map((claim) => ({ id: claim.id, subject: claim.subject, predicate: claim.predicate, objectValue: claim.objectValue }))
27184
27321
  }
27185
27322
  });
27186
27323
  }
27324
+ if (action === "claim_list") {
27325
+ return text({
27326
+ claims: claims.listClaims(project.id, { limit: 100 }).map((claim) => ({
27327
+ id: claim.id,
27328
+ subject: claim.subject,
27329
+ predicate: claim.predicate,
27330
+ objectValue: claim.objectValue,
27331
+ status: claim.status,
27332
+ reviewState: claim.reviewState,
27333
+ origin: claim.origin,
27334
+ confidence: claim.confidence,
27335
+ evidenceCount: claims.listEvidence(claim.id).length
27336
+ })),
27337
+ next: "Approve only after checking the linked evidence. Rejected claims stay out of retrieval and publication."
27338
+ });
27339
+ }
27340
+ if (action === "claim_review") {
27341
+ const requestedClaimId = requireText(claimId, "claimId");
27342
+ const detail = requireText(reviewDetail, "reviewDetail");
27343
+ if (!requestedClaimId || !claimReviewState || !detail) {
27344
+ return text({ error: "claimId, claimReviewState, and reviewDetail are required for claim_review." }, true);
27345
+ }
27346
+ const existing = claims.getClaim(requestedClaimId);
27347
+ if (!existing || existing.projectId !== project.id) {
27348
+ return text({ error: "Claim was not found for the current project." }, true);
27349
+ }
27350
+ const claim = reviewClaim2(claims, {
27351
+ claimId: requestedClaimId,
27352
+ reviewState: claimReviewState,
27353
+ detail
27354
+ });
27355
+ return text({
27356
+ claim: {
27357
+ id: claim.id,
27358
+ status: claim.status,
27359
+ reviewState: claim.reviewState,
27360
+ updatedAt: claim.updatedAt
27361
+ },
27362
+ next: claim.reviewState === "approved" ? "This approved source-backed claim can now be considered by knowledge compilation." : "This rejected claim is excluded from retrieval and knowledge compilation."
27363
+ });
27364
+ }
27187
27365
  if (action === "compile") {
27188
27366
  const result = await compileKnowledgeWorkspace2({ workspace, claims });
27189
27367
  return text({
@@ -27222,7 +27400,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27222
27400
  if (action === "workflow_import") {
27223
27401
  const result = await importWindsurfWorkflows2({ workspace, projectRoot });
27224
27402
  return text({
27225
- imported: result.imported.map((workflow2) => ({ id: workflow2.id, title: workflow2.title, sourcePath: workflow2.sourcePath })),
27403
+ imported: result.imported.map((workflow2) => ({
27404
+ id: workflow2.id,
27405
+ title: workflow2.title,
27406
+ sourcePath: workflow2.sourcePath,
27407
+ importedFrom: workflow2.importedFrom,
27408
+ verificationGates: workflow2.verificationGates
27409
+ })),
27226
27410
  skipped: result.skipped
27227
27411
  });
27228
27412
  }
@@ -27234,7 +27418,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27234
27418
  title: workflow2.title,
27235
27419
  status: workflow2.status,
27236
27420
  taskLenses: workflow2.taskLenses,
27237
- sourcePath: workflow2.sourcePath
27421
+ sourcePath: workflow2.sourcePath,
27422
+ importedFrom: workflow2.importedFrom,
27423
+ verificationGates: workflow2.verificationGates
27238
27424
  })),
27239
27425
  parseErrors: synced.errors
27240
27426
  });
@@ -28063,13 +28249,11 @@ Archived memories are hidden from default search but can be found with status: "
28063
28249
  async function scopeGraphToProject(graph) {
28064
28250
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
28065
28251
  const allObs = await withFreshIndex(() => getAllObservations2());
28066
- const projectEntityNames = new Set(
28067
- allObs.filter((o) => o.projectId === project.id && (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
28252
+ const scoped = scopeKnowledgeGraphToProject(
28253
+ graph,
28254
+ allObs.filter((observation) => observation.projectId === project.id)
28068
28255
  );
28069
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
28070
- const entityNameSet = new Set(entities.map((e) => e.name));
28071
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
28072
- return { entities, relations };
28256
+ return { entities: scoped.entities, relations: scoped.relations };
28073
28257
  }
28074
28258
  server.registerTool(
28075
28259
  "read_graph",