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/sdk.js CHANGED
@@ -9066,6 +9066,36 @@ var init_session_store = __esm({
9066
9066
  }
9067
9067
  });
9068
9068
 
9069
+ // src/memory/graph-scope.ts
9070
+ function projectGraphEntityNames(observations2) {
9071
+ const entityNames = /* @__PURE__ */ new Set();
9072
+ for (const observation of observations2) {
9073
+ if ((observation.status ?? "active") !== "active") continue;
9074
+ const entityName = observation.entityName?.trim();
9075
+ if (entityName) entityNames.add(entityName);
9076
+ for (const relatedEntity of observation.relatedEntities ?? []) {
9077
+ const name = relatedEntity.trim();
9078
+ if (name) entityNames.add(name);
9079
+ }
9080
+ }
9081
+ return entityNames;
9082
+ }
9083
+ function scopeKnowledgeGraphToProject(graph, observations2) {
9084
+ const entityNames = projectGraphEntityNames(observations2);
9085
+ const entities = graph.entities.filter((entity) => entityNames.has(entity.name));
9086
+ const visibleNames = new Set(entities.map((entity) => entity.name));
9087
+ const relations = graph.relations.filter(
9088
+ (relation) => visibleNames.has(relation.from) && visibleNames.has(relation.to)
9089
+ );
9090
+ return { entities, relations, entityNames };
9091
+ }
9092
+ var init_graph_scope = __esm({
9093
+ "src/memory/graph-scope.ts"() {
9094
+ "use strict";
9095
+ init_esm_shims();
9096
+ }
9097
+ });
9098
+
9069
9099
  // src/memory/disclosure-policy.ts
9070
9100
  function resolveEvidenceBasis(fields) {
9071
9101
  if (fields.commitHash || fields.source === "git" || fields.sourceDetail === "git-ingest") {
@@ -12178,6 +12208,7 @@ __export(claims_exports, {
12178
12208
  buildClaimIdentity: () => buildClaimIdentity,
12179
12209
  deriveLowRiskClaimsFromObservation: () => deriveLowRiskClaimsFromObservation,
12180
12210
  requalifyClaimsForCodeState: () => requalifyClaimsForCodeState,
12211
+ reviewClaim: () => reviewClaim,
12181
12212
  selectClaimsForTask: () => selectClaimsForTask,
12182
12213
  supersedeClaim: () => supersedeClaim,
12183
12214
  writeClaim: () => writeClaim
@@ -12364,6 +12395,39 @@ function writeClaim(store, input) {
12364
12395
  };
12365
12396
  });
12366
12397
  }
12398
+ function reviewClaim(store, input) {
12399
+ if (input.reviewState !== "approved" && input.reviewState !== "rejected") {
12400
+ throw new Error("A claim review must approve or reject the claim");
12401
+ }
12402
+ const detail = compactText(input.detail, "review detail");
12403
+ return store.transaction(() => {
12404
+ const claim = store.getClaim(input.claimId);
12405
+ if (!claim) throw new Error("Claim was not found");
12406
+ if (input.reviewState === "approved" && claim.status === "superseded") {
12407
+ throw new Error("A superseded claim cannot be approved");
12408
+ }
12409
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
12410
+ const nextStatus = input.reviewState === "rejected" ? "unknown" : claim.status === "unknown" && claim.reviewState === "rejected" ? "active" : claim.status;
12411
+ const updated = {
12412
+ ...claim,
12413
+ status: nextStatus,
12414
+ reviewState: input.reviewState,
12415
+ updatedAt: now3
12416
+ };
12417
+ store.updateClaim(updated);
12418
+ store.recordEvent({
12419
+ projectId: claim.projectId,
12420
+ claimId: claim.id,
12421
+ kind: "reviewed",
12422
+ fromStatus: claim.status,
12423
+ toStatus: updated.status,
12424
+ detail: "Review: " + claim.reviewState + " -> " + input.reviewState + ". " + detail,
12425
+ createdAt: now3
12426
+ });
12427
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now3);
12428
+ return store.getClaim(claim.id) ?? updated;
12429
+ });
12430
+ }
12367
12431
  function supersedeClaim(store, input) {
12368
12432
  const evidence = validateEvidence(input.evidence);
12369
12433
  return store.transaction(() => {
@@ -12452,7 +12516,7 @@ function deriveLowRiskClaimsFromObservation(store, observation, codeStore) {
12452
12516
  scope: "project",
12453
12517
  confidence,
12454
12518
  observedAt: observation.updatedAt ?? observation.createdAt,
12455
- reviewState: "approved",
12519
+ reviewState: isGit ? "approved" : "needs-review",
12456
12520
  origin: isGit ? "git" : "derived",
12457
12521
  evidence
12458
12522
  });
@@ -13328,148 +13392,492 @@ var init_workflow_store = __esm({
13328
13392
  }
13329
13393
  });
13330
13394
 
13331
- // src/knowledge/workflows.ts
13332
- var workflows_exports = {};
13333
- __export(workflows_exports, {
13334
- WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
13335
- applyWorkflowAdapter: () => applyWorkflowAdapter,
13336
- importWindsurfWorkflows: () => importWindsurfWorkflows,
13337
- parseWorkflowMarkdown: () => parseWorkflowMarkdown,
13338
- previewWorkflowAdapter: () => previewWorkflowAdapter,
13339
- recordWorkflowRun: () => recordWorkflowRun,
13340
- renderWorkflowMarkdown: () => renderWorkflowMarkdown,
13341
- selectWorkflows: () => selectWorkflows,
13342
- selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
13343
- syncCanonicalWorkflows: () => syncCanonicalWorkflows,
13344
- writeCanonicalWorkflow: () => writeCanonicalWorkflow
13395
+ // src/codegraph/task-lens.ts
13396
+ var task_lens_exports = {};
13397
+ __export(task_lens_exports, {
13398
+ containsTaskKeyword: () => containsTaskKeyword,
13399
+ lensPathCandidates: () => lensPathCandidates,
13400
+ lensVerificationHints: () => lensVerificationHints,
13401
+ rankLensPaths: () => rankLensPaths,
13402
+ rankLensSources: () => rankLensSources,
13403
+ resolveTaskLens: () => resolveTaskLens,
13404
+ scoreLensSource: () => scoreLensSource,
13405
+ shouldShowLensSource: () => shouldShowLensSource
13345
13406
  });
13346
- import { createHash as createHash12 } from "crypto";
13347
- import { promises as fs14 } from "fs";
13348
- import path17 from "path";
13349
- import matter11 from "gray-matter";
13350
- function hash3(value) {
13351
- return createHash12("sha256").update(value).digest("hex");
13352
- }
13353
- function now2() {
13354
- return (/* @__PURE__ */ new Date()).toISOString();
13355
- }
13356
- function slug(value) {
13357
- const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
13358
- return normalized || "workflow";
13407
+ function normalizePath2(path25) {
13408
+ return path25.replace(/\\/g, "/");
13359
13409
  }
13360
- function titleFromName(name) {
13361
- return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
13410
+ function tokenize2(text) {
13411
+ return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
13362
13412
  }
13363
- function requiredText(data, key) {
13364
- const value = data[key];
13365
- if (typeof value !== "string" || !value.trim()) {
13366
- throw new Error("workflow frontmatter requires " + key);
13413
+ function containsTaskKeyword(text, keyword) {
13414
+ 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");
13415
+ let match;
13416
+ while ((match = matcher.exec(text)) !== null) {
13417
+ const keywordIndex = match.index + (match[1]?.length ?? 0);
13418
+ if (!isNegatedTaskKeyword(text, keywordIndex)) return true;
13367
13419
  }
13368
- return value.trim();
13420
+ return false;
13369
13421
  }
13370
- function optionalText4(data, key) {
13371
- const value = data[key];
13372
- if (value === void 0 || value === null || value === "") return void 0;
13373
- if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
13374
- return value.trim() || void 0;
13422
+ function isNegatedTaskKeyword(text, keywordIndex) {
13423
+ const boundary = Math.max(
13424
+ text.lastIndexOf(".", keywordIndex - 1),
13425
+ text.lastIndexOf("!", keywordIndex - 1),
13426
+ text.lastIndexOf("?", keywordIndex - 1),
13427
+ text.lastIndexOf(";", keywordIndex - 1),
13428
+ text.lastIndexOf("\n", keywordIndex - 1),
13429
+ text.lastIndexOf("\u3002", keywordIndex - 1),
13430
+ text.lastIndexOf("\uFF01", keywordIndex - 1),
13431
+ text.lastIndexOf("\uFF1F", keywordIndex - 1),
13432
+ text.lastIndexOf("\uFF1B", keywordIndex - 1)
13433
+ );
13434
+ let prefix = text.slice(boundary + 1, keywordIndex).toLowerCase();
13435
+ const contrast = /(?:,|\uff0c)\s*(?:but|however|instead|yet|\u4f46\u662f|\u4f46|\u800c\u662f)\s*/gi;
13436
+ let contrastMatch;
13437
+ let contrastEnd = -1;
13438
+ while ((contrastMatch = contrast.exec(prefix)) !== null) contrastEnd = contrast.lastIndex;
13439
+ if (contrastEnd >= 0) prefix = prefix.slice(contrastEnd);
13440
+ if (/\b(?:do|does|did|should|must|can|could|will|would|may|might)\s+not\b/.test(prefix)) return true;
13441
+ if (/\b(?:don't|dont|never|without|avoid|skip)\b/.test(prefix)) return true;
13442
+ if (/(?:^|[\s,])no\s+(?:[a-z0-9_-]+\s*){0,4}$/i.test(prefix)) return true;
13443
+ const chineseClauseStart = Math.max(prefix.lastIndexOf(","), prefix.lastIndexOf("\uFF0C"), prefix.lastIndexOf("\u3001"));
13444
+ const chineseClause = prefix.slice(chineseClauseStart + 1);
13445
+ 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);
13375
13446
  }
13376
- function optionalStringArray(data, key) {
13377
- const value = data[key];
13378
- if (value === void 0 || value === null) return [];
13379
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
13380
- throw new Error("workflow frontmatter field " + key + " must be a string array");
13447
+ function resolveTaskLens(task) {
13448
+ const normalized = (task ?? "").toLowerCase();
13449
+ if (!normalized.trim()) return LENSES.general;
13450
+ let best = {
13451
+ id: "general",
13452
+ score: 0,
13453
+ priority: Number.MAX_SAFE_INTEGER
13454
+ };
13455
+ for (const id of LENS_PRIORITY) {
13456
+ const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsTaskKeyword(normalized, keyword) ? 1 : 0), 0);
13457
+ const priority = LENS_PRIORITY.indexOf(id);
13458
+ if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
13459
+ best = { id, score, priority };
13460
+ }
13381
13461
  }
13382
- return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
13462
+ return best.score > 0 ? LENSES[best.id] : LENSES.general;
13383
13463
  }
13384
- function optionalNumber(data, key, fallback) {
13385
- const value = data[key];
13386
- if (value === void 0 || value === null || value === "") return fallback;
13387
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
13388
- throw new Error("workflow frontmatter field " + key + " must be a positive integer");
13464
+ function pathKindScore(path25, lens) {
13465
+ const normalized = normalizePath2(path25).toLowerCase();
13466
+ const name = normalized.split("/").pop() ?? normalized;
13467
+ const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
13468
+ const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
13469
+ const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
13470
+ const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
13471
+ const isChangelog = name === "changelog.md" || name === "changes.md";
13472
+ const isWorkflow = normalized.startsWith(".github/workflows/");
13473
+ switch (lens.id) {
13474
+ case "bugfix":
13475
+ return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
13476
+ case "test":
13477
+ return (isTest ? 90 : 0) + (isSource ? 45 : 0);
13478
+ case "release":
13479
+ return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
13480
+ case "onboarding":
13481
+ return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
13482
+ case "docs":
13483
+ return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
13484
+ case "refactor":
13485
+ return (isSource ? 70 : 0) + (isTest ? 65 : 0);
13486
+ case "feature":
13487
+ return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
13488
+ default:
13489
+ if (isSource || isTest) return 60;
13490
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
13491
+ if (inDocs) return 20;
13492
+ return 0;
13389
13493
  }
13390
- return value;
13391
13494
  }
13392
- function optionalStatus(data) {
13393
- const value = data.status;
13394
- if (value === void 0 || value === null || value === "") return "draft";
13395
- if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
13396
- throw new Error("workflow frontmatter field status is invalid");
13397
- }
13398
- return value;
13495
+ function tokenScore(text, tokens) {
13496
+ if (tokens.length === 0) return 0;
13497
+ const normalized = text.toLowerCase();
13498
+ return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
13399
13499
  }
13400
- function normalizeAgents(values) {
13401
- const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
13402
- if (unknown.length > 0) {
13403
- throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
13404
- }
13405
- return values;
13500
+ function sourceTaskMatchScore(source, task) {
13501
+ const tokens = tokenize2(task ?? "");
13502
+ if (tokens.length === 0) return 0;
13503
+ const text = [
13504
+ source.title,
13505
+ source.path ?? "",
13506
+ source.symbol ?? ""
13507
+ ].join("\n").toLowerCase();
13508
+ const matches = tokens.filter((token) => text.includes(token)).length;
13509
+ if (matches >= 2) return 40;
13510
+ if (matches === 1) return 24;
13511
+ return 0;
13406
13512
  }
13407
- function phaseFromValue(value, index) {
13408
- if (!value || typeof value !== "object" || Array.isArray(value)) {
13409
- throw new Error("workflow frontmatter phases must contain objects");
13410
- }
13411
- const data = value;
13412
- const title = requiredText(data, "title");
13413
- const phaseId = optionalText4(data, "id") ?? slug(title) + "-" + (index + 1);
13414
- return {
13415
- id: phaseId,
13416
- title,
13417
- instructions: optionalText4(data, "instructions") ?? "",
13418
- branches: optionalStringArray(data, "branches"),
13419
- expectedOutputs: optionalStringArray(data, "expectedOutputs"),
13420
- verificationGates: optionalStringArray(data, "verificationGates")
13421
- };
13513
+ function fallbackPathRank(path25) {
13514
+ const normalized = normalizePath2(path25);
13515
+ if (normalized.startsWith("src/")) return 0;
13516
+ if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
13517
+ if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
13518
+ if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
13519
+ return 4;
13422
13520
  }
13423
- function phasesFromBody(body) {
13424
- const headers = [...body.matchAll(/^##\s+(.+?)\s*$/gm)];
13425
- if (headers.length === 0) {
13426
- const instructions = body.trim();
13427
- return instructions ? [{
13428
- id: "execute",
13429
- title: "Execute",
13430
- instructions,
13431
- branches: [],
13432
- expectedOutputs: [],
13433
- verificationGates: []
13434
- }] : [];
13435
- }
13436
- return headers.map((match, index) => {
13437
- const title = match[1].trim();
13438
- const start = (match.index ?? 0) + match[0].length;
13439
- const end = index + 1 < headers.length ? headers[index + 1].index ?? body.length : body.length;
13440
- return {
13441
- id: slug(title) + "-" + (index + 1),
13442
- title,
13443
- instructions: body.slice(start, end).trim(),
13444
- branches: [],
13445
- expectedOutputs: [],
13446
- verificationGates: []
13447
- };
13448
- });
13521
+ function rankLensPaths(paths, lens, task) {
13522
+ const tokens = tokenize2(task ?? "");
13523
+ return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
13524
+ path: path25,
13525
+ index,
13526
+ score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
13527
+ fallback: fallbackPathRank(path25)
13528
+ })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
13449
13529
  }
13450
- function normalizeSourcePath(sourcePath) {
13451
- const normalized = sourcePath.replace(/\\/g, "/");
13452
- if (!normalized.startsWith("workflows/") || !normalized.toLowerCase().endsWith(".md") || normalized.includes("../") || path17.posix.normalize(normalized) !== normalized) {
13453
- throw new Error("workflow source path must be a safe workflows/*.md path");
13530
+ function lensPathCandidates(lens) {
13531
+ switch (lens.id) {
13532
+ case "release":
13533
+ return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
13534
+ case "onboarding":
13535
+ return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
13536
+ case "docs":
13537
+ return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
13538
+ case "test":
13539
+ case "bugfix":
13540
+ return ["tests", "test"];
13541
+ default:
13542
+ return [];
13454
13543
  }
13455
- return normalized;
13456
13544
  }
13457
- function sourceHashFor(workflow) {
13458
- return hash3(JSON.stringify({
13459
- id: workflow.id,
13460
- title: workflow.title,
13461
- description: workflow.description,
13462
- status: workflow.status,
13463
- version: workflow.version,
13464
- taskLenses: workflow.taskLenses,
13465
- triggers: workflow.triggers,
13466
- assumptions: workflow.assumptions,
13467
- requiredContext: workflow.requiredContext,
13468
- guardrails: workflow.guardrails,
13469
- allowedTools: workflow.allowedTools,
13470
- phases: workflow.phases,
13471
- verificationGates: workflow.verificationGates,
13472
- claimIds: workflow.claimIds,
13545
+ function scoreLensSource(source, lens, task) {
13546
+ const tokens = tokenize2(task ?? "");
13547
+ const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
13548
+ return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
13549
+ }
13550
+ function rankLensSources(sources, lens, task) {
13551
+ return [...sources].map((source, index) => ({
13552
+ source,
13553
+ index,
13554
+ score: scoreLensSource(source, lens, task)
13555
+ })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
13556
+ }
13557
+ function lensVerificationHints(lens) {
13558
+ switch (lens.id) {
13559
+ case "bugfix":
13560
+ return [
13561
+ "run the smallest failing test or repro first",
13562
+ "inspect the changed code path before trusting old memory"
13563
+ ];
13564
+ case "test":
13565
+ return [
13566
+ "run the exact focused test file or test name first",
13567
+ "inspect fixtures and harness setup before changing assertions"
13568
+ ];
13569
+ case "release":
13570
+ return [
13571
+ "run build, tests, package smoke, and publish dry-run where available",
13572
+ "verify package metadata, changelog, and Git state before publishing"
13573
+ ];
13574
+ case "onboarding":
13575
+ return [
13576
+ "read the docs/start files first, then inspect only the code paths needed for the task",
13577
+ "treat old implementation memories as leads until current code confirms them"
13578
+ ];
13579
+ case "refactor":
13580
+ return [
13581
+ "inspect call sites and tests before editing shared code",
13582
+ "run the narrow affected test plus one regression smoke"
13583
+ ];
13584
+ case "docs":
13585
+ return [
13586
+ "check headings, links, commands, and examples against current code",
13587
+ "run the smallest docs or package smoke available"
13588
+ ];
13589
+ case "feature":
13590
+ return [
13591
+ "inspect the closest existing implementation pattern before adding new code",
13592
+ "run focused tests plus one user-flow smoke after changes"
13593
+ ];
13594
+ default:
13595
+ return [
13596
+ "inspect the Start here files before editing",
13597
+ "run the smallest relevant test or smoke command after changes"
13598
+ ];
13599
+ }
13600
+ }
13601
+ function shouldShowLensSource(source, lens, task) {
13602
+ if (lens.id === "general") return true;
13603
+ const score = scoreLensSource(source, lens, task);
13604
+ if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13605
+ return score > 0;
13606
+ }
13607
+ var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
13608
+ var init_task_lens = __esm({
13609
+ "src/codegraph/task-lens.ts"() {
13610
+ "use strict";
13611
+ init_esm_shims();
13612
+ STOP_WORDS = /* @__PURE__ */ new Set([
13613
+ "a",
13614
+ "an",
13615
+ "and",
13616
+ "as",
13617
+ "for",
13618
+ "in",
13619
+ "into",
13620
+ "of",
13621
+ "on",
13622
+ "onto",
13623
+ "the",
13624
+ "this",
13625
+ "that",
13626
+ "to",
13627
+ "with",
13628
+ "work",
13629
+ "project",
13630
+ "continue",
13631
+ "\u7EE7\u7EED",
13632
+ "\u9879\u76EE"
13633
+ ]);
13634
+ LENSES = {
13635
+ bugfix: {
13636
+ id: "bugfix",
13637
+ description: "debug the failure with current code and the smallest repro first",
13638
+ sourceLimit: 6,
13639
+ cautionLimit: 4,
13640
+ hideUnrelatedCautionDetails: true,
13641
+ hideUnrelatedReliableDetails: false
13642
+ },
13643
+ feature: {
13644
+ id: "feature",
13645
+ description: "build a scoped feature from nearby source, types, and user flow",
13646
+ sourceLimit: 6,
13647
+ cautionLimit: 3,
13648
+ hideUnrelatedCautionDetails: true,
13649
+ hideUnrelatedReliableDetails: false
13650
+ },
13651
+ release: {
13652
+ id: "release",
13653
+ description: "prepare a release using current metadata, changelog, build, and package checks",
13654
+ sourceLimit: 4,
13655
+ cautionLimit: 2,
13656
+ hideUnrelatedCautionDetails: true,
13657
+ hideUnrelatedReliableDetails: true
13658
+ },
13659
+ onboarding: {
13660
+ id: "onboarding",
13661
+ description: "understand the project shape before trusting old implementation details",
13662
+ sourceLimit: 4,
13663
+ cautionLimit: 2,
13664
+ hideUnrelatedCautionDetails: true,
13665
+ hideUnrelatedReliableDetails: true
13666
+ },
13667
+ refactor: {
13668
+ id: "refactor",
13669
+ description: "change structure carefully by reading shared code, call sites, and tests",
13670
+ sourceLimit: 6,
13671
+ cautionLimit: 4,
13672
+ hideUnrelatedCautionDetails: true,
13673
+ hideUnrelatedReliableDetails: false
13674
+ },
13675
+ docs: {
13676
+ id: "docs",
13677
+ description: "update documentation against current code and public entry points",
13678
+ sourceLimit: 5,
13679
+ cautionLimit: 2,
13680
+ hideUnrelatedCautionDetails: true,
13681
+ hideUnrelatedReliableDetails: true
13682
+ },
13683
+ test: {
13684
+ id: "test",
13685
+ description: "work from tests, fixtures, harnesses, and the related source files",
13686
+ sourceLimit: 6,
13687
+ cautionLimit: 3,
13688
+ hideUnrelatedCautionDetails: true,
13689
+ hideUnrelatedReliableDetails: false
13690
+ },
13691
+ general: {
13692
+ id: "general",
13693
+ description: "balanced project handoff with current facts, code memory, and verification hints",
13694
+ sourceLimit: 8,
13695
+ cautionLimit: 5,
13696
+ hideUnrelatedCautionDetails: false,
13697
+ hideUnrelatedReliableDetails: false
13698
+ }
13699
+ };
13700
+ KEYWORDS = {
13701
+ bugfix: [
13702
+ "bug",
13703
+ "crash",
13704
+ "incident",
13705
+ "debug",
13706
+ "error",
13707
+ "fail",
13708
+ "failing",
13709
+ "fix",
13710
+ "issue",
13711
+ "regression",
13712
+ "repro",
13713
+ "\u62A5\u9519",
13714
+ "\u5D29\u6E83",
13715
+ "\u6545\u969C",
13716
+ "\u5931\u8D25",
13717
+ "\u4FEE\u590D",
13718
+ "\u95EE\u9898"
13719
+ ],
13720
+ feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
13721
+ release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
13722
+ onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
13723
+ refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
13724
+ docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
13725
+ test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
13726
+ };
13727
+ LENS_PRIORITY = [
13728
+ "bugfix",
13729
+ "release",
13730
+ "test",
13731
+ "refactor",
13732
+ "feature",
13733
+ "docs",
13734
+ "onboarding"
13735
+ ];
13736
+ }
13737
+ });
13738
+
13739
+ // src/knowledge/workflows.ts
13740
+ var workflows_exports = {};
13741
+ __export(workflows_exports, {
13742
+ WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
13743
+ applyWorkflowAdapter: () => applyWorkflowAdapter,
13744
+ importWindsurfWorkflows: () => importWindsurfWorkflows,
13745
+ parseWorkflowMarkdown: () => parseWorkflowMarkdown,
13746
+ previewWorkflowAdapter: () => previewWorkflowAdapter,
13747
+ recordWorkflowRun: () => recordWorkflowRun,
13748
+ renderWorkflowMarkdown: () => renderWorkflowMarkdown,
13749
+ selectWorkflows: () => selectWorkflows,
13750
+ selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
13751
+ syncCanonicalWorkflows: () => syncCanonicalWorkflows,
13752
+ writeCanonicalWorkflow: () => writeCanonicalWorkflow
13753
+ });
13754
+ import { createHash as createHash12 } from "crypto";
13755
+ import { promises as fs14 } from "fs";
13756
+ import path17 from "path";
13757
+ import matter11 from "gray-matter";
13758
+ function hash3(value) {
13759
+ return createHash12("sha256").update(value).digest("hex");
13760
+ }
13761
+ function now2() {
13762
+ return (/* @__PURE__ */ new Date()).toISOString();
13763
+ }
13764
+ function slug(value) {
13765
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
13766
+ return normalized || "workflow";
13767
+ }
13768
+ function titleFromName(name) {
13769
+ return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
13770
+ }
13771
+ function requiredText(data, key) {
13772
+ const value = data[key];
13773
+ if (typeof value !== "string" || !value.trim()) {
13774
+ throw new Error("workflow frontmatter requires " + key);
13775
+ }
13776
+ return value.trim();
13777
+ }
13778
+ function optionalText4(data, key) {
13779
+ const value = data[key];
13780
+ if (value === void 0 || value === null || value === "") return void 0;
13781
+ if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
13782
+ return value.trim() || void 0;
13783
+ }
13784
+ function optionalStringArray(data, key) {
13785
+ const value = data[key];
13786
+ if (value === void 0 || value === null) return [];
13787
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
13788
+ throw new Error("workflow frontmatter field " + key + " must be a string array");
13789
+ }
13790
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
13791
+ }
13792
+ function optionalNumber(data, key, fallback) {
13793
+ const value = data[key];
13794
+ if (value === void 0 || value === null || value === "") return fallback;
13795
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
13796
+ throw new Error("workflow frontmatter field " + key + " must be a positive integer");
13797
+ }
13798
+ return value;
13799
+ }
13800
+ function optionalStatus(data) {
13801
+ const value = data.status;
13802
+ if (value === void 0 || value === null || value === "") return "draft";
13803
+ if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
13804
+ throw new Error("workflow frontmatter field status is invalid");
13805
+ }
13806
+ return value;
13807
+ }
13808
+ function normalizeAgents(values) {
13809
+ const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
13810
+ if (unknown.length > 0) {
13811
+ throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
13812
+ }
13813
+ return values;
13814
+ }
13815
+ function phaseFromValue(value, index) {
13816
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13817
+ throw new Error("workflow frontmatter phases must contain objects");
13818
+ }
13819
+ const data = value;
13820
+ const title = requiredText(data, "title");
13821
+ const phaseId = optionalText4(data, "id") ?? slug(title) + "-" + (index + 1);
13822
+ return {
13823
+ id: phaseId,
13824
+ title,
13825
+ instructions: optionalText4(data, "instructions") ?? "",
13826
+ branches: optionalStringArray(data, "branches"),
13827
+ expectedOutputs: optionalStringArray(data, "expectedOutputs"),
13828
+ verificationGates: optionalStringArray(data, "verificationGates")
13829
+ };
13830
+ }
13831
+ function phasesFromBody(body) {
13832
+ const headers = [...body.matchAll(/^##\s+(.+?)\s*$/gm)];
13833
+ if (headers.length === 0) {
13834
+ const instructions = body.trim();
13835
+ return instructions ? [{
13836
+ id: "execute",
13837
+ title: "Execute",
13838
+ instructions,
13839
+ branches: [],
13840
+ expectedOutputs: [],
13841
+ verificationGates: []
13842
+ }] : [];
13843
+ }
13844
+ return headers.map((match, index) => {
13845
+ const title = match[1].trim();
13846
+ const start = (match.index ?? 0) + match[0].length;
13847
+ const end = index + 1 < headers.length ? headers[index + 1].index ?? body.length : body.length;
13848
+ return {
13849
+ id: slug(title) + "-" + (index + 1),
13850
+ title,
13851
+ instructions: body.slice(start, end).trim(),
13852
+ branches: [],
13853
+ expectedOutputs: [],
13854
+ verificationGates: []
13855
+ };
13856
+ });
13857
+ }
13858
+ function normalizeSourcePath(sourcePath) {
13859
+ const normalized = sourcePath.replace(/\\/g, "/");
13860
+ if (!normalized.startsWith("workflows/") || !normalized.toLowerCase().endsWith(".md") || normalized.includes("../") || path17.posix.normalize(normalized) !== normalized) {
13861
+ throw new Error("workflow source path must be a safe workflows/*.md path");
13862
+ }
13863
+ return normalized;
13864
+ }
13865
+ function sourceHashFor(workflow) {
13866
+ return hash3(JSON.stringify({
13867
+ id: workflow.id,
13868
+ title: workflow.title,
13869
+ description: workflow.description,
13870
+ status: workflow.status,
13871
+ version: workflow.version,
13872
+ taskLenses: workflow.taskLenses,
13873
+ triggers: workflow.triggers,
13874
+ assumptions: workflow.assumptions,
13875
+ requiredContext: workflow.requiredContext,
13876
+ guardrails: workflow.guardrails,
13877
+ allowedTools: workflow.allowedTools,
13878
+ phases: workflow.phases,
13879
+ verificationGates: workflow.verificationGates,
13880
+ claimIds: workflow.claimIds,
13473
13881
  evidenceRefs: workflow.evidenceRefs,
13474
13882
  codeRefs: workflow.codeRefs,
13475
13883
  compatibleAgents: workflow.compatibleAgents,
@@ -13650,20 +14058,27 @@ function taskScore(workflow, task) {
13650
14058
  const text = task.toLowerCase();
13651
14059
  let score = 0;
13652
14060
  const reasons = [];
14061
+ const matchedLenses = /* @__PURE__ */ new Set();
14062
+ const matchedTriggers = /* @__PURE__ */ new Set();
13653
14063
  for (const lens of workflow.taskLenses) {
13654
14064
  const terms = LENS_TERMS[lens.toLowerCase()] ?? [lens.toLowerCase()];
13655
- if (terms.some((term) => text.includes(term))) {
14065
+ if (terms.some((term) => containsTaskKeyword(task, term))) {
13656
14066
  score += 20;
13657
14067
  reasons.push("matches " + lens + " workflow");
14068
+ matchedLenses.add(lens.toLowerCase());
13658
14069
  }
13659
14070
  }
13660
14071
  for (const trigger of workflow.triggers) {
13661
14072
  const term = trigger.toLowerCase().trim();
13662
- if (term.length >= 2 && text.includes(term)) {
14073
+ if (term.length >= 2 && containsTaskKeyword(task, term)) {
13663
14074
  score += 8;
13664
14075
  reasons.push('matches trigger "' + trigger + '"');
14076
+ matchedTriggers.add(term);
13665
14077
  }
13666
14078
  }
14079
+ const hasSpecificLens = workflow.taskLenses.some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens.toLowerCase()));
14080
+ const hasSpecificMatch = [...matchedLenses].some((lens) => !GENERIC_WORKFLOW_LENSES.has(lens)) || [...matchedTriggers].some((trigger) => !GENERIC_WORKFLOW_TERMS.has(trigger));
14081
+ if (hasSpecificLens && !hasSpecificMatch) return { score: 0, reasons: [] };
13667
14082
  return { score, reasons: [...new Set(reasons)] };
13668
14083
  }
13669
14084
  function selectWorkflows(input) {
@@ -13681,9 +14096,28 @@ function selectWorkflows(input) {
13681
14096
  return selected.slice(0, Math.max(1, Math.min(input.limit ?? 2, 2)));
13682
14097
  }
13683
14098
  function importedWorkflowSpec(input) {
14099
+ let sourceMatter;
14100
+ try {
14101
+ sourceMatter = matter11(input.raw);
14102
+ } catch (error) {
14103
+ throw new Error("Malformed Windsurf workflow Markdown: " + (error instanceof Error ? error.message : String(error)));
14104
+ }
14105
+ if (sourceMatter.data.id !== void 0) {
14106
+ const id = requiredText(sourceMatter.data, "id");
14107
+ const sourcePath2 = "workflows/" + slug(id) + ".md";
14108
+ const canonical = parsedWorkflow(sourceMatter.data, sourceMatter.content, {
14109
+ workspaceId: input.workspace.id,
14110
+ sourcePath: sourcePath2,
14111
+ contentHash: hash3(input.raw)
14112
+ });
14113
+ return materializeWorkflow({
14114
+ ...canonical,
14115
+ importedFrom: input.sourcePath.replace(/\\/g, "/")
14116
+ });
14117
+ }
13684
14118
  const entry = new WorkflowSyncer().parseWindsurfWorkflow(input.sourceName, input.raw);
13685
14119
  const content = entry.content.trim() || "## Execute\n\nFollow the imported workflow.";
13686
- const lenses = inferTaskLenses(entry.name + "\n" + entry.description + "\n" + content);
14120
+ const lenses = inferTaskLenses(entry.name + "\n" + entry.description);
13687
14121
  const createdAt = now2();
13688
14122
  const sourcePath = "workflows/" + slug(entry.name) + ".md";
13689
14123
  const spec = {
@@ -13904,7 +14338,7 @@ async function selectWorkspaceWorkflows(input) {
13904
14338
  errors: synced.errors
13905
14339
  };
13906
14340
  }
13907
- var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS;
14341
+ var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS, GENERIC_WORKFLOW_LENSES, GENERIC_WORKFLOW_TERMS;
13908
14342
  var init_workflows = __esm({
13909
14343
  "src/knowledge/workflows.ts"() {
13910
14344
  "use strict";
@@ -13913,6 +14347,7 @@ var init_workflows = __esm({
13913
14347
  init_workflow_sync();
13914
14348
  init_workspace();
13915
14349
  init_workflow_store();
14350
+ init_task_lens();
13916
14351
  KNOWN_AGENTS = [
13917
14352
  "windsurf",
13918
14353
  "cursor",
@@ -13944,6 +14379,8 @@ var init_workflows = __esm({
13944
14379
  refactor: ["refactor", "cleanup", "restructure", "\u91CD\u6784", "\u6574\u7406"],
13945
14380
  test: ["test", "verify", "smoke", "\u6D4B\u8BD5", "\u9A8C\u8BC1"]
13946
14381
  };
14382
+ GENERIC_WORKFLOW_LENSES = /* @__PURE__ */ new Set(["review", "test"]);
14383
+ GENERIC_WORKFLOW_TERMS = /* @__PURE__ */ new Set(["review", "audit", "test", "verify", "smoke"]);
13947
14384
  }
13948
14385
  });
13949
14386
 
@@ -15128,7 +15565,8 @@ var init_workset = __esm({
15128
15565
  // src/codegraph/current-facts.ts
15129
15566
  var current_facts_exports = {};
15130
15567
  __export(current_facts_exports, {
15131
- collectCurrentProjectFacts: () => collectCurrentProjectFacts
15568
+ collectCurrentProjectFacts: () => collectCurrentProjectFacts,
15569
+ formatGitFact: () => formatGitFact
15132
15570
  });
15133
15571
  import { execFileSync } from "child_process";
15134
15572
  import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
@@ -15173,11 +15611,16 @@ function runGit2(rootPath, args) {
15173
15611
  }
15174
15612
  }
15175
15613
  function readGitFacts(rootPath) {
15176
- const branch = runGit2(rootPath, ["branch", "--show-current"]);
15614
+ const available = runGit2(rootPath, ["rev-parse", "--is-inside-work-tree"]) === "true";
15615
+ if (!available) {
15616
+ return { available: false, dirty: false, detached: false };
15617
+ }
15618
+ const branch = runGit2(rootPath, ["symbolic-ref", "--quiet", "--short", "HEAD"]) ?? runGit2(rootPath, ["branch", "--show-current"]);
15177
15619
  const commit = runGit2(rootPath, ["rev-parse", "--short", "HEAD"]);
15178
15620
  const latestCommit = runGit2(rootPath, ["log", "-1", "--pretty=%s"]);
15179
15621
  const dirty = Boolean(runGit2(rootPath, ["status", "--porcelain"]));
15180
15622
  return {
15623
+ available: true,
15181
15624
  ...branch ? { branch } : {},
15182
15625
  ...commit ? { commit } : {},
15183
15626
  ...latestCommit ? { latestCommit } : {},
@@ -15185,6 +15628,15 @@ function readGitFacts(rootPath) {
15185
15628
  detached: !branch
15186
15629
  };
15187
15630
  }
15631
+ function formatGitFact(git) {
15632
+ if (!git.available) return "Git: unavailable";
15633
+ const parts = [];
15634
+ if (git.detached) parts.push("detached HEAD");
15635
+ else if (git.branch) parts.push("branch " + git.branch);
15636
+ if (git.commit) parts.push("commit " + git.commit);
15637
+ parts.push(git.dirty ? "dirty worktree" : "clean worktree");
15638
+ return "Git: " + parts.join(", ");
15639
+ }
15188
15640
  function parseProgressNote(content) {
15189
15641
  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);
15190
15642
  const branchHint = content.match(/Branch\*\*:\s*([^\r\n]+)/i) ?? content.match(/Branch:\s*([^\r\n]+)/i);
@@ -15686,485 +16138,172 @@ var init_external_provider = __esm({
15686
16138
  ...typeof exitCode === "number" ? { exitCode } : {},
15687
16139
  ...timedOut ? { timedOut } : {},
15688
16140
  ...outputLimited ? { outputLimited } : {},
15689
- ...spawnError ? { error: spawnError } : {}
15690
- });
15691
- });
15692
- });
15693
- }
15694
- };
15695
- }
15696
- });
15697
-
15698
- // src/codegraph/freshness.ts
15699
- function evaluateCodeRefFreshness(ref, file, symbol) {
15700
- if (!file) {
15701
- return { status: "stale", reason: "referenced file is no longer indexed" };
15702
- }
15703
- if (ref.symbolId && !symbol) {
15704
- return { status: "stale", reason: "referenced symbol is no longer indexed" };
15705
- }
15706
- if (ref.capturedSymbolHash && symbol?.contentHash) {
15707
- if (symbol.contentHash !== ref.capturedSymbolHash) {
15708
- return { status: "stale", reason: "referenced symbol content changed" };
15709
- }
15710
- return { status: "current", reason: "referenced symbol content still matches" };
15711
- }
15712
- if (ref.capturedFileHash && file.contentHash !== ref.capturedFileHash) {
15713
- return { status: "suspect", reason: "referenced file changed since capture" };
15714
- }
15715
- return { status: "current", reason: "referenced file still matches" };
15716
- }
15717
- var init_freshness2 = __esm({
15718
- "src/codegraph/freshness.ts"() {
15719
- "use strict";
15720
- init_esm_shims();
15721
- }
15722
- });
15723
-
15724
- // src/codegraph/project-context.ts
15725
- function uniq(items) {
15726
- return [...new Set(items)];
15727
- }
15728
- function activeObservations(observations2, projectId) {
15729
- return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
15730
- }
15731
- function countLanguages(files) {
15732
- const counts = /* @__PURE__ */ new Map();
15733
- for (const file of files) {
15734
- const language = file.language ?? "unknown";
15735
- counts.set(language, (counts.get(language) ?? 0) + 1);
15736
- }
15737
- return [...counts.entries()].map(([language, files2]) => ({ language, files: files2 })).sort((a, b) => a.language.localeCompare(b.language));
15738
- }
15739
- function suggestedReadRank(path25) {
15740
- const normalized = path25.replace(/\\/g, "/");
15741
- if (normalized.startsWith("src/")) return 0;
15742
- if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 0;
15743
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
15744
- return 3;
15745
- }
15746
- function compactSuggestedReads(paths, limit = 8, exclude) {
15747
- return uniq(paths).filter((path25) => !isCodeGraphExcludedPath(path25, exclude)).sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b)).slice(0, limit);
15748
- }
15749
- function collectGraph(store, projectId, observations2, exclude) {
15750
- const files = store.listFiles(projectId);
15751
- const symbols = store.listReferencedSymbols(projectId);
15752
- const filesById = new Map(files.map((file) => [file.id, file]));
15753
- const symbolsById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
15754
- const observationsById = new Map(observations2.map((obs) => [obs.id, obs]));
15755
- const activeObservationIds = new Set(observations2.map((obs) => obs.id));
15756
- const refs = store.listProjectObservationRefs(projectId).filter((ref) => activeObservationIds.has(ref.observationId));
15757
- const freshness = {
15758
- current: 0,
15759
- suspect: 0,
15760
- stale: 0,
15761
- unbound: 0
15762
- };
15763
- const sources = [];
15764
- const suggestedReads = [];
15765
- for (const ref of refs) {
15766
- const file = ref.fileId ? filesById.get(ref.fileId) : void 0;
15767
- const symbol = ref.symbolId ? symbolsById.get(ref.symbolId) : void 0;
15768
- const result = evaluateCodeRefFreshness(ref, file, symbol);
15769
- freshness[result.status] += 1;
15770
- const observation = observationsById.get(ref.observationId);
15771
- if (!observation) continue;
15772
- const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
15773
- if (result.status === "current" && file && !excluded) suggestedReads.push(file.path);
15774
- if (excluded) continue;
15775
- sources.push({
15776
- observationId: observation.id,
15777
- title: observation.title,
15778
- type: observation.type,
15779
- ...file ? { path: file.path } : {},
15780
- ...symbol ? { symbol: symbol.name } : {},
15781
- status: result.status,
15782
- reason: result.reason
15783
- });
15784
- }
15785
- if (files.length === 0) {
15786
- for (const observation of observations2) {
15787
- for (const path25 of observation.filesModified ?? []) {
15788
- if (!path25 || isCodeGraphExcludedPath(path25, exclude)) continue;
15789
- suggestedReads.push(path25);
15790
- sources.push({
15791
- observationId: observation.id,
15792
- title: observation.title,
15793
- type: observation.type,
15794
- path: path25,
15795
- status: "unbound",
15796
- reason: "Recorded file hint; Code Memory refresh is pending."
16141
+ ...spawnError ? { error: spawnError } : {}
16142
+ });
16143
+ });
15797
16144
  });
15798
- freshness.unbound++;
15799
16145
  }
16146
+ };
16147
+ }
16148
+ });
16149
+
16150
+ // src/codegraph/freshness.ts
16151
+ function evaluateCodeRefFreshness(ref, file, symbol) {
16152
+ if (!file) {
16153
+ return { status: "stale", reason: "referenced file is no longer indexed" };
16154
+ }
16155
+ if (ref.symbolId && !symbol) {
16156
+ return { status: "stale", reason: "referenced symbol is no longer indexed" };
16157
+ }
16158
+ if (ref.capturedSymbolHash && symbol?.contentHash) {
16159
+ if (symbol.contentHash !== ref.capturedSymbolHash) {
16160
+ return { status: "stale", reason: "referenced symbol content changed" };
15800
16161
  }
16162
+ return { status: "current", reason: "referenced symbol content still matches" };
15801
16163
  }
15802
- return {
15803
- files,
15804
- symbols,
15805
- refs,
15806
- freshness,
15807
- sources,
15808
- suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
15809
- };
15810
- }
15811
- function overviewFromGraph(input) {
15812
- const status = input.store.status(input.project.id);
15813
- return {
15814
- project: input.project,
15815
- code: {
15816
- provider: status.provider,
15817
- files: status.files,
15818
- symbols: status.symbols,
15819
- edges: status.edges,
15820
- refs: status.refs,
15821
- ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
15822
- languages: countLanguages(input.graph.files),
15823
- ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
15824
- },
15825
- memory: {
15826
- total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
15827
- active: input.active.length
15828
- },
15829
- freshness: input.graph.freshness,
15830
- suggestedReads: input.graph.suggestedReads
15831
- };
15832
- }
15833
- function buildProjectContextExplain(input) {
15834
- const active = activeObservations(input.observations, input.project.id);
15835
- const graph = collectGraph(input.store, input.project.id, active, input.exclude);
15836
- const overview = overviewFromGraph({
15837
- project: input.project,
15838
- store: input.store,
15839
- observations: input.observations,
15840
- active,
15841
- graph
15842
- });
15843
- return {
15844
- project: input.project,
15845
- sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
15846
- overview
15847
- };
16164
+ if (ref.capturedFileHash && file.contentHash !== ref.capturedFileHash) {
16165
+ return { status: "suspect", reason: "referenced file changed since capture" };
16166
+ }
16167
+ return { status: "current", reason: "referenced file still matches" };
15848
16168
  }
15849
- var init_project_context = __esm({
15850
- "src/codegraph/project-context.ts"() {
16169
+ var init_freshness2 = __esm({
16170
+ "src/codegraph/freshness.ts"() {
15851
16171
  "use strict";
15852
16172
  init_esm_shims();
15853
- init_freshness2();
15854
- init_exclude();
15855
16173
  }
15856
16174
  });
15857
16175
 
15858
- // src/codegraph/task-lens.ts
15859
- var task_lens_exports = {};
15860
- __export(task_lens_exports, {
15861
- lensPathCandidates: () => lensPathCandidates,
15862
- lensVerificationHints: () => lensVerificationHints,
15863
- rankLensPaths: () => rankLensPaths,
15864
- rankLensSources: () => rankLensSources,
15865
- resolveTaskLens: () => resolveTaskLens,
15866
- scoreLensSource: () => scoreLensSource,
15867
- shouldShowLensSource: () => shouldShowLensSource
15868
- });
15869
- function normalizePath2(path25) {
15870
- return path25.replace(/\\/g, "/");
15871
- }
15872
- function tokenize2(text) {
15873
- return (text.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !STOP_WORDS.has(token));
15874
- }
15875
- function containsKeyword(text, keyword) {
15876
- if (/^[a-z0-9_-]+$/i.test(keyword)) {
15877
- return new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9_-]|$)`, "i").test(text);
15878
- }
15879
- return text.includes(keyword);
16176
+ // src/codegraph/project-context.ts
16177
+ function uniq(items) {
16178
+ return [...new Set(items)];
15880
16179
  }
15881
- function resolveTaskLens(task) {
15882
- const normalized = (task ?? "").toLowerCase();
15883
- if (!normalized.trim()) return LENSES.general;
15884
- let best = {
15885
- id: "general",
15886
- score: 0,
15887
- priority: Number.MAX_SAFE_INTEGER
15888
- };
15889
- for (const id of LENS_PRIORITY) {
15890
- const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsKeyword(normalized, keyword) ? 1 : 0), 0);
15891
- const priority = LENS_PRIORITY.indexOf(id);
15892
- if (score > best.score || score === best.score && score > 0 && priority < best.priority) {
15893
- best = { id, score, priority };
15894
- }
15895
- }
15896
- return best.score > 0 ? LENSES[best.id] : LENSES.general;
16180
+ function activeObservations(observations2, projectId) {
16181
+ return observations2.filter((obs) => obs.projectId === projectId && (obs.status ?? "active") === "active");
15897
16182
  }
15898
- function pathKindScore(path25, lens) {
15899
- const normalized = normalizePath2(path25).toLowerCase();
15900
- const name = normalized.split("/").pop() ?? normalized;
15901
- const inDocs = normalized.startsWith("docs/") || name === "readme.md" || name.includes("readme");
15902
- const isTest = /(^|\/)(tests?|__tests__|spec|fixtures?)\//.test(normalized) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) || /\.(test|spec)\.py$/.test(normalized);
15903
- const isSource = normalized.startsWith("src/") || normalized.includes("/src/");
15904
- const isPackage = name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock";
15905
- const isChangelog = name === "changelog.md" || name === "changes.md";
15906
- const isWorkflow = normalized.startsWith(".github/workflows/");
15907
- switch (lens.id) {
15908
- case "bugfix":
15909
- return (isTest ? 80 : 0) + (isSource ? 50 : 0) + (normalized.includes("debug") ? 20 : 0);
15910
- case "test":
15911
- return (isTest ? 90 : 0) + (isSource ? 45 : 0);
15912
- case "release":
15913
- return (isChangelog ? 100 : 0) + (isPackage ? 90 : 0) + (isWorkflow ? 70 : 0) + (inDocs ? 35 : 0);
15914
- case "onboarding":
15915
- return (name === "readme.md" ? 100 : 0) + (inDocs ? 80 : 0) + (isPackage ? 45 : 0) + (isSource ? 20 : 0);
15916
- case "docs":
15917
- return (inDocs ? 100 : 0) + (isChangelog ? 60 : 0) + (isSource ? 25 : 0);
15918
- case "refactor":
15919
- return (isSource ? 70 : 0) + (isTest ? 65 : 0);
15920
- case "feature":
15921
- return (isSource ? 80 : 0) + (isTest ? 45 : 0) + (inDocs ? 20 : 0);
15922
- default:
15923
- if (isSource || isTest) return 60;
15924
- if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 45;
15925
- if (inDocs) return 20;
15926
- return 0;
16183
+ function countLanguages(files) {
16184
+ const counts = /* @__PURE__ */ new Map();
16185
+ for (const file of files) {
16186
+ const language = file.language ?? "unknown";
16187
+ counts.set(language, (counts.get(language) ?? 0) + 1);
15927
16188
  }
16189
+ return [...counts.entries()].map(([language, files2]) => ({ language, files: files2 })).sort((a, b) => a.language.localeCompare(b.language));
15928
16190
  }
15929
- function tokenScore(text, tokens) {
15930
- if (tokens.length === 0) return 0;
15931
- const normalized = text.toLowerCase();
15932
- return tokens.reduce((sum, token) => sum + (normalized.includes(token) ? 12 : 0), 0);
15933
- }
15934
- function sourceTaskMatchScore(source, task) {
15935
- const tokens = tokenize2(task ?? "");
15936
- if (tokens.length === 0) return 0;
15937
- const text = [
15938
- source.title,
15939
- source.path ?? "",
15940
- source.symbol ?? ""
15941
- ].join("\n").toLowerCase();
15942
- const matches = tokens.filter((token) => text.includes(token)).length;
15943
- if (matches >= 2) return 40;
15944
- if (matches === 1) return 24;
15945
- return 0;
15946
- }
15947
- function fallbackPathRank(path25) {
15948
- const normalized = normalizePath2(path25);
16191
+ function suggestedReadRank(path25) {
16192
+ const normalized = path25.replace(/\\/g, "/");
15949
16193
  if (normalized.startsWith("src/")) return 0;
15950
- if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 1;
16194
+ if (normalized.startsWith("tests/") || normalized.startsWith("test/")) return 0;
15951
16195
  if (normalized.startsWith("packages/") && normalized.includes("/src/")) return 2;
15952
- if (normalized.startsWith("docs/") || normalized.toLowerCase() === "readme.md") return 3;
15953
- return 4;
15954
- }
15955
- function rankLensPaths(paths, lens, task) {
15956
- const tokens = tokenize2(task ?? "");
15957
- return [...new Set(paths.map(normalizePath2))].map((path25, index) => ({
15958
- path: path25,
15959
- index,
15960
- score: pathKindScore(path25, lens) + tokenScore(path25, tokens),
15961
- fallback: fallbackPathRank(path25)
15962
- })).sort((a, b) => b.score - a.score || a.fallback - b.fallback || a.index - b.index || a.path.localeCompare(b.path)).map((item) => item.path);
15963
- }
15964
- function lensPathCandidates(lens) {
15965
- switch (lens.id) {
15966
- case "release":
15967
- return ["CHANGELOG.md", "package.json", "package-lock.json", ".github/workflows/ci.yml", ".github/workflows/test.yml", "README.md"];
15968
- case "onboarding":
15969
- return ["README.md", "docs/README.md", "docs/API_REFERENCE.md", "package.json"];
15970
- case "docs":
15971
- return ["README.md", "README.zh-CN.md", "docs/README.md", "docs/API_REFERENCE.md", "CHANGELOG.md"];
15972
- case "test":
15973
- case "bugfix":
15974
- return ["tests", "test"];
15975
- default:
15976
- return [];
15977
- }
15978
- }
15979
- function scoreLensSource(source, lens, task) {
15980
- const tokens = tokenize2(task ?? "");
15981
- const text = [source.title, source.type, source.path ?? "", source.symbol ?? ""].join("\n");
15982
- return pathKindScore(source.path ?? "", lens) + tokenScore(text, tokens) + sourceTaskMatchScore(source, task);
16196
+ return 3;
15983
16197
  }
15984
- function rankLensSources(sources, lens, task) {
15985
- return [...sources].map((source, index) => ({
15986
- source,
15987
- index,
15988
- score: scoreLensSource(source, lens, task)
15989
- })).sort((a, b) => b.score - a.score || a.source.observationId - b.source.observationId || a.index - b.index).map((item) => item.source);
16198
+ function compactSuggestedReads(paths, limit = 8, exclude) {
16199
+ return uniq(paths).filter((path25) => !isCodeGraphExcludedPath(path25, exclude)).sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b)).slice(0, limit);
15990
16200
  }
15991
- function lensVerificationHints(lens) {
15992
- switch (lens.id) {
15993
- case "bugfix":
15994
- return [
15995
- "run the smallest failing test or repro first",
15996
- "inspect the changed code path before trusting old memory"
15997
- ];
15998
- case "test":
15999
- return [
16000
- "run the exact focused test file or test name first",
16001
- "inspect fixtures and harness setup before changing assertions"
16002
- ];
16003
- case "release":
16004
- return [
16005
- "run build, tests, package smoke, and publish dry-run where available",
16006
- "verify package metadata, changelog, and Git state before publishing"
16007
- ];
16008
- case "onboarding":
16009
- return [
16010
- "read the docs/start files first, then inspect only the code paths needed for the task",
16011
- "treat old implementation memories as leads until current code confirms them"
16012
- ];
16013
- case "refactor":
16014
- return [
16015
- "inspect call sites and tests before editing shared code",
16016
- "run the narrow affected test plus one regression smoke"
16017
- ];
16018
- case "docs":
16019
- return [
16020
- "check headings, links, commands, and examples against current code",
16021
- "run the smallest docs or package smoke available"
16022
- ];
16023
- case "feature":
16024
- return [
16025
- "inspect the closest existing implementation pattern before adding new code",
16026
- "run focused tests plus one user-flow smoke after changes"
16027
- ];
16028
- default:
16029
- return [
16030
- "inspect the Start here files before editing",
16031
- "run the smallest relevant test or smoke command after changes"
16032
- ];
16201
+ function collectGraph(store, projectId, observations2, exclude) {
16202
+ const files = store.listFiles(projectId);
16203
+ const symbols = store.listReferencedSymbols(projectId);
16204
+ const filesById = new Map(files.map((file) => [file.id, file]));
16205
+ const symbolsById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
16206
+ const observationsById = new Map(observations2.map((obs) => [obs.id, obs]));
16207
+ const activeObservationIds = new Set(observations2.map((obs) => obs.id));
16208
+ const refs = store.listProjectObservationRefs(projectId).filter((ref) => activeObservationIds.has(ref.observationId));
16209
+ const freshness = {
16210
+ current: 0,
16211
+ suspect: 0,
16212
+ stale: 0,
16213
+ unbound: 0
16214
+ };
16215
+ const sources = [];
16216
+ const suggestedReads = [];
16217
+ for (const ref of refs) {
16218
+ const file = ref.fileId ? filesById.get(ref.fileId) : void 0;
16219
+ const symbol = ref.symbolId ? symbolsById.get(ref.symbolId) : void 0;
16220
+ const result = evaluateCodeRefFreshness(ref, file, symbol);
16221
+ freshness[result.status] += 1;
16222
+ const observation = observationsById.get(ref.observationId);
16223
+ if (!observation) continue;
16224
+ const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
16225
+ if (result.status === "current" && file && !excluded) suggestedReads.push(file.path);
16226
+ if (excluded) continue;
16227
+ sources.push({
16228
+ observationId: observation.id,
16229
+ title: observation.title,
16230
+ type: observation.type,
16231
+ ...file ? { path: file.path } : {},
16232
+ ...symbol ? { symbol: symbol.name } : {},
16233
+ status: result.status,
16234
+ reason: result.reason
16235
+ });
16236
+ }
16237
+ if (files.length === 0) {
16238
+ for (const observation of observations2) {
16239
+ for (const path25 of observation.filesModified ?? []) {
16240
+ if (!path25 || isCodeGraphExcludedPath(path25, exclude)) continue;
16241
+ suggestedReads.push(path25);
16242
+ sources.push({
16243
+ observationId: observation.id,
16244
+ title: observation.title,
16245
+ type: observation.type,
16246
+ path: path25,
16247
+ status: "unbound",
16248
+ reason: "Recorded file hint; Code Memory refresh is pending."
16249
+ });
16250
+ freshness.unbound++;
16251
+ }
16252
+ }
16033
16253
  }
16254
+ return {
16255
+ files,
16256
+ symbols,
16257
+ refs,
16258
+ freshness,
16259
+ sources,
16260
+ suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude)
16261
+ };
16034
16262
  }
16035
- function shouldShowLensSource(source, lens, task) {
16036
- if (lens.id === "general") return true;
16037
- const score = scoreLensSource(source, lens, task);
16038
- if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
16039
- return score > 0;
16263
+ function overviewFromGraph(input) {
16264
+ const status = input.store.status(input.project.id);
16265
+ return {
16266
+ project: input.project,
16267
+ code: {
16268
+ provider: status.provider,
16269
+ files: status.files,
16270
+ symbols: status.symbols,
16271
+ edges: status.edges,
16272
+ refs: status.refs,
16273
+ ...status.indexedAt ? { indexedAt: status.indexedAt } : {},
16274
+ languages: countLanguages(input.graph.files),
16275
+ ...status.latestSnapshot ? { latestSnapshot: status.latestSnapshot } : {}
16276
+ },
16277
+ memory: {
16278
+ total: input.observations.filter((obs) => obs.projectId === input.project.id).length,
16279
+ active: input.active.length
16280
+ },
16281
+ freshness: input.graph.freshness,
16282
+ suggestedReads: input.graph.suggestedReads
16283
+ };
16040
16284
  }
16041
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
16042
- var init_task_lens = __esm({
16043
- "src/codegraph/task-lens.ts"() {
16285
+ function buildProjectContextExplain(input) {
16286
+ const active = activeObservations(input.observations, input.project.id);
16287
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
16288
+ const overview = overviewFromGraph({
16289
+ project: input.project,
16290
+ store: input.store,
16291
+ observations: input.observations,
16292
+ active,
16293
+ graph
16294
+ });
16295
+ return {
16296
+ project: input.project,
16297
+ sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? "").localeCompare(b.path ?? "")),
16298
+ overview
16299
+ };
16300
+ }
16301
+ var init_project_context = __esm({
16302
+ "src/codegraph/project-context.ts"() {
16044
16303
  "use strict";
16045
16304
  init_esm_shims();
16046
- STOP_WORDS = /* @__PURE__ */ new Set([
16047
- "a",
16048
- "an",
16049
- "and",
16050
- "as",
16051
- "for",
16052
- "in",
16053
- "into",
16054
- "of",
16055
- "on",
16056
- "onto",
16057
- "the",
16058
- "this",
16059
- "that",
16060
- "to",
16061
- "with",
16062
- "work",
16063
- "project",
16064
- "continue",
16065
- "\u7EE7\u7EED",
16066
- "\u9879\u76EE"
16067
- ]);
16068
- LENSES = {
16069
- bugfix: {
16070
- id: "bugfix",
16071
- description: "debug the failure with current code and the smallest repro first",
16072
- sourceLimit: 6,
16073
- cautionLimit: 4,
16074
- hideUnrelatedCautionDetails: true,
16075
- hideUnrelatedReliableDetails: false
16076
- },
16077
- feature: {
16078
- id: "feature",
16079
- description: "build a scoped feature from nearby source, types, and user flow",
16080
- sourceLimit: 6,
16081
- cautionLimit: 3,
16082
- hideUnrelatedCautionDetails: true,
16083
- hideUnrelatedReliableDetails: false
16084
- },
16085
- release: {
16086
- id: "release",
16087
- description: "prepare a release using current metadata, changelog, build, and package checks",
16088
- sourceLimit: 4,
16089
- cautionLimit: 2,
16090
- hideUnrelatedCautionDetails: true,
16091
- hideUnrelatedReliableDetails: true
16092
- },
16093
- onboarding: {
16094
- id: "onboarding",
16095
- description: "understand the project shape before trusting old implementation details",
16096
- sourceLimit: 4,
16097
- cautionLimit: 2,
16098
- hideUnrelatedCautionDetails: true,
16099
- hideUnrelatedReliableDetails: true
16100
- },
16101
- refactor: {
16102
- id: "refactor",
16103
- description: "change structure carefully by reading shared code, call sites, and tests",
16104
- sourceLimit: 6,
16105
- cautionLimit: 4,
16106
- hideUnrelatedCautionDetails: true,
16107
- hideUnrelatedReliableDetails: false
16108
- },
16109
- docs: {
16110
- id: "docs",
16111
- description: "update documentation against current code and public entry points",
16112
- sourceLimit: 5,
16113
- cautionLimit: 2,
16114
- hideUnrelatedCautionDetails: true,
16115
- hideUnrelatedReliableDetails: true
16116
- },
16117
- test: {
16118
- id: "test",
16119
- description: "work from tests, fixtures, harnesses, and the related source files",
16120
- sourceLimit: 6,
16121
- cautionLimit: 3,
16122
- hideUnrelatedCautionDetails: true,
16123
- hideUnrelatedReliableDetails: false
16124
- },
16125
- general: {
16126
- id: "general",
16127
- description: "balanced project handoff with current facts, code memory, and verification hints",
16128
- sourceLimit: 8,
16129
- cautionLimit: 5,
16130
- hideUnrelatedCautionDetails: false,
16131
- hideUnrelatedReliableDetails: false
16132
- }
16133
- };
16134
- KEYWORDS = {
16135
- bugfix: [
16136
- "bug",
16137
- "crash",
16138
- "debug",
16139
- "error",
16140
- "fail",
16141
- "failing",
16142
- "fix",
16143
- "issue",
16144
- "regression",
16145
- "repro",
16146
- "\u62A5\u9519",
16147
- "\u5D29\u6E83",
16148
- "\u5931\u8D25",
16149
- "\u4FEE\u590D",
16150
- "\u95EE\u9898"
16151
- ],
16152
- feature: ["add", "build", "feature", "implement", "new", "support", "\u65B0\u589E", "\u5B9E\u73B0", "\u652F\u6301", "\u529F\u80FD"],
16153
- release: ["bump", "changelog", "npm", "pack", "publish", "release", "version", "\u53D1\u7248", "\u53D1\u5E03", "\u7248\u672C"],
16154
- onboarding: ["architecture", "handoff", "onboard", "overview", "understand", "\u63A5\u624B", "\u4E86\u89E3", "\u7406\u89E3", "\u67B6\u6784"],
16155
- refactor: ["cleanup", "migrate", "refactor", "rename", "restructure", "\u91CD\u6784", "\u8FC1\u79FB", "\u6539\u9020"],
16156
- docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
16157
- test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
16158
- };
16159
- LENS_PRIORITY = [
16160
- "bugfix",
16161
- "release",
16162
- "test",
16163
- "refactor",
16164
- "feature",
16165
- "docs",
16166
- "onboarding"
16167
- ];
16305
+ init_freshness2();
16306
+ init_exclude();
16168
16307
  }
16169
16308
  });
16170
16309
 
@@ -16463,15 +16602,7 @@ function formatCurrentFactsLines(facts) {
16463
16602
  if (facts.latestChangelog) {
16464
16603
  lines.push(`- Latest changelog: ${facts.latestChangelog.version}${facts.latestChangelog.date ? ` (${facts.latestChangelog.date})` : ""}`);
16465
16604
  }
16466
- const gitParts = [];
16467
- if (facts.git.detached) {
16468
- gitParts.push("detached HEAD");
16469
- } else if (facts.git.branch) {
16470
- gitParts.push(`branch ${facts.git.branch}`);
16471
- }
16472
- if (facts.git.commit) gitParts.push(`commit ${facts.git.commit}`);
16473
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16474
- lines.push(`- Git: ${gitParts.join(", ")}`);
16605
+ lines.push("- " + formatGitFact(facts.git));
16475
16606
  if (facts.git.latestCommit) lines.push(`- Latest commit: ${facts.git.latestCommit}`);
16476
16607
  lines.push("- Current facts above outrank progress/dev-log files when they conflict.");
16477
16608
  if (facts.staleNotes.length > 0) {
@@ -16493,11 +16624,7 @@ function worksetFactLines(facts) {
16493
16624
  if (facts.latestChangelog) {
16494
16625
  lines.push("Latest changelog: " + facts.latestChangelog.version + (facts.latestChangelog.date ? " (" + facts.latestChangelog.date + ")" : ""));
16495
16626
  }
16496
- const gitParts = [];
16497
- if (facts.git.branch) gitParts.push("branch " + facts.git.branch);
16498
- if (facts.git.commit) gitParts.push("commit " + facts.git.commit);
16499
- gitParts.push(facts.git.dirty ? "dirty worktree" : "clean worktree");
16500
- lines.push("Git: " + gitParts.join(", "));
16627
+ lines.push(formatGitFact(facts.git));
16501
16628
  for (const note of facts.staleNotes.slice(0, 1)) {
16502
16629
  lines.push(
16503
16630
  "Historical note: " + note.path + (note.branchHint ? " (branch hint " + note.branchHint + "; " + note.reason + ")" : " (" + note.reason + ")")
@@ -18979,13 +19106,8 @@ function prepareDashboardConfig(projectRoot) {
18979
19106
  }
18980
19107
  }
18981
19108
  function computeProjectGraphCounts(allEntities, allRelations, projectObs) {
18982
- const entityNames = new Set(
18983
- projectObs.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
18984
- );
18985
- const entities = allEntities.filter((e) => entityNames.has(e.name));
18986
- const entityNameSet = new Set(entities.map((e) => e.name));
18987
- const relations = allRelations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
18988
- return { entities: entities.length, relations: relations.length, entityNames };
19109
+ const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
19110
+ return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
18989
19111
  }
18990
19112
  async function handleApi(req, res, dataDir, projectId, projectName, baseDir, projectRoot, projectResolved, mode = "standalone", port = 3210) {
18991
19113
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -19058,13 +19180,8 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19058
19180
  const gStore = getGraphStore();
19059
19181
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
19060
19182
  const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19061
- const projectEntityNames = new Set(
19062
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19063
- );
19064
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
19065
- const entityNameSet = new Set(entities.map((e) => e.name));
19066
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
19067
- sendJson(res, { entities, relations });
19183
+ const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
19184
+ sendJson(res, { entities: scoped.entities, relations: scoped.relations });
19068
19185
  break;
19069
19186
  }
19070
19187
  case "/observations": {
@@ -19256,19 +19373,13 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19256
19373
  const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19257
19374
  const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
19258
19375
  const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
19259
- const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19260
- const projectEntityNames = new Set(
19261
- graphObs.filter((o) => o.entityName).map((o) => o.entityName)
19262
- );
19263
- const scopedEntities = fullGraph.entities.filter((e) => projectEntityNames.has(e.name));
19264
- const scopedEntityNameSet = new Set(scopedEntities.map((e) => e.name));
19265
- const scopedRelations = fullGraph.relations.filter((r) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
19376
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
19266
19377
  const graph = generateKnowledgeGraph2({
19267
19378
  projectId: effectiveProjectId,
19268
19379
  observations: allObs,
19269
19380
  miniSkills: skills,
19270
- graphEntities: scopedEntities,
19271
- graphRelations: scopedRelations
19381
+ graphEntities: scoped.entities,
19382
+ graphRelations: scoped.relations
19272
19383
  });
19273
19384
  sendJson(res, graph);
19274
19385
  break;
@@ -19490,16 +19601,11 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
19490
19601
  const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
19491
19602
  const observations2 = await getObservationStore().loadByProject(effectiveProjectId, { status: "active" });
19492
19603
  const nextId2 = await getObservationStore().loadIdCounter();
19493
- const exportEntityNames = new Set(
19494
- observations2.filter((o) => (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
19495
- );
19496
- const exportEntities = fullGraph.entities.filter((e) => exportEntityNames.has(e.name));
19497
- const exportEntitySet = new Set(exportEntities.map((e) => e.name));
19498
- const exportRelations = fullGraph.relations.filter((r) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
19604
+ const scoped = scopeKnowledgeGraphToProject(fullGraph, observations2);
19499
19605
  const exportData = {
19500
19606
  project: { id: effectiveProjectId, name: effectiveProjectName },
19501
19607
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
19502
- graph: { entities: exportEntities, relations: exportRelations },
19608
+ graph: { entities: scoped.entities, relations: scoped.relations },
19503
19609
  observations: observations2,
19504
19610
  nextId: nextId2
19505
19611
  };
@@ -19816,6 +19922,7 @@ var init_server = __esm({
19816
19922
  init_dotenv_loader();
19817
19923
  init_yaml_loader();
19818
19924
  init_yaml_loader();
19925
+ init_graph_scope();
19819
19926
  MIME_TYPES = {
19820
19927
  ".html": "text/html; charset=utf-8",
19821
19928
  ".css": "text/css; charset=utf-8",
@@ -22443,6 +22550,24 @@ async function createAutoRelations(obs, extracted, graphManager) {
22443
22550
  const relationType = inferRelationType(obs);
22444
22551
  const relations = [];
22445
22552
  const selfName = obs.entityName.toLowerCase();
22553
+ const explicitRelated = [...new Set((obs.relatedEntities ?? []).map((name) => name.trim()).filter((name) => name && name.toLowerCase() !== selfName))];
22554
+ if (explicitRelated.length > 0) {
22555
+ await graphManager.createEntities(explicitRelated.map((name) => ({
22556
+ name,
22557
+ entityType: "related",
22558
+ observations: []
22559
+ })));
22560
+ for (const name of explicitRelated) {
22561
+ const matchedEntity = graphManager.findEntityByName(name);
22562
+ if (matchedEntity) {
22563
+ relations.push({
22564
+ from: obs.entityName,
22565
+ to: matchedEntity.name,
22566
+ relationType: "related_entity"
22567
+ });
22568
+ }
22569
+ }
22570
+ }
22446
22571
  const candidates = [
22447
22572
  ...extracted.identifiers,
22448
22573
  ...extracted.files.map((f) => f.split("/").pop()?.replace(/\.\w+$/, "") ?? ""),
@@ -22484,6 +22609,7 @@ async function createAutoRelations(obs, extracted, graphManager) {
22484
22609
 
22485
22610
  // src/server.ts
22486
22611
  init_entity_extractor();
22612
+ init_graph_scope();
22487
22613
 
22488
22614
  // src/compact/engine.ts
22489
22615
  init_esm_shims();
@@ -26169,7 +26295,7 @@ The path should point to a directory containing a .git folder.`
26169
26295
  };
26170
26296
  const server = existingServer ?? new McpServer({
26171
26297
  name: "memorix",
26172
- version: true ? "1.2.0" : "1.0.1"
26298
+ version: true ? "1.2.1" : "1.0.1"
26173
26299
  });
26174
26300
  const originalRegisterTool = server.registerTool.bind(server);
26175
26301
  server.registerTool = ((name, ...args) => {
@@ -26901,7 +27027,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26901
27027
  { getResolvedConfig: getResolvedConfig2 },
26902
27028
  { getExternalCodeGraphContext: getExternalCodeGraphContext2 },
26903
27029
  { getObservationStore: getObservationStore2 },
26904
- { collectCurrentProjectFacts: collectCurrentProjectFacts2 },
27030
+ { collectCurrentProjectFacts: collectCurrentProjectFacts2, formatGitFact: formatGitFact2 },
26905
27031
  { resolveTaskLens: resolveTaskLens2 }
26906
27032
  ] = await Promise.all([
26907
27033
  Promise.resolve().then(() => (init_store(), store_exports)),
@@ -26942,7 +27068,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26942
27068
  if (currentFacts.latestChangelog) {
26943
27069
  worksetFacts.push("Latest changelog: " + currentFacts.latestChangelog.version + (currentFacts.latestChangelog.date ? " (" + currentFacts.latestChangelog.date + ")" : ""));
26944
27070
  }
26945
- worksetFacts.push("Git: " + (currentFacts.git.branch ? "branch " + currentFacts.git.branch + ", " : "") + (currentFacts.git.dirty ? "dirty worktree" : "clean worktree"));
27071
+ worksetFacts.push(formatGitFact2(currentFacts.git));
26946
27072
  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";
26947
27073
  const pack = await attachTaskWorkset2({
26948
27074
  pack: basePack,
@@ -26974,11 +27100,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26974
27100
  "memorix_knowledge",
26975
27101
  {
26976
27102
  title: "Knowledge Workspace",
26977
- 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.",
27103
+ 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.",
26978
27104
  inputSchema: {
26979
27105
  action: z2.enum([
26980
27106
  "workspace_init",
26981
27107
  "status",
27108
+ "claim_list",
27109
+ "claim_review",
26982
27110
  "compile",
26983
27111
  "lint",
26984
27112
  "proposal_apply",
@@ -26993,6 +27121,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
26993
27121
  path: z2.string().optional().describe("Explicit versioned workspace path, used only by workspace_init"),
26994
27122
  proposalId: z2.string().optional().describe("Pending proposal id for proposal_apply"),
26995
27123
  allowManualOverwrite: z2.boolean().optional().default(false).describe("Explicitly allow proposal_apply to replace a manually edited page"),
27124
+ claimId: z2.string().optional().describe("Source-backed claim id for claim_review"),
27125
+ claimReviewState: z2.enum(["approved", "rejected"]).optional().describe("Deliberate review verdict for claim_review"),
27126
+ reviewDetail: z2.string().max(2e3).optional().describe("Evidence check performed before approving or rejecting a claim"),
26996
27127
  workflowId: z2.string().optional().describe("Canonical workflow id for workflow preview, apply, or run"),
26997
27128
  agent: z2.string().optional().describe("Target agent for a workflow adapter"),
26998
27129
  task: z2.string().optional().describe("Task text for workflow selection or a workflow run"),
@@ -27009,6 +27140,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27009
27140
  path: workspacePath,
27010
27141
  proposalId,
27011
27142
  allowManualOverwrite,
27143
+ claimId,
27144
+ claimReviewState,
27145
+ reviewDetail,
27012
27146
  workflowId,
27013
27147
  agent,
27014
27148
  task,
@@ -27031,6 +27165,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27031
27165
  };
27032
27166
  const [
27033
27167
  { ClaimStore: ClaimStore2 },
27168
+ { reviewClaim: reviewClaim2 },
27034
27169
  { CodeGraphStore: CodeGraphStore2 },
27035
27170
  { initializeKnowledgeWorkspace: initializeKnowledgeWorkspace2, loadKnowledgeWorkspace: loadKnowledgeWorkspace2 },
27036
27171
  { KnowledgeWorkspaceStore: KnowledgeWorkspaceStore2 },
@@ -27046,6 +27181,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27046
27181
  }
27047
27182
  ] = await Promise.all([
27048
27183
  Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
27184
+ Promise.resolve().then(() => (init_claims(), claims_exports)),
27049
27185
  Promise.resolve().then(() => (init_store(), store_exports)),
27050
27186
  Promise.resolve().then(() => (init_workspace(), workspace_exports)),
27051
27187
  Promise.resolve().then(() => (init_workspace_store(), workspace_store_exports)),
@@ -27095,10 +27231,52 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27095
27231
  targetPath: proposal.targetPath,
27096
27232
  reason: proposal.reason,
27097
27233
  createdAt: proposal.createdAt
27098
- }))
27234
+ })),
27235
+ 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 }))
27099
27236
  }
27100
27237
  });
27101
27238
  }
27239
+ if (action === "claim_list") {
27240
+ return text({
27241
+ claims: claims.listClaims(project.id, { limit: 100 }).map((claim) => ({
27242
+ id: claim.id,
27243
+ subject: claim.subject,
27244
+ predicate: claim.predicate,
27245
+ objectValue: claim.objectValue,
27246
+ status: claim.status,
27247
+ reviewState: claim.reviewState,
27248
+ origin: claim.origin,
27249
+ confidence: claim.confidence,
27250
+ evidenceCount: claims.listEvidence(claim.id).length
27251
+ })),
27252
+ next: "Approve only after checking the linked evidence. Rejected claims stay out of retrieval and publication."
27253
+ });
27254
+ }
27255
+ if (action === "claim_review") {
27256
+ const requestedClaimId = requireText(claimId, "claimId");
27257
+ const detail = requireText(reviewDetail, "reviewDetail");
27258
+ if (!requestedClaimId || !claimReviewState || !detail) {
27259
+ return text({ error: "claimId, claimReviewState, and reviewDetail are required for claim_review." }, true);
27260
+ }
27261
+ const existing = claims.getClaim(requestedClaimId);
27262
+ if (!existing || existing.projectId !== project.id) {
27263
+ return text({ error: "Claim was not found for the current project." }, true);
27264
+ }
27265
+ const claim = reviewClaim2(claims, {
27266
+ claimId: requestedClaimId,
27267
+ reviewState: claimReviewState,
27268
+ detail
27269
+ });
27270
+ return text({
27271
+ claim: {
27272
+ id: claim.id,
27273
+ status: claim.status,
27274
+ reviewState: claim.reviewState,
27275
+ updatedAt: claim.updatedAt
27276
+ },
27277
+ 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."
27278
+ });
27279
+ }
27102
27280
  if (action === "compile") {
27103
27281
  const result = await compileKnowledgeWorkspace2({ workspace, claims });
27104
27282
  return text({
@@ -27137,7 +27315,13 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27137
27315
  if (action === "workflow_import") {
27138
27316
  const result = await importWindsurfWorkflows2({ workspace, projectRoot });
27139
27317
  return text({
27140
- imported: result.imported.map((workflow2) => ({ id: workflow2.id, title: workflow2.title, sourcePath: workflow2.sourcePath })),
27318
+ imported: result.imported.map((workflow2) => ({
27319
+ id: workflow2.id,
27320
+ title: workflow2.title,
27321
+ sourcePath: workflow2.sourcePath,
27322
+ importedFrom: workflow2.importedFrom,
27323
+ verificationGates: workflow2.verificationGates
27324
+ })),
27141
27325
  skipped: result.skipped
27142
27326
  });
27143
27327
  }
@@ -27149,7 +27333,9 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27149
27333
  title: workflow2.title,
27150
27334
  status: workflow2.status,
27151
27335
  taskLenses: workflow2.taskLenses,
27152
- sourcePath: workflow2.sourcePath
27336
+ sourcePath: workflow2.sourcePath,
27337
+ importedFrom: workflow2.importedFrom,
27338
+ verificationGates: workflow2.verificationGates
27153
27339
  })),
27154
27340
  parseErrors: synced.errors
27155
27341
  });
@@ -27978,13 +28164,11 @@ Archived memories are hidden from default search but can be found with status: "
27978
28164
  async function scopeGraphToProject(graph) {
27979
28165
  const { getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
27980
28166
  const allObs = await withFreshIndex(() => getAllObservations2());
27981
- const projectEntityNames = new Set(
27982
- allObs.filter((o) => o.projectId === project.id && (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
28167
+ const scoped = scopeKnowledgeGraphToProject(
28168
+ graph,
28169
+ allObs.filter((observation) => observation.projectId === project.id)
27983
28170
  );
27984
- const entities = graph.entities.filter((e) => projectEntityNames.has(e.name));
27985
- const entityNameSet = new Set(entities.map((e) => e.name));
27986
- const relations = graph.relations.filter((r) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
27987
- return { entities, relations };
28171
+ return { entities: scoped.entities, relations: scoped.relations };
27988
28172
  }
27989
28173
  server.registerTool(
27990
28174
  "read_graph",