memorix 1.1.13 → 1.2.0

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -2
  3. package/README.zh-CN.md +3 -2
  4. package/dist/cli/index.js +36313 -31189
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +5348 -674
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3661 -293
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +19 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +5346 -672
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  17. package/docs/1.2.0-CODE-STATE.md +61 -0
  18. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  19. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  20. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  21. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  22. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  24. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  25. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  26. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  27. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  28. package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
  29. package/docs/API_REFERENCE.md +25 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/README.md +17 -2
  32. package/docs/dev-log/progress.txt +120 -40
  33. package/llms-full.txt +16 -2
  34. package/llms.txt +9 -4
  35. package/package.json +3 -2
  36. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  37. package/src/cli/capability-map.ts +1 -0
  38. package/src/cli/commands/codegraph.ts +112 -9
  39. package/src/cli/commands/context.ts +2 -0
  40. package/src/cli/commands/doctor.ts +73 -4
  41. package/src/cli/commands/knowledge.ts +282 -0
  42. package/src/cli/commands/serve-http.ts +12 -1
  43. package/src/cli/index.ts +3 -1
  44. package/src/cli/tui/App.tsx +1 -1
  45. package/src/cli/tui/Panels.tsx +8 -8
  46. package/src/cli/tui/theme.ts +1 -1
  47. package/src/cli/tui/views/GraphView.tsx +8 -7
  48. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  49. package/src/codegraph/auto-context.ts +171 -9
  50. package/src/codegraph/code-state.ts +95 -0
  51. package/src/codegraph/context-pack.ts +82 -1
  52. package/src/codegraph/external-provider.ts +581 -0
  53. package/src/codegraph/lite-provider.ts +64 -19
  54. package/src/codegraph/project-context.ts +9 -1
  55. package/src/codegraph/store.ts +154 -6
  56. package/src/codegraph/types.ts +117 -0
  57. package/src/config/resolved-config.ts +28 -0
  58. package/src/config/toml-loader.ts +3 -0
  59. package/src/config/yaml-loader.ts +6 -0
  60. package/src/dashboard/server.ts +15 -1
  61. package/src/evaluation/workset-evaluation.ts +120 -0
  62. package/src/hooks/handler.ts +48 -6
  63. package/src/knowledge/claim-store.ts +267 -0
  64. package/src/knowledge/claims.ts +537 -0
  65. package/src/knowledge/markdown.ts +129 -0
  66. package/src/knowledge/types.ts +157 -0
  67. package/src/knowledge/wiki.ts +524 -0
  68. package/src/knowledge/workflow-store.ts +168 -0
  69. package/src/knowledge/workflow-types.ts +95 -0
  70. package/src/knowledge/workflows.ts +743 -0
  71. package/src/knowledge/workset.ts +515 -0
  72. package/src/knowledge/workspace-store.ts +220 -0
  73. package/src/knowledge/workspace-types.ts +106 -0
  74. package/src/knowledge/workspace.ts +220 -0
  75. package/src/memory/observations.ts +19 -0
  76. package/src/runtime/control-plane-maintenance.ts +5 -0
  77. package/src/runtime/isolated-maintenance.ts +5 -0
  78. package/src/runtime/lifecycle-status.ts +102 -0
  79. package/src/runtime/lifecycle.ts +107 -0
  80. package/src/runtime/maintenance-jobs.ts +5 -0
  81. package/src/runtime/project-maintenance.ts +190 -0
  82. package/src/server/tool-profile.ts +3 -2
  83. package/src/server.ts +354 -14
  84. package/src/store/file-lock.ts +24 -4
  85. package/src/store/sqlite-db.ts +307 -0
  86. package/src/wiki/generator.ts +4 -2
  87. package/src/wiki/knowledge-graph.ts +7 -4
  88. package/src/wiki/types.ts +16 -4
@@ -45,9 +45,9 @@ function loadSqlite() {
45
45
  throw new Error("[memorix] Neither better-sqlite3 nor bun:sqlite is available");
46
46
  }
47
47
  }
48
- function createDatabase(path8, options) {
48
+ function createDatabase(path13, options) {
49
49
  const Sqlite = loadSqlite();
50
- const db2 = new Sqlite(path8, options);
50
+ const db2 = new Sqlite(path13, options);
51
51
  if (!db2.pragma) {
52
52
  db2.pragma = function(pragma, options2) {
53
53
  if (options2 && options2.simple) {
@@ -81,6 +81,28 @@ function loadBetterSqlite3() {
81
81
  throw new Error("[memorix] SQLite is not available (neither better-sqlite3 nor bun:sqlite)");
82
82
  }
83
83
  }
84
+ function hasColumn(db2, table, column) {
85
+ return db2.prepare("PRAGMA table_info(" + table + ")").all().some((row) => row.name === column);
86
+ }
87
+ function addColumnIfMissing(db2, table, column, definition) {
88
+ if (hasColumn(db2, table, column)) return;
89
+ db2.exec("ALTER TABLE " + table + " ADD COLUMN " + definition);
90
+ }
91
+ function applySchemaMigrations(db2) {
92
+ db2.exec(CREATE_SCHEMA_MIGRATIONS_TABLE);
93
+ for (const migration of SCHEMA_MIGRATIONS) {
94
+ const applied = db2.prepare("SELECT 1 FROM schema_migrations WHERE id = ?").get(migration.id);
95
+ if (applied) continue;
96
+ const apply = db2.transaction(() => {
97
+ migration.apply(db2);
98
+ db2.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)").run(
99
+ migration.id,
100
+ (/* @__PURE__ */ new Date()).toISOString()
101
+ );
102
+ });
103
+ apply();
104
+ }
105
+ }
84
106
  function getDatabase(dataDir) {
85
107
  const normalized = path2.resolve(dataDir);
86
108
  const existing = _dbCache.get(normalized);
@@ -143,6 +165,7 @@ function getDatabase(dataDir) {
143
165
  db2.exec(`ALTER TABLE team_messages ADD COLUMN handoff_status TEXT`);
144
166
  } catch {
145
167
  }
168
+ applySchemaMigrations(db2);
146
169
  db2.exec(CREATE_INDEXES);
147
170
  db2.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('storage_generation', '0')`).run();
148
171
  db2.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('next_id', '1')`).run();
@@ -159,7 +182,7 @@ function closeAllDatabases() {
159
182
  _dbCache.delete(key);
160
183
  }
161
184
  }
162
- var BetterSqlite3, CREATE_OBSERVATIONS_TABLE, CREATE_MINI_SKILLS_TABLE, CREATE_SESSIONS_TABLE, CREATE_META_TABLE, CREATE_TEAM_AGENTS_TABLE, CREATE_TEAM_MESSAGES_TABLE, CREATE_TEAM_TASKS_TABLE, CREATE_TEAM_TASK_DEPS_TABLE, CREATE_TEAM_LOCKS_TABLE, CREATE_TEAM_ROLES_TABLE, CREATE_CHAT_TRANSCRIPT_TABLE, CREATE_GRAPH_ENTITIES_TABLE, CREATE_GRAPH_RELATIONS_TABLE, CREATE_CODE_FILES_TABLE, CREATE_CODE_SYMBOLS_TABLE, CREATE_CODE_EDGES_TABLE, CREATE_OBSERVATION_CODE_REFS_TABLE, CREATE_MAINTENANCE_JOBS_TABLE, CREATE_MAINTENANCE_TARGETS_TABLE, CREATE_INDEXES, _dbCache;
185
+ var BetterSqlite3, CREATE_OBSERVATIONS_TABLE, CREATE_MINI_SKILLS_TABLE, CREATE_SESSIONS_TABLE, CREATE_META_TABLE, CREATE_TEAM_AGENTS_TABLE, CREATE_TEAM_MESSAGES_TABLE, CREATE_TEAM_TASKS_TABLE, CREATE_TEAM_TASK_DEPS_TABLE, CREATE_TEAM_LOCKS_TABLE, CREATE_TEAM_ROLES_TABLE, CREATE_CHAT_TRANSCRIPT_TABLE, CREATE_GRAPH_ENTITIES_TABLE, CREATE_GRAPH_RELATIONS_TABLE, CREATE_CODE_FILES_TABLE, CREATE_CODE_SYMBOLS_TABLE, CREATE_CODE_EDGES_TABLE, CREATE_OBSERVATION_CODE_REFS_TABLE, CREATE_SCHEMA_MIGRATIONS_TABLE, CREATE_CODE_STATE_SNAPSHOTS_TABLE, CREATE_KNOWLEDGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_CLAIM_EVIDENCE_TABLE, CREATE_KNOWLEDGE_CLAIM_EVENTS_TABLE, CREATE_KNOWLEDGE_WORKSPACES_TABLE, CREATE_KNOWLEDGE_PAGES_TABLE, CREATE_KNOWLEDGE_PAGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_PAGE_LINKS_TABLE, CREATE_KNOWLEDGE_PROPOSALS_TABLE, CREATE_KNOWLEDGE_WORKFLOWS_TABLE, CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE, CREATE_MAINTENANCE_JOBS_TABLE, CREATE_MAINTENANCE_TARGETS_TABLE, CREATE_INDEXES, SCHEMA_MIGRATIONS, _dbCache;
163
186
  var init_sqlite_db = __esm({
164
187
  "src/store/sqlite-db.ts"() {
165
188
  "use strict";
@@ -399,6 +422,188 @@ CREATE TABLE IF NOT EXISTS observation_code_refs (
399
422
  updatedAt TEXT
400
423
  );
401
424
  `;
425
+ CREATE_SCHEMA_MIGRATIONS_TABLE = [
426
+ "CREATE TABLE IF NOT EXISTS schema_migrations (",
427
+ " id TEXT PRIMARY KEY,",
428
+ " applied_at TEXT NOT NULL",
429
+ ");"
430
+ ].join("\n");
431
+ CREATE_CODE_STATE_SNAPSHOTS_TABLE = [
432
+ "CREATE TABLE IF NOT EXISTS code_state_snapshots (",
433
+ " id TEXT PRIMARY KEY,",
434
+ " projectId TEXT NOT NULL,",
435
+ " provider TEXT NOT NULL,",
436
+ " baseRevision TEXT,",
437
+ " worktreeFingerprint TEXT NOT NULL,",
438
+ " worktreeState TEXT NOT NULL,",
439
+ " changedPathCount INTEGER NOT NULL DEFAULT 0,",
440
+ " indexedAt TEXT NOT NULL,",
441
+ " sourceEpoch INTEGER NOT NULL,",
442
+ " completenessJson TEXT NOT NULL DEFAULT '{}',",
443
+ " previousSnapshotId TEXT,",
444
+ " UNIQUE(projectId, sourceEpoch)",
445
+ ");"
446
+ ].join("\n");
447
+ CREATE_KNOWLEDGE_CLAIMS_TABLE = `
448
+ CREATE TABLE IF NOT EXISTS knowledge_claims (
449
+ id TEXT PRIMARY KEY,
450
+ projectId TEXT NOT NULL,
451
+ subject TEXT NOT NULL,
452
+ predicate TEXT NOT NULL,
453
+ objectValue TEXT NOT NULL,
454
+ scope TEXT NOT NULL,
455
+ claimKey TEXT NOT NULL,
456
+ conflictKey TEXT NOT NULL,
457
+ status TEXT NOT NULL,
458
+ confidence REAL NOT NULL,
459
+ observedAt TEXT NOT NULL,
460
+ validFrom TEXT,
461
+ validTo TEXT,
462
+ supersededBy TEXT,
463
+ reviewState TEXT NOT NULL,
464
+ origin TEXT NOT NULL,
465
+ createdAt TEXT NOT NULL,
466
+ updatedAt TEXT NOT NULL
467
+ );
468
+ `;
469
+ CREATE_KNOWLEDGE_CLAIM_EVIDENCE_TABLE = `
470
+ CREATE TABLE IF NOT EXISTS knowledge_claim_evidence (
471
+ id TEXT PRIMARY KEY,
472
+ claimId TEXT NOT NULL,
473
+ evidenceKind TEXT NOT NULL,
474
+ evidenceId TEXT NOT NULL,
475
+ relation TEXT NOT NULL,
476
+ snapshotId TEXT,
477
+ locator TEXT,
478
+ capturedHash TEXT,
479
+ evidenceKey TEXT NOT NULL,
480
+ createdAt TEXT NOT NULL,
481
+ UNIQUE(claimId, evidenceKey),
482
+ FOREIGN KEY (claimId) REFERENCES knowledge_claims(id) ON DELETE CASCADE
483
+ );
484
+ `;
485
+ CREATE_KNOWLEDGE_CLAIM_EVENTS_TABLE = `
486
+ CREATE TABLE IF NOT EXISTS knowledge_claim_events (
487
+ id TEXT PRIMARY KEY,
488
+ projectId TEXT NOT NULL,
489
+ claimId TEXT NOT NULL,
490
+ kind TEXT NOT NULL,
491
+ fromStatus TEXT,
492
+ toStatus TEXT,
493
+ relatedClaimId TEXT,
494
+ detail TEXT,
495
+ createdAt TEXT NOT NULL,
496
+ FOREIGN KEY (claimId) REFERENCES knowledge_claims(id) ON DELETE CASCADE
497
+ );
498
+ `;
499
+ CREATE_KNOWLEDGE_WORKSPACES_TABLE = [
500
+ "CREATE TABLE IF NOT EXISTS knowledge_workspaces (",
501
+ " id TEXT PRIMARY KEY,",
502
+ " projectId TEXT NOT NULL,",
503
+ " mode TEXT NOT NULL,",
504
+ " rootPath TEXT NOT NULL,",
505
+ " projectRoot TEXT,",
506
+ " status TEXT NOT NULL,",
507
+ " createdAt TEXT NOT NULL,",
508
+ " updatedAt TEXT NOT NULL,",
509
+ " lastCompiledAt TEXT,",
510
+ " lastLintedAt TEXT,",
511
+ " UNIQUE(projectId, mode)",
512
+ ");"
513
+ ].join("\n");
514
+ CREATE_KNOWLEDGE_PAGES_TABLE = [
515
+ "CREATE TABLE IF NOT EXISTS knowledge_pages (",
516
+ " id TEXT PRIMARY KEY,",
517
+ " workspaceId TEXT NOT NULL,",
518
+ " relativePath TEXT NOT NULL,",
519
+ " title TEXT NOT NULL,",
520
+ " kind TEXT NOT NULL,",
521
+ " status TEXT NOT NULL,",
522
+ " reviewState TEXT NOT NULL,",
523
+ " contentHash TEXT NOT NULL,",
524
+ " sourceHash TEXT NOT NULL,",
525
+ " claimIdsJson TEXT NOT NULL DEFAULT '[]',",
526
+ " snapshotId TEXT,",
527
+ " tagsJson TEXT NOT NULL DEFAULT '[]',",
528
+ " generatedAt TEXT NOT NULL,",
529
+ " updatedAt TEXT NOT NULL,",
530
+ " lastLintedAt TEXT,",
531
+ " manualContentHash TEXT,",
532
+ " UNIQUE(workspaceId, relativePath),",
533
+ " FOREIGN KEY (workspaceId) REFERENCES knowledge_workspaces(id) ON DELETE CASCADE",
534
+ ");"
535
+ ].join("\n");
536
+ CREATE_KNOWLEDGE_PAGE_CLAIMS_TABLE = [
537
+ "CREATE TABLE IF NOT EXISTS knowledge_page_claims (",
538
+ " pageId TEXT NOT NULL,",
539
+ " claimId TEXT NOT NULL,",
540
+ " role TEXT NOT NULL,",
541
+ " PRIMARY KEY (pageId, claimId),",
542
+ " FOREIGN KEY (pageId) REFERENCES knowledge_pages(id) ON DELETE CASCADE,",
543
+ " FOREIGN KEY (claimId) REFERENCES knowledge_claims(id) ON DELETE RESTRICT",
544
+ ");"
545
+ ].join("\n");
546
+ CREATE_KNOWLEDGE_PAGE_LINKS_TABLE = [
547
+ "CREATE TABLE IF NOT EXISTS knowledge_page_links (",
548
+ " sourcePageId TEXT NOT NULL,",
549
+ " targetPath TEXT NOT NULL,",
550
+ " PRIMARY KEY (sourcePageId, targetPath),",
551
+ " FOREIGN KEY (sourcePageId) REFERENCES knowledge_pages(id) ON DELETE CASCADE",
552
+ ");"
553
+ ].join("\n");
554
+ CREATE_KNOWLEDGE_PROPOSALS_TABLE = [
555
+ "CREATE TABLE IF NOT EXISTS knowledge_proposals (",
556
+ " id TEXT PRIMARY KEY,",
557
+ " workspaceId TEXT NOT NULL,",
558
+ " pageId TEXT NOT NULL,",
559
+ " targetPath TEXT NOT NULL,",
560
+ " proposalPath TEXT NOT NULL,",
561
+ " baseContentHash TEXT,",
562
+ " sourceHash TEXT NOT NULL,",
563
+ " reason TEXT NOT NULL,",
564
+ " status TEXT NOT NULL,",
565
+ " createdAt TEXT NOT NULL,",
566
+ " appliedAt TEXT,",
567
+ " UNIQUE(workspaceId, proposalPath),",
568
+ " FOREIGN KEY (workspaceId) REFERENCES knowledge_workspaces(id) ON DELETE CASCADE,",
569
+ " FOREIGN KEY (pageId) REFERENCES knowledge_pages(id) ON DELETE CASCADE",
570
+ ");"
571
+ ].join("\n");
572
+ CREATE_KNOWLEDGE_WORKFLOWS_TABLE = [
573
+ "CREATE TABLE IF NOT EXISTS knowledge_workflows (",
574
+ " id TEXT PRIMARY KEY,",
575
+ " workspaceId TEXT NOT NULL,",
576
+ " sourcePath TEXT NOT NULL,",
577
+ " title TEXT NOT NULL,",
578
+ " status TEXT NOT NULL,",
579
+ " version INTEGER NOT NULL,",
580
+ " sourceHash TEXT NOT NULL,",
581
+ " contentHash TEXT NOT NULL,",
582
+ " specJson TEXT NOT NULL,",
583
+ " importedFrom TEXT,",
584
+ " createdAt TEXT NOT NULL,",
585
+ " updatedAt TEXT NOT NULL,",
586
+ " UNIQUE(workspaceId, sourcePath),",
587
+ " FOREIGN KEY (workspaceId) REFERENCES knowledge_workspaces(id) ON DELETE CASCADE",
588
+ ");"
589
+ ].join("\n");
590
+ CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE = [
591
+ "CREATE TABLE IF NOT EXISTS knowledge_workflow_runs (",
592
+ " id TEXT PRIMARY KEY,",
593
+ " workflowId TEXT NOT NULL,",
594
+ " projectId TEXT NOT NULL,",
595
+ " task TEXT NOT NULL,",
596
+ " startingSnapshotId TEXT,",
597
+ " selectedEvidenceJson TEXT NOT NULL DEFAULT '[]',",
598
+ " phaseStateJson TEXT NOT NULL DEFAULT '{}',",
599
+ " outcome TEXT NOT NULL,",
600
+ " verificationVerdict TEXT NOT NULL,",
601
+ " failureReason TEXT,",
602
+ " startedAt TEXT NOT NULL,",
603
+ " completedAt TEXT,",
604
+ " FOREIGN KEY (workflowId) REFERENCES knowledge_workflows(id) ON DELETE CASCADE",
605
+ ");"
606
+ ].join("\n");
402
607
  CREATE_MAINTENANCE_JOBS_TABLE = `
403
608
  CREATE TABLE IF NOT EXISTS maintenance_jobs (
404
609
  id TEXT PRIMARY KEY,
@@ -450,8 +655,23 @@ CREATE INDEX IF NOT EXISTS idx_code_files_project ON code_files(projectId);
450
655
  CREATE INDEX IF NOT EXISTS idx_code_symbols_project_name ON code_symbols(projectId, name);
451
656
  CREATE INDEX IF NOT EXISTS idx_code_symbols_file ON code_symbols(fileId);
452
657
  CREATE INDEX IF NOT EXISTS idx_code_edges_project ON code_edges(projectId, type);
658
+ CREATE INDEX IF NOT EXISTS idx_code_snapshots_project_epoch ON code_state_snapshots(projectId, sourceEpoch DESC);
659
+ CREATE INDEX IF NOT EXISTS idx_code_files_snapshot ON code_files(projectId, snapshotId);
660
+ CREATE INDEX IF NOT EXISTS idx_code_symbols_snapshot ON code_symbols(projectId, snapshotId);
661
+ CREATE INDEX IF NOT EXISTS idx_code_edges_snapshot ON code_edges(projectId, snapshotId);
453
662
  CREATE INDEX IF NOT EXISTS idx_observation_code_refs_obs ON observation_code_refs(projectId, observationId);
454
663
  CREATE INDEX IF NOT EXISTS idx_observation_code_refs_status ON observation_code_refs(projectId, status);
664
+ CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_status ON knowledge_claims(projectId, status, updatedAt DESC);
665
+ CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_conflict ON knowledge_claims(projectId, conflictKey, status);
666
+ CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_key ON knowledge_claims(projectId, claimKey, status);
667
+ CREATE INDEX IF NOT EXISTS idx_knowledge_claim_evidence_claim ON knowledge_claim_evidence(claimId, createdAt);
668
+ CREATE INDEX IF NOT EXISTS idx_knowledge_claim_events_claim ON knowledge_claim_events(claimId, createdAt);
669
+ CREATE INDEX IF NOT EXISTS idx_knowledge_workspaces_project ON knowledge_workspaces(projectId, mode);
670
+ CREATE INDEX IF NOT EXISTS idx_knowledge_pages_workspace_status ON knowledge_pages(workspaceId, status, updatedAt DESC);
671
+ CREATE INDEX IF NOT EXISTS idx_knowledge_page_claims_claim ON knowledge_page_claims(claimId);
672
+ CREATE INDEX IF NOT EXISTS idx_knowledge_proposals_workspace_status ON knowledge_proposals(workspaceId, status, createdAt DESC);
673
+ CREATE INDEX IF NOT EXISTS idx_knowledge_workflows_workspace_status ON knowledge_workflows(workspaceId, status, updatedAt DESC);
674
+ CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowledge_workflow_runs(projectId, workflowId, startedAt DESC);
455
675
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_ready ON maintenance_jobs(status, run_after);
456
676
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_project ON maintenance_jobs(project_id, status, run_after);
457
677
  CREATE INDEX IF NOT EXISTS idx_maintenance_targets_updated ON maintenance_targets(updated_at);
@@ -459,6 +679,61 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
459
679
  ON maintenance_jobs(project_id, kind, dedupe_key)
460
680
  WHERE status IN ('pending', 'running', 'retry');
461
681
  `;
682
+ SCHEMA_MIGRATIONS = [
683
+ {
684
+ id: "1.2-code-state-snapshots",
685
+ apply: (db2) => {
686
+ db2.exec(CREATE_CODE_STATE_SNAPSHOTS_TABLE);
687
+ addColumnIfMissing(db2, "code_files", "snapshotId", "snapshotId TEXT");
688
+ addColumnIfMissing(db2, "code_files", "sourceEpoch", "sourceEpoch INTEGER");
689
+ addColumnIfMissing(db2, "code_symbols", "snapshotId", "snapshotId TEXT");
690
+ addColumnIfMissing(db2, "code_symbols", "sourceEpoch", "sourceEpoch INTEGER");
691
+ addColumnIfMissing(db2, "code_edges", "snapshotId", "snapshotId TEXT");
692
+ addColumnIfMissing(db2, "code_edges", "sourceEpoch", "sourceEpoch INTEGER");
693
+ addColumnIfMissing(db2, "observation_code_refs", "snapshotId", "snapshotId TEXT");
694
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_code_snapshots_project_epoch ON code_state_snapshots(projectId, sourceEpoch DESC)");
695
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_code_files_snapshot ON code_files(projectId, snapshotId)");
696
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_code_symbols_snapshot ON code_symbols(projectId, snapshotId)");
697
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_code_edges_snapshot ON code_edges(projectId, snapshotId)");
698
+ }
699
+ },
700
+ {
701
+ id: "1.2-knowledge-claim-ledger",
702
+ apply: (db2) => {
703
+ db2.exec(CREATE_KNOWLEDGE_CLAIMS_TABLE);
704
+ db2.exec(CREATE_KNOWLEDGE_CLAIM_EVIDENCE_TABLE);
705
+ db2.exec(CREATE_KNOWLEDGE_CLAIM_EVENTS_TABLE);
706
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_status ON knowledge_claims(projectId, status, updatedAt DESC)");
707
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_conflict ON knowledge_claims(projectId, conflictKey, status)");
708
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_claims_project_key ON knowledge_claims(projectId, claimKey, status)");
709
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_claim_evidence_claim ON knowledge_claim_evidence(claimId, createdAt)");
710
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_claim_events_claim ON knowledge_claim_events(claimId, createdAt)");
711
+ }
712
+ },
713
+ {
714
+ id: "1.2-knowledge-workspace",
715
+ apply: (db2) => {
716
+ db2.exec(CREATE_KNOWLEDGE_WORKSPACES_TABLE);
717
+ db2.exec(CREATE_KNOWLEDGE_PAGES_TABLE);
718
+ db2.exec(CREATE_KNOWLEDGE_PAGE_CLAIMS_TABLE);
719
+ db2.exec(CREATE_KNOWLEDGE_PAGE_LINKS_TABLE);
720
+ db2.exec(CREATE_KNOWLEDGE_PROPOSALS_TABLE);
721
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workspaces_project ON knowledge_workspaces(projectId, mode)");
722
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_pages_workspace_status ON knowledge_pages(workspaceId, status, updatedAt DESC)");
723
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_page_claims_claim ON knowledge_page_claims(claimId)");
724
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_proposals_workspace_status ON knowledge_proposals(workspaceId, status, createdAt DESC)");
725
+ }
726
+ },
727
+ {
728
+ id: "1.2-workflow-inheritance",
729
+ apply: (db2) => {
730
+ db2.exec(CREATE_KNOWLEDGE_WORKFLOWS_TABLE);
731
+ db2.exec(CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE);
732
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workflows_workspace_status ON knowledge_workflows(workspaceId, status, updatedAt DESC)");
733
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowledge_workflow_runs(projectId, workflowId, startedAt DESC)");
734
+ }
735
+ }
736
+ ];
462
737
  _dbCache = /* @__PURE__ */ new Map();
463
738
  }
464
739
  });
@@ -598,8 +873,8 @@ function parseYaml(content) {
598
873
  return yaml.load(content) ?? {};
599
874
  } catch {
600
875
  try {
601
- const matter = require2("gray-matter");
602
- const parsed = matter(`---
876
+ const matter4 = require2("gray-matter");
877
+ const parsed = matter4(`---
603
878
  ${content}
604
879
  ---`);
605
880
  return parsed.data ?? {};
@@ -851,9 +1126,9 @@ function parseTomlConfig(content, filePath) {
851
1126
  }
852
1127
  return root;
853
1128
  }
854
- function ensureTable(root, path8) {
1129
+ function ensureTable(root, path13) {
855
1130
  let cursor = root;
856
- for (const part of path8) {
1131
+ for (const part of path13) {
857
1132
  const value = cursor[part];
858
1133
  if (!value || typeof value !== "object" || Array.isArray(value)) {
859
1134
  cursor[part] = {};
@@ -1000,7 +1275,23 @@ function getResolvedConfig(options = {}) {
1000
1275
  },
1001
1276
  codegraph: {
1002
1277
  excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
1003
- maxFileBytes: firstNumber(toml.codegraph?.max_file_bytes, yaml.codegraph?.maxFileBytes)
1278
+ maxFileBytes: firstNumber(toml.codegraph?.max_file_bytes, yaml.codegraph?.maxFileBytes),
1279
+ externalContext: first(
1280
+ normalizeExternalContext(process.env.MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT),
1281
+ toml.codegraph?.external_context,
1282
+ yaml.codegraph?.externalContext,
1283
+ "auto"
1284
+ ),
1285
+ externalCommand: first(
1286
+ process.env.MEMORIX_CODEGRAPH_EXTERNAL_COMMAND,
1287
+ toml.codegraph?.external_command,
1288
+ yaml.codegraph?.externalCommand
1289
+ ),
1290
+ externalTimeoutMs: firstNumber(
1291
+ parseNumber(process.env.MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS),
1292
+ toml.codegraph?.external_timeout_ms,
1293
+ yaml.codegraph?.externalTimeoutMs
1294
+ )
1004
1295
  },
1005
1296
  server: {
1006
1297
  transport: first(toml.server?.transport, yaml.server?.transport),
@@ -1085,6 +1376,9 @@ function getEnvSourceNames() {
1085
1376
  "MEMORIX_EMBEDDING_BASE_URL",
1086
1377
  "MEMORIX_EMBEDDING_MODEL",
1087
1378
  "MEMORIX_EMBEDDING_DIMENSIONS",
1379
+ "MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT",
1380
+ "MEMORIX_CODEGRAPH_EXTERNAL_COMMAND",
1381
+ "MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS",
1088
1382
  "OPENROUTER_API_KEY"
1089
1383
  ].filter((name) => process.env[name]);
1090
1384
  }
@@ -1097,6 +1391,11 @@ function isOpenRouterUrl(value) {
1097
1391
  return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
1098
1392
  }
1099
1393
  }
1394
+ function normalizeExternalContext(value) {
1395
+ const normalized = value?.trim().toLowerCase();
1396
+ if (normalized === "auto" || normalized === "off") return normalized;
1397
+ return void 0;
1398
+ }
1100
1399
  var init_resolved_config = __esm({
1101
1400
  "src/config/resolved-config.ts"() {
1102
1401
  "use strict";
@@ -2394,7 +2693,12 @@ var init_maintenance_jobs = __esm({
2394
2693
  "vector-backfill",
2395
2694
  "retention-archive",
2396
2695
  "consolidation",
2397
- "codegraph-refresh"
2696
+ "codegraph-refresh",
2697
+ "claim-derive",
2698
+ "claim-requalification",
2699
+ "knowledge-compile",
2700
+ "knowledge-lint",
2701
+ "workflow-index"
2398
2702
  ];
2399
2703
  DEFAULT_MAX_ATTEMPTS = 8;
2400
2704
  DEFAULT_LEASE_MS = 3e4;
@@ -2407,14 +2711,14 @@ var init_maintenance_jobs = __esm({
2407
2711
  this.db = getDatabase(dataDir);
2408
2712
  }
2409
2713
  enqueue(input) {
2410
- const now = input.now ?? Date.now();
2714
+ const now3 = input.now ?? Date.now();
2411
2715
  const dedupeKey = input.dedupeKey ?? input.kind;
2412
- const runAfter = input.runAfter ?? now;
2716
+ const runAfter = input.runAfter ?? now3;
2413
2717
  const maxAttempts = clampPositiveInteger(input.maxAttempts, DEFAULT_MAX_ATTEMPTS);
2414
2718
  const payloadJson = JSON.stringify(input.payload ?? {});
2415
2719
  this.begin();
2416
2720
  try {
2417
- this.pruneCompletedHistory({ now });
2721
+ this.pruneCompletedHistory({ now: now3 });
2418
2722
  const existing = this.db.prepare(`
2419
2723
  SELECT * FROM maintenance_jobs
2420
2724
  WHERE project_id = ? AND kind = ? AND dedupe_key = ?
@@ -2427,7 +2731,7 @@ var init_maintenance_jobs = __esm({
2427
2731
  UPDATE maintenance_jobs
2428
2732
  SET payload_json = ?, run_after = ?, updated_at = ?
2429
2733
  WHERE id = ?
2430
- `).run(payloadJson, nextRunAfter, now, existing.id);
2734
+ `).run(payloadJson, nextRunAfter, now3, existing.id);
2431
2735
  const updated = this.getRow(existing.id);
2432
2736
  this.commit();
2433
2737
  return rowToJob(updated);
@@ -2438,7 +2742,7 @@ var init_maintenance_jobs = __esm({
2438
2742
  id, project_id, kind, dedupe_key, payload_json, status, attempts,
2439
2743
  max_attempts, run_after, created_at, updated_at
2440
2744
  ) VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?, ?, ?)
2441
- `).run(id, input.projectId, input.kind, dedupeKey, payloadJson, maxAttempts, runAfter, now, now);
2745
+ `).run(id, input.projectId, input.kind, dedupeKey, payloadJson, maxAttempts, runAfter, now3, now3);
2442
2746
  const created = this.getRow(id);
2443
2747
  this.commit();
2444
2748
  return rowToJob(created);
@@ -2507,18 +2811,18 @@ var init_maintenance_jobs = __esm({
2507
2811
  * resolves them or clears the database.
2508
2812
  */
2509
2813
  pruneCompletedHistory(options = {}) {
2510
- const now = options.now ?? Date.now();
2814
+ const now3 = options.now ?? Date.now();
2511
2815
  const maxAgeMs = Number.isFinite(options.maxAgeMs) ? Math.max(0, Math.floor(options.maxAgeMs)) : DEFAULT_COMPLETED_HISTORY_RETENTION_MS;
2512
2816
  const result = this.db.prepare(`
2513
2817
  DELETE FROM maintenance_jobs
2514
2818
  WHERE status = 'completed'
2515
2819
  AND completed_at IS NOT NULL
2516
2820
  AND completed_at < ?
2517
- `).run(now - maxAgeMs);
2821
+ `).run(now3 - maxAgeMs);
2518
2822
  return Number(result.changes ?? 0);
2519
2823
  }
2520
2824
  claimNext(options) {
2521
- const now = options.now ?? Date.now();
2825
+ const now3 = options.now ?? Date.now();
2522
2826
  const leaseMs = clampPositiveInteger(options.leaseMs, DEFAULT_LEASE_MS);
2523
2827
  const kinds = normalizedKinds(options.kinds);
2524
2828
  this.begin();
@@ -2533,12 +2837,12 @@ var init_maintenance_jobs = __esm({
2533
2837
  last_error = COALESCE(last_error, 'worker lease expired'),
2534
2838
  updated_at = ?
2535
2839
  WHERE status = 'running' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?
2536
- `).run(now, now, now);
2840
+ `).run(now3, now3, now3);
2537
2841
  this.db.prepare(`
2538
2842
  UPDATE maintenance_jobs
2539
2843
  SET status = 'failed', updated_at = ?
2540
2844
  WHERE status IN ('pending', 'retry') AND attempts >= max_attempts
2541
- `).run(now);
2845
+ `).run(now3);
2542
2846
  if (options.kinds && kinds?.length === 0) {
2543
2847
  this.commit();
2544
2848
  return void 0;
@@ -2546,7 +2850,7 @@ var init_maintenance_jobs = __esm({
2546
2850
  const projectClause = options.projectId ? "AND project_id = ?" : "";
2547
2851
  const kindClause = kinds?.length ? `AND kind IN (${kinds.map(() => "?").join(", ")})` : "";
2548
2852
  const values = [
2549
- now,
2853
+ now3,
2550
2854
  ...kinds ?? [],
2551
2855
  ...options.projectId ? [options.projectId] : []
2552
2856
  ];
@@ -2562,13 +2866,13 @@ var init_maintenance_jobs = __esm({
2562
2866
  this.commit();
2563
2867
  return void 0;
2564
2868
  }
2565
- const leaseExpiresAt = now + leaseMs;
2869
+ const leaseExpiresAt = now3 + leaseMs;
2566
2870
  this.db.prepare(`
2567
2871
  UPDATE maintenance_jobs
2568
2872
  SET status = 'running', attempts = attempts + 1, lease_owner = ?,
2569
2873
  lease_expires_at = ?, updated_at = ?
2570
2874
  WHERE id = ?
2571
- `).run(options.workerId, leaseExpiresAt, now, candidate.id);
2875
+ `).run(options.workerId, leaseExpiresAt, now3, candidate.id);
2572
2876
  const claimed = this.getRow(candidate.id);
2573
2877
  this.commit();
2574
2878
  return rowToJob(claimed);
@@ -2581,26 +2885,26 @@ var init_maintenance_jobs = __esm({
2581
2885
  * Extend a running job's lease. A worker may only renew its own lease, so a
2582
2886
  * stale worker cannot reclaim work that was already recovered elsewhere.
2583
2887
  */
2584
- renewLease(id, workerId, leaseMs = DEFAULT_LEASE_MS, now = Date.now()) {
2585
- const expiresAt = now + clampPositiveInteger(leaseMs, DEFAULT_LEASE_MS);
2888
+ renewLease(id, workerId, leaseMs = DEFAULT_LEASE_MS, now3 = Date.now()) {
2889
+ const expiresAt = now3 + clampPositiveInteger(leaseMs, DEFAULT_LEASE_MS);
2586
2890
  const result = this.db.prepare(`
2587
2891
  UPDATE maintenance_jobs
2588
2892
  SET lease_expires_at = ?, updated_at = ?
2589
2893
  WHERE id = ? AND status = 'running' AND lease_owner = ?
2590
- `).run(expiresAt, now, id, workerId);
2894
+ `).run(expiresAt, now3, id, workerId);
2591
2895
  return Number(result.changes) > 0;
2592
2896
  }
2593
- complete(id, workerId, now = Date.now()) {
2897
+ complete(id, workerId, now3 = Date.now()) {
2594
2898
  this.db.prepare(`
2595
2899
  UPDATE maintenance_jobs
2596
2900
  SET status = 'completed', lease_owner = NULL, lease_expires_at = NULL,
2597
2901
  completed_at = ?, updated_at = ?
2598
2902
  WHERE id = ? AND status = 'running' AND lease_owner = ?
2599
- `).run(now, now, id, workerId);
2903
+ `).run(now3, now3, id, workerId);
2600
2904
  return this.get(id);
2601
2905
  }
2602
2906
  reschedule(id, workerId, options) {
2603
- const now = options.now ?? Date.now();
2907
+ const now3 = options.now ?? Date.now();
2604
2908
  const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
2605
2909
  const payloadJson = options.payload === void 0 ? null : JSON.stringify(options.payload);
2606
2910
  this.db.prepare(`
@@ -2609,10 +2913,10 @@ var init_maintenance_jobs = __esm({
2609
2913
  payload_json = COALESCE(?, payload_json),
2610
2914
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
2611
2915
  WHERE id = ? AND status = 'running' AND lease_owner = ?
2612
- `).run(now + delayMs, options.resetAttempts ? 1 : 0, payloadJson, now, id, workerId);
2916
+ `).run(now3 + delayMs, options.resetAttempts ? 1 : 0, payloadJson, now3, id, workerId);
2613
2917
  return this.get(id);
2614
2918
  }
2615
- fail(id, workerId, error, now = Date.now()) {
2919
+ fail(id, workerId, error, now3 = Date.now()) {
2616
2920
  this.begin();
2617
2921
  try {
2618
2922
  const current = this.db.prepare(`
@@ -2626,13 +2930,13 @@ var init_maintenance_jobs = __esm({
2626
2930
  const attempts = Number(current.attempts);
2627
2931
  const exhausted = attempts >= Number(current.max_attempts);
2628
2932
  const status = exhausted ? "failed" : "retry";
2629
- const runAfter = exhausted ? Number(current.run_after) : now + getMaintenanceRetryDelayMs(attempts);
2933
+ const runAfter = exhausted ? Number(current.run_after) : now3 + getMaintenanceRetryDelayMs(attempts);
2630
2934
  this.db.prepare(`
2631
2935
  UPDATE maintenance_jobs
2632
2936
  SET status = ?, run_after = ?, lease_owner = NULL, lease_expires_at = NULL,
2633
2937
  last_error = ?, updated_at = ?
2634
2938
  WHERE id = ?
2635
- `).run(status, runAfter, errorText(error), now, id);
2939
+ `).run(status, runAfter, errorText(error), now3, id);
2636
2940
  const updated = this.getRow(id);
2637
2941
  this.commit();
2638
2942
  return rowToJob(updated);
@@ -2676,7 +2980,7 @@ var init_maintenance_jobs = __esm({
2676
2980
  kinds;
2677
2981
  timer = null;
2678
2982
  running = false;
2679
- async runOnce(now = Date.now()) {
2983
+ async runOnce(now3 = Date.now()) {
2680
2984
  if (this.running) return { state: "busy" };
2681
2985
  this.running = true;
2682
2986
  try {
@@ -2684,7 +2988,7 @@ var init_maintenance_jobs = __esm({
2684
2988
  workerId: this.workerId,
2685
2989
  projectId: this.projectId,
2686
2990
  kinds: this.kinds,
2687
- now,
2991
+ now: now3,
2688
2992
  leaseMs: this.leaseMs
2689
2993
  });
2690
2994
  if (!job) return { state: "idle" };
@@ -2707,14 +3011,14 @@ var init_maintenance_jobs = __esm({
2707
3011
  delayMs: result.delayMs,
2708
3012
  resetAttempts: result.resetAttempts,
2709
3013
  payload: result.payload,
2710
- now
3014
+ now: now3
2711
3015
  });
2712
3016
  return { state: "rescheduled", job: updated2 };
2713
3017
  }
2714
- const updated = this.store.complete(job.id, this.workerId, now);
3018
+ const updated = this.store.complete(job.id, this.workerId, now3);
2715
3019
  return { state: "completed", job: updated };
2716
3020
  } catch (error) {
2717
- const updated = this.store.fail(job.id, this.workerId, error, now);
3021
+ const updated = this.store.fail(job.id, this.workerId, error, now3);
2718
3022
  return { state: "failed", job: updated };
2719
3023
  } finally {
2720
3024
  clearInterval(heartbeat);
@@ -2742,6 +3046,527 @@ var init_maintenance_jobs = __esm({
2742
3046
  }
2743
3047
  });
2744
3048
 
3049
+ // src/runtime/lifecycle.ts
3050
+ function queueFor(input) {
3051
+ return input.queue ?? new MaintenanceJobStore(input.dataDir);
3052
+ }
3053
+ function enqueueClaimRequalification(input) {
3054
+ queueFor(input).enqueue({
3055
+ projectId: input.projectId,
3056
+ kind: "claim-requalification",
3057
+ dedupeKey: "claim-requalification",
3058
+ payload: {
3059
+ source: input.source,
3060
+ ...input.snapshotId ? { snapshotId: input.snapshotId } : {}
3061
+ }
3062
+ });
3063
+ }
3064
+ function enqueueClaimDerivation(input) {
3065
+ queueFor(input).enqueue({
3066
+ projectId: input.projectId,
3067
+ kind: "claim-derive",
3068
+ dedupeKey: "claim-derive:" + input.observationId,
3069
+ payload: { observationId: input.observationId }
3070
+ });
3071
+ }
3072
+ function enqueueKnowledgeFollowups(input) {
3073
+ const queue = queueFor(input);
3074
+ const payload = {
3075
+ source: input.source,
3076
+ ...input.workspaceMode ? { workspaceMode: input.workspaceMode } : {},
3077
+ ...input.allowVersionedWrite ? { allowVersionedWrite: true } : {}
3078
+ };
3079
+ if (input.includeCompile) {
3080
+ queue.enqueue({
3081
+ projectId: input.projectId,
3082
+ kind: "knowledge-compile",
3083
+ dedupeKey: "knowledge-compile",
3084
+ payload
3085
+ });
3086
+ }
3087
+ queue.enqueue({
3088
+ projectId: input.projectId,
3089
+ kind: "knowledge-lint",
3090
+ dedupeKey: "knowledge-lint",
3091
+ payload
3092
+ });
3093
+ queue.enqueue({
3094
+ projectId: input.projectId,
3095
+ kind: "workflow-index",
3096
+ dedupeKey: "workflow-index",
3097
+ payload
3098
+ });
3099
+ }
3100
+ var init_lifecycle = __esm({
3101
+ "src/runtime/lifecycle.ts"() {
3102
+ "use strict";
3103
+ init_esm_shims();
3104
+ init_maintenance_jobs();
3105
+ }
3106
+ });
3107
+
3108
+ // src/store/file-lock.ts
3109
+ import { promises as fs3 } from "fs";
3110
+ import { randomUUID as randomUUID2 } from "crypto";
3111
+ import path6 from "path";
3112
+ async function acquireLock(lockPath) {
3113
+ for (let i = 0; i < MAX_RETRIES; i++) {
3114
+ try {
3115
+ const fd = await fs3.open(lockPath, "wx");
3116
+ await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3117
+ await fd.close();
3118
+ return;
3119
+ } catch (err) {
3120
+ const code = err instanceof Error && "code" in err ? err.code : void 0;
3121
+ if (code === "EEXIST" || code === "EPERM") {
3122
+ try {
3123
+ const stat = await fs3.stat(lockPath);
3124
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
3125
+ await fs3.unlink(lockPath).catch(() => {
3126
+ });
3127
+ continue;
3128
+ }
3129
+ } catch {
3130
+ continue;
3131
+ }
3132
+ await new Promise((r) => setTimeout(r, RETRY_INTERVAL_MS));
3133
+ } else {
3134
+ throw err;
3135
+ }
3136
+ }
3137
+ }
3138
+ await fs3.unlink(lockPath).catch(() => {
3139
+ });
3140
+ try {
3141
+ const fd = await fs3.open(lockPath, "wx");
3142
+ await fd.writeFile(JSON.stringify({ pid: process.pid, time: Date.now() }));
3143
+ await fd.close();
3144
+ return;
3145
+ } catch {
3146
+ throw new Error(`Failed to acquire lock: ${lockPath} (timeout after ${MAX_RETRIES * RETRY_INTERVAL_MS}ms)`);
3147
+ }
3148
+ }
3149
+ async function releaseLock(lockPath) {
3150
+ await fs3.unlink(lockPath).catch(() => {
3151
+ });
3152
+ }
3153
+ async function withFileLock(projectDir2, fn) {
3154
+ const lockPath = path6.join(projectDir2, ".memorix.lock");
3155
+ await acquireLock(lockPath);
3156
+ try {
3157
+ return await fn();
3158
+ } finally {
3159
+ await releaseLock(lockPath);
3160
+ }
3161
+ }
3162
+ async function atomicWriteFileOnce(filePath, data) {
3163
+ const tmpPath = filePath + `.tmp.${process.pid}.${randomUUID2()}`;
3164
+ try {
3165
+ await fs3.writeFile(tmpPath, data, "utf-8");
3166
+ await fs3.rename(tmpPath, filePath);
3167
+ } catch (error) {
3168
+ await fs3.unlink(tmpPath).catch(() => {
3169
+ });
3170
+ throw error;
3171
+ }
3172
+ }
3173
+ function atomicWriteFile(filePath, data) {
3174
+ const previous = atomicWriteTails.get(filePath) ?? Promise.resolve();
3175
+ const next = previous.catch(() => void 0).then(() => atomicWriteFileOnce(filePath, data));
3176
+ atomicWriteTails.set(filePath, next);
3177
+ return next.finally(() => {
3178
+ if (atomicWriteTails.get(filePath) === next) atomicWriteTails.delete(filePath);
3179
+ });
3180
+ }
3181
+ var LOCK_STALE_MS, RETRY_INTERVAL_MS, MAX_RETRIES, atomicWriteTails;
3182
+ var init_file_lock = __esm({
3183
+ "src/store/file-lock.ts"() {
3184
+ "use strict";
3185
+ init_esm_shims();
3186
+ LOCK_STALE_MS = 1e4;
3187
+ RETRY_INTERVAL_MS = 50;
3188
+ MAX_RETRIES = 60;
3189
+ atomicWriteTails = /* @__PURE__ */ new Map();
3190
+ }
3191
+ });
3192
+
3193
+ // src/knowledge/workspace-store.ts
3194
+ import { randomUUID as randomUUID3 } from "crypto";
3195
+ function arrayValue(raw) {
3196
+ if (typeof raw !== "string" || !raw) return [];
3197
+ try {
3198
+ const parsed = JSON.parse(raw);
3199
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
3200
+ } catch {
3201
+ return [];
3202
+ }
3203
+ }
3204
+ function optionalText(value) {
3205
+ return typeof value === "string" && value ? value : void 0;
3206
+ }
3207
+ function rowToWorkspace(row) {
3208
+ return {
3209
+ id: row.id,
3210
+ projectId: row.projectId,
3211
+ mode: row.mode,
3212
+ rootPath: row.rootPath,
3213
+ ...optionalText(row.projectRoot) ? { projectRoot: row.projectRoot } : {},
3214
+ status: row.status,
3215
+ createdAt: row.createdAt,
3216
+ updatedAt: row.updatedAt,
3217
+ ...optionalText(row.lastCompiledAt) ? { lastCompiledAt: row.lastCompiledAt } : {},
3218
+ ...optionalText(row.lastLintedAt) ? { lastLintedAt: row.lastLintedAt } : {}
3219
+ };
3220
+ }
3221
+ function rowToPage(row) {
3222
+ return {
3223
+ id: row.id,
3224
+ workspaceId: row.workspaceId,
3225
+ relativePath: row.relativePath,
3226
+ title: row.title,
3227
+ kind: row.kind,
3228
+ status: row.status,
3229
+ reviewState: row.reviewState,
3230
+ contentHash: row.contentHash,
3231
+ sourceHash: row.sourceHash,
3232
+ claimIds: arrayValue(row.claimIdsJson),
3233
+ ...optionalText(row.snapshotId) ? { snapshotId: row.snapshotId } : {},
3234
+ tags: arrayValue(row.tagsJson),
3235
+ generatedAt: row.generatedAt,
3236
+ updatedAt: row.updatedAt,
3237
+ ...optionalText(row.lastLintedAt) ? { lastLintedAt: row.lastLintedAt } : {},
3238
+ ...optionalText(row.manualContentHash) ? { manualContentHash: row.manualContentHash } : {}
3239
+ };
3240
+ }
3241
+ function rowToProposal(row) {
3242
+ return {
3243
+ id: row.id,
3244
+ workspaceId: row.workspaceId,
3245
+ pageId: row.pageId,
3246
+ targetPath: row.targetPath,
3247
+ proposalPath: row.proposalPath,
3248
+ ...optionalText(row.baseContentHash) ? { baseContentHash: row.baseContentHash } : {},
3249
+ sourceHash: row.sourceHash,
3250
+ reason: row.reason,
3251
+ status: row.status,
3252
+ createdAt: row.createdAt,
3253
+ ...optionalText(row.appliedAt) ? { appliedAt: row.appliedAt } : {}
3254
+ };
3255
+ }
3256
+ var KnowledgeWorkspaceStore;
3257
+ var init_workspace_store = __esm({
3258
+ "src/knowledge/workspace-store.ts"() {
3259
+ "use strict";
3260
+ init_esm_shims();
3261
+ init_sqlite_db();
3262
+ KnowledgeWorkspaceStore = class {
3263
+ db = null;
3264
+ async init(dataDir) {
3265
+ this.db = getDatabase(dataDir);
3266
+ }
3267
+ upsertWorkspace(input) {
3268
+ const now3 = input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
3269
+ const createdAt = input.createdAt ?? now3;
3270
+ this.db.prepare(
3271
+ "INSERT INTO knowledge_workspaces (id, projectId, mode, rootPath, projectRoot, status, createdAt, updatedAt, lastCompiledAt, lastLintedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET mode = excluded.mode, rootPath = excluded.rootPath, projectRoot = excluded.projectRoot, status = excluded.status, updatedAt = excluded.updatedAt, lastCompiledAt = excluded.lastCompiledAt, lastLintedAt = excluded.lastLintedAt"
3272
+ ).run(
3273
+ input.id,
3274
+ input.projectId,
3275
+ input.mode,
3276
+ input.rootPath,
3277
+ input.projectRoot ?? null,
3278
+ input.status,
3279
+ createdAt,
3280
+ now3,
3281
+ input.lastCompiledAt ?? null,
3282
+ input.lastLintedAt ?? null
3283
+ );
3284
+ return this.getWorkspace(input.id);
3285
+ }
3286
+ getWorkspace(id) {
3287
+ const row = this.db.prepare("SELECT * FROM knowledge_workspaces WHERE id = ?").get(id);
3288
+ return row ? rowToWorkspace(row) : void 0;
3289
+ }
3290
+ findWorkspace(projectId, mode) {
3291
+ const row = this.db.prepare("SELECT * FROM knowledge_workspaces WHERE projectId = ? AND mode = ? ORDER BY updatedAt DESC LIMIT 1").get(projectId, mode);
3292
+ return row ? rowToWorkspace(row) : void 0;
3293
+ }
3294
+ upsertPage(page) {
3295
+ this.db.prepare(
3296
+ "INSERT INTO knowledge_pages (id, workspaceId, relativePath, title, kind, status, reviewState, contentHash, sourceHash, claimIdsJson, snapshotId, tagsJson, generatedAt, updatedAt, lastLintedAt, manualContentHash) VALUES (@id, @workspaceId, @relativePath, @title, @kind, @status, @reviewState, @contentHash, @sourceHash, @claimIdsJson, @snapshotId, @tagsJson, @generatedAt, @updatedAt, @lastLintedAt, @manualContentHash) ON CONFLICT(workspaceId, relativePath) DO UPDATE SET id = excluded.id, title = excluded.title, kind = excluded.kind, status = excluded.status, reviewState = excluded.reviewState, contentHash = excluded.contentHash, sourceHash = excluded.sourceHash, claimIdsJson = excluded.claimIdsJson, snapshotId = excluded.snapshotId, tagsJson = excluded.tagsJson, generatedAt = excluded.generatedAt, updatedAt = excluded.updatedAt, lastLintedAt = excluded.lastLintedAt, manualContentHash = excluded.manualContentHash"
3297
+ ).run({
3298
+ ...page,
3299
+ claimIdsJson: JSON.stringify(page.claimIds),
3300
+ snapshotId: page.snapshotId ?? null,
3301
+ tagsJson: JSON.stringify(page.tags),
3302
+ lastLintedAt: page.lastLintedAt ?? null,
3303
+ manualContentHash: page.manualContentHash ?? null
3304
+ });
3305
+ }
3306
+ getPage(workspaceId2, relativePath) {
3307
+ const row = this.db.prepare("SELECT * FROM knowledge_pages WHERE workspaceId = ? AND relativePath = ?").get(workspaceId2, relativePath);
3308
+ return row ? rowToPage(row) : void 0;
3309
+ }
3310
+ getPageById(id) {
3311
+ const row = this.db.prepare("SELECT * FROM knowledge_pages WHERE id = ?").get(id);
3312
+ return row ? rowToPage(row) : void 0;
3313
+ }
3314
+ listPages(workspaceId2) {
3315
+ return this.db.prepare("SELECT * FROM knowledge_pages WHERE workspaceId = ? ORDER BY relativePath").all(workspaceId2).map(rowToPage);
3316
+ }
3317
+ replacePageClaims(pageId, claimIds) {
3318
+ const remove3 = this.db.prepare("DELETE FROM knowledge_page_claims WHERE pageId = ?");
3319
+ const insert3 = this.db.prepare("INSERT OR IGNORE INTO knowledge_page_claims (pageId, claimId, role) VALUES (?, ?, 'primary')");
3320
+ this.db.transaction(() => {
3321
+ remove3.run(pageId);
3322
+ for (const claimId of [...new Set(claimIds)]) insert3.run(pageId, claimId);
3323
+ })();
3324
+ }
3325
+ replacePageLinks(pageId, targets) {
3326
+ const remove3 = this.db.prepare("DELETE FROM knowledge_page_links WHERE sourcePageId = ?");
3327
+ const insert3 = this.db.prepare("INSERT OR IGNORE INTO knowledge_page_links (sourcePageId, targetPath) VALUES (?, ?)");
3328
+ this.db.transaction(() => {
3329
+ remove3.run(pageId);
3330
+ for (const target of [...new Set(targets)]) insert3.run(pageId, target);
3331
+ })();
3332
+ }
3333
+ createProposal(input) {
3334
+ const proposal = {
3335
+ id: input.id ?? randomUUID3(),
3336
+ ...input,
3337
+ status: input.status ?? "pending",
3338
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
3339
+ };
3340
+ this.db.prepare(
3341
+ "INSERT INTO knowledge_proposals (id, workspaceId, pageId, targetPath, proposalPath, baseContentHash, sourceHash, reason, status, createdAt, appliedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(workspaceId, proposalPath) DO UPDATE SET pageId = excluded.pageId, targetPath = excluded.targetPath, baseContentHash = excluded.baseContentHash, sourceHash = excluded.sourceHash, reason = excluded.reason, status = excluded.status, createdAt = excluded.createdAt, appliedAt = excluded.appliedAt"
3342
+ ).run(
3343
+ proposal.id,
3344
+ proposal.workspaceId,
3345
+ proposal.pageId,
3346
+ proposal.targetPath,
3347
+ proposal.proposalPath,
3348
+ proposal.baseContentHash ?? null,
3349
+ proposal.sourceHash,
3350
+ proposal.reason,
3351
+ proposal.status,
3352
+ proposal.createdAt,
3353
+ proposal.appliedAt ?? null
3354
+ );
3355
+ const row = this.db.prepare("SELECT * FROM knowledge_proposals WHERE workspaceId = ? AND proposalPath = ?").get(proposal.workspaceId, proposal.proposalPath);
3356
+ return rowToProposal(row);
3357
+ }
3358
+ getProposal(id) {
3359
+ const row = this.db.prepare("SELECT * FROM knowledge_proposals WHERE id = ?").get(id);
3360
+ return row ? rowToProposal(row) : void 0;
3361
+ }
3362
+ listProposals(workspaceId2, status) {
3363
+ const rows = status ? this.db.prepare("SELECT * FROM knowledge_proposals WHERE workspaceId = ? AND status = ? ORDER BY createdAt DESC, id").all(workspaceId2, status) : this.db.prepare("SELECT * FROM knowledge_proposals WHERE workspaceId = ? ORDER BY createdAt DESC, id").all(workspaceId2);
3364
+ return rows.map(rowToProposal);
3365
+ }
3366
+ markProposalApplied(id, appliedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3367
+ this.db.prepare("UPDATE knowledge_proposals SET status = 'applied', appliedAt = ? WHERE id = ?").run(appliedAt, id);
3368
+ return this.getProposal(id);
3369
+ }
3370
+ markWorkspaceCompiled(id, at = (/* @__PURE__ */ new Date()).toISOString()) {
3371
+ this.db.prepare("UPDATE knowledge_workspaces SET lastCompiledAt = ?, updatedAt = ? WHERE id = ?").run(at, at, id);
3372
+ }
3373
+ markWorkspaceLinted(id, status, at = (/* @__PURE__ */ new Date()).toISOString()) {
3374
+ this.db.prepare("UPDATE knowledge_workspaces SET status = ?, lastLintedAt = ?, updatedAt = ? WHERE id = ?").run(status, at, at, id);
3375
+ }
3376
+ };
3377
+ }
3378
+ });
3379
+
3380
+ // src/knowledge/workspace.ts
3381
+ var workspace_exports = {};
3382
+ __export(workspace_exports, {
3383
+ getKnowledgeWorkspacePaths: () => getKnowledgeWorkspacePaths,
3384
+ initializeKnowledgeWorkspace: () => initializeKnowledgeWorkspace,
3385
+ loadKnowledgeWorkspace: () => loadKnowledgeWorkspace,
3386
+ resolveKnowledgeWorkspaceFile: () => resolveKnowledgeWorkspaceFile
3387
+ });
3388
+ import { createHash } from "crypto";
3389
+ import { execFile } from "child_process";
3390
+ import { promises as fs4 } from "fs";
3391
+ import path7 from "path";
3392
+ import { promisify } from "util";
3393
+ function hash(value) {
3394
+ return createHash("sha256").update(value).digest("hex");
3395
+ }
3396
+ function workspaceId(projectId, mode) {
3397
+ return "workspace:" + hash(projectId + ":" + mode).slice(0, 24);
3398
+ }
3399
+ function localWorkspacePath(dataDir, projectId) {
3400
+ return path7.join(path7.resolve(dataDir), "knowledge-workspaces", hash(projectId).slice(0, 24));
3401
+ }
3402
+ function isWithin(root, candidate) {
3403
+ const relative2 = path7.relative(root, candidate);
3404
+ return relative2 !== "" && !relative2.startsWith(".." + path7.sep) && relative2 !== ".." && !path7.isAbsolute(relative2);
3405
+ }
3406
+ function ensureVersionedPath(projectRoot, rootPath) {
3407
+ if (!path7.isAbsolute(rootPath)) {
3408
+ throw new Error("A versioned knowledge workspace requires an explicit absolute path");
3409
+ }
3410
+ const resolvedProject = path7.resolve(projectRoot);
3411
+ const resolvedWorkspace = path7.resolve(rootPath);
3412
+ if (!isWithin(resolvedProject, resolvedWorkspace)) {
3413
+ throw new Error("A versioned knowledge workspace must live inside the Git project");
3414
+ }
3415
+ const segments = path7.relative(resolvedProject, resolvedWorkspace).split(path7.sep);
3416
+ if (segments.includes(".git") || segments.includes("node_modules") || segments.includes("dist")) {
3417
+ throw new Error("The selected knowledge workspace path is not safe for project artifacts");
3418
+ }
3419
+ return resolvedWorkspace;
3420
+ }
3421
+ async function isIgnoredByGit(projectRoot, workspaceRoot) {
3422
+ const relative2 = path7.relative(projectRoot, workspaceRoot).split(path7.sep).join("/");
3423
+ const probes = [relative2, relative2.replace(/\/+$/, "") + "/.memorix-workspace-probe"];
3424
+ for (const probe of probes) {
3425
+ try {
3426
+ await execFileAsync("git", ["-C", projectRoot, "check-ignore", "--quiet", "--no-index", "--", probe], {
3427
+ timeout: 3e3,
3428
+ windowsHide: true
3429
+ });
3430
+ return true;
3431
+ } catch (error) {
3432
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
3433
+ if (code === 1 || code === "1") continue;
3434
+ return false;
3435
+ }
3436
+ }
3437
+ return false;
3438
+ }
3439
+ async function assertNoSymlinkEscape(projectRoot, workspaceRoot) {
3440
+ const [realProject, realWorkspace] = await Promise.all([
3441
+ fs4.realpath(projectRoot),
3442
+ fs4.realpath(workspaceRoot)
3443
+ ]);
3444
+ if (!isWithin(realProject, realWorkspace)) {
3445
+ throw new Error("The selected knowledge workspace resolves outside the Git project");
3446
+ }
3447
+ }
3448
+ function getKnowledgeWorkspacePaths(workspace) {
3449
+ const root = path7.resolve(workspace.rootPath);
3450
+ return {
3451
+ root,
3452
+ pages: path7.join(root, "pages"),
3453
+ proposals: path7.join(root, "proposals"),
3454
+ workflows: path7.join(root, "workflows"),
3455
+ index: path7.join(root, "index.md"),
3456
+ log: path7.join(root, "log.md"),
3457
+ schema: path7.join(root, "schema.md")
3458
+ };
3459
+ }
3460
+ function resolveKnowledgeWorkspaceFile(workspace, relativePath) {
3461
+ if (!relativePath || path7.isAbsolute(relativePath)) {
3462
+ throw new Error("Knowledge workspace paths must be relative");
3463
+ }
3464
+ const root = path7.resolve(workspace.rootPath);
3465
+ const candidate = path7.resolve(root, relativePath);
3466
+ if (candidate === root || !isWithin(root, candidate)) {
3467
+ throw new Error("Knowledge workspace path escapes its root");
3468
+ }
3469
+ return candidate;
3470
+ }
3471
+ async function writeIfMissing(filePath, content) {
3472
+ try {
3473
+ await fs4.access(filePath);
3474
+ } catch {
3475
+ await atomicWriteFile(filePath, content);
3476
+ }
3477
+ }
3478
+ function schemaContent() {
3479
+ return [
3480
+ "# Memorix Knowledge Workspace",
3481
+ "",
3482
+ "This workspace is compiled from source-qualified Memorix claims.",
3483
+ "Topic pages are proposal-first: review and apply a proposal before relying",
3484
+ "on it as a published project knowledge page.",
3485
+ "",
3486
+ "Do not remove claim ids, evidence references, source hashes, or snapshot",
3487
+ "references from page frontmatter. Manual body edits are preserved and cause",
3488
+ "later compiler output to remain a proposal.",
3489
+ ""
3490
+ ].join("\n");
3491
+ }
3492
+ function indexContent() {
3493
+ return [
3494
+ "# Knowledge Workspace",
3495
+ "",
3496
+ "## Published pages",
3497
+ "",
3498
+ "No published pages yet.",
3499
+ "",
3500
+ "## Review queue",
3501
+ "",
3502
+ "No pending proposals.",
3503
+ ""
3504
+ ].join("\n");
3505
+ }
3506
+ function logContent() {
3507
+ return "# Knowledge log\n\n";
3508
+ }
3509
+ async function initializeKnowledgeWorkspace(input) {
3510
+ if (!input.projectId.trim()) throw new Error("Knowledge workspace project id is required");
3511
+ if (!input.dataDir.trim()) throw new Error("Knowledge workspace data directory is required");
3512
+ let rootPath;
3513
+ let projectRoot;
3514
+ if (input.mode === "local") {
3515
+ rootPath = localWorkspacePath(input.dataDir, input.projectId);
3516
+ } else {
3517
+ if (!input.projectRoot || !input.rootPath) {
3518
+ throw new Error("A versioned knowledge workspace requires projectRoot and rootPath");
3519
+ }
3520
+ projectRoot = path7.resolve(input.projectRoot);
3521
+ rootPath = ensureVersionedPath(projectRoot, input.rootPath);
3522
+ if (await isIgnoredByGit(projectRoot, rootPath)) {
3523
+ throw new Error("The selected versioned knowledge workspace is ignored by Git");
3524
+ }
3525
+ }
3526
+ const paths = getKnowledgeWorkspacePaths({ rootPath });
3527
+ await fs4.mkdir(paths.pages, { recursive: true });
3528
+ await Promise.all([
3529
+ fs4.mkdir(paths.proposals, { recursive: true }),
3530
+ fs4.mkdir(paths.workflows, { recursive: true })
3531
+ ]);
3532
+ if (projectRoot) await assertNoSymlinkEscape(projectRoot, paths.root);
3533
+ const workspace = {
3534
+ id: workspaceId(input.projectId, input.mode),
3535
+ projectId: input.projectId,
3536
+ dataDir: path7.resolve(input.dataDir),
3537
+ mode: input.mode,
3538
+ rootPath: paths.root,
3539
+ ...projectRoot ? { projectRoot } : {},
3540
+ status: "ready",
3541
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3542
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3543
+ };
3544
+ await withFileLock(paths.root, async () => {
3545
+ await writeIfMissing(paths.schema, schemaContent());
3546
+ await writeIfMissing(paths.index, indexContent());
3547
+ await writeIfMissing(paths.log, logContent());
3548
+ });
3549
+ const store = new KnowledgeWorkspaceStore();
3550
+ await store.init(input.dataDir);
3551
+ return { ...store.upsertWorkspace(workspace), dataDir: workspace.dataDir };
3552
+ }
3553
+ async function loadKnowledgeWorkspace(input) {
3554
+ const store = new KnowledgeWorkspaceStore();
3555
+ await store.init(input.dataDir);
3556
+ const workspace = store.findWorkspace(input.projectId, input.mode ?? "local");
3557
+ return workspace ? { ...workspace, dataDir: path7.resolve(input.dataDir) } : void 0;
3558
+ }
3559
+ var execFileAsync;
3560
+ var init_workspace = __esm({
3561
+ "src/knowledge/workspace.ts"() {
3562
+ "use strict";
3563
+ init_esm_shims();
3564
+ init_file_lock();
3565
+ init_workspace_store();
3566
+ execFileAsync = promisify(execFile);
3567
+ }
3568
+ });
3569
+
2745
3570
  // src/memory/consolidation.ts
2746
3571
  var consolidation_exports = {};
2747
3572
  __export(consolidation_exports, {
@@ -3174,12 +3999,12 @@ function getImportanceLevel(doc) {
3174
3999
  return TYPE_IMPORTANCE[doc.type] ?? "medium";
3175
4000
  }
3176
4001
  function calculateRelevance(doc, referenceTime) {
3177
- const now = referenceTime ?? /* @__PURE__ */ new Date();
4002
+ const now3 = referenceTime ?? /* @__PURE__ */ new Date();
3178
4003
  const importance = getImportanceLevel(doc);
3179
4004
  const base = BASE_IMPORTANCE[importance];
3180
4005
  const retention = getEffectiveRetentionDays(doc);
3181
4006
  const createdAt = new Date(doc.createdAt);
3182
- const ageDays = Math.max(0, (now.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24));
4007
+ const ageDays = Math.max(0, (now3.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24));
3183
4008
  const decayFactor = Math.exp(-ageDays / retention);
3184
4009
  const accessCount = doc.accessCount ?? 0;
3185
4010
  const accessBoost = Math.min(2, 1 + 0.1 * accessCount);
@@ -3202,14 +4027,14 @@ function rankByRelevance(docs, referenceTime) {
3202
4027
  return docs.map((doc) => calculateRelevance(doc, referenceTime)).sort((a, b) => b.totalScore - a.totalScore);
3203
4028
  }
3204
4029
  function getRetentionZone(doc, referenceTime) {
3205
- const now = referenceTime ?? /* @__PURE__ */ new Date();
4030
+ const now3 = referenceTime ?? /* @__PURE__ */ new Date();
3206
4031
  const importance = getImportanceLevel(doc);
3207
4032
  const retention = getEffectiveRetentionDays(doc);
3208
4033
  const createdAt = new Date(doc.createdAt);
3209
- const ageDays = (now.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24);
4034
+ const ageDays = (now3.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24);
3210
4035
  if (doc.lastAccessedAt) {
3211
4036
  const lastAccess = new Date(doc.lastAccessedAt);
3212
- const daysSinceAccess = (now.getTime() - lastAccess.getTime()) / (1e3 * 60 * 60 * 24);
4037
+ const daysSinceAccess = (now3.getTime() - lastAccess.getTime()) / (1e3 * 60 * 60 * 24);
3213
4038
  if (daysSinceAccess < 7) return "active";
3214
4039
  }
3215
4040
  if (isImmune(doc)) return "active";
@@ -3243,10 +4068,10 @@ function explainRetention(doc, referenceTime) {
3243
4068
  const zone = getRetentionZone(doc, referenceTime);
3244
4069
  const immuneFlag = isImmune(doc);
3245
4070
  const immunityReason = getImmunityReason(doc);
3246
- const now = referenceTime ?? /* @__PURE__ */ new Date();
4071
+ const now3 = referenceTime ?? /* @__PURE__ */ new Date();
3247
4072
  const ageDays = Math.max(
3248
4073
  0,
3249
- (now.getTime() - new Date(doc.createdAt).getTime()) / (1e3 * 60 * 60 * 24)
4074
+ (now3.getTime() - new Date(doc.createdAt).getTime()) / (1e3 * 60 * 60 * 24)
3250
4075
  );
3251
4076
  const parts = [];
3252
4077
  parts.push(`${importance} importance (${baseRetention}d base)`);
@@ -3402,16 +4227,16 @@ var init_retention = __esm({
3402
4227
  });
3403
4228
 
3404
4229
  // src/codegraph/ids.ts
3405
- import { createHash } from "crypto";
4230
+ import { createHash as createHash2 } from "crypto";
3406
4231
  function digest(input) {
3407
- return createHash("sha256").update(input).digest("hex").slice(0, 16);
4232
+ return createHash2("sha256").update(input).digest("hex").slice(0, 16);
3408
4233
  }
3409
- function normalizeCodePath(path8) {
3410
- return path8.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/");
4234
+ function normalizeCodePath(path13) {
4235
+ return path13.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/");
3411
4236
  }
3412
- function makeCodeFileId(projectId, path8) {
4237
+ function makeCodeFileId(projectId, path13) {
3413
4238
  return `file:${digest(`${projectId}
3414
- ${normalizeCodePath(path8)}`)}`;
4239
+ ${normalizeCodePath(path13)}`)}`;
3415
4240
  }
3416
4241
  function makeCodeSymbolId(input) {
3417
4242
  return `symbol:${digest([
@@ -3424,8 +4249,8 @@ function makeCodeSymbolId(input) {
3424
4249
  function makeCodeEdgeId(projectId, from, type, to) {
3425
4250
  return `edge:${digest([projectId, from, type, to].join("\n"))}`;
3426
4251
  }
3427
- function makeObservationCodeRefId(projectId, observationId, fileId, symbolId) {
3428
- return `coderef:${digest([projectId, String(observationId), fileId ?? "", symbolId ?? ""].join("\n"))}`;
4252
+ function makeObservationCodeRefId(projectId, observationId2, fileId, symbolId) {
4253
+ return `coderef:${digest([projectId, String(observationId2), fileId ?? "", symbolId ?? ""].join("\n"))}`;
3429
4254
  }
3430
4255
  var init_ids = __esm({
3431
4256
  "src/codegraph/ids.ts"() {
@@ -3439,6 +4264,7 @@ var store_exports = {};
3439
4264
  __export(store_exports, {
3440
4265
  CodeGraphStore: () => CodeGraphStore
3441
4266
  });
4267
+ import { randomUUID as randomUUID4 } from "crypto";
3442
4268
  function rowToFile(row) {
3443
4269
  return {
3444
4270
  id: row.id,
@@ -3449,7 +4275,9 @@ function rowToFile(row) {
3449
4275
  ...row.mtimeMs != null ? { mtimeMs: row.mtimeMs } : {},
3450
4276
  ...row.sizeBytes != null ? { sizeBytes: row.sizeBytes } : {},
3451
4277
  indexedAt: row.indexedAt,
3452
- ...row.gitCommit ? { gitCommit: row.gitCommit } : {}
4278
+ ...row.gitCommit ? { gitCommit: row.gitCommit } : {},
4279
+ ...row.snapshotId ? { snapshotId: row.snapshotId } : {},
4280
+ ...row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}
3453
4281
  };
3454
4282
  }
3455
4283
  function rowToSymbol(row) {
@@ -3466,7 +4294,9 @@ function rowToSymbol(row) {
3466
4294
  ...row.signature ? { signature: row.signature } : {},
3467
4295
  ...row.contentHash ? { contentHash: row.contentHash } : {},
3468
4296
  indexedAt: row.indexedAt,
3469
- stale: !!row.stale
4297
+ stale: !!row.stale,
4298
+ ...row.snapshotId ? { snapshotId: row.snapshotId } : {},
4299
+ ...row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}
3470
4300
  };
3471
4301
  }
3472
4302
  function rowToEdge(row) {
@@ -3480,7 +4310,9 @@ function rowToEdge(row) {
3480
4310
  type: row.type,
3481
4311
  confidence: row.confidence,
3482
4312
  ...row.evidence ? { evidence: row.evidence } : {},
3483
- indexedAt: row.indexedAt
4313
+ indexedAt: row.indexedAt,
4314
+ ...row.snapshotId ? { snapshotId: row.snapshotId } : {},
4315
+ ...row.sourceEpoch != null ? { sourceEpoch: Number(row.sourceEpoch) } : {}
3484
4316
  };
3485
4317
  }
3486
4318
  function rowToRef(row) {
@@ -3495,7 +4327,52 @@ function rowToRef(row) {
3495
4327
  status: row.status,
3496
4328
  ...row.reason ? { reason: row.reason } : {},
3497
4329
  createdAt: row.createdAt,
3498
- ...row.updatedAt ? { updatedAt: row.updatedAt } : {}
4330
+ ...row.updatedAt ? { updatedAt: row.updatedAt } : {},
4331
+ ...row.snapshotId ? { snapshotId: row.snapshotId } : {}
4332
+ };
4333
+ }
4334
+ function parseCompleteness(raw) {
4335
+ try {
4336
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
4337
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid completeness");
4338
+ return {
4339
+ scannedFiles: Number(parsed.scannedFiles) || 0,
4340
+ maxFiles: Number(parsed.maxFiles) || 0,
4341
+ changedFiles: Number(parsed.changedFiles) || 0,
4342
+ unchangedFiles: Number(parsed.unchangedFiles) || 0,
4343
+ metadataOnlyFiles: Number(parsed.metadataOnlyFiles) || 0,
4344
+ removedFiles: Number(parsed.removedFiles) || 0,
4345
+ skippedOversizedFiles: Number(parsed.skippedOversizedFiles) || 0,
4346
+ unreadableFiles: Number(parsed.unreadableFiles) || 0,
4347
+ removalScanDeferred: Boolean(parsed.removalScanDeferred)
4348
+ };
4349
+ } catch {
4350
+ return {
4351
+ scannedFiles: 0,
4352
+ maxFiles: 0,
4353
+ changedFiles: 0,
4354
+ unchangedFiles: 0,
4355
+ metadataOnlyFiles: 0,
4356
+ removedFiles: 0,
4357
+ skippedOversizedFiles: 0,
4358
+ unreadableFiles: 0,
4359
+ removalScanDeferred: false
4360
+ };
4361
+ }
4362
+ }
4363
+ function rowToSnapshot(row) {
4364
+ return {
4365
+ id: row.id,
4366
+ projectId: row.projectId,
4367
+ provider: row.provider,
4368
+ ...row.baseRevision ? { baseRevision: row.baseRevision } : {},
4369
+ worktreeFingerprint: row.worktreeFingerprint,
4370
+ worktreeState: row.worktreeState,
4371
+ changedPathCount: Number(row.changedPathCount),
4372
+ indexedAt: row.indexedAt,
4373
+ sourceEpoch: Number(row.sourceEpoch),
4374
+ completeness: parseCompleteness(row.completenessJson),
4375
+ ...row.previousSnapshotId ? { previousSnapshotId: row.previousSnapshotId } : {}
3499
4376
  };
3500
4377
  }
3501
4378
  var CodeGraphStore;
@@ -3507,9 +4384,16 @@ var init_store = __esm({
3507
4384
  init_ids();
3508
4385
  CodeGraphStore = class {
3509
4386
  db = null;
4387
+ dataDir = null;
3510
4388
  async init(dataDir) {
4389
+ this.dataDir = dataDir;
3511
4390
  this.db = getDatabase(dataDir);
3512
4391
  }
4392
+ /** Data directory shared with the rest of the local project evidence stores. */
4393
+ getDataDir() {
4394
+ if (!this.dataDir) throw new Error("CodeGraphStore is not initialized");
4395
+ return this.dataDir;
4396
+ }
3513
4397
  upsertFiles(files) {
3514
4398
  if (files.length === 0) return;
3515
4399
  const stmt = this.db.prepare(`
@@ -3770,9 +4654,9 @@ var init_store = __esm({
3770
4654
  if (refs.length === 0) return;
3771
4655
  const stmt = this.db.prepare(`
3772
4656
  INSERT OR REPLACE INTO observation_code_refs
3773
- (id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt)
4657
+ (id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt, snapshotId)
3774
4658
  VALUES
3775
- (@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt)
4659
+ (@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt, @snapshotId)
3776
4660
  `);
3777
4661
  const tx = this.db.transaction((items) => {
3778
4662
  for (const ref of items) {
@@ -3787,25 +4671,26 @@ var init_store = __esm({
3787
4671
  status: ref.status,
3788
4672
  reason: ref.reason ?? null,
3789
4673
  createdAt: ref.createdAt,
3790
- updatedAt: ref.updatedAt ?? null
4674
+ updatedAt: ref.updatedAt ?? null,
4675
+ snapshotId: ref.snapshotId ?? this.latestSnapshot(ref.projectId)?.id ?? null
3791
4676
  });
3792
4677
  }
3793
4678
  });
3794
4679
  tx(refs);
3795
4680
  }
3796
- replaceObservationRefs(projectId, observationId, refs) {
4681
+ replaceObservationRefs(projectId, observationId2, refs) {
3797
4682
  const deleteRefs = this.db.prepare(`
3798
4683
  DELETE FROM observation_code_refs
3799
4684
  WHERE projectId = ? AND observationId = ?
3800
4685
  `);
3801
4686
  const insertRef = this.db.prepare(`
3802
4687
  INSERT OR REPLACE INTO observation_code_refs
3803
- (id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt)
4688
+ (id, projectId, observationId, fileId, symbolId, capturedFileHash, capturedSymbolHash, status, reason, createdAt, updatedAt, snapshotId)
3804
4689
  VALUES
3805
- (@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt)
4690
+ (@id, @projectId, @observationId, @fileId, @symbolId, @capturedFileHash, @capturedSymbolHash, @status, @reason, @createdAt, @updatedAt, @snapshotId)
3806
4691
  `);
3807
4692
  const tx = this.db.transaction(() => {
3808
- deleteRefs.run(projectId, observationId);
4693
+ deleteRefs.run(projectId, observationId2);
3809
4694
  for (const ref of refs) {
3810
4695
  insertRef.run({
3811
4696
  id: ref.id,
@@ -3818,14 +4703,15 @@ var init_store = __esm({
3818
4703
  status: ref.status,
3819
4704
  reason: ref.reason ?? null,
3820
4705
  createdAt: ref.createdAt,
3821
- updatedAt: ref.updatedAt ?? null
4706
+ updatedAt: ref.updatedAt ?? null,
4707
+ snapshotId: ref.snapshotId ?? this.latestSnapshot(ref.projectId)?.id ?? null
3822
4708
  });
3823
4709
  }
3824
4710
  });
3825
4711
  tx();
3826
4712
  }
3827
- getFile(projectId, path8) {
3828
- const row = this.db.prepare(`SELECT * FROM code_files WHERE projectId = ? AND path = ?`).get(projectId, normalizeCodePath(path8));
4713
+ getFile(projectId, path13) {
4714
+ const row = this.db.prepare(`SELECT * FROM code_files WHERE projectId = ? AND path = ?`).get(projectId, normalizeCodePath(path13));
3829
4715
  return row ? rowToFile(row) : null;
3830
4716
  }
3831
4717
  listFiles(projectId) {
@@ -3884,12 +4770,12 @@ var init_store = __esm({
3884
4770
  listEdges(projectId) {
3885
4771
  return this.db.prepare(`SELECT * FROM code_edges WHERE projectId = ? ORDER BY type, id`).all(projectId).map(rowToEdge);
3886
4772
  }
3887
- listObservationRefs(projectId, observationId) {
4773
+ listObservationRefs(projectId, observationId2) {
3888
4774
  return this.db.prepare(`
3889
4775
  SELECT * FROM observation_code_refs
3890
4776
  WHERE projectId = ? AND observationId = ?
3891
4777
  ORDER BY status, id
3892
- `).all(projectId, observationId).map(rowToRef);
4778
+ `).all(projectId, observationId2).map(rowToRef);
3893
4779
  }
3894
4780
  listProjectObservationRefs(projectId) {
3895
4781
  return this.db.prepare(`
@@ -3907,18 +4793,152 @@ var init_store = __esm({
3907
4793
  ORDER BY symbols.path, symbols.startLine
3908
4794
  `).all(projectId, projectId).map(rowToSymbol);
3909
4795
  }
4796
+ latestSnapshot(projectId) {
4797
+ const row = this.db.prepare(
4798
+ "SELECT * FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT 1"
4799
+ ).get(projectId);
4800
+ return row ? rowToSnapshot(row) : void 0;
4801
+ }
4802
+ listSnapshots(projectId, limit = 20) {
4803
+ const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(200, Math.floor(limit))) : 20;
4804
+ return this.db.prepare(
4805
+ "SELECT * FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT ?"
4806
+ ).all(projectId, safeLimit).map(rowToSnapshot);
4807
+ }
4808
+ /**
4809
+ * Record a completed scan and mark all current structural facts with its
4810
+ * epoch. The snapshot is written only after refresh reconciliation succeeds,
4811
+ * so an interrupted scan cannot be advertised as complete.
4812
+ */
4813
+ recordCodeStateSnapshot(input) {
4814
+ const id = randomUUID4();
4815
+ let snapshot;
4816
+ const tx = this.db.transaction(() => {
4817
+ const previous = this.db.prepare(
4818
+ "SELECT id, sourceEpoch FROM code_state_snapshots WHERE projectId = ? ORDER BY sourceEpoch DESC LIMIT 1"
4819
+ ).get(input.projectId);
4820
+ const sourceEpoch = Number(previous?.sourceEpoch ?? 0) + 1;
4821
+ const previousSnapshotId = previous?.id;
4822
+ this.db.prepare(
4823
+ "INSERT INTO code_state_snapshots (id, projectId, provider, baseRevision, worktreeFingerprint, worktreeState, changedPathCount, indexedAt, sourceEpoch, completenessJson, previousSnapshotId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4824
+ ).run(
4825
+ id,
4826
+ input.projectId,
4827
+ input.provider,
4828
+ input.baseRevision ?? null,
4829
+ input.worktreeFingerprint,
4830
+ input.worktreeState,
4831
+ input.changedPathCount,
4832
+ input.indexedAt,
4833
+ sourceEpoch,
4834
+ JSON.stringify(input.completeness),
4835
+ previousSnapshotId ?? null
4836
+ );
4837
+ this.db.prepare(
4838
+ "UPDATE code_files SET snapshotId = ?, sourceEpoch = ?, gitCommit = COALESCE(?, gitCommit) WHERE projectId = ?"
4839
+ ).run(id, sourceEpoch, input.baseRevision ?? null, input.projectId);
4840
+ this.db.prepare(
4841
+ "UPDATE code_symbols SET snapshotId = ?, sourceEpoch = ? WHERE projectId = ? AND stale = 0"
4842
+ ).run(id, sourceEpoch, input.projectId);
4843
+ this.db.prepare(
4844
+ "UPDATE code_edges SET snapshotId = ?, sourceEpoch = ? WHERE projectId = ?"
4845
+ ).run(id, sourceEpoch, input.projectId);
4846
+ this.db.prepare(
4847
+ "UPDATE observation_code_refs SET snapshotId = ? WHERE projectId = ? AND status = 'current'"
4848
+ ).run(id, input.projectId);
4849
+ snapshot = {
4850
+ ...input,
4851
+ id,
4852
+ sourceEpoch,
4853
+ ...previousSnapshotId ? { previousSnapshotId } : {}
4854
+ };
4855
+ });
4856
+ tx();
4857
+ return snapshot;
4858
+ }
3910
4859
  status(projectId) {
3911
4860
  const files = this.db.prepare(`SELECT COUNT(*) AS count FROM code_files WHERE projectId = ?`).get(projectId).count;
3912
4861
  const symbols = this.db.prepare(`SELECT COUNT(*) AS count FROM code_symbols WHERE projectId = ? AND stale = 0`).get(projectId).count;
3913
4862
  const edges = this.db.prepare(`SELECT COUNT(*) AS count FROM code_edges WHERE projectId = ?`).get(projectId).count;
3914
4863
  const refs = this.db.prepare(`SELECT COUNT(*) AS count FROM observation_code_refs WHERE projectId = ?`).get(projectId).count;
3915
4864
  const latest = this.db.prepare(`SELECT MAX(indexedAt) AS indexedAt FROM code_files WHERE projectId = ?`).get(projectId);
3916
- return { provider: "lite", files, symbols, edges, refs, ...latest?.indexedAt ? { indexedAt: latest.indexedAt } : {} };
4865
+ const latestSnapshot = this.latestSnapshot(projectId);
4866
+ return {
4867
+ provider: "lite",
4868
+ files,
4869
+ symbols,
4870
+ edges,
4871
+ refs,
4872
+ ...latest?.indexedAt ? { indexedAt: latest.indexedAt } : {},
4873
+ ...latestSnapshot ? { latestSnapshot } : {}
4874
+ };
3917
4875
  }
3918
4876
  };
3919
4877
  }
3920
4878
  });
3921
4879
 
4880
+ // src/codegraph/code-state.ts
4881
+ import { execFile as execFile2 } from "child_process";
4882
+ import { createHash as createHash3 } from "crypto";
4883
+ import { promisify as promisify2 } from "util";
4884
+ async function runGit(rootPath, args) {
4885
+ try {
4886
+ const result = await execFileAsync2("git", ["-C", rootPath, ...args], {
4887
+ encoding: "buffer",
4888
+ windowsHide: true,
4889
+ timeout: GIT_TIMEOUT_MS,
4890
+ maxBuffer: GIT_MAX_BUFFER_BYTES
4891
+ });
4892
+ const output = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "");
4893
+ return { ok: true, output };
4894
+ } catch {
4895
+ return { ok: false, output: Buffer.alloc(0) };
4896
+ }
4897
+ }
4898
+ function changedPathCount(status) {
4899
+ const entries = status.toString("utf8").split("\0");
4900
+ let count2 = 0;
4901
+ for (let index = 0; index < entries.length; index++) {
4902
+ const entry = entries[index];
4903
+ if (!entry) continue;
4904
+ count2++;
4905
+ const code = entry.slice(0, 2);
4906
+ if (code.includes("R") || code.includes("C")) index++;
4907
+ }
4908
+ return count2;
4909
+ }
4910
+ function fingerprint(baseRevision, status) {
4911
+ return createHash3("sha256").update(baseRevision ?? "").update("\0").update(status).digest("hex");
4912
+ }
4913
+ async function collectCodeStateSnapshot(input) {
4914
+ const [revision, status] = await Promise.all([
4915
+ runGit(input.projectRoot, ["rev-parse", "HEAD"]),
4916
+ runGit(input.projectRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=normal"])
4917
+ ]);
4918
+ const baseRevision = revision.ok ? revision.output.toString("utf8").trim() || void 0 : void 0;
4919
+ const worktreeState = status.ok ? status.output.length > 0 ? "dirty" : "clean" : "unavailable";
4920
+ return {
4921
+ projectId: input.projectId,
4922
+ provider: input.provider,
4923
+ ...baseRevision ? { baseRevision } : {},
4924
+ worktreeFingerprint: fingerprint(baseRevision, status.ok ? status.output : Buffer.alloc(0)),
4925
+ worktreeState,
4926
+ changedPathCount: status.ok ? changedPathCount(status.output) : 0,
4927
+ indexedAt: input.indexedAt,
4928
+ completeness: input.completeness
4929
+ };
4930
+ }
4931
+ var execFileAsync2, GIT_TIMEOUT_MS, GIT_MAX_BUFFER_BYTES;
4932
+ var init_code_state = __esm({
4933
+ "src/codegraph/code-state.ts"() {
4934
+ "use strict";
4935
+ init_esm_shims();
4936
+ execFileAsync2 = promisify2(execFile2);
4937
+ GIT_TIMEOUT_MS = 3e3;
4938
+ GIT_MAX_BUFFER_BYTES = 256 * 1024;
4939
+ }
4940
+ });
4941
+
3922
4942
  // src/codegraph/exclude.ts
3923
4943
  function normalizeCodeGraphExcludePatterns(exclude) {
3924
4944
  return [.../* @__PURE__ */ new Set([
@@ -3926,30 +4946,30 @@ function normalizeCodeGraphExcludePatterns(exclude) {
3926
4946
  ...(exclude ?? []).map((pattern) => pattern.trim()).filter(Boolean)
3927
4947
  ])];
3928
4948
  }
3929
- function isCodeGraphExcludedPath(path8, exclude) {
3930
- const normalized = normalizeCodePath2(path8);
4949
+ function isCodeGraphExcludedPath(path13, exclude) {
4950
+ const normalized = normalizeCodePath2(path13);
3931
4951
  return normalizeCodeGraphExcludePatterns(exclude).some((pattern) => matchesPattern(normalized, normalizeCodePath2(pattern)));
3932
4952
  }
3933
- function normalizeCodePath2(path8) {
3934
- return path8.replace(/\\/g, "/").replace(/^\.\/+/, "");
4953
+ function normalizeCodePath2(path13) {
4954
+ return path13.replace(/\\/g, "/").replace(/^\.\/+/, "");
3935
4955
  }
3936
- function matchesPattern(path8, pattern) {
4956
+ function matchesPattern(path13, pattern) {
3937
4957
  if (pattern.endsWith("/**")) {
3938
4958
  const base = pattern.slice(0, -3);
3939
4959
  if (base.startsWith("**/")) {
3940
4960
  const suffix = base.slice(3);
3941
- return path8 === suffix || path8.endsWith(`/${suffix}`) || path8.includes(`/${suffix}/`);
4961
+ return path13 === suffix || path13.endsWith(`/${suffix}`) || path13.includes(`/${suffix}/`);
3942
4962
  }
3943
4963
  if (!base.includes("/")) {
3944
- return path8 === base || path8.startsWith(`${base}/`) || path8.includes(`/${base}/`);
4964
+ return path13 === base || path13.startsWith(`${base}/`) || path13.includes(`/${base}/`);
3945
4965
  }
3946
- return path8 === base || path8.startsWith(`${base}/`);
4966
+ return path13 === base || path13.startsWith(`${base}/`);
3947
4967
  }
3948
4968
  if (pattern.startsWith("**/")) {
3949
4969
  const suffix = pattern.slice(3);
3950
- return path8 === suffix || path8.endsWith(`/${suffix}`);
4970
+ return path13 === suffix || path13.endsWith(`/${suffix}`);
3951
4971
  }
3952
- return path8 === pattern || path8.startsWith(`${pattern}/`);
4972
+ return path13 === pattern || path13.startsWith(`${pattern}/`);
3953
4973
  }
3954
4974
  var DEFAULT_CODEGRAPH_EXCLUDES;
3955
4975
  var init_exclude = __esm({
@@ -3988,18 +5008,18 @@ __export(lite_provider_exports, {
3988
5008
  indexProjectLite: () => indexProjectLite,
3989
5009
  refreshProjectLite: () => refreshProjectLite
3990
5010
  });
3991
- import { createHash as createHash2 } from "crypto";
5011
+ import { createHash as createHash4 } from "crypto";
3992
5012
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3993
5013
  import { join as join2, relative } from "path";
3994
5014
  function hashText(text) {
3995
- return createHash2("sha256").update(text).digest("hex");
5015
+ return createHash4("sha256").update(text).digest("hex");
3996
5016
  }
3997
- function extension(path8) {
3998
- const index = path8.lastIndexOf(".");
3999
- return index === -1 ? "" : path8.slice(index);
5017
+ function extension(path13) {
5018
+ const index = path13.lastIndexOf(".");
5019
+ return index === -1 ? "" : path13.slice(index);
4000
5020
  }
4001
- function languageForPath(path8) {
4002
- const ext = extension(path8);
5021
+ function languageForPath(path13) {
5022
+ const ext = extension(path13);
4003
5023
  return LANGUAGE_BY_EXTENSION.get(ext) ?? "unknown";
4004
5024
  }
4005
5025
  function resolveMaxFileBytes(value) {
@@ -4117,17 +5137,22 @@ function indexFileLite(projectId, projectRoot, abs, indexedAt, fileStat) {
4117
5137
  path: rel,
4118
5138
  language: languageForPath(rel),
4119
5139
  contentHash: hashText(text),
4120
- mtimeMs: Math.round(Number(stat.mtimeMs)),
5140
+ // Preserve filesystem precision. Rounding here made same-size edits in
5141
+ // a short Windows timestamp window indistinguishable to refresh.
5142
+ mtimeMs: Number(stat.mtimeMs),
4121
5143
  sizeBytes: Number(stat.size),
4122
5144
  indexedAt
4123
5145
  };
4124
5146
  return {
4125
- file,
4126
- symbols: extractSymbols(projectId, file, text, indexedAt),
4127
- edges: extractImportEdges(projectId, file, text, indexedAt)
5147
+ delta: {
5148
+ file,
5149
+ symbols: extractSymbols(projectId, file, text, indexedAt),
5150
+ edges: extractImportEdges(projectId, file, text, indexedAt)
5151
+ },
5152
+ unreadable: false
4128
5153
  };
4129
5154
  } catch {
4130
- return void 0;
5155
+ return { unreadable: true };
4131
5156
  }
4132
5157
  }
4133
5158
  async function indexProjectLite(options) {
@@ -4140,24 +5165,29 @@ async function indexProjectLite(options) {
4140
5165
  const symbols = [];
4141
5166
  const edges = [];
4142
5167
  let skippedOversizedFiles = 0;
5168
+ let unreadableFiles = 0;
4143
5169
  for (const abs of paths) {
4144
5170
  let stat;
4145
5171
  try {
4146
5172
  stat = statSync2(abs);
4147
5173
  } catch {
5174
+ unreadableFiles++;
4148
5175
  continue;
4149
5176
  }
4150
5177
  if (Number(stat.size) > maxFileBytes) {
4151
5178
  skippedOversizedFiles++;
4152
5179
  continue;
4153
5180
  }
4154
- const delta = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
4155
- if (!delta) continue;
4156
- files.push(delta.file);
4157
- symbols.push(...delta.symbols);
4158
- edges.push(...delta.edges);
5181
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
5182
+ if (!attempt.delta) {
5183
+ if (attempt.unreadable) unreadableFiles++;
5184
+ continue;
5185
+ }
5186
+ files.push(attempt.delta.file);
5187
+ symbols.push(...attempt.delta.symbols);
5188
+ edges.push(...attempt.delta.edges);
4159
5189
  }
4160
- return { files, symbols, edges, skippedOversizedFiles };
5190
+ return { files, symbols, edges, skippedOversizedFiles, unreadableFiles };
4161
5191
  }
4162
5192
  async function refreshProjectLite(store, options) {
4163
5193
  const exclude = normalizeCodeGraphExcludePatterns(options.exclude);
@@ -4172,29 +5202,35 @@ async function refreshProjectLite(store, options) {
4172
5202
  const oversizedExistingFileIds = /* @__PURE__ */ new Set();
4173
5203
  let unchangedFiles = 0;
4174
5204
  let skippedOversizedFiles = 0;
5205
+ let unreadableFiles = 0;
4175
5206
  for (const abs of paths) {
4176
5207
  const rel = normalizeCodePath(relative(options.projectRoot, abs));
5208
+ seenPaths.add(rel);
4177
5209
  let stat;
4178
5210
  try {
4179
5211
  stat = statSync2(abs);
4180
5212
  } catch {
5213
+ unreadableFiles++;
4181
5214
  continue;
4182
5215
  }
4183
- seenPaths.add(rel);
4184
5216
  const existing = existingByPath.get(rel);
4185
- const roundedMtime = Math.round(Number(stat.mtimeMs));
5217
+ const mtimeMs = Number(stat.mtimeMs);
4186
5218
  const sizeBytes = Number(stat.size);
4187
5219
  if (sizeBytes > maxFileBytes) {
4188
5220
  skippedOversizedFiles++;
4189
5221
  if (existing) oversizedExistingFileIds.add(existing.id);
4190
5222
  continue;
4191
5223
  }
4192
- if (existing && existing.mtimeMs === roundedMtime && existing.sizeBytes === sizeBytes) {
5224
+ if (existing && existing.mtimeMs === mtimeMs && existing.sizeBytes === sizeBytes) {
4193
5225
  unchangedFiles++;
4194
5226
  continue;
4195
5227
  }
4196
- const delta = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
4197
- if (!delta) continue;
5228
+ const attempt = indexFileLite(options.projectId, options.projectRoot, abs, indexedAt, stat);
5229
+ if (!attempt.delta) {
5230
+ if (attempt.unreadable) unreadableFiles++;
5231
+ continue;
5232
+ }
5233
+ const delta = attempt.delta;
4198
5234
  if (existing?.contentHash === delta.file.contentHash) {
4199
5235
  metadataOnly.push(delta.file);
4200
5236
  unchangedFiles++;
@@ -4212,23 +5248,44 @@ async function refreshProjectLite(store, options) {
4212
5248
  metadataOnly,
4213
5249
  removedFileIds
4214
5250
  });
4215
- return {
5251
+ const completeness = {
4216
5252
  scannedFiles: paths.length,
5253
+ maxFiles,
4217
5254
  changedFiles: changed.length,
4218
5255
  unchangedFiles,
4219
5256
  metadataOnlyFiles: metadataOnly.length,
4220
5257
  removedFiles: removedFileIds.length,
4221
- indexedSymbols: changed.reduce((total, delta) => total + delta.symbols.length, 0),
4222
- indexedEdges: changed.reduce((total, delta) => total + delta.edges.length, 0),
4223
5258
  skippedOversizedFiles,
5259
+ unreadableFiles,
4224
5260
  removalScanDeferred
4225
5261
  };
4226
- }
4227
- var DEFAULT_CODEGRAPH_MAX_FILE_BYTES, LANGUAGE_BY_EXTENSION, SUPPORTED_EXTENSIONS, identifier, languageProfiles;
4228
- var init_lite_provider = __esm({
5262
+ const snapshot = store.recordCodeStateSnapshot(await collectCodeStateSnapshot({
5263
+ projectId: options.projectId,
5264
+ projectRoot: options.projectRoot,
5265
+ provider: "lite",
5266
+ indexedAt,
5267
+ completeness
5268
+ }));
5269
+ return {
5270
+ scannedFiles: paths.length,
5271
+ changedFiles: changed.length,
5272
+ unchangedFiles,
5273
+ metadataOnlyFiles: metadataOnly.length,
5274
+ removedFiles: removedFileIds.length,
5275
+ indexedSymbols: changed.reduce((total, delta) => total + delta.symbols.length, 0),
5276
+ indexedEdges: changed.reduce((total, delta) => total + delta.edges.length, 0),
5277
+ skippedOversizedFiles,
5278
+ unreadableFiles,
5279
+ removalScanDeferred,
5280
+ snapshot
5281
+ };
5282
+ }
5283
+ var DEFAULT_CODEGRAPH_MAX_FILE_BYTES, LANGUAGE_BY_EXTENSION, SUPPORTED_EXTENSIONS, identifier, languageProfiles;
5284
+ var init_lite_provider = __esm({
4229
5285
  "src/codegraph/lite-provider.ts"() {
4230
5286
  "use strict";
4231
5287
  init_esm_shims();
5288
+ init_code_state();
4232
5289
  init_ids();
4233
5290
  init_exclude();
4234
5291
  DEFAULT_CODEGRAPH_MAX_FILE_BYTES = 2 * 1024 * 1024;
@@ -4382,138 +5439,2274 @@ function codeIdentifierCandidates(text) {
4382
5439
  for (const match of text.matchAll(/\b([A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?)\s*\(/g)) explicit.add(match[1]);
4383
5440
  return identifierCandidates(text).filter((candidate) => explicit.has(candidate) || candidate.includes("::") || /[!?=]$/.test(candidate) || /^[A-Z]/.test(candidate) || /[_$]/.test(candidate) || /[a-z0-9][A-Z]/.test(candidate) || /[A-Z]{2}/.test(candidate));
4384
5441
  }
4385
- function fileRef(projectId, obs, file) {
5442
+ function fileRef(projectId, obs, file) {
5443
+ return {
5444
+ id: makeObservationCodeRefId(projectId, obs.id, file.id),
5445
+ projectId,
5446
+ observationId: obs.id,
5447
+ fileId: file.id,
5448
+ capturedFileHash: file.contentHash,
5449
+ status: "current",
5450
+ reason: "bound by file path",
5451
+ createdAt: obs.createdAt
5452
+ };
5453
+ }
5454
+ function symbolRef(projectId, obs, file, symbol) {
5455
+ return {
5456
+ id: makeObservationCodeRefId(projectId, obs.id, file.id, symbol.id),
5457
+ projectId,
5458
+ observationId: obs.id,
5459
+ fileId: file.id,
5460
+ symbolId: symbol.id,
5461
+ capturedFileHash: file.contentHash,
5462
+ ...symbol.contentHash ? { capturedSymbolHash: symbol.contentHash } : {},
5463
+ status: "current",
5464
+ reason: "bound by symbol mention",
5465
+ createdAt: obs.createdAt
5466
+ };
5467
+ }
5468
+ function resolveObservationCodeRefs(obs, lookup) {
5469
+ const refs = /* @__PURE__ */ new Map();
5470
+ const text = observationText(obs);
5471
+ const candidateFiles = /* @__PURE__ */ new Map();
5472
+ for (const rawPath of obs.filesModified ?? []) {
5473
+ const file = lookup.findFile(normalizeCodePath(rawPath));
5474
+ if (!file) continue;
5475
+ candidateFiles.set(file.id, file);
5476
+ const ref = fileRef(obs.projectId, obs, file);
5477
+ refs.set(ref.id, ref);
5478
+ }
5479
+ const hintedFileIds = new Set(candidateFiles.keys());
5480
+ const symbolNames = hintedFileIds.size > 0 ? identifierCandidates(text) : codeIdentifierCandidates(text);
5481
+ const symbols = lookup.findSymbols(symbolNames, [...hintedFileIds]);
5482
+ const symbolsByName = /* @__PURE__ */ new Map();
5483
+ for (const symbol of symbols) {
5484
+ const group = symbolsByName.get(symbol.name) ?? [];
5485
+ group.push(symbol);
5486
+ symbolsByName.set(symbol.name, group);
5487
+ }
5488
+ for (const group of symbolsByName.values()) {
5489
+ const candidates = group.length === 1 ? group : [];
5490
+ for (const symbol of candidates) {
5491
+ if (!mentionsSymbol(text, symbol)) continue;
5492
+ const file = candidateFiles.get(symbol.fileId) ?? lookup.findFile(symbol.path);
5493
+ if (!file) continue;
5494
+ candidateFiles.set(file.id, file);
5495
+ const ref = symbolRef(obs.projectId, obs, file, symbol);
5496
+ refs.set(ref.id, ref);
5497
+ }
5498
+ }
5499
+ return [...refs.values()];
5500
+ }
5501
+ function createStoreLookup(store, projectId) {
5502
+ return {
5503
+ findFile: (path13) => store.getFile(projectId, path13) ?? void 0,
5504
+ findSymbols: (names, hintedFileIds) => store.findSymbolsByNames(projectId, names, hintedFileIds)
5505
+ };
5506
+ }
5507
+ function createSnapshotLookup(files, symbols) {
5508
+ const filesByPath = new Map(files.map((file) => [normalizeCodePath(file.path), file]));
5509
+ const symbolsByName = /* @__PURE__ */ new Map();
5510
+ for (const symbol of symbols) {
5511
+ const group = symbolsByName.get(symbol.name) ?? [];
5512
+ group.push(symbol);
5513
+ symbolsByName.set(symbol.name, group);
5514
+ }
5515
+ return {
5516
+ findFile: (path13) => filesByPath.get(normalizeCodePath(path13)),
5517
+ findSymbols: (names, hintedFileIds) => {
5518
+ const hinted = new Set(hintedFileIds);
5519
+ const found = [];
5520
+ for (const name of names) {
5521
+ const candidates = symbolsByName.get(name) ?? [];
5522
+ found.push(...hinted.size > 0 ? candidates.filter((symbol) => hinted.has(symbol.fileId)) : candidates);
5523
+ }
5524
+ return found;
5525
+ }
5526
+ };
5527
+ }
5528
+ async function bindObservationToCode(store, obs) {
5529
+ const refs = resolveObservationCodeRefs(obs, createStoreLookup(store, obs.projectId));
5530
+ store.replaceObservationRefs(obs.projectId, obs.id, refs);
5531
+ return refs;
5532
+ }
5533
+ async function backfillMissingObservationCodeRefs(store, observations2) {
5534
+ let observationsBackfilled = 0;
5535
+ let refsBackfilled = 0;
5536
+ const boundObservationIdsByProject = /* @__PURE__ */ new Map();
5537
+ const lookupByProject = /* @__PURE__ */ new Map();
5538
+ for (const projectId of new Set(observations2.map((observation) => observation.projectId))) {
5539
+ boundObservationIdsByProject.set(
5540
+ projectId,
5541
+ new Set(store.listProjectObservationRefs(projectId).map((ref) => ref.observationId))
5542
+ );
5543
+ lookupByProject.set(
5544
+ projectId,
5545
+ createSnapshotLookup(store.listFiles(projectId), store.listSymbols(projectId))
5546
+ );
5547
+ }
5548
+ const refsToInsert = [];
5549
+ for (const observation of observations2) {
5550
+ if (boundObservationIdsByProject.get(observation.projectId)?.has(observation.id)) continue;
5551
+ if ((observation.filesModified?.length ?? 0) === 0 && codeIdentifierCandidates(observationText(observation)).length === 0) {
5552
+ continue;
5553
+ }
5554
+ const lookup = lookupByProject.get(observation.projectId);
5555
+ if (!lookup) continue;
5556
+ const refs = resolveObservationCodeRefs(observation, lookup);
5557
+ if (refs.length === 0) continue;
5558
+ observationsBackfilled += 1;
5559
+ refsBackfilled += refs.length;
5560
+ refsToInsert.push(...refs);
5561
+ }
5562
+ store.upsertObservationRefs(refsToInsert);
5563
+ return {
5564
+ observationsScanned: observations2.length,
5565
+ observationsBackfilled,
5566
+ refsBackfilled
5567
+ };
5568
+ }
5569
+ var init_binder = __esm({
5570
+ "src/codegraph/binder.ts"() {
5571
+ "use strict";
5572
+ init_esm_shims();
5573
+ init_ids();
5574
+ }
5575
+ });
5576
+
5577
+ // src/knowledge/claim-store.ts
5578
+ var claim_store_exports = {};
5579
+ __export(claim_store_exports, {
5580
+ ClaimStore: () => ClaimStore
5581
+ });
5582
+ import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
5583
+ function optionalText2(value) {
5584
+ return typeof value === "string" && value ? value : void 0;
5585
+ }
5586
+ function rowToClaim(row) {
5587
+ return {
5588
+ id: row.id,
5589
+ projectId: row.projectId,
5590
+ subject: row.subject,
5591
+ predicate: row.predicate,
5592
+ objectValue: row.objectValue,
5593
+ scope: row.scope,
5594
+ claimKey: row.claimKey,
5595
+ conflictKey: row.conflictKey,
5596
+ status: row.status,
5597
+ confidence: Number(row.confidence),
5598
+ observedAt: row.observedAt,
5599
+ ...optionalText2(row.validFrom) ? { validFrom: row.validFrom } : {},
5600
+ ...optionalText2(row.validTo) ? { validTo: row.validTo } : {},
5601
+ ...optionalText2(row.supersededBy) ? { supersededBy: row.supersededBy } : {},
5602
+ reviewState: row.reviewState,
5603
+ origin: row.origin,
5604
+ createdAt: row.createdAt,
5605
+ updatedAt: row.updatedAt
5606
+ };
5607
+ }
5608
+ function rowToEvidence(row) {
5609
+ return {
5610
+ id: row.id,
5611
+ claimId: row.claimId,
5612
+ evidenceKind: row.evidenceKind,
5613
+ evidenceId: row.evidenceId,
5614
+ relation: row.relation,
5615
+ ...optionalText2(row.snapshotId) ? { snapshotId: row.snapshotId } : {},
5616
+ ...optionalText2(row.locator) ? { locator: row.locator } : {},
5617
+ ...optionalText2(row.capturedHash) ? { capturedHash: row.capturedHash } : {},
5618
+ createdAt: row.createdAt
5619
+ };
5620
+ }
5621
+ function rowToEvent(row) {
5622
+ return {
5623
+ id: row.id,
5624
+ projectId: row.projectId,
5625
+ claimId: row.claimId,
5626
+ kind: row.kind,
5627
+ ...optionalText2(row.fromStatus) ? { fromStatus: row.fromStatus } : {},
5628
+ ...optionalText2(row.toStatus) ? { toStatus: row.toStatus } : {},
5629
+ ...optionalText2(row.relatedClaimId) ? { relatedClaimId: row.relatedClaimId } : {},
5630
+ ...optionalText2(row.detail) ? { detail: row.detail } : {},
5631
+ createdAt: row.createdAt
5632
+ };
5633
+ }
5634
+ function evidenceKey(input) {
5635
+ return createHash5("sha256").update(JSON.stringify([
5636
+ input.evidenceKind,
5637
+ input.evidenceId,
5638
+ input.relation,
5639
+ input.snapshotId ?? "",
5640
+ input.locator ?? "",
5641
+ input.capturedHash ?? ""
5642
+ ])).digest("hex");
5643
+ }
5644
+ var ClaimStore;
5645
+ var init_claim_store = __esm({
5646
+ "src/knowledge/claim-store.ts"() {
5647
+ "use strict";
5648
+ init_esm_shims();
5649
+ init_secret_filter();
5650
+ init_sqlite_db();
5651
+ ClaimStore = class {
5652
+ db = null;
5653
+ async init(dataDir) {
5654
+ this.db = getDatabase(dataDir);
5655
+ }
5656
+ transaction(fn) {
5657
+ return this.db.transaction(fn)();
5658
+ }
5659
+ getClaim(id) {
5660
+ const row = this.db.prepare("SELECT * FROM knowledge_claims WHERE id = ?").get(id);
5661
+ return row ? rowToClaim(row) : void 0;
5662
+ }
5663
+ listClaims(projectId, options = {}) {
5664
+ const limit = Math.max(1, Math.min(1e3, Math.floor(options.limit ?? 500)));
5665
+ if (options.statuses && options.statuses.length > 0) {
5666
+ return this.db.prepare(`
5667
+ SELECT * FROM knowledge_claims
5668
+ WHERE projectId = ? AND status IN (SELECT value FROM json_each(?))
5669
+ ORDER BY updatedAt DESC, id
5670
+ LIMIT ?
5671
+ `).all(projectId, JSON.stringify([...new Set(options.statuses)]), limit).map(rowToClaim);
5672
+ }
5673
+ return this.db.prepare(`
5674
+ SELECT * FROM knowledge_claims
5675
+ WHERE projectId = ?
5676
+ ORDER BY updatedAt DESC, id
5677
+ LIMIT ?
5678
+ `).all(projectId, limit).map(rowToClaim);
5679
+ }
5680
+ findReusableClaim(projectId, claimKey) {
5681
+ const row = this.db.prepare(`
5682
+ SELECT * FROM knowledge_claims
5683
+ WHERE projectId = ? AND claimKey = ? AND status IN ('active', 'disputed')
5684
+ ORDER BY updatedAt DESC
5685
+ LIMIT 1
5686
+ `).get(projectId, claimKey);
5687
+ return row ? rowToClaim(row) : void 0;
5688
+ }
5689
+ listClaimsByConflictKey(projectId, conflictKey) {
5690
+ return this.db.prepare(`
5691
+ SELECT * FROM knowledge_claims
5692
+ WHERE projectId = ? AND conflictKey = ?
5693
+ ORDER BY createdAt ASC, id
5694
+ `).all(projectId, conflictKey).map(rowToClaim);
5695
+ }
5696
+ listConflicts(projectId) {
5697
+ const rows = this.db.prepare(`
5698
+ SELECT conflictKey
5699
+ FROM knowledge_claims
5700
+ WHERE projectId = ? AND status IN ('active', 'disputed') AND reviewState = 'approved'
5701
+ GROUP BY conflictKey
5702
+ HAVING COUNT(DISTINCT claimKey) > 1
5703
+ ORDER BY conflictKey
5704
+ `).all(projectId);
5705
+ return rows.map(({ conflictKey }) => ({
5706
+ conflictKey,
5707
+ claims: this.listClaimsByConflictKey(projectId, conflictKey).filter((claim) => claim.status === "active" || claim.status === "disputed")
5708
+ }));
5709
+ }
5710
+ insertClaim(claim) {
5711
+ this.db.prepare(`
5712
+ INSERT INTO knowledge_claims (
5713
+ id, projectId, subject, predicate, objectValue, scope, claimKey, conflictKey,
5714
+ status, confidence, observedAt, validFrom, validTo, supersededBy,
5715
+ reviewState, origin, createdAt, updatedAt
5716
+ ) VALUES (
5717
+ @id, @projectId, @subject, @predicate, @objectValue, @scope, @claimKey, @conflictKey,
5718
+ @status, @confidence, @observedAt, @validFrom, @validTo, @supersededBy,
5719
+ @reviewState, @origin, @createdAt, @updatedAt
5720
+ )
5721
+ `).run({
5722
+ ...claim,
5723
+ validFrom: claim.validFrom ?? null,
5724
+ validTo: claim.validTo ?? null,
5725
+ supersededBy: claim.supersededBy ?? null
5726
+ });
5727
+ }
5728
+ updateClaim(claim) {
5729
+ this.db.prepare(`
5730
+ UPDATE knowledge_claims SET
5731
+ subject = @subject,
5732
+ predicate = @predicate,
5733
+ objectValue = @objectValue,
5734
+ scope = @scope,
5735
+ claimKey = @claimKey,
5736
+ conflictKey = @conflictKey,
5737
+ status = @status,
5738
+ confidence = @confidence,
5739
+ observedAt = @observedAt,
5740
+ validFrom = @validFrom,
5741
+ validTo = @validTo,
5742
+ supersededBy = @supersededBy,
5743
+ reviewState = @reviewState,
5744
+ origin = @origin,
5745
+ updatedAt = @updatedAt
5746
+ WHERE id = @id
5747
+ `).run({
5748
+ ...claim,
5749
+ validFrom: claim.validFrom ?? null,
5750
+ validTo: claim.validTo ?? null,
5751
+ supersededBy: claim.supersededBy ?? null
5752
+ });
5753
+ }
5754
+ touchClaim(id, updatedAt) {
5755
+ this.db.prepare("UPDATE knowledge_claims SET updatedAt = ? WHERE id = ?").run(updatedAt, id);
5756
+ }
5757
+ insertEvidence(input) {
5758
+ const createdAt = input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
5759
+ const key = evidenceKey(input);
5760
+ const result = this.db.prepare(`
5761
+ INSERT OR IGNORE INTO knowledge_claim_evidence (
5762
+ id, claimId, evidenceKind, evidenceId, relation, snapshotId,
5763
+ locator, capturedHash, evidenceKey, createdAt
5764
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5765
+ `).run(
5766
+ randomUUID5(),
5767
+ input.claimId,
5768
+ input.evidenceKind,
5769
+ input.evidenceId,
5770
+ input.relation,
5771
+ input.snapshotId ?? null,
5772
+ input.locator ? sanitizeCredentials(input.locator) : null,
5773
+ input.capturedHash ?? null,
5774
+ key,
5775
+ createdAt
5776
+ );
5777
+ return Number(result.changes ?? 0) > 0;
5778
+ }
5779
+ listEvidence(claimId) {
5780
+ return this.db.prepare(`
5781
+ SELECT * FROM knowledge_claim_evidence
5782
+ WHERE claimId = ?
5783
+ ORDER BY createdAt ASC, id
5784
+ `).all(claimId).map(rowToEvidence);
5785
+ }
5786
+ recordEvent(input) {
5787
+ const event = {
5788
+ id: randomUUID5(),
5789
+ ...input,
5790
+ ...input.detail ? { detail: sanitizeCredentials(input.detail) } : {},
5791
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
5792
+ };
5793
+ this.db.prepare(`
5794
+ INSERT INTO knowledge_claim_events (
5795
+ id, projectId, claimId, kind, fromStatus, toStatus,
5796
+ relatedClaimId, detail, createdAt
5797
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5798
+ `).run(
5799
+ event.id,
5800
+ event.projectId,
5801
+ event.claimId,
5802
+ event.kind,
5803
+ event.fromStatus ?? null,
5804
+ event.toStatus ?? null,
5805
+ event.relatedClaimId ?? null,
5806
+ event.detail ?? null,
5807
+ event.createdAt
5808
+ );
5809
+ return event;
5810
+ }
5811
+ listEvents(claimId) {
5812
+ return this.db.prepare(`
5813
+ SELECT * FROM knowledge_claim_events
5814
+ WHERE claimId = ?
5815
+ ORDER BY createdAt ASC, id
5816
+ `).all(claimId).map(rowToEvent);
5817
+ }
5818
+ };
5819
+ }
5820
+ });
5821
+
5822
+ // src/compact/token-budget.ts
5823
+ import { countTokens, isWithinTokenLimit } from "gpt-tokenizer";
5824
+ function countTextTokens(text) {
5825
+ return countTokens(text);
5826
+ }
5827
+ var init_token_budget = __esm({
5828
+ "src/compact/token-budget.ts"() {
5829
+ "use strict";
5830
+ init_esm_shims();
5831
+ }
5832
+ });
5833
+
5834
+ // src/knowledge/claims.ts
5835
+ var claims_exports = {};
5836
+ __export(claims_exports, {
5837
+ buildClaimIdentity: () => buildClaimIdentity,
5838
+ deriveLowRiskClaimsFromObservation: () => deriveLowRiskClaimsFromObservation,
5839
+ requalifyClaimsForCodeState: () => requalifyClaimsForCodeState,
5840
+ selectClaimsForTask: () => selectClaimsForTask,
5841
+ supersedeClaim: () => supersedeClaim,
5842
+ writeClaim: () => writeClaim
5843
+ });
5844
+ import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
5845
+ function compactText(value, field) {
5846
+ if (typeof value !== "string") throw new Error("Claim " + field + " must be text");
5847
+ const sanitized = sanitizeCredentials(value).trim().replace(/\s+/g, " ");
5848
+ if (!sanitized) throw new Error("Claim " + field + " is required");
5849
+ if (sanitized.length > MAX_TERM_LENGTH) throw new Error("Claim " + field + " is too long");
5850
+ return sanitized;
5851
+ }
5852
+ function normalizedTerm(value) {
5853
+ return compactText(value, "identity").toLocaleLowerCase("en-US");
5854
+ }
5855
+ function hashIdentity(parts) {
5856
+ return createHash6("sha256").update(parts.join(String.fromCharCode(31))).digest("hex");
5857
+ }
5858
+ function buildClaimIdentity(input) {
5859
+ const subject = normalizedTerm(input.subject);
5860
+ const predicate = normalizedTerm(input.predicate);
5861
+ const objectValue = normalizedTerm(input.objectValue);
5862
+ return {
5863
+ claimKey: hashIdentity([subject, predicate, objectValue, input.scope]),
5864
+ conflictKey: hashIdentity([subject, predicate, input.scope])
5865
+ };
5866
+ }
5867
+ function clampConfidence(value) {
5868
+ if (value === void 0) return 0.7;
5869
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
5870
+ throw new Error("Claim confidence must be a number between 0 and 1");
5871
+ }
5872
+ return value;
5873
+ }
5874
+ function validateEvidence(input) {
5875
+ if (!Array.isArray(input) || input.length === 0) {
5876
+ throw new Error("A knowledge claim requires at least one evidence reference");
5877
+ }
5878
+ return input.map((evidence) => {
5879
+ if (!evidence || typeof evidence !== "object") {
5880
+ throw new Error("Claim evidence must be an object");
5881
+ }
5882
+ const evidenceId = compactText(evidence.evidenceId, "evidence id");
5883
+ if (evidenceId.length > MAX_EVIDENCE_ID_LENGTH) {
5884
+ throw new Error("Claim evidence id is too long");
5885
+ }
5886
+ if (!["observation", "git", "code", "test", "document", "workflow", "run"].includes(evidence.evidenceKind)) {
5887
+ throw new Error("Claim evidence kind is invalid");
5888
+ }
5889
+ if (!["supports", "contradicts", "derives", "verifies"].includes(evidence.relation)) {
5890
+ throw new Error("Claim evidence relation is invalid");
5891
+ }
5892
+ return {
5893
+ evidenceKind: evidence.evidenceKind,
5894
+ evidenceId,
5895
+ relation: evidence.relation,
5896
+ ...evidence.snapshotId ? { snapshotId: compactText(evidence.snapshotId, "evidence snapshot id") } : {},
5897
+ ...evidence.locator ? { locator: compactText(evidence.locator, "evidence locator") } : {},
5898
+ ...evidence.capturedHash ? { capturedHash: compactText(evidence.capturedHash, "evidence hash") } : {}
5899
+ };
5900
+ });
5901
+ }
5902
+ function normalizeClaimInput(input) {
5903
+ if (!["project", "workspace", "team", "workflow", "task"].includes(input.scope)) {
5904
+ throw new Error("Claim scope is invalid");
5905
+ }
5906
+ const projectId = compactText(input.projectId, "project id");
5907
+ const subject = compactText(input.subject, "subject");
5908
+ const predicate = compactText(input.predicate, "predicate");
5909
+ const objectValue = compactText(input.objectValue, "object value");
5910
+ const identity = buildClaimIdentity({ subject, predicate, objectValue, scope: input.scope });
5911
+ const origin = input.origin ?? "explicit";
5912
+ let reviewState = input.reviewState ?? "approved";
5913
+ let status = input.status ?? "active";
5914
+ if (origin === "model") {
5915
+ if (input.reviewState === "approved" || input.status === "active") {
5916
+ throw new Error("A model-origin claim cannot be approved or active without explicit review");
5917
+ }
5918
+ reviewState = input.reviewState ?? "draft";
5919
+ status = input.status ?? "unknown";
5920
+ }
5921
+ if (!["approved", "needs-review", "draft", "rejected"].includes(reviewState)) {
5922
+ throw new Error("Claim review state is invalid");
5923
+ }
5924
+ if (!["active", "superseded", "disputed", "unknown"].includes(status)) {
5925
+ throw new Error("Claim status is invalid");
5926
+ }
5927
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
5928
+ return {
5929
+ claim: {
5930
+ id: randomUUID6(),
5931
+ projectId,
5932
+ subject,
5933
+ predicate,
5934
+ objectValue,
5935
+ scope: input.scope,
5936
+ claimKey: identity.claimKey,
5937
+ conflictKey: identity.conflictKey,
5938
+ status,
5939
+ confidence: clampConfidence(input.confidence),
5940
+ observedAt: input.observedAt ?? now3,
5941
+ ...input.validFrom ? { validFrom: input.validFrom } : {},
5942
+ ...input.validTo ? { validTo: input.validTo } : {},
5943
+ reviewState,
5944
+ origin,
5945
+ createdAt: now3,
5946
+ updatedAt: now3
5947
+ },
5948
+ evidence: validateEvidence(input.evidence)
5949
+ };
5950
+ }
5951
+ function activeConflictClaims(store, projectId, conflictKey) {
5952
+ return store.listClaimsByConflictKey(projectId, conflictKey).filter((claim) => (claim.status === "active" || claim.status === "disputed") && claim.reviewState === "approved");
5953
+ }
5954
+ function reconcileConflicts(store, projectId, conflictKey, now3) {
5955
+ const claims = activeConflictClaims(store, projectId, conflictKey);
5956
+ const hasConflict = new Set(claims.map((claim) => claim.claimKey)).size > 1;
5957
+ for (const claim of claims) {
5958
+ const nextStatus = hasConflict ? "disputed" : "active";
5959
+ if (claim.status === nextStatus) continue;
5960
+ store.updateClaim({ ...claim, status: nextStatus, updatedAt: now3 });
5961
+ store.recordEvent({
5962
+ projectId,
5963
+ claimId: claim.id,
5964
+ kind: hasConflict ? "conflicted" : "requalified",
5965
+ fromStatus: claim.status,
5966
+ toStatus: nextStatus,
5967
+ detail: hasConflict ? "A different approved assertion has the same subject, predicate, and scope." : "No remaining approved competing assertion exists.",
5968
+ createdAt: now3
5969
+ });
5970
+ }
5971
+ return store.listConflicts(projectId).filter((conflict) => conflict.conflictKey === conflictKey);
5972
+ }
5973
+ function persistEvidence(store, claim, evidence, now3) {
5974
+ let added = false;
5975
+ for (const item of evidence) {
5976
+ added = store.insertEvidence({ claimId: claim.id, ...item, createdAt: now3 }) || added;
5977
+ }
5978
+ if (added) {
5979
+ store.touchClaim(claim.id, now3);
5980
+ store.recordEvent({
5981
+ projectId: claim.projectId,
5982
+ claimId: claim.id,
5983
+ kind: "evidence-added",
5984
+ detail: "Additional source evidence was attached.",
5985
+ createdAt: now3
5986
+ });
5987
+ }
5988
+ }
5989
+ function writeClaim(store, input) {
5990
+ const normalized = normalizeClaimInput(input);
5991
+ return store.transaction(() => {
5992
+ const existing = store.findReusableClaim(normalized.claim.projectId, normalized.claim.claimKey);
5993
+ if (existing) {
5994
+ persistEvidence(store, existing, normalized.evidence, normalized.claim.updatedAt);
5995
+ return {
5996
+ claim: store.getClaim(existing.id) ?? existing,
5997
+ created: false,
5998
+ conflicts: store.listConflicts(existing.projectId).filter((conflict) => conflict.conflictKey === existing.conflictKey)
5999
+ };
6000
+ }
6001
+ store.insertClaim(normalized.claim);
6002
+ for (const evidence of normalized.evidence) {
6003
+ store.insertEvidence({ claimId: normalized.claim.id, ...evidence, createdAt: normalized.claim.createdAt });
6004
+ }
6005
+ store.recordEvent({
6006
+ projectId: normalized.claim.projectId,
6007
+ claimId: normalized.claim.id,
6008
+ kind: "created",
6009
+ toStatus: normalized.claim.status,
6010
+ detail: "Claim created with source evidence.",
6011
+ createdAt: normalized.claim.createdAt
6012
+ });
6013
+ const conflicts = reconcileConflicts(
6014
+ store,
6015
+ normalized.claim.projectId,
6016
+ normalized.claim.conflictKey,
6017
+ normalized.claim.updatedAt
6018
+ );
6019
+ return {
6020
+ claim: store.getClaim(normalized.claim.id) ?? normalized.claim,
6021
+ created: true,
6022
+ conflicts
6023
+ };
6024
+ });
6025
+ }
6026
+ function supersedeClaim(store, input) {
6027
+ const evidence = validateEvidence(input.evidence);
6028
+ return store.transaction(() => {
6029
+ const claim = store.getClaim(input.claimId);
6030
+ const replacement = store.getClaim(input.replacementClaimId);
6031
+ if (!claim || !replacement) throw new Error("Both claims must exist before supersession");
6032
+ if (claim.projectId !== replacement.projectId || claim.conflictKey !== replacement.conflictKey) {
6033
+ throw new Error("A replacement claim must address the same project assertion");
6034
+ }
6035
+ if (claim.id === replacement.id) throw new Error("A claim cannot supersede itself");
6036
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
6037
+ persistEvidence(store, claim, evidence, now3);
6038
+ const updated = {
6039
+ ...claim,
6040
+ status: "superseded",
6041
+ validTo: now3,
6042
+ supersededBy: replacement.id,
6043
+ updatedAt: now3
6044
+ };
6045
+ store.updateClaim(updated);
6046
+ store.recordEvent({
6047
+ projectId: claim.projectId,
6048
+ claimId: claim.id,
6049
+ kind: "superseded",
6050
+ fromStatus: claim.status,
6051
+ toStatus: "superseded",
6052
+ relatedClaimId: replacement.id,
6053
+ detail: "Superseded by an evidence-backed replacement claim.",
6054
+ createdAt: now3
6055
+ });
6056
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now3);
6057
+ return {
6058
+ superseded: store.getClaim(claim.id) ?? updated,
6059
+ replacement: store.getClaim(replacement.id) ?? replacement
6060
+ };
6061
+ });
6062
+ }
6063
+ function hashObservation(observation) {
6064
+ return createHash6("sha256").update(JSON.stringify([
6065
+ observation.title,
6066
+ observation.narrative,
6067
+ observation.facts,
6068
+ observation.filesModified,
6069
+ observation.updatedAt ?? observation.createdAt
6070
+ ])).digest("hex");
6071
+ }
6072
+ function evidenceFromCodeRef(ref) {
6073
+ return {
6074
+ evidenceKind: "code",
6075
+ evidenceId: "code-ref:" + ref.id,
6076
+ relation: "supports",
6077
+ ...ref.snapshotId ? { snapshotId: ref.snapshotId } : {},
6078
+ locator: "code-ref/" + ref.id,
6079
+ ...ref.capturedSymbolHash || ref.capturedFileHash ? { capturedHash: ref.capturedSymbolHash ?? ref.capturedFileHash } : {}
6080
+ };
6081
+ }
6082
+ function deriveLowRiskClaimsFromObservation(store, observation, codeStore) {
6083
+ const predicate = PREDICATE_BY_OBSERVATION_TYPE[observation.type];
6084
+ const isExplicit = observation.sourceDetail === "explicit";
6085
+ const isGit = observation.source === "git" || observation.sourceDetail === "git-ingest";
6086
+ if (!predicate || observation.status !== "active" || !isExplicit && !isGit) return [];
6087
+ const evidence = [{
6088
+ evidenceKind: "observation",
6089
+ evidenceId: "observation:" + observation.id,
6090
+ relation: "supports",
6091
+ locator: "observation/" + observation.id,
6092
+ capturedHash: hashObservation(observation)
6093
+ }];
6094
+ if (observation.commitHash) {
6095
+ evidence.push({
6096
+ evidenceKind: "git",
6097
+ evidenceId: "git:" + observation.commitHash,
6098
+ relation: "verifies",
6099
+ locator: "git:" + observation.commitHash,
6100
+ capturedHash: observation.commitHash
6101
+ });
6102
+ }
6103
+ const refs = codeStore?.listObservationRefs(observation.projectId, observation.id) ?? [];
6104
+ for (const ref of refs) evidence.push(evidenceFromCodeRef(ref));
6105
+ const confidence = Math.min(0.9, (isGit ? 0.75 : 0.7) + refs.filter((ref) => ref.status === "current").length * 0.05);
6106
+ const result = writeClaim(store, {
6107
+ projectId: observation.projectId,
6108
+ subject: observation.entityName,
6109
+ predicate,
6110
+ objectValue: observation.title,
6111
+ scope: "project",
6112
+ confidence,
6113
+ observedAt: observation.updatedAt ?? observation.createdAt,
6114
+ reviewState: "approved",
6115
+ origin: isGit ? "git" : "derived",
6116
+ evidence
6117
+ });
6118
+ return [result.claim];
6119
+ }
6120
+ function incompleteSnapshot(snapshot) {
6121
+ if (!snapshot) return false;
6122
+ return snapshot.completeness.skippedOversizedFiles > 0 || (snapshot.completeness.unreadableFiles ?? 0) > 0 || snapshot.completeness.removalScanDeferred;
6123
+ }
6124
+ function requalification(claim, refs, evidence, snapshot) {
6125
+ if (claim.status === "superseded" || claim.reviewState === "draft" || claim.reviewState === "rejected") return void 0;
6126
+ const codeEvidence = evidence.filter((item) => item.evidenceKind === "code");
6127
+ if (codeEvidence.length === 0) return void 0;
6128
+ const linkedRefs = codeEvidence.map((item) => refs.get(item.evidenceId.replace(/^code-ref:/, ""))).filter((ref) => !!ref);
6129
+ const missingOrStale = linkedRefs.length !== codeEvidence.length || linkedRefs.some((ref) => ref.status === "stale");
6130
+ if (missingOrStale) {
6131
+ return {
6132
+ status: "unknown",
6133
+ confidence: Math.min(claim.confidence, 0.2),
6134
+ reviewState: "needs-review",
6135
+ reason: "Bound code evidence is no longer current."
6136
+ };
6137
+ }
6138
+ if (linkedRefs.some((ref) => ref.status === "suspect")) {
6139
+ return {
6140
+ status: claim.status === "disputed" ? "disputed" : "active",
6141
+ confidence: Math.min(claim.confidence, 0.5),
6142
+ reviewState: "needs-review",
6143
+ reason: "Bound file evidence changed and needs review."
6144
+ };
6145
+ }
6146
+ if (incompleteSnapshot(snapshot)) {
6147
+ return {
6148
+ status: claim.status === "disputed" ? "disputed" : "active",
6149
+ confidence: Math.min(claim.confidence, 0.55),
6150
+ reviewState: "needs-review",
6151
+ reason: "The latest code scan is incomplete."
6152
+ };
6153
+ }
6154
+ return void 0;
6155
+ }
6156
+ function requalifyClaimsForCodeState(store, codeStore, projectId) {
6157
+ const snapshot = codeStore.latestSnapshot(projectId);
6158
+ const refs = new Map(codeStore.listProjectObservationRefs(projectId).map((ref) => [ref.id, ref]));
6159
+ let requalified = 0;
6160
+ store.transaction(() => {
6161
+ for (const claim of store.listClaims(projectId)) {
6162
+ const next = requalification(claim, refs, store.listEvidence(claim.id), snapshot);
6163
+ if (!next) continue;
6164
+ if (next.status === claim.status && next.confidence === claim.confidence && next.reviewState === claim.reviewState) {
6165
+ continue;
6166
+ }
6167
+ const updated = {
6168
+ ...claim,
6169
+ status: next.status,
6170
+ confidence: next.confidence,
6171
+ reviewState: next.reviewState,
6172
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6173
+ };
6174
+ store.updateClaim(updated);
6175
+ store.recordEvent({
6176
+ projectId,
6177
+ claimId: claim.id,
6178
+ kind: "requalified",
6179
+ fromStatus: claim.status,
6180
+ toStatus: next.status,
6181
+ detail: next.reason,
6182
+ createdAt: updated.updatedAt
6183
+ });
6184
+ requalified += 1;
6185
+ }
6186
+ });
6187
+ return { requalified, incompleteSnapshot: incompleteSnapshot(snapshot) };
6188
+ }
6189
+ function normalizedTokens(value) {
6190
+ const raw = sanitizeCredentials(value).toLocaleLowerCase("en-US").match(TOKEN_PATTERN) ?? [];
6191
+ return new Set(raw.map((token) => token.replace(/^[,;:!?]+|[,;:!?.]+$/g, "")).filter((token) => token.length > 1));
6192
+ }
6193
+ function taskTokens(task) {
6194
+ return normalizedTokens(task);
6195
+ }
6196
+ function scoreClaim(claim, tokens) {
6197
+ const text = [claim.subject, claim.predicate, claim.objectValue].join(" ").toLocaleLowerCase("en-US");
6198
+ const words = normalizedTokens(text);
6199
+ let matches = 0;
6200
+ for (const token of tokens) {
6201
+ if (words.has(token)) matches += 1;
6202
+ }
6203
+ if (matches === 0) return 0;
6204
+ const statusWeight = claim.status === "active" ? 0.25 : claim.status === "disputed" ? 0.2 : 0.1;
6205
+ return matches * 10 + claim.confidence + statusWeight;
6206
+ }
6207
+ function formatClaim(claim) {
6208
+ return sanitizeCredentials([claim.subject, claim.predicate, claim.objectValue].join(" "));
6209
+ }
6210
+ function selectClaimsForTask(store, input) {
6211
+ const limit = Math.max(1, Math.min(12, Math.floor(input.limit ?? 4)));
6212
+ const maxTokens = Math.max(16, Math.min(1e3, Math.floor(input.maxTokens ?? 160)));
6213
+ const tokens = taskTokens(input.task);
6214
+ if (tokens.size === 0) return { claims: [], cautions: [], tokenCount: 0, reasons: {} };
6215
+ const candidates = store.listClaims(input.projectId, {
6216
+ statuses: ["active", "disputed", "unknown"],
6217
+ limit: 1e3
6218
+ }).filter((claim) => claim.reviewState !== "draft" && claim.reviewState !== "rejected");
6219
+ const scored = candidates.map((claim) => ({ claim, score: scoreClaim(claim, tokens) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.claim.id.localeCompare(right.claim.id));
6220
+ if (scored.length === 0) return { claims: [], cautions: [], tokenCount: 0, reasons: {} };
6221
+ const selected = /* @__PURE__ */ new Map();
6222
+ for (const { claim } of scored.slice(0, limit)) selected.set(claim.id, claim);
6223
+ for (const claim of [...selected.values()]) {
6224
+ for (const sibling of store.listClaimsByConflictKey(input.projectId, claim.conflictKey)) {
6225
+ if (sibling.status === "active" || sibling.status === "disputed") selected.set(sibling.id, sibling);
6226
+ }
6227
+ }
6228
+ const ordered = [...selected.values()].sort((left, right) => scoreClaim(right, tokens) - scoreClaim(left, tokens) || left.id.localeCompare(right.id));
6229
+ const included = [];
6230
+ let tokenCount = 0;
6231
+ for (const claim of ordered) {
6232
+ const nextTokens = countTextTokens(formatClaim(claim));
6233
+ if (included.length > 0 && tokenCount + nextTokens > maxTokens) continue;
6234
+ included.push(claim);
6235
+ tokenCount += nextTokens;
6236
+ }
6237
+ const cautions = /* @__PURE__ */ new Set();
6238
+ const reasons = {};
6239
+ for (const claim of included) {
6240
+ reasons[claim.id] = "task terms matched source-qualified claim";
6241
+ if (claim.status === "disputed") cautions.add("claim-conflict");
6242
+ if (claim.status === "unknown" || claim.reviewState === "needs-review") {
6243
+ cautions.add("claim-needs-review");
6244
+ }
6245
+ }
6246
+ return { claims: included, cautions: [...cautions].sort(), tokenCount, reasons };
6247
+ }
6248
+ var MAX_TERM_LENGTH, MAX_EVIDENCE_ID_LENGTH, TOKEN_PATTERN, PREDICATE_BY_OBSERVATION_TYPE;
6249
+ var init_claims = __esm({
6250
+ "src/knowledge/claims.ts"() {
6251
+ "use strict";
6252
+ init_esm_shims();
6253
+ init_token_budget();
6254
+ init_secret_filter();
6255
+ MAX_TERM_LENGTH = 2e3;
6256
+ MAX_EVIDENCE_ID_LENGTH = 1e3;
6257
+ TOKEN_PATTERN = /[\p{L}\p{N}_./:-]+/gu;
6258
+ PREDICATE_BY_OBSERVATION_TYPE = {
6259
+ decision: "decision",
6260
+ "trade-off": "trade-off",
6261
+ "why-it-exists": "rationale",
6262
+ "what-changed": "changed",
6263
+ "problem-solution": "workaround",
6264
+ gotcha: "caution",
6265
+ "how-it-works": "behavior",
6266
+ discovery: "finding",
6267
+ reasoning: "rationale"
6268
+ };
6269
+ }
6270
+ });
6271
+
6272
+ // src/knowledge/markdown.ts
6273
+ import { createHash as createHash7 } from "crypto";
6274
+ import { promises as fs5 } from "fs";
6275
+ import path8 from "path";
6276
+ import matter from "gray-matter";
6277
+ function hashContent(content) {
6278
+ return createHash7("sha256").update(content).digest("hex");
6279
+ }
6280
+ function stringField(data, key) {
6281
+ const value = data[key];
6282
+ if (typeof value !== "string" || !value.trim()) {
6283
+ throw new Error("missing required field " + key);
6284
+ }
6285
+ return value.trim();
6286
+ }
6287
+ function stringArray(data, key) {
6288
+ const value = data[key];
6289
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
6290
+ throw new Error("field " + key + " must be a string array");
6291
+ }
6292
+ return value.map((item) => item.trim()).filter(Boolean);
6293
+ }
6294
+ function optionalString(data, key) {
6295
+ const value = data[key];
6296
+ if (value === void 0 || value === null || value === "") return void 0;
6297
+ if (typeof value !== "string") throw new Error("field " + key + " must be text");
6298
+ return value.trim() || void 0;
6299
+ }
6300
+ function validateKnowledgePageFrontmatter(data) {
6301
+ const kind = stringField(data, "kind");
6302
+ const status = stringField(data, "status");
6303
+ const reviewState = stringField(data, "reviewState");
6304
+ if (!PAGE_KINDS.includes(kind)) throw new Error("field kind is invalid");
6305
+ if (!PAGE_STATUSES.includes(status)) throw new Error("field status is invalid");
6306
+ if (!REVIEW_STATES.includes(reviewState)) throw new Error("field reviewState is invalid");
6307
+ return {
6308
+ id: stringField(data, "id"),
6309
+ title: stringField(data, "title"),
6310
+ kind,
6311
+ status,
6312
+ reviewState,
6313
+ claimIds: stringArray(data, "claimIds"),
6314
+ evidenceRefs: stringArray(data, "evidenceRefs"),
6315
+ ...optionalString(data, "snapshotId") ? { snapshotId: optionalString(data, "snapshotId") } : {},
6316
+ tags: stringArray(data, "tags"),
6317
+ sourceHash: stringField(data, "sourceHash"),
6318
+ generatedAt: stringField(data, "generatedAt"),
6319
+ updatedAt: stringField(data, "updatedAt")
6320
+ };
6321
+ }
6322
+ function extractInternalMarkdownLinks(body) {
6323
+ const links = /* @__PURE__ */ new Set();
6324
+ for (const match of body.matchAll(LINK_PATTERN)) {
6325
+ const raw = match[1].trim();
6326
+ if (!raw || raw.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(raw)) continue;
6327
+ const pathPart = raw.split("#", 1)[0].replace(/\\/g, "/");
6328
+ if (!pathPart.endsWith(".md")) continue;
6329
+ links.add(pathPart);
6330
+ }
6331
+ return [...links].sort();
6332
+ }
6333
+ function resolvePageLink(sourceRelativePath, target) {
6334
+ const sourceDir = path8.posix.dirname(sourceRelativePath.replace(/\\/g, "/"));
6335
+ const normalized = path8.posix.normalize(path8.posix.join(sourceDir, target.replace(/\\/g, "/")));
6336
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) return void 0;
6337
+ return normalized;
6338
+ }
6339
+ function renderKnowledgePage(frontmatter, body) {
6340
+ const normalizedBody = body.trimEnd() + "\n";
6341
+ return matter.stringify(normalizedBody, {
6342
+ id: frontmatter.id,
6343
+ title: frontmatter.title,
6344
+ kind: frontmatter.kind,
6345
+ status: frontmatter.status,
6346
+ reviewState: frontmatter.reviewState,
6347
+ claimIds: frontmatter.claimIds,
6348
+ evidenceRefs: frontmatter.evidenceRefs,
6349
+ ...frontmatter.snapshotId ? { snapshotId: frontmatter.snapshotId } : {},
6350
+ tags: frontmatter.tags,
6351
+ sourceHash: frontmatter.sourceHash,
6352
+ generatedAt: frontmatter.generatedAt,
6353
+ updatedAt: frontmatter.updatedAt
6354
+ });
6355
+ }
6356
+ async function readKnowledgePage(absolutePath, workspaceRoot) {
6357
+ const raw = await fs5.readFile(absolutePath, "utf8");
6358
+ try {
6359
+ const parsed = matter(raw);
6360
+ const frontmatter = validateKnowledgePageFrontmatter(parsed.data);
6361
+ return {
6362
+ absolutePath,
6363
+ relativePath: workspaceRoot ? path8.relative(workspaceRoot, absolutePath).split(path8.sep).join("/") : path8.basename(absolutePath),
6364
+ frontmatter,
6365
+ body: parsed.content.trim(),
6366
+ contentHash: hashContent(raw),
6367
+ links: extractInternalMarkdownLinks(parsed.content)
6368
+ };
6369
+ } catch (error) {
6370
+ const detail = error instanceof Error ? error.message : "invalid page data";
6371
+ throw new Error("Malformed knowledge page frontmatter in " + absolutePath + ": " + detail);
6372
+ }
6373
+ }
6374
+ async function writeKnowledgePage(absolutePath, content) {
6375
+ await fs5.mkdir(path8.dirname(absolutePath), { recursive: true });
6376
+ await atomicWriteFile(absolutePath, content);
6377
+ return hashContent(content);
6378
+ }
6379
+ function pageContentHash(content) {
6380
+ return hashContent(content);
6381
+ }
6382
+ var PAGE_KINDS, PAGE_STATUSES, REVIEW_STATES, LINK_PATTERN;
6383
+ var init_markdown = __esm({
6384
+ "src/knowledge/markdown.ts"() {
6385
+ "use strict";
6386
+ init_esm_shims();
6387
+ init_file_lock();
6388
+ PAGE_KINDS = ["topic", "decision", "risk", "index", "schema", "log"];
6389
+ PAGE_STATUSES = ["active", "proposed"];
6390
+ REVIEW_STATES = ["approved", "needs-review"];
6391
+ LINK_PATTERN = /\[[^\]]+\]\(([^)]+)\)/g;
6392
+ }
6393
+ });
6394
+
6395
+ // src/knowledge/wiki.ts
6396
+ var wiki_exports = {};
6397
+ __export(wiki_exports, {
6398
+ applyKnowledgeProposal: () => applyKnowledgeProposal,
6399
+ compileKnowledgeWorkspace: () => compileKnowledgeWorkspace,
6400
+ lintKnowledgeWorkspace: () => lintKnowledgeWorkspace,
6401
+ readKnowledgePage: () => readKnowledgePage
6402
+ });
6403
+ import { createHash as createHash8 } from "crypto";
6404
+ import { promises as fs6 } from "fs";
6405
+ import path9 from "path";
6406
+ function hash2(value) {
6407
+ return createHash8("sha256").update(value).digest("hex");
6408
+ }
6409
+ function now() {
6410
+ return (/* @__PURE__ */ new Date()).toISOString();
6411
+ }
6412
+ async function storeFor(workspace) {
6413
+ if (!workspace.dataDir) throw new Error("Knowledge workspace has no operational data directory");
6414
+ const store = new KnowledgeWorkspaceStore();
6415
+ await store.init(workspace.dataDir);
6416
+ return store;
6417
+ }
6418
+ function topicSlug(subject) {
6419
+ const safe = subject.toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
6420
+ return (safe || "topic") + "-" + hash2(subject).slice(0, 8);
6421
+ }
6422
+ function groupClaims(claims) {
6423
+ const groups = /* @__PURE__ */ new Map();
6424
+ for (const claim of claims) {
6425
+ const group = groups.get(claim.subject) ?? [];
6426
+ group.push(claim);
6427
+ groups.set(claim.subject, group);
6428
+ }
6429
+ return groups;
6430
+ }
6431
+ function selectPublishableClaims(claims, projectId) {
6432
+ return claims.listClaims(projectId, { statuses: ["active"], limit: 1e3 }).filter((claim) => claim.reviewState === "approved");
6433
+ }
6434
+ function evidenceRefs(claims, store) {
6435
+ const refs = /* @__PURE__ */ new Set();
6436
+ for (const claim of claims) {
6437
+ for (const evidence of store.listEvidence(claim.id)) {
6438
+ refs.add("claim:" + claim.id + ":evidence:" + evidence.id);
6439
+ }
6440
+ }
6441
+ return [...refs].sort();
6442
+ }
6443
+ function snapshotIdForClaims(claims, store) {
6444
+ const ids = /* @__PURE__ */ new Set();
6445
+ for (const claim of claims) {
6446
+ for (const evidence of store.listEvidence(claim.id)) {
6447
+ if (evidence.snapshotId) ids.add(evidence.snapshotId);
6448
+ }
6449
+ }
6450
+ return ids.size === 1 ? [...ids][0] : void 0;
6451
+ }
6452
+ function sourceHashForClaims(claims, store) {
6453
+ const source = claims.map((claim) => ({
6454
+ id: claim.id,
6455
+ status: claim.status,
6456
+ confidence: claim.confidence,
6457
+ evidence: store.listEvidence(claim.id).map((item) => ({
6458
+ id: item.id,
6459
+ kind: item.evidenceKind,
6460
+ evidenceId: item.evidenceId,
6461
+ relation: item.relation,
6462
+ snapshotId: item.snapshotId ?? "",
6463
+ capturedHash: item.capturedHash ?? ""
6464
+ }))
6465
+ }));
6466
+ return hash2(JSON.stringify(source));
6467
+ }
6468
+ function renderTopicBody(title, claims, store) {
6469
+ const lines = [
6470
+ "# " + title,
6471
+ "",
6472
+ "This page is compiled from approved source-qualified claims.",
6473
+ "",
6474
+ "## Current claims",
6475
+ ""
6476
+ ];
6477
+ for (const claim of claims) {
6478
+ lines.push("- " + claim.predicate + ": " + claim.objectValue + " (claim " + claim.id + ")");
6479
+ }
6480
+ lines.push("");
6481
+ lines.push("## Evidence");
6482
+ lines.push("");
6483
+ for (const claim of claims) {
6484
+ lines.push("### Claim " + claim.id);
6485
+ const evidence = store.listEvidence(claim.id);
6486
+ if (evidence.length === 0) {
6487
+ lines.push("- Missing evidence.");
6488
+ continue;
6489
+ }
6490
+ for (const item of evidence) {
6491
+ const location = item.locator ? " at " + item.locator : "";
6492
+ const snapshot = item.snapshotId ? " (snapshot " + item.snapshotId + ")" : "";
6493
+ lines.push("- " + item.evidenceKind + ": " + item.evidenceId + location + snapshot);
6494
+ }
6495
+ }
6496
+ lines.push("");
6497
+ return lines.join("\n");
6498
+ }
6499
+ function candidatePage(workspace, title, claims, store) {
6500
+ const sourceHash = sourceHashForClaims(claims, store);
6501
+ const generatedAt = now();
6502
+ const slug2 = topicSlug(title);
6503
+ const targetRelativePath = "pages/" + slug2 + ".md";
6504
+ const proposalRelativePath = "proposals/" + slug2 + ".md";
6505
+ const frontmatter = {
6506
+ id: "page:" + hash2(workspace.projectId + ":" + title).slice(0, 24),
6507
+ title,
6508
+ kind: "topic",
6509
+ status: "proposed",
6510
+ reviewState: "needs-review",
6511
+ claimIds: claims.map((claim) => claim.id).sort(),
6512
+ evidenceRefs: evidenceRefs(claims, store),
6513
+ ...snapshotIdForClaims(claims, store) ? { snapshotId: snapshotIdForClaims(claims, store) } : {},
6514
+ tags: ["generated", "topic"],
6515
+ sourceHash,
6516
+ generatedAt,
6517
+ updatedAt: generatedAt
6518
+ };
6519
+ const content = renderKnowledgePage(frontmatter, renderTopicBody(title, claims, store));
6520
+ const proposalPath = resolveKnowledgeWorkspaceFile(workspace, proposalRelativePath);
6521
+ return {
6522
+ page: {
6523
+ absolutePath: proposalPath,
6524
+ relativePath: proposalRelativePath,
6525
+ frontmatter,
6526
+ body: renderTopicBody(title, claims, store),
6527
+ contentHash: pageContentHash(content),
6528
+ links: []
6529
+ },
6530
+ content,
6531
+ targetRelativePath,
6532
+ proposalRelativePath
6533
+ };
6534
+ }
6535
+ async function fileHashIfPresent(filePath) {
6536
+ try {
6537
+ return pageContentHash(await fs6.readFile(filePath, "utf8"));
6538
+ } catch (error) {
6539
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
6540
+ if (code === "ENOENT") return void 0;
6541
+ throw error;
6542
+ }
6543
+ }
6544
+ async function writeIndex(workspace, store) {
6545
+ const paths = getKnowledgeWorkspacePaths(workspace);
6546
+ const published = store.listPages(workspace.id).filter((page) => page.status === "active").sort((left, right) => left.title.localeCompare(right.title));
6547
+ const pending = store.listProposals(workspace.id, "pending").sort((left, right) => left.proposalPath.localeCompare(right.proposalPath));
6548
+ const lines = ["# Knowledge Workspace", "", "## Published pages", ""];
6549
+ if (published.length === 0) {
6550
+ lines.push("No published pages yet.");
6551
+ } else {
6552
+ for (const page of published) lines.push("- [" + page.title + "](" + page.relativePath + ")");
6553
+ }
6554
+ lines.push("");
6555
+ lines.push("## Review queue");
6556
+ lines.push("");
6557
+ if (pending.length === 0) {
6558
+ lines.push("No pending proposals.");
6559
+ } else {
6560
+ for (const proposal of pending) {
6561
+ const relative2 = path9.relative(paths.root, proposal.proposalPath).split(path9.sep).join("/");
6562
+ lines.push("- [" + path9.basename(proposal.proposalPath, ".md") + "](" + relative2 + ")");
6563
+ }
6564
+ }
6565
+ lines.push("");
6566
+ await writeKnowledgePage(paths.index, lines.join("\n"));
6567
+ }
6568
+ async function appendLog(workspace, line) {
6569
+ const logPath = getKnowledgeWorkspacePaths(workspace).log;
6570
+ await fs6.appendFile(logPath, "- " + now() + " " + line + "\n", "utf8");
6571
+ }
6572
+ function toPageRecord(workspace, page, status, contentHash, manualContentHash) {
6573
+ return {
6574
+ id: page.frontmatter.id,
6575
+ workspaceId: workspace.id,
6576
+ relativePath: page.relativePath.replace(/^proposals\//, "pages/"),
6577
+ title: page.frontmatter.title,
6578
+ kind: page.frontmatter.kind,
6579
+ status,
6580
+ reviewState: page.frontmatter.reviewState,
6581
+ contentHash,
6582
+ sourceHash: page.frontmatter.sourceHash,
6583
+ claimIds: page.frontmatter.claimIds,
6584
+ ...page.frontmatter.snapshotId ? { snapshotId: page.frontmatter.snapshotId } : {},
6585
+ tags: page.frontmatter.tags,
6586
+ generatedAt: page.frontmatter.generatedAt,
6587
+ updatedAt: page.frontmatter.updatedAt,
6588
+ ...manualContentHash ? { manualContentHash } : {}
6589
+ };
6590
+ }
6591
+ async function compileKnowledgeWorkspace(input) {
6592
+ const store = await storeFor(input.workspace);
6593
+ const published = [];
6594
+ const proposals = [];
6595
+ const claims = selectPublishableClaims(input.claims, input.workspace.projectId);
6596
+ await withFileLock(getKnowledgeWorkspacePaths(input.workspace).root, async () => {
6597
+ for (const [title, group] of groupClaims(claims)) {
6598
+ const candidate = candidatePage(input.workspace, title, group, input.claims);
6599
+ const targetPath = resolveKnowledgeWorkspaceFile(input.workspace, candidate.targetRelativePath);
6600
+ const proposalPath = resolveKnowledgeWorkspaceFile(input.workspace, candidate.proposalRelativePath);
6601
+ let existing = store.getPage(input.workspace.id, candidate.targetRelativePath);
6602
+ const actualTargetHash = await fileHashIfPresent(targetPath);
6603
+ if (actualTargetHash && !existing) {
6604
+ const manualRecord = {
6605
+ ...toPageRecord(input.workspace, candidate.page, "active", actualTargetHash, actualTargetHash),
6606
+ reviewState: "needs-review",
6607
+ sourceHash: "manual:" + actualTargetHash
6608
+ };
6609
+ store.upsertPage(manualRecord);
6610
+ existing = manualRecord;
6611
+ }
6612
+ if (actualTargetHash && existing && existing.status === "active" && !existing.manualContentHash && actualTargetHash === existing.contentHash && existing.sourceHash === candidate.page.frontmatter.sourceHash) {
6613
+ published.push(existing);
6614
+ continue;
6615
+ }
6616
+ let reason = "new-page";
6617
+ let baseContentHash;
6618
+ if (actualTargetHash) {
6619
+ baseContentHash = existing?.contentHash;
6620
+ if (!existing || existing.status !== "active" || !!existing.manualContentHash || actualTargetHash !== existing.contentHash) {
6621
+ reason = "manual-edit-protected";
6622
+ } else {
6623
+ reason = "source-changed";
6624
+ }
6625
+ }
6626
+ if (!actualTargetHash) {
6627
+ store.upsertPage(toPageRecord(input.workspace, candidate.page, "proposed", candidate.page.contentHash));
6628
+ }
6629
+ await writeKnowledgePage(proposalPath, candidate.content);
6630
+ const proposal = store.createProposal({
6631
+ workspaceId: input.workspace.id,
6632
+ pageId: candidate.page.frontmatter.id,
6633
+ targetPath,
6634
+ proposalPath,
6635
+ ...baseContentHash ? { baseContentHash } : {},
6636
+ sourceHash: candidate.page.frontmatter.sourceHash,
6637
+ reason
6638
+ });
6639
+ proposals.push(proposal);
6640
+ await appendLog(input.workspace, "proposed " + candidate.targetRelativePath + " (" + reason + ").");
6641
+ }
6642
+ store.markWorkspaceCompiled(input.workspace.id);
6643
+ await writeIndex(input.workspace, store);
6644
+ });
6645
+ return { proposals, published };
6646
+ }
6647
+ async function applyKnowledgeProposal(input) {
6648
+ const store = await storeFor(input.workspace);
6649
+ const proposal = store.getProposal(input.proposalId);
6650
+ if (!proposal || proposal.workspaceId !== input.workspace.id) {
6651
+ throw new Error("Knowledge proposal was not found for this workspace");
6652
+ }
6653
+ if (proposal.status !== "pending") throw new Error("Knowledge proposal is not pending review");
6654
+ const root = getKnowledgeWorkspacePaths(input.workspace).root;
6655
+ const targetPath = path9.resolve(proposal.targetPath);
6656
+ const proposalPath = path9.resolve(proposal.proposalPath);
6657
+ if (!targetPath.startsWith(root + path9.sep) || !proposalPath.startsWith(root + path9.sep)) {
6658
+ throw new Error("Knowledge proposal path escapes its workspace");
6659
+ }
6660
+ await withFileLock(root, async () => {
6661
+ const actualTargetHash = await fileHashIfPresent(targetPath);
6662
+ if (proposal.baseContentHash && actualTargetHash !== proposal.baseContentHash && !input.allowManualOverwrite) {
6663
+ throw new Error("The target page has manual changes; review the proposal before applying it");
6664
+ }
6665
+ if (!proposal.baseContentHash && actualTargetHash && !input.allowManualOverwrite) {
6666
+ throw new Error("The target page already exists and has no known safe base revision");
6667
+ }
6668
+ const proposalPage = await readKnowledgePage(proposalPath, root);
6669
+ const activeFrontmatter = {
6670
+ ...proposalPage.frontmatter,
6671
+ status: "active",
6672
+ reviewState: "approved",
6673
+ updatedAt: now()
6674
+ };
6675
+ const content = renderKnowledgePage(activeFrontmatter, proposalPage.body);
6676
+ const contentHash = await writeKnowledgePage(targetPath, content);
6677
+ const activePage = await readKnowledgePage(targetPath, root);
6678
+ const record = toPageRecord(input.workspace, activePage, "active", contentHash);
6679
+ store.upsertPage(record);
6680
+ store.replacePageClaims(record.id, record.claimIds);
6681
+ store.replacePageLinks(record.id, activePage.links.map((link) => resolvePageLink(record.relativePath, link)).filter((link) => !!link));
6682
+ store.markProposalApplied(proposal.id);
6683
+ await appendLog(
6684
+ input.workspace,
6685
+ "applied " + record.relativePath + (input.allowManualOverwrite ? " after explicit manual overwrite review." : ".")
6686
+ );
6687
+ await writeIndex(input.workspace, store);
6688
+ });
6689
+ return { proposal: store.getProposal(proposal.id), targetPath };
6690
+ }
6691
+ async function markdownFiles(directory) {
6692
+ const files = [];
6693
+ let entries;
6694
+ try {
6695
+ entries = await fs6.readdir(directory, { withFileTypes: true });
6696
+ } catch {
6697
+ return files;
6698
+ }
6699
+ for (const entry of entries) {
6700
+ const filePath = path9.join(directory, entry.name);
6701
+ if (entry.isDirectory()) {
6702
+ files.push(...await markdownFiles(filePath));
6703
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
6704
+ files.push(filePath);
6705
+ }
6706
+ }
6707
+ return files;
6708
+ }
6709
+ async function lintKnowledgeWorkspace(input) {
6710
+ const store = await storeFor(input.workspace);
6711
+ const paths = getKnowledgeWorkspacePaths(input.workspace);
6712
+ const issues = [];
6713
+ const validPages = [];
6714
+ const files = await markdownFiles(paths.pages);
6715
+ for (const filePath of files) {
6716
+ try {
6717
+ validPages.push(await readKnowledgePage(filePath, paths.root));
6718
+ } catch (error) {
6719
+ issues.push({
6720
+ kind: "malformed-frontmatter",
6721
+ severity: "error",
6722
+ message: error instanceof Error ? error.message : "Malformed knowledge page.",
6723
+ relativePath: path9.relative(paths.root, filePath).split(path9.sep).join("/")
6724
+ });
6725
+ }
6726
+ }
6727
+ const indexed = /* @__PURE__ */ new Set();
6728
+ try {
6729
+ const index = await fs6.readFile(paths.index, "utf8");
6730
+ for (const link of extractInternalMarkdownLinks(index)) {
6731
+ const resolved = resolvePageLink("index.md", link);
6732
+ if (resolved) indexed.add(resolved);
6733
+ }
6734
+ } catch {
6735
+ issues.push({
6736
+ kind: "broken-link",
6737
+ severity: "error",
6738
+ message: "Knowledge index is missing.",
6739
+ relativePath: "index.md"
6740
+ });
6741
+ }
6742
+ const latestSnapshotId = input.codeStore?.latestSnapshot(input.workspace.projectId)?.id;
6743
+ for (const page of validPages) {
6744
+ if (!indexed.has(page.relativePath)) {
6745
+ issues.push({
6746
+ kind: "orphan-page",
6747
+ severity: "warning",
6748
+ message: "Page is not linked from index.md.",
6749
+ relativePath: page.relativePath
6750
+ });
6751
+ }
6752
+ for (const link of page.links) {
6753
+ const target = resolvePageLink(page.relativePath, link);
6754
+ if (!target || !await fileHashIfPresent(resolveKnowledgeWorkspaceFile(input.workspace, target))) {
6755
+ issues.push({
6756
+ kind: "broken-link",
6757
+ severity: "error",
6758
+ message: "Internal Markdown link does not resolve to a workspace page.",
6759
+ relativePath: page.relativePath
6760
+ });
6761
+ }
6762
+ }
6763
+ for (const claimId of page.frontmatter.claimIds) {
6764
+ const claim = input.claims.getClaim(claimId);
6765
+ if (!claim) {
6766
+ issues.push({
6767
+ kind: "missing-claim",
6768
+ severity: "error",
6769
+ message: "Page references a claim that is not present in the ledger.",
6770
+ relativePath: page.relativePath,
6771
+ claimId
6772
+ });
6773
+ continue;
6774
+ }
6775
+ if (input.claims.listEvidence(claim.id).length === 0) {
6776
+ issues.push({
6777
+ kind: "missing-evidence",
6778
+ severity: "error",
6779
+ message: "Page claim has no source evidence.",
6780
+ relativePath: page.relativePath,
6781
+ claimId
6782
+ });
6783
+ }
6784
+ if (claim.status === "superseded") {
6785
+ issues.push({
6786
+ kind: "superseded-claim",
6787
+ severity: "error",
6788
+ message: "Page still presents a superseded primary claim.",
6789
+ relativePath: page.relativePath,
6790
+ claimId
6791
+ });
6792
+ }
6793
+ if (claim.status === "disputed") {
6794
+ issues.push({
6795
+ kind: "unresolved-conflict",
6796
+ severity: "error",
6797
+ message: "Page claim has an unresolved competing assertion.",
6798
+ relativePath: page.relativePath,
6799
+ claimId
6800
+ });
6801
+ }
6802
+ }
6803
+ if (latestSnapshotId && page.frontmatter.snapshotId && page.frontmatter.snapshotId !== latestSnapshotId) {
6804
+ issues.push({
6805
+ kind: "stale-snapshot",
6806
+ severity: "warning",
6807
+ message: "Page was compiled against an older code snapshot.",
6808
+ relativePath: page.relativePath
6809
+ });
6810
+ }
6811
+ }
6812
+ for (const proposal of store.listProposals(input.workspace.id, "pending")) {
6813
+ if (proposal.reason === "manual-edit-protected") {
6814
+ issues.push({
6815
+ kind: "manual-edit-protected",
6816
+ severity: "warning",
6817
+ message: "A newer proposal was held because the target page has manual edits.",
6818
+ relativePath: path9.relative(paths.root, proposal.targetPath).split(path9.sep).join("/")
6819
+ });
6820
+ }
6821
+ }
6822
+ const valid = !issues.some((issue) => issue.severity === "error");
6823
+ store.markWorkspaceLinted(input.workspace.id, valid ? "ready" : "needs-review");
6824
+ return { valid, pagesScanned: validPages.length, issues };
6825
+ }
6826
+ var init_wiki = __esm({
6827
+ "src/knowledge/wiki.ts"() {
6828
+ "use strict";
6829
+ init_esm_shims();
6830
+ init_file_lock();
6831
+ init_markdown();
6832
+ init_workspace_store();
6833
+ init_workspace();
6834
+ init_markdown();
6835
+ }
6836
+ });
6837
+
6838
+ // src/workspace/workflow-sync.ts
6839
+ import matter2 from "gray-matter";
6840
+ var WorkflowSyncer;
6841
+ var init_workflow_sync = __esm({
6842
+ "src/workspace/workflow-sync.ts"() {
6843
+ "use strict";
6844
+ init_esm_shims();
6845
+ WorkflowSyncer = class {
6846
+ /**
6847
+ * Parse a Windsurf workflow markdown file into a WorkflowEntry.
6848
+ */
6849
+ parseWindsurfWorkflow(fileName, raw) {
6850
+ const name = fileName.replace(/\.md$/i, "");
6851
+ let description = "";
6852
+ let content = raw;
6853
+ try {
6854
+ const parsed = matter2(raw);
6855
+ description = parsed.data?.description ?? "";
6856
+ content = parsed.content.trim();
6857
+ } catch {
6858
+ }
6859
+ return {
6860
+ name,
6861
+ description,
6862
+ content,
6863
+ source: "windsurf",
6864
+ filePath: `.windsurf/workflows/${fileName}`
6865
+ };
6866
+ }
6867
+ /**
6868
+ * Convert a workflow to Codex SKILL.md format.
6869
+ */
6870
+ toCodexSkill(wf) {
6871
+ const safeName = this.sanitizeName(wf.name);
6872
+ const fm = { name: safeName };
6873
+ if (wf.description) {
6874
+ fm.description = wf.description;
6875
+ }
6876
+ const content = matter2.stringify(wf.content, fm);
6877
+ return {
6878
+ filePath: `.agents/skills/${safeName}/SKILL.md`,
6879
+ content
6880
+ };
6881
+ }
6882
+ /**
6883
+ * Convert a workflow to Cursor .mdc rule format.
6884
+ */
6885
+ toCursorRule(wf) {
6886
+ const safeName = this.sanitizeName(wf.name);
6887
+ const fm = {};
6888
+ if (wf.description) {
6889
+ fm.description = wf.description;
6890
+ }
6891
+ fm.globs = "";
6892
+ fm.alwaysApply = "false";
6893
+ const content = matter2.stringify(wf.content, fm);
6894
+ return {
6895
+ filePath: `.cursor/rules/${safeName}.mdc`,
6896
+ content
6897
+ };
6898
+ }
6899
+ /**
6900
+ * Convert a workflow to a CLAUDE.md section string.
6901
+ */
6902
+ toClaudeSection(wf) {
6903
+ const lines = [];
6904
+ lines.push(`## Workflow: ${wf.name}`);
6905
+ if (wf.description) {
6906
+ lines.push("");
6907
+ lines.push(`> ${wf.description}`);
6908
+ }
6909
+ lines.push("");
6910
+ lines.push(wf.content);
6911
+ return lines.join("\n");
6912
+ }
6913
+ /**
6914
+ * Convert all workflows to the target agent format.
6915
+ * Returns an array of { filePath, content } for each generated file.
6916
+ */
6917
+ convertAll(workflows, target) {
6918
+ if (target === "windsurf") {
6919
+ return [];
6920
+ }
6921
+ if (target === "codex") {
6922
+ return workflows.map((wf) => this.toCodexSkill(wf));
6923
+ }
6924
+ if (target === "cursor") {
6925
+ return workflows.map((wf) => this.toCursorRule(wf));
6926
+ }
6927
+ if (target === "claude-code") {
6928
+ const sections = workflows.map((wf) => this.toClaudeSection(wf));
6929
+ return [
6930
+ {
6931
+ filePath: "CLAUDE.md",
6932
+ content: sections.join("\n\n")
6933
+ }
6934
+ ];
6935
+ }
6936
+ return [];
6937
+ }
6938
+ // ---- Helpers ----
6939
+ sanitizeName(name) {
6940
+ return name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "workflow";
6941
+ }
6942
+ };
6943
+ }
6944
+ });
6945
+
6946
+ // src/knowledge/workflow-store.ts
6947
+ import { randomUUID as randomUUID7 } from "crypto";
6948
+ function parseJson(value, fallback) {
6949
+ if (typeof value !== "string" || !value) return fallback;
6950
+ try {
6951
+ return JSON.parse(value);
6952
+ } catch {
6953
+ return fallback;
6954
+ }
6955
+ }
6956
+ function optionalText3(value) {
6957
+ return typeof value === "string" && value.trim() ? value : void 0;
6958
+ }
6959
+ function rowToWorkflow(row) {
6960
+ return parseJson(row.specJson, {
6961
+ id: row.id,
6962
+ workspaceId: row.workspaceId,
6963
+ title: row.title,
6964
+ description: "",
6965
+ status: row.status,
6966
+ version: row.version,
6967
+ taskLenses: [],
6968
+ triggers: [],
6969
+ assumptions: [],
6970
+ requiredContext: [],
6971
+ guardrails: [],
6972
+ allowedTools: [],
6973
+ phases: [],
6974
+ verificationGates: [],
6975
+ claimIds: [],
6976
+ evidenceRefs: [],
6977
+ codeRefs: [],
6978
+ compatibleAgents: [],
6979
+ body: "",
6980
+ sourcePath: row.sourcePath,
6981
+ sourceHash: row.sourceHash,
6982
+ contentHash: row.contentHash,
6983
+ createdAt: row.createdAt,
6984
+ updatedAt: row.updatedAt
6985
+ });
6986
+ }
6987
+ function rowToRun(row) {
6988
+ return {
6989
+ id: row.id,
6990
+ workflowId: row.workflowId,
6991
+ projectId: row.projectId,
6992
+ task: row.task,
6993
+ ...optionalText3(row.startingSnapshotId) ? { startingSnapshotId: row.startingSnapshotId } : {},
6994
+ selectedEvidence: parseJson(row.selectedEvidenceJson, []),
6995
+ phaseState: parseJson(row.phaseStateJson, {}),
6996
+ outcome: row.outcome,
6997
+ verificationVerdict: row.verificationVerdict,
6998
+ ...optionalText3(row.failureReason) ? { failureReason: row.failureReason } : {},
6999
+ startedAt: row.startedAt,
7000
+ ...optionalText3(row.completedAt) ? { completedAt: row.completedAt } : {}
7001
+ };
7002
+ }
7003
+ var WorkflowStore;
7004
+ var init_workflow_store = __esm({
7005
+ "src/knowledge/workflow-store.ts"() {
7006
+ "use strict";
7007
+ init_esm_shims();
7008
+ init_sqlite_db();
7009
+ WorkflowStore = class {
7010
+ db = null;
7011
+ async init(dataDir) {
7012
+ this.db = getDatabase(dataDir);
7013
+ }
7014
+ upsertWorkflow(workflow) {
7015
+ this.db.prepare(
7016
+ "INSERT INTO knowledge_workflows (id, workspaceId, sourcePath, title, status, version, sourceHash, contentHash, specJson, importedFrom, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(workspaceId, sourcePath) DO UPDATE SET id = excluded.id, title = excluded.title, status = excluded.status, version = excluded.version, sourceHash = excluded.sourceHash, contentHash = excluded.contentHash, specJson = excluded.specJson, importedFrom = excluded.importedFrom, updatedAt = excluded.updatedAt"
7017
+ ).run(
7018
+ workflow.id,
7019
+ workflow.workspaceId,
7020
+ workflow.sourcePath,
7021
+ workflow.title,
7022
+ workflow.status,
7023
+ workflow.version,
7024
+ workflow.sourceHash,
7025
+ workflow.contentHash,
7026
+ JSON.stringify(workflow),
7027
+ workflow.importedFrom ?? null,
7028
+ workflow.createdAt,
7029
+ workflow.updatedAt
7030
+ );
7031
+ return this.getWorkflowByPath(workflow.workspaceId, workflow.sourcePath);
7032
+ }
7033
+ getWorkflow(id) {
7034
+ const row = this.db.prepare("SELECT * FROM knowledge_workflows WHERE id = ?").get(id);
7035
+ return row ? rowToWorkflow(row) : void 0;
7036
+ }
7037
+ getWorkflowByPath(workspaceId2, sourcePath) {
7038
+ const row = this.db.prepare("SELECT * FROM knowledge_workflows WHERE workspaceId = ? AND sourcePath = ?").get(workspaceId2, sourcePath);
7039
+ return row ? rowToWorkflow(row) : void 0;
7040
+ }
7041
+ listWorkflows(workspaceId2, status) {
7042
+ const rows = status ? this.db.prepare("SELECT * FROM knowledge_workflows WHERE workspaceId = ? AND status = ? ORDER BY title, id").all(workspaceId2, status) : this.db.prepare("SELECT * FROM knowledge_workflows WHERE workspaceId = ? ORDER BY title, id").all(workspaceId2);
7043
+ return rows.map(rowToWorkflow);
7044
+ }
7045
+ recordRun(input) {
7046
+ const run = {
7047
+ id: randomUUID7(),
7048
+ workflowId: input.workflowId,
7049
+ projectId: input.projectId,
7050
+ task: input.task.trim(),
7051
+ ...input.startingSnapshotId ? { startingSnapshotId: input.startingSnapshotId } : {},
7052
+ selectedEvidence: [...new Set(input.selectedEvidence ?? [])],
7053
+ phaseState: input.phaseState ?? {},
7054
+ outcome: input.outcome,
7055
+ verificationVerdict: input.verificationVerdict ?? (input.outcome === "failed" ? "failed" : "not-run"),
7056
+ ...input.failureReason?.trim() ? { failureReason: input.failureReason.trim() } : {},
7057
+ startedAt: input.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7058
+ ...input.completedAt ? { completedAt: input.completedAt } : input.outcome !== "in-progress" ? { completedAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
7059
+ };
7060
+ this.db.prepare(
7061
+ "INSERT INTO knowledge_workflow_runs (id, workflowId, projectId, task, startingSnapshotId, selectedEvidenceJson, phaseStateJson, outcome, verificationVerdict, failureReason, startedAt, completedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
7062
+ ).run(
7063
+ run.id,
7064
+ run.workflowId,
7065
+ run.projectId,
7066
+ run.task,
7067
+ run.startingSnapshotId ?? null,
7068
+ JSON.stringify(run.selectedEvidence),
7069
+ JSON.stringify(run.phaseState),
7070
+ run.outcome,
7071
+ run.verificationVerdict,
7072
+ run.failureReason ?? null,
7073
+ run.startedAt,
7074
+ run.completedAt ?? null
7075
+ );
7076
+ return this.getRun(run.id);
7077
+ }
7078
+ getRun(id) {
7079
+ const row = this.db.prepare("SELECT * FROM knowledge_workflow_runs WHERE id = ?").get(id);
7080
+ return row ? rowToRun(row) : void 0;
7081
+ }
7082
+ listRuns(projectId, workflowId, limit = 20) {
7083
+ const boundedLimit = Math.max(1, Math.min(Math.floor(limit), 100));
7084
+ const rows = workflowId ? this.db.prepare("SELECT * FROM knowledge_workflow_runs WHERE projectId = ? AND workflowId = ? ORDER BY startedAt DESC LIMIT ?").all(projectId, workflowId, boundedLimit) : this.db.prepare("SELECT * FROM knowledge_workflow_runs WHERE projectId = ? ORDER BY startedAt DESC LIMIT ?").all(projectId, boundedLimit);
7085
+ return rows.map(rowToRun);
7086
+ }
7087
+ recentFailureCautions(projectId, workflowId, limit = 2) {
7088
+ return this.listRuns(projectId, workflowId, limit).filter((run) => run.outcome === "failed" || run.verificationVerdict === "failed").map((run) => run.failureReason ? "Previous run failed: " + run.failureReason : "Previous run has a failed verification gate.");
7089
+ }
7090
+ };
7091
+ }
7092
+ });
7093
+
7094
+ // src/knowledge/workflows.ts
7095
+ var workflows_exports = {};
7096
+ __export(workflows_exports, {
7097
+ WORKFLOW_ADAPTER_TARGETS: () => WORKFLOW_ADAPTER_TARGETS,
7098
+ applyWorkflowAdapter: () => applyWorkflowAdapter,
7099
+ importWindsurfWorkflows: () => importWindsurfWorkflows,
7100
+ parseWorkflowMarkdown: () => parseWorkflowMarkdown,
7101
+ previewWorkflowAdapter: () => previewWorkflowAdapter,
7102
+ recordWorkflowRun: () => recordWorkflowRun,
7103
+ renderWorkflowMarkdown: () => renderWorkflowMarkdown,
7104
+ selectWorkflows: () => selectWorkflows,
7105
+ selectWorkspaceWorkflows: () => selectWorkspaceWorkflows,
7106
+ syncCanonicalWorkflows: () => syncCanonicalWorkflows,
7107
+ writeCanonicalWorkflow: () => writeCanonicalWorkflow
7108
+ });
7109
+ import { createHash as createHash9 } from "crypto";
7110
+ import { promises as fs7 } from "fs";
7111
+ import path10 from "path";
7112
+ import matter3 from "gray-matter";
7113
+ function hash3(value) {
7114
+ return createHash9("sha256").update(value).digest("hex");
7115
+ }
7116
+ function now2() {
7117
+ return (/* @__PURE__ */ new Date()).toISOString();
7118
+ }
7119
+ function slug(value) {
7120
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
7121
+ return normalized || "workflow";
7122
+ }
7123
+ function titleFromName(name) {
7124
+ return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ") || "Imported Workflow";
7125
+ }
7126
+ function requiredText(data, key) {
7127
+ const value = data[key];
7128
+ if (typeof value !== "string" || !value.trim()) {
7129
+ throw new Error("workflow frontmatter requires " + key);
7130
+ }
7131
+ return value.trim();
7132
+ }
7133
+ function optionalText4(data, key) {
7134
+ const value = data[key];
7135
+ if (value === void 0 || value === null || value === "") return void 0;
7136
+ if (typeof value !== "string") throw new Error("workflow frontmatter field " + key + " must be text");
7137
+ return value.trim() || void 0;
7138
+ }
7139
+ function optionalStringArray(data, key) {
7140
+ const value = data[key];
7141
+ if (value === void 0 || value === null) return [];
7142
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
7143
+ throw new Error("workflow frontmatter field " + key + " must be a string array");
7144
+ }
7145
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
7146
+ }
7147
+ function optionalNumber(data, key, fallback) {
7148
+ const value = data[key];
7149
+ if (value === void 0 || value === null || value === "") return fallback;
7150
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
7151
+ throw new Error("workflow frontmatter field " + key + " must be a positive integer");
7152
+ }
7153
+ return value;
7154
+ }
7155
+ function optionalStatus(data) {
7156
+ const value = data.status;
7157
+ if (value === void 0 || value === null || value === "") return "draft";
7158
+ if (typeof value !== "string" || !STATUS_VALUES.includes(value)) {
7159
+ throw new Error("workflow frontmatter field status is invalid");
7160
+ }
7161
+ return value;
7162
+ }
7163
+ function normalizeAgents(values) {
7164
+ const unknown = values.filter((value) => !KNOWN_AGENTS.includes(value));
7165
+ if (unknown.length > 0) {
7166
+ throw new Error("workflow frontmatter has unsupported agent: " + unknown[0]);
7167
+ }
7168
+ return values;
7169
+ }
7170
+ function phaseFromValue(value, index) {
7171
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7172
+ throw new Error("workflow frontmatter phases must contain objects");
7173
+ }
7174
+ const data = value;
7175
+ const title = requiredText(data, "title");
7176
+ const phaseId = optionalText4(data, "id") ?? slug(title) + "-" + (index + 1);
7177
+ return {
7178
+ id: phaseId,
7179
+ title,
7180
+ instructions: optionalText4(data, "instructions") ?? "",
7181
+ branches: optionalStringArray(data, "branches"),
7182
+ expectedOutputs: optionalStringArray(data, "expectedOutputs"),
7183
+ verificationGates: optionalStringArray(data, "verificationGates")
7184
+ };
7185
+ }
7186
+ function phasesFromBody(body) {
7187
+ const headers = [...body.matchAll(/^##\s+(.+?)\s*$/gm)];
7188
+ if (headers.length === 0) {
7189
+ const instructions = body.trim();
7190
+ return instructions ? [{
7191
+ id: "execute",
7192
+ title: "Execute",
7193
+ instructions,
7194
+ branches: [],
7195
+ expectedOutputs: [],
7196
+ verificationGates: []
7197
+ }] : [];
7198
+ }
7199
+ return headers.map((match, index) => {
7200
+ const title = match[1].trim();
7201
+ const start = (match.index ?? 0) + match[0].length;
7202
+ const end = index + 1 < headers.length ? headers[index + 1].index ?? body.length : body.length;
7203
+ return {
7204
+ id: slug(title) + "-" + (index + 1),
7205
+ title,
7206
+ instructions: body.slice(start, end).trim(),
7207
+ branches: [],
7208
+ expectedOutputs: [],
7209
+ verificationGates: []
7210
+ };
7211
+ });
7212
+ }
7213
+ function normalizeSourcePath(sourcePath) {
7214
+ const normalized = sourcePath.replace(/\\/g, "/");
7215
+ if (!normalized.startsWith("workflows/") || !normalized.toLowerCase().endsWith(".md") || normalized.includes("../") || path10.posix.normalize(normalized) !== normalized) {
7216
+ throw new Error("workflow source path must be a safe workflows/*.md path");
7217
+ }
7218
+ return normalized;
7219
+ }
7220
+ function sourceHashFor(workflow) {
7221
+ return hash3(JSON.stringify({
7222
+ id: workflow.id,
7223
+ title: workflow.title,
7224
+ description: workflow.description,
7225
+ status: workflow.status,
7226
+ version: workflow.version,
7227
+ taskLenses: workflow.taskLenses,
7228
+ triggers: workflow.triggers,
7229
+ assumptions: workflow.assumptions,
7230
+ requiredContext: workflow.requiredContext,
7231
+ guardrails: workflow.guardrails,
7232
+ allowedTools: workflow.allowedTools,
7233
+ phases: workflow.phases,
7234
+ verificationGates: workflow.verificationGates,
7235
+ claimIds: workflow.claimIds,
7236
+ evidenceRefs: workflow.evidenceRefs,
7237
+ codeRefs: workflow.codeRefs,
7238
+ compatibleAgents: workflow.compatibleAgents,
7239
+ body: workflow.body,
7240
+ sourcePath: workflow.sourcePath,
7241
+ importedFrom: workflow.importedFrom ?? null
7242
+ }));
7243
+ }
7244
+ function workflowFrontmatter(workflow, sourceHash) {
4386
7245
  return {
4387
- id: makeObservationCodeRefId(projectId, obs.id, file.id),
4388
- projectId,
4389
- observationId: obs.id,
4390
- fileId: file.id,
4391
- capturedFileHash: file.contentHash,
4392
- status: "current",
4393
- reason: "bound by file path",
4394
- createdAt: obs.createdAt
7246
+ id: workflow.id,
7247
+ title: workflow.title,
7248
+ description: workflow.description,
7249
+ status: workflow.status,
7250
+ version: workflow.version,
7251
+ taskLenses: workflow.taskLenses,
7252
+ triggers: workflow.triggers,
7253
+ assumptions: workflow.assumptions,
7254
+ requiredContext: workflow.requiredContext,
7255
+ guardrails: workflow.guardrails,
7256
+ allowedTools: workflow.allowedTools,
7257
+ phases: workflow.phases.map((phase) => ({
7258
+ id: phase.id,
7259
+ title: phase.title,
7260
+ ...phase.instructions ? { instructions: phase.instructions } : {},
7261
+ ...phase.branches.length ? { branches: phase.branches } : {},
7262
+ ...phase.expectedOutputs.length ? { expectedOutputs: phase.expectedOutputs } : {},
7263
+ ...phase.verificationGates.length ? { verificationGates: phase.verificationGates } : {}
7264
+ })),
7265
+ verificationGates: workflow.verificationGates,
7266
+ claimIds: workflow.claimIds,
7267
+ evidenceRefs: workflow.evidenceRefs,
7268
+ codeRefs: workflow.codeRefs,
7269
+ compatibleAgents: workflow.compatibleAgents,
7270
+ sourceHash,
7271
+ createdAt: workflow.createdAt,
7272
+ updatedAt: workflow.updatedAt,
7273
+ ...workflow.importedFrom ? { importedFrom: workflow.importedFrom } : {}
4395
7274
  };
4396
7275
  }
4397
- function symbolRef(projectId, obs, file, symbol) {
7276
+ function renderWorkflowMarkdown(workflow) {
7277
+ const sourceHash = sourceHashFor(workflow);
7278
+ return matter3.stringify(workflow.body.trimEnd() + "\n", workflowFrontmatter(workflow, sourceHash));
7279
+ }
7280
+ function parsedWorkflow(data, body, input) {
7281
+ const phasesValue = data.phases;
7282
+ const parsedPhases = phasesValue === void 0 || phasesValue === null ? phasesFromBody(body) : Array.isArray(phasesValue) ? phasesValue.map(phaseFromValue) : (() => {
7283
+ throw new Error("workflow frontmatter field phases must be an array");
7284
+ })();
7285
+ const candidate = {
7286
+ id: requiredText(data, "id"),
7287
+ workspaceId: input.workspaceId,
7288
+ title: requiredText(data, "title"),
7289
+ description: optionalText4(data, "description") ?? "",
7290
+ status: optionalStatus(data),
7291
+ version: optionalNumber(data, "version", 1),
7292
+ taskLenses: optionalStringArray(data, "taskLenses"),
7293
+ triggers: optionalStringArray(data, "triggers"),
7294
+ assumptions: optionalStringArray(data, "assumptions"),
7295
+ requiredContext: optionalStringArray(data, "requiredContext"),
7296
+ guardrails: optionalStringArray(data, "guardrails"),
7297
+ allowedTools: optionalStringArray(data, "allowedTools"),
7298
+ phases: parsedPhases,
7299
+ verificationGates: optionalStringArray(data, "verificationGates"),
7300
+ claimIds: optionalStringArray(data, "claimIds"),
7301
+ evidenceRefs: optionalStringArray(data, "evidenceRefs"),
7302
+ codeRefs: optionalStringArray(data, "codeRefs"),
7303
+ compatibleAgents: normalizeAgents(optionalStringArray(data, "compatibleAgents").length ? optionalStringArray(data, "compatibleAgents") : optionalStringArray(data, "allowedAgents")),
7304
+ body: body.trim(),
7305
+ sourcePath: normalizeSourcePath(input.sourcePath),
7306
+ sourceHash: "",
7307
+ contentHash: input.contentHash,
7308
+ createdAt: optionalText4(data, "createdAt") ?? now2(),
7309
+ updatedAt: optionalText4(data, "updatedAt") ?? now2(),
7310
+ ...optionalText4(data, "importedFrom") ? { importedFrom: optionalText4(data, "importedFrom") } : {}
7311
+ };
7312
+ if (candidate.phases.length === 0) {
7313
+ throw new Error("workflow must contain at least one phase or a non-empty body");
7314
+ }
7315
+ return { ...candidate, sourceHash: sourceHashFor(candidate) };
7316
+ }
7317
+ function parseWorkflowMarkdown(raw, input) {
7318
+ let parsed;
7319
+ try {
7320
+ parsed = matter3(raw);
7321
+ } catch (error) {
7322
+ throw new Error("Malformed workflow Markdown: " + (error instanceof Error ? error.message : String(error)));
7323
+ }
7324
+ return parsedWorkflow(parsed.data, parsed.content, {
7325
+ workspaceId: input.workspaceId,
7326
+ sourcePath: input.sourcePath,
7327
+ contentHash: hash3(raw)
7328
+ });
7329
+ }
7330
+ function workflowStoreDataDir(workspace) {
7331
+ if (!workspace.dataDir) throw new Error("Knowledge workspace is missing its local data directory");
7332
+ return workspace.dataDir;
7333
+ }
7334
+ async function storeFor2(workspace) {
7335
+ const store = new WorkflowStore();
7336
+ await store.init(workflowStoreDataDir(workspace));
7337
+ return store;
7338
+ }
7339
+ function materializeWorkflow(workflow) {
7340
+ const updatedAt = workflow.updatedAt || now2();
7341
+ const withDates = {
7342
+ ...workflow,
7343
+ createdAt: workflow.createdAt || updatedAt,
7344
+ updatedAt
7345
+ };
7346
+ const sourceHash = sourceHashFor(withDates);
7347
+ const content = renderWorkflowMarkdown({ ...withDates, sourceHash, contentHash: "" });
4398
7348
  return {
4399
- id: makeObservationCodeRefId(projectId, obs.id, file.id, symbol.id),
4400
- projectId,
4401
- observationId: obs.id,
4402
- fileId: file.id,
4403
- symbolId: symbol.id,
4404
- capturedFileHash: file.contentHash,
4405
- ...symbol.contentHash ? { capturedSymbolHash: symbol.contentHash } : {},
4406
- status: "current",
4407
- reason: "bound by symbol mention",
4408
- createdAt: obs.createdAt
7349
+ ...withDates,
7350
+ sourceHash,
7351
+ contentHash: hash3(content)
4409
7352
  };
4410
7353
  }
4411
- function resolveObservationCodeRefs(obs, lookup) {
4412
- const refs = /* @__PURE__ */ new Map();
4413
- const text = observationText(obs);
4414
- const candidateFiles = /* @__PURE__ */ new Map();
4415
- for (const rawPath of obs.filesModified ?? []) {
4416
- const file = lookup.findFile(normalizeCodePath(rawPath));
4417
- if (!file) continue;
4418
- candidateFiles.set(file.id, file);
4419
- const ref = fileRef(obs.projectId, obs, file);
4420
- refs.set(ref.id, ref);
7354
+ async function writeCanonicalWorkflow(input) {
7355
+ const paths = getKnowledgeWorkspacePaths(input.workspace);
7356
+ const workflow = materializeWorkflow({
7357
+ ...input.workflow,
7358
+ workspaceId: input.workspace.id,
7359
+ sourcePath: normalizeSourcePath(input.workflow.sourcePath)
7360
+ });
7361
+ const filePath = resolveKnowledgeWorkspaceFile(input.workspace, workflow.sourcePath);
7362
+ const content = renderWorkflowMarkdown(workflow);
7363
+ await withFileLock(paths.root, async () => {
7364
+ await fs7.mkdir(path10.dirname(filePath), { recursive: true });
7365
+ await atomicWriteFile(filePath, content);
7366
+ });
7367
+ const store = await storeFor2(input.workspace);
7368
+ return store.upsertWorkflow({ ...workflow, contentHash: hash3(content) });
7369
+ }
7370
+ async function markdownFiles2(directory) {
7371
+ let entries;
7372
+ try {
7373
+ entries = await fs7.readdir(directory, { withFileTypes: true });
7374
+ } catch {
7375
+ return [];
4421
7376
  }
4422
- const hintedFileIds = new Set(candidateFiles.keys());
4423
- const symbolNames = hintedFileIds.size > 0 ? identifierCandidates(text) : codeIdentifierCandidates(text);
4424
- const symbols = lookup.findSymbols(symbolNames, [...hintedFileIds]);
4425
- const symbolsByName = /* @__PURE__ */ new Map();
4426
- for (const symbol of symbols) {
4427
- const group = symbolsByName.get(symbol.name) ?? [];
4428
- group.push(symbol);
4429
- symbolsByName.set(symbol.name, group);
7377
+ const files = [];
7378
+ for (const entry of entries) {
7379
+ const absolutePath = path10.join(directory, entry.name);
7380
+ if (entry.isDirectory()) {
7381
+ files.push(...await markdownFiles2(absolutePath));
7382
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
7383
+ files.push(absolutePath);
7384
+ }
7385
+ }
7386
+ return files;
7387
+ }
7388
+ async function syncCanonicalWorkflows(workspace) {
7389
+ const paths = getKnowledgeWorkspacePaths(workspace);
7390
+ const store = await storeFor2(workspace);
7391
+ const workflows = [];
7392
+ const errors = [];
7393
+ for (const absolutePath of await markdownFiles2(paths.workflows)) {
7394
+ const sourcePath = path10.relative(paths.root, absolutePath).split(path10.sep).join("/");
7395
+ try {
7396
+ const raw = await fs7.readFile(absolutePath, "utf8");
7397
+ const workflow = parseWorkflowMarkdown(raw, { workspaceId: workspace.id, sourcePath });
7398
+ workflows.push(store.upsertWorkflow(workflow));
7399
+ } catch (error) {
7400
+ errors.push({
7401
+ sourcePath,
7402
+ message: error instanceof Error ? error.message : String(error)
7403
+ });
7404
+ }
4430
7405
  }
4431
- for (const group of symbolsByName.values()) {
4432
- const candidates = group.length === 1 ? group : [];
4433
- for (const symbol of candidates) {
4434
- if (!mentionsSymbol(text, symbol)) continue;
4435
- const file = candidateFiles.get(symbol.fileId) ?? lookup.findFile(symbol.path);
4436
- if (!file) continue;
4437
- candidateFiles.set(file.id, file);
4438
- const ref = symbolRef(obs.projectId, obs, file, symbol);
4439
- refs.set(ref.id, ref);
7406
+ return { workflows, errors };
7407
+ }
7408
+ function inferTaskLenses(value) {
7409
+ const text = value.toLowerCase();
7410
+ return Object.entries(LENS_TERMS).filter(([, terms]) => terms.some((term) => text.includes(term))).map(([lens]) => lens);
7411
+ }
7412
+ function taskScore(workflow, task) {
7413
+ const text = task.toLowerCase();
7414
+ let score = 0;
7415
+ const reasons = [];
7416
+ for (const lens of workflow.taskLenses) {
7417
+ const terms = LENS_TERMS[lens.toLowerCase()] ?? [lens.toLowerCase()];
7418
+ if (terms.some((term) => text.includes(term))) {
7419
+ score += 20;
7420
+ reasons.push("matches " + lens + " workflow");
4440
7421
  }
4441
7422
  }
4442
- return [...refs.values()];
7423
+ for (const trigger of workflow.triggers) {
7424
+ const term = trigger.toLowerCase().trim();
7425
+ if (term.length >= 2 && text.includes(term)) {
7426
+ score += 8;
7427
+ reasons.push('matches trigger "' + trigger + '"');
7428
+ }
7429
+ }
7430
+ return { score, reasons: [...new Set(reasons)] };
4443
7431
  }
4444
- function createStoreLookup(store, projectId) {
4445
- return {
4446
- findFile: (path8) => store.getFile(projectId, path8) ?? void 0,
4447
- findSymbols: (names, hintedFileIds) => store.findSymbolsByNames(projectId, names, hintedFileIds)
7432
+ function selectWorkflows(input) {
7433
+ const selected = input.workflows.filter((workflow) => workflow.status === "active").map((workflow) => {
7434
+ const match = taskScore(workflow, input.task);
7435
+ const cautions = input.store && input.projectId ? input.store.recentFailureCautions(input.projectId, workflow.id) : [];
7436
+ return {
7437
+ workflow,
7438
+ score: match.score,
7439
+ reasons: match.reasons,
7440
+ firstPhase: workflow.phases[0],
7441
+ cautions
7442
+ };
7443
+ }).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.workflow.title.localeCompare(right.workflow.title));
7444
+ return selected.slice(0, Math.max(1, Math.min(input.limit ?? 2, 2)));
7445
+ }
7446
+ function importedWorkflowSpec(input) {
7447
+ const entry = new WorkflowSyncer().parseWindsurfWorkflow(input.sourceName, input.raw);
7448
+ const content = entry.content.trim() || "## Execute\n\nFollow the imported workflow.";
7449
+ const lenses = inferTaskLenses(entry.name + "\n" + entry.description + "\n" + content);
7450
+ const createdAt = now2();
7451
+ const sourcePath = "workflows/" + slug(entry.name) + ".md";
7452
+ const spec = {
7453
+ id: "workflow:" + hash3(input.workspace.id + ":" + sourcePath).slice(0, 24),
7454
+ workspaceId: input.workspace.id,
7455
+ title: titleFromName(entry.name),
7456
+ description: entry.description || "Imported from " + input.sourcePath,
7457
+ status: "active",
7458
+ version: 1,
7459
+ taskLenses: lenses,
7460
+ triggers: lenses,
7461
+ assumptions: [],
7462
+ requiredContext: [],
7463
+ guardrails: [],
7464
+ allowedTools: [],
7465
+ phases: phasesFromBody(content),
7466
+ verificationGates: [],
7467
+ claimIds: [],
7468
+ evidenceRefs: [],
7469
+ codeRefs: [],
7470
+ compatibleAgents: WORKFLOW_ADAPTER_TARGETS,
7471
+ body: content,
7472
+ sourcePath,
7473
+ sourceHash: "",
7474
+ contentHash: "",
7475
+ createdAt,
7476
+ updatedAt: createdAt,
7477
+ importedFrom: input.sourcePath.replace(/\\/g, "/")
4448
7478
  };
4449
- }
4450
- function createSnapshotLookup(files, symbols) {
4451
- const filesByPath = new Map(files.map((file) => [normalizeCodePath(file.path), file]));
4452
- const symbolsByName = /* @__PURE__ */ new Map();
4453
- for (const symbol of symbols) {
4454
- const group = symbolsByName.get(symbol.name) ?? [];
4455
- group.push(symbol);
4456
- symbolsByName.set(symbol.name, group);
7479
+ return materializeWorkflow(spec);
7480
+ }
7481
+ async function importWindsurfWorkflows(input) {
7482
+ const sourceDirectory = path10.join(path10.resolve(input.projectRoot), ".windsurf", "workflows");
7483
+ const paths = getKnowledgeWorkspacePaths(input.workspace);
7484
+ const imported = [];
7485
+ const skipped = [];
7486
+ let entries;
7487
+ try {
7488
+ entries = await fs7.readdir(sourceDirectory, { withFileTypes: true });
7489
+ } catch {
7490
+ return { imported, skipped };
4457
7491
  }
4458
- return {
4459
- findFile: (path8) => filesByPath.get(normalizeCodePath(path8)),
4460
- findSymbols: (names, hintedFileIds) => {
4461
- const hinted = new Set(hintedFileIds);
4462
- const found = [];
4463
- for (const name of names) {
4464
- const candidates = symbolsByName.get(name) ?? [];
4465
- found.push(...hinted.size > 0 ? candidates.filter((symbol) => hinted.has(symbol.fileId)) : candidates);
7492
+ for (const entry of entries.filter((item) => item.isFile() && item.name.toLowerCase().endsWith(".md"))) {
7493
+ const originalPath = path10.join(sourceDirectory, entry.name);
7494
+ const sourcePath = path10.relative(path10.resolve(input.projectRoot), originalPath).split(path10.sep).join("/");
7495
+ try {
7496
+ const raw = await fs7.readFile(originalPath, "utf8");
7497
+ const workflow = importedWorkflowSpec({
7498
+ workspace: input.workspace,
7499
+ sourceName: entry.name,
7500
+ sourcePath,
7501
+ raw
7502
+ });
7503
+ const targetPath = resolveKnowledgeWorkspaceFile(input.workspace, workflow.sourcePath);
7504
+ try {
7505
+ await fs7.access(targetPath);
7506
+ skipped.push({ sourcePath, reason: "canonical workflow already exists and was preserved" });
7507
+ continue;
7508
+ } catch {
4466
7509
  }
4467
- return found;
7510
+ imported.push(await writeCanonicalWorkflow({ workspace: input.workspace, workflow }));
7511
+ } catch (error) {
7512
+ skipped.push({
7513
+ sourcePath,
7514
+ reason: error instanceof Error ? error.message : String(error)
7515
+ });
4468
7516
  }
4469
- };
7517
+ }
7518
+ await fs7.mkdir(paths.workflows, { recursive: true });
7519
+ return { imported, skipped };
7520
+ }
7521
+ function isWithin2(root, candidate) {
7522
+ const relative2 = path10.relative(root, candidate);
7523
+ return relative2 !== "" && relative2 !== ".." && !relative2.startsWith(".." + path10.sep) && !path10.isAbsolute(relative2);
7524
+ }
7525
+ function adapterRelativePath(workflow, agent) {
7526
+ const name = "memorix-workflow-" + slug(workflow.id.replace(/^workflow:/, ""));
7527
+ if (agent === "codex") return ".agents/skills/" + name + "/SKILL.md";
7528
+ if (agent === "claude-code") return ".claude/skills/" + name + "/SKILL.md";
7529
+ if (agent === "cursor") return ".cursor/rules/" + name + ".mdc";
7530
+ return ".windsurf/workflows/" + name + ".md";
7531
+ }
7532
+ function adapterMarker(workflow) {
7533
+ return '<!-- memorix:workflow-adapter id="' + workflow.id + '" -->';
7534
+ }
7535
+ function adapterContent(workflow, agent) {
7536
+ const marker = adapterMarker(workflow);
7537
+ const firstPhase = workflow.phases[0];
7538
+ const gates = workflow.verificationGates.length ? workflow.verificationGates : firstPhase.verificationGates;
7539
+ if (agent === "codex" || agent === "claude-code") {
7540
+ return matter3.stringify([
7541
+ marker,
7542
+ "",
7543
+ "# " + workflow.title,
7544
+ "",
7545
+ workflow.description,
7546
+ "",
7547
+ "## Start",
7548
+ "",
7549
+ firstPhase.instructions || firstPhase.title,
7550
+ "",
7551
+ "## Verification",
7552
+ "",
7553
+ ...gates.length ? gates.map((gate) => "- " + gate) : ["- Follow the project verification standards."],
7554
+ "",
7555
+ "## Full workflow",
7556
+ "",
7557
+ workflow.body.trim(),
7558
+ ""
7559
+ ].join("\n"), {
7560
+ name: "memorix-workflow-" + slug(workflow.id.replace(/^workflow:/, "")),
7561
+ description: workflow.description || workflow.title
7562
+ });
7563
+ }
7564
+ if (agent === "cursor") {
7565
+ return matter3.stringify([
7566
+ marker,
7567
+ "",
7568
+ "# " + workflow.title,
7569
+ "",
7570
+ workflow.body.trim(),
7571
+ ""
7572
+ ].join("\n"), {
7573
+ description: workflow.description || workflow.title,
7574
+ globs: "",
7575
+ alwaysApply: false
7576
+ });
7577
+ }
7578
+ return matter3.stringify([
7579
+ marker,
7580
+ "",
7581
+ "# " + workflow.title,
7582
+ "",
7583
+ workflow.body.trim(),
7584
+ ""
7585
+ ].join("\n"), {
7586
+ description: workflow.description || workflow.title
7587
+ });
4470
7588
  }
4471
- async function bindObservationToCode(store, obs) {
4472
- const refs = resolveObservationCodeRefs(obs, createStoreLookup(store, obs.projectId));
4473
- store.replaceObservationRefs(obs.projectId, obs.id, refs);
4474
- return refs;
7589
+ function isWorkflowAdapterTarget(agent) {
7590
+ return WORKFLOW_ADAPTER_TARGETS.includes(agent);
4475
7591
  }
4476
- async function backfillMissingObservationCodeRefs(store, observations2) {
4477
- let observationsBackfilled = 0;
4478
- let refsBackfilled = 0;
4479
- const boundObservationIdsByProject = /* @__PURE__ */ new Map();
4480
- const lookupByProject = /* @__PURE__ */ new Map();
4481
- for (const projectId of new Set(observations2.map((observation) => observation.projectId))) {
4482
- boundObservationIdsByProject.set(
4483
- projectId,
4484
- new Set(store.listProjectObservationRefs(projectId).map((ref) => ref.observationId))
4485
- );
4486
- lookupByProject.set(
4487
- projectId,
4488
- createSnapshotLookup(store.listFiles(projectId), store.listSymbols(projectId))
4489
- );
7592
+ async function previewWorkflowAdapter(input) {
7593
+ if (!isWorkflowAdapterTarget(input.agent)) {
7594
+ return {
7595
+ agent: input.agent,
7596
+ workflowId: input.workflow.id,
7597
+ status: "unsupported",
7598
+ reason: "This agent has no safe native workflow adapter. Use Memorix Project Context instead."
7599
+ };
4490
7600
  }
4491
- const refsToInsert = [];
4492
- for (const observation of observations2) {
4493
- if (boundObservationIdsByProject.get(observation.projectId)?.has(observation.id)) continue;
4494
- if ((observation.filesModified?.length ?? 0) === 0 && codeIdentifierCandidates(observationText(observation)).length === 0) {
4495
- continue;
4496
- }
4497
- const lookup = lookupByProject.get(observation.projectId);
4498
- if (!lookup) continue;
4499
- const refs = resolveObservationCodeRefs(observation, lookup);
4500
- if (refs.length === 0) continue;
4501
- observationsBackfilled += 1;
4502
- refsBackfilled += refs.length;
4503
- refsToInsert.push(...refs);
7601
+ if (input.workflow.compatibleAgents.length > 0 && !input.workflow.compatibleAgents.includes(input.agent)) {
7602
+ return {
7603
+ agent: input.agent,
7604
+ workflowId: input.workflow.id,
7605
+ status: "unsupported",
7606
+ reason: "The canonical workflow does not declare compatibility with this agent."
7607
+ };
4504
7608
  }
4505
- store.upsertObservationRefs(refsToInsert);
7609
+ const projectRoot = path10.resolve(input.projectRoot);
7610
+ const targetPath = path10.resolve(projectRoot, adapterRelativePath(input.workflow, input.agent));
7611
+ if (!isWithin2(projectRoot, targetPath)) {
7612
+ throw new Error("Workflow adapter target escapes the project root");
7613
+ }
7614
+ const content = adapterContent(input.workflow, input.agent);
7615
+ let existing;
7616
+ try {
7617
+ existing = await fs7.readFile(targetPath, "utf8");
7618
+ } catch (error) {
7619
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
7620
+ if (code !== "ENOENT") throw error;
7621
+ }
7622
+ if (existing === void 0) {
7623
+ return { agent: input.agent, workflowId: input.workflow.id, targetPath, content, status: "create", reason: "No existing project adapter file." };
7624
+ }
7625
+ if (existing === content) {
7626
+ return { agent: input.agent, workflowId: input.workflow.id, targetPath, content, status: "unchanged", reason: "Existing Memorix adapter already matches the canonical workflow." };
7627
+ }
7628
+ if (existing.includes(adapterMarker(input.workflow))) {
7629
+ return { agent: input.agent, workflowId: input.workflow.id, targetPath, content, status: "update", reason: "Existing Memorix-owned adapter will be updated." };
7630
+ }
7631
+ return { agent: input.agent, workflowId: input.workflow.id, targetPath, content, status: "conflict", reason: "A user-owned project file already occupies this adapter path." };
7632
+ }
7633
+ async function applyWorkflowAdapter(input) {
7634
+ const initial = await previewWorkflowAdapter(input);
7635
+ if (initial.status === "unsupported" || initial.status === "conflict" || initial.status === "unchanged") {
7636
+ return initial;
7637
+ }
7638
+ const projectRoot = path10.resolve(input.projectRoot);
7639
+ return withFileLock(projectRoot, async () => {
7640
+ const current = await previewWorkflowAdapter(input);
7641
+ if (current.status !== "create" && current.status !== "update") return current;
7642
+ await fs7.mkdir(path10.dirname(current.targetPath), { recursive: true });
7643
+ await atomicWriteFile(current.targetPath, current.content);
7644
+ return current;
7645
+ });
7646
+ }
7647
+ async function recordWorkflowRun(input) {
7648
+ const store = await storeFor2(input.workspace);
7649
+ const workflow = store.getWorkflow(input.run.workflowId);
7650
+ if (!workflow || workflow.workspaceId !== input.workspace.id) {
7651
+ throw new Error("Workflow was not found for this workspace");
7652
+ }
7653
+ if (!input.run.task.trim()) throw new Error("Workflow run task is required");
7654
+ return store.recordRun(input.run);
7655
+ }
7656
+ async function selectWorkspaceWorkflows(input) {
7657
+ const synced = await syncCanonicalWorkflows(input.workspace);
7658
+ const store = await storeFor2(input.workspace);
4506
7659
  return {
4507
- observationsScanned: observations2.length,
4508
- observationsBackfilled,
4509
- refsBackfilled
7660
+ selections: selectWorkflows({
7661
+ workflows: synced.workflows,
7662
+ task: input.task,
7663
+ projectId: input.workspace.projectId,
7664
+ store,
7665
+ limit: input.limit
7666
+ }),
7667
+ errors: synced.errors
4510
7668
  };
4511
7669
  }
4512
- var init_binder = __esm({
4513
- "src/codegraph/binder.ts"() {
7670
+ var KNOWN_AGENTS, WORKFLOW_ADAPTER_TARGETS, STATUS_VALUES, LENS_TERMS;
7671
+ var init_workflows = __esm({
7672
+ "src/knowledge/workflows.ts"() {
4514
7673
  "use strict";
4515
7674
  init_esm_shims();
4516
- init_ids();
7675
+ init_file_lock();
7676
+ init_workflow_sync();
7677
+ init_workspace();
7678
+ init_workflow_store();
7679
+ KNOWN_AGENTS = [
7680
+ "windsurf",
7681
+ "cursor",
7682
+ "claude-code",
7683
+ "codex",
7684
+ "copilot",
7685
+ "antigravity",
7686
+ "gemini-cli",
7687
+ "openclaw",
7688
+ "hermes",
7689
+ "omp",
7690
+ "kiro",
7691
+ "opencode",
7692
+ "trae"
7693
+ ];
7694
+ WORKFLOW_ADAPTER_TARGETS = [
7695
+ "codex",
7696
+ "claude-code",
7697
+ "cursor",
7698
+ "windsurf"
7699
+ ];
7700
+ STATUS_VALUES = ["draft", "active", "archived"];
7701
+ LENS_TERMS = {
7702
+ release: ["release", "publish", "npm", "version", "\u53D1\u7248", "\u53D1\u5E03"],
7703
+ bugfix: ["bug", "fix", "error", "failure", "issue", "repair", "\u4FEE\u590D", "\u62A5\u9519", "\u6545\u969C"],
7704
+ migration: ["migration", "migrate", "upgrade", "\u8FC1\u79FB", "\u5347\u7EA7"],
7705
+ review: ["review", "audit", "pr", "code review", "\u5BA1\u67E5", "\u8BC4\u5BA1"],
7706
+ onboarding: ["onboard", "onboarding", "understand", "introduce", "\u63A5\u624B", "\u4E86\u89E3"],
7707
+ refactor: ["refactor", "cleanup", "restructure", "\u91CD\u6784", "\u6574\u7406"],
7708
+ test: ["test", "verify", "smoke", "\u6D4B\u8BD5", "\u9A8C\u8BC1"]
7709
+ };
4517
7710
  }
4518
7711
  });
4519
7712
 
@@ -4522,12 +7715,12 @@ var fastembed_provider_exports = {};
4522
7715
  __export(fastembed_provider_exports, {
4523
7716
  FastEmbedProvider: () => FastEmbedProvider
4524
7717
  });
4525
- import { createHash as createHash3 } from "crypto";
7718
+ import { createHash as createHash10 } from "crypto";
4526
7719
  import { readFile, writeFile, mkdir } from "fs/promises";
4527
7720
  import { join as join3 } from "path";
4528
7721
  import { homedir as homedir6 } from "os";
4529
7722
  function textHash(text) {
4530
- return createHash3("sha256").update(text).digest("hex").slice(0, 16);
7723
+ return createHash10("sha256").update(text).digest("hex").slice(0, 16);
4531
7724
  }
4532
7725
  async function loadDiskCache() {
4533
7726
  try {
@@ -4579,15 +7772,15 @@ var init_fastembed_provider = __esm({
4579
7772
  return new _FastEmbedProvider(model);
4580
7773
  }
4581
7774
  async embed(text) {
4582
- const hash = textHash(text);
4583
- const cached = cache.get(hash);
7775
+ const hash4 = textHash(text);
7776
+ const cached = cache.get(hash4);
4584
7777
  if (cached) return cached;
4585
7778
  const raw = await this.model.queryEmbed(text);
4586
7779
  const result = Array.from(raw);
4587
7780
  if (result.length !== this.dimensions) {
4588
7781
  throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
4589
7782
  }
4590
- this.cacheSet(hash, result);
7783
+ this.cacheSet(hash4, result);
4591
7784
  return result;
4592
7785
  }
4593
7786
  async embedBatch(texts) {
@@ -4595,8 +7788,8 @@ var init_fastembed_provider = __esm({
4595
7788
  const uncachedIndices = [];
4596
7789
  const uncachedTexts = [];
4597
7790
  for (let i = 0; i < texts.length; i++) {
4598
- const hash = textHash(texts[i]);
4599
- const cached = cache.get(hash);
7791
+ const hash4 = textHash(texts[i]);
7792
+ const cached = cache.get(hash4);
4600
7793
  if (cached) {
4601
7794
  results[i] = cached;
4602
7795
  } else {
@@ -4620,12 +7813,12 @@ var init_fastembed_provider = __esm({
4620
7813
  }
4621
7814
  return results;
4622
7815
  }
4623
- cacheSet(hash, value) {
7816
+ cacheSet(hash4, value) {
4624
7817
  if (cache.size >= MAX_CACHE_SIZE) {
4625
7818
  const firstKey = cache.keys().next().value;
4626
7819
  if (firstKey !== void 0) cache.delete(firstKey);
4627
7820
  }
4628
- cache.set(hash, value);
7821
+ cache.set(hash4, value);
4629
7822
  diskCacheDirty = true;
4630
7823
  }
4631
7824
  };
@@ -4724,7 +7917,7 @@ var api_provider_exports = {};
4724
7917
  __export(api_provider_exports, {
4725
7918
  APIEmbeddingProvider: () => APIEmbeddingProvider
4726
7919
  });
4727
- import { createHash as createHash4 } from "crypto";
7920
+ import { createHash as createHash11 } from "crypto";
4728
7921
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
4729
7922
  import { join as join4 } from "path";
4730
7923
  import { homedir as homedir7 } from "os";
@@ -4740,7 +7933,7 @@ function cacheNamespace(config) {
4740
7933
  ].join("|");
4741
7934
  }
4742
7935
  function textHash2(text, namespace) {
4743
- return createHash4("sha256").update(`${namespace}\0${text}`).digest("hex").slice(0, 16);
7936
+ return createHash11("sha256").update(`${namespace}\0${text}`).digest("hex").slice(0, 16);
4744
7937
  }
4745
7938
  async function loadDiskCache2() {
4746
7939
  if (diskCacheLoaded) return;
@@ -4894,12 +8087,12 @@ function scheduleDiskSave() {
4894
8087
  diskSaveTimer = null;
4895
8088
  }, DISK_SAVE_DEBOUNCE_MS);
4896
8089
  }
4897
- function cacheSet(hash, value) {
8090
+ function cacheSet(hash4, value) {
4898
8091
  if (cache3.size >= MAX_CACHE_SIZE3) {
4899
8092
  const firstKey = cache3.keys().next().value;
4900
8093
  if (firstKey !== void 0) cache3.delete(firstKey);
4901
8094
  }
4902
- cache3.set(hash, value);
8095
+ cache3.set(hash4, value);
4903
8096
  diskCacheDirty2 = true;
4904
8097
  }
4905
8098
  function resolveEnvEmbeddingApiKey() {
@@ -4946,18 +8139,18 @@ async function fetchWithRetry(url, apiKey, body, attempt = 0) {
4946
8139
  if (response.ok) {
4947
8140
  return response.json();
4948
8141
  }
4949
- if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
8142
+ if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
4950
8143
  const delay = BASE_DELAY_MS * Math.pow(2, attempt);
4951
8144
  const retryAfter = response.headers.get("retry-after");
4952
8145
  const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : delay;
4953
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
8146
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
4954
8147
  await new Promise((resolve) => setTimeout(resolve, waitMs));
4955
8148
  return fetchWithRetry(url, apiKey, body, attempt + 1);
4956
8149
  }
4957
8150
  const errorText2 = await response.text().catch(() => "unknown error");
4958
8151
  throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
4959
8152
  }
4960
- var CACHE_DIR2, CACHE_FILE2, DIMS_CACHE_FILE, CACHE_META_FILE, cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES, BASE_DELAY_MS, APIEmbeddingProvider;
8153
+ var CACHE_DIR2, CACHE_FILE2, DIMS_CACHE_FILE, CACHE_META_FILE, cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES2, BASE_DELAY_MS, APIEmbeddingProvider;
4961
8154
  var init_api_provider = __esm({
4962
8155
  "src/embedding/api-provider.ts"() {
4963
8156
  "use strict";
@@ -4977,7 +8170,7 @@ var init_api_provider = __esm({
4977
8170
  DISK_SAVE_DEBOUNCE_MS = 5e3;
4978
8171
  DEFAULT_MAX_BATCH_SIZE = 2048;
4979
8172
  DASHSCOPE_MAX_BATCH_SIZE = 10;
4980
- MAX_RETRIES = 3;
8173
+ MAX_RETRIES2 = 3;
4981
8174
  BASE_DELAY_MS = 500;
4982
8175
  APIEmbeddingProvider = class _APIEmbeddingProvider {
4983
8176
  name;
@@ -5070,9 +8263,9 @@ var init_api_provider = __esm({
5070
8263
  }
5071
8264
  async embed(text) {
5072
8265
  const normalized = normalizeText(text);
5073
- const hash = textHash2(normalized, this.cacheKeyNamespace);
8266
+ const hash4 = textHash2(normalized, this.cacheKeyNamespace);
5074
8267
  if (diskCacheLoaded) {
5075
- const cached = cache3.get(hash);
8268
+ const cached = cache3.get(hash4);
5076
8269
  if (cached) return cached;
5077
8270
  }
5078
8271
  const apiCall = async () => {
@@ -5098,7 +8291,7 @@ var init_api_provider = __esm({
5098
8291
  let embedding;
5099
8292
  if (!diskCacheLoaded && diskCacheLoadPromise) {
5100
8293
  const cacheRace = diskCacheLoadPromise.then(() => {
5101
- const cached = cache3.get(hash);
8294
+ const cached = cache3.get(hash4);
5102
8295
  if (cached) return cached;
5103
8296
  return null;
5104
8297
  });
@@ -5116,7 +8309,7 @@ var init_api_provider = __esm({
5116
8309
  } else {
5117
8310
  embedding = await apiCall();
5118
8311
  }
5119
- cacheSet(hash, embedding);
8312
+ cacheSet(hash4, embedding);
5120
8313
  scheduleDiskSave();
5121
8314
  return embedding;
5122
8315
  }
@@ -5127,8 +8320,8 @@ var init_api_provider = __esm({
5127
8320
  const uncachedIndices = [];
5128
8321
  const uncachedTexts = [];
5129
8322
  for (let i = 0; i < normalizedTexts.length; i++) {
5130
- const hash = textHash2(normalizedTexts[i], this.cacheKeyNamespace);
5131
- const cached = cache3.get(hash);
8323
+ const hash4 = textHash2(normalizedTexts[i], this.cacheKeyNamespace);
8324
+ const cached = cache3.get(hash4);
5132
8325
  if (cached) {
5133
8326
  results[i] = cached;
5134
8327
  } else {
@@ -5198,8 +8391,8 @@ var init_api_provider = __esm({
5198
8391
  async getCachedEmbeddings(texts) {
5199
8392
  await ensureDiskCacheLoaded();
5200
8393
  return texts.map((text) => {
5201
- const hash = textHash2(normalizeText(text), this.cacheKeyNamespace);
5202
- return cache3.get(hash) ?? null;
8394
+ const hash4 = textHash2(normalizeText(text), this.cacheKeyNamespace);
8395
+ return cache3.get(hash4) ?? null;
5203
8396
  });
5204
8397
  }
5205
8398
  getStats() {
@@ -5228,12 +8421,12 @@ __export(provider_exports2, {
5228
8421
  resetProvider: () => resetProvider
5229
8422
  });
5230
8423
  function warnOnce(message) {
5231
- const now = Date.now();
5232
- if (message === lastEmbeddingWarning && now - lastEmbeddingWarningAt < WARNING_COOLDOWN_MS) {
8424
+ const now3 = Date.now();
8425
+ if (message === lastEmbeddingWarning && now3 - lastEmbeddingWarningAt < WARNING_COOLDOWN_MS) {
5233
8426
  return;
5234
8427
  }
5235
8428
  lastEmbeddingWarning = message;
5236
- lastEmbeddingWarningAt = now;
8429
+ lastEmbeddingWarningAt = now3;
5237
8430
  console.error(message);
5238
8431
  }
5239
8432
  function getEmbeddingMode2() {
@@ -5512,8 +8705,8 @@ var init_types = __esm({
5512
8705
  });
5513
8706
 
5514
8707
  // src/store/mini-skill-store.ts
5515
- import path6 from "path";
5516
- import fs3 from "fs";
8708
+ import path11 from "path";
8709
+ import fs8 from "fs";
5517
8710
  function isMiniSkillStoreInitialized() {
5518
8711
  return _store2 !== null;
5519
8712
  }
@@ -5532,18 +8725,6 @@ var init_mini_skill_store = __esm({
5532
8725
  }
5533
8726
  });
5534
8727
 
5535
- // src/compact/token-budget.ts
5536
- import { countTokens, isWithinTokenLimit } from "gpt-tokenizer";
5537
- function countTextTokens(text) {
5538
- return countTokens(text);
5539
- }
5540
- var init_token_budget = __esm({
5541
- "src/compact/token-budget.ts"() {
5542
- "use strict";
5543
- init_esm_shims();
5544
- }
5545
- });
5546
-
5547
8728
  // src/skills/mini-skills.ts
5548
8729
  function resolveKnowledgeLayer(documentType, sourceDetail, source) {
5549
8730
  if (documentType === "mini-skill") return "promoted";
@@ -5939,8 +9120,8 @@ __export(aliases_exports, {
5939
9120
  resetAliasCache: () => resetAliasCache,
5940
9121
  resolveAliases: () => resolveAliases
5941
9122
  });
5942
- import { promises as fs4 } from "fs";
5943
- import path7 from "path";
9123
+ import { promises as fs9 } from "fs";
9124
+ import path12 from "path";
5944
9125
  import os from "os";
5945
9126
  function normalizePath(p) {
5946
9127
  let normalized = p.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -5955,12 +9136,12 @@ function idPriority(id) {
5955
9136
  return 2;
5956
9137
  }
5957
9138
  function getRegistryPath(baseDir) {
5958
- return path7.join(baseDir ?? registryDir ?? DEFAULT_DATA_DIR, ALIAS_FILE);
9139
+ return path12.join(baseDir ?? registryDir ?? DEFAULT_DATA_DIR, ALIAS_FILE);
5959
9140
  }
5960
9141
  async function loadRegistry(baseDir) {
5961
9142
  if (registryCache) return registryCache;
5962
9143
  try {
5963
- const data = await fs4.readFile(getRegistryPath(baseDir), "utf-8");
9144
+ const data = await fs9.readFile(getRegistryPath(baseDir), "utf-8");
5964
9145
  const parsed = JSON.parse(data);
5965
9146
  if (parsed.version === 1 && Array.isArray(parsed.groups)) {
5966
9147
  registryCache = parsed;
@@ -5974,8 +9155,8 @@ async function loadRegistry(baseDir) {
5974
9155
  async function saveRegistry(baseDir) {
5975
9156
  if (!registryCache) return;
5976
9157
  const filePath = getRegistryPath(baseDir);
5977
- await fs4.mkdir(path7.dirname(filePath), { recursive: true });
5978
- await fs4.writeFile(filePath, JSON.stringify(registryCache, null, 2), "utf-8");
9158
+ await fs9.mkdir(path12.dirname(filePath), { recursive: true });
9159
+ await fs9.writeFile(filePath, JSON.stringify(registryCache, null, 2), "utf-8");
5979
9160
  }
5980
9161
  function findMatchingGroup(registry, projectInfo) {
5981
9162
  const normalizedRoot = normalizePath(projectInfo.rootPath);
@@ -6132,7 +9313,7 @@ var init_aliases = __esm({
6132
9313
  "src/project/aliases.ts"() {
6133
9314
  "use strict";
6134
9315
  init_esm_shims();
6135
- DEFAULT_DATA_DIR = process.env.MEMORIX_DATA_DIR || path7.join(os.homedir(), ".memorix", "data");
9316
+ DEFAULT_DATA_DIR = process.env.MEMORIX_DATA_DIR || path12.join(os.homedir(), ".memorix", "data");
6136
9317
  ALIAS_FILE = ".project-aliases.json";
6137
9318
  registryCache = null;
6138
9319
  registryDir = null;
@@ -6406,11 +9587,11 @@ import { create, insert as insert2, search, remove as remove2, update, count, ge
6406
9587
  function getLastSearchMode(projectId) {
6407
9588
  return lastSearchModeByProject.get(projectId ?? SEARCH_MODE_DEFAULT_KEY) ?? "fulltext";
6408
9589
  }
6409
- function makeOramaObservationId(projectId, observationId) {
6410
- return `obs-${encodeURIComponent(projectId)}-${observationId}`;
9590
+ function makeOramaObservationId(projectId, observationId2) {
9591
+ return `obs-${encodeURIComponent(projectId)}-${observationId2}`;
6411
9592
  }
6412
- function makeEntryKey(projectId, observationId) {
6413
- return `${projectId ?? ""}::${observationId}`;
9593
+ function makeEntryKey(projectId, observationId2) {
9594
+ return `${projectId ?? ""}::${observationId2}`;
6414
9595
  }
6415
9596
  function rememberObservationDoc(doc) {
6416
9597
  const publicDoc = { ...doc };
@@ -6639,9 +9820,9 @@ async function hydrateIndexForStartup(observations2) {
6639
9820
  function getDeferredCachedVectorHydration() {
6640
9821
  return deferredCachedVectorHydration;
6641
9822
  }
6642
- function hasObservationVector(projectId, observationId) {
9823
+ function hasObservationVector(projectId, observationId2) {
6643
9824
  if (!db || !embeddingEnabled || embeddingDimensions === null) return false;
6644
- const document = getByID(db, makeOramaObservationId(projectId, observationId));
9825
+ const document = getByID(db, makeOramaObservationId(projectId, observationId2));
6645
9826
  return Array.isArray(document?.embedding) && document.embedding.length === embeddingDimensions && document.embedding.every(Number.isFinite);
6646
9827
  }
6647
9828
  async function insertObservation(doc) {
@@ -6664,8 +9845,8 @@ async function searchObservations(options) {
6664
9845
  const t0 = perf ? performance.now() : 0;
6665
9846
  const mark = (label) => {
6666
9847
  if (perf) {
6667
- const now = performance.now();
6668
- process.stderr.write(` [search-perf] ${label}: ${(now - t0).toFixed(0)}ms
9848
+ const now3 = performance.now();
9849
+ process.stderr.write(` [search-perf] ${label}: ${(now3 - t0).toFixed(0)}ms
6669
9850
  `);
6670
9851
  }
6671
9852
  };
@@ -7129,13 +10310,13 @@ async function getTimeline(anchorId, projectId, depthBefore = 3, depthAfter = 3)
7129
10310
  }
7130
10311
  async function recordAccessBatch(hitDocs) {
7131
10312
  const database = await getDb();
7132
- const now = (/* @__PURE__ */ new Date()).toISOString();
10313
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7133
10314
  for (const { id, doc } of hitDocs) {
7134
10315
  try {
7135
10316
  await update(database, id, {
7136
10317
  ...doc,
7137
10318
  accessCount: (doc.accessCount ?? 0) + 1,
7138
- lastAccessedAt: now
10319
+ lastAccessedAt: now3
7139
10320
  });
7140
10321
  } catch {
7141
10322
  }
@@ -7333,10 +10514,10 @@ __export(observations_exports, {
7333
10514
  withFreshObservations: () => withFreshObservations
7334
10515
  });
7335
10516
  function logEmbeddingFailureOnce(key, message) {
7336
- const now = Date.now();
10517
+ const now3 = Date.now();
7337
10518
  const last = embeddingFailureLogTimestamps.get(key) ?? 0;
7338
- if (now - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
7339
- embeddingFailureLogTimestamps.set(key, now);
10519
+ if (now3 - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
10520
+ embeddingFailureLogTimestamps.set(key, now3);
7340
10521
  console.error(message);
7341
10522
  }
7342
10523
  function normalizeEmbeddingFailure(error) {
@@ -7382,6 +10563,20 @@ async function bindObservationCodeRefsBestEffort(observation) {
7382
10563
  } catch {
7383
10564
  }
7384
10565
  }
10566
+ function queueClaimDerivation(observation) {
10567
+ const dataDir = projectDir;
10568
+ const isExplicit = observation.sourceDetail === "explicit";
10569
+ const isGit = observation.source === "git" || observation.sourceDetail === "git-ingest";
10570
+ if (!dataDir || !isExplicit && !isGit) return;
10571
+ try {
10572
+ enqueueClaimDerivation({
10573
+ dataDir,
10574
+ projectId: observation.projectId,
10575
+ observationId: observation.id
10576
+ });
10577
+ } catch {
10578
+ }
10579
+ }
7385
10580
  function isVectorCompatibleWithCurrentIndex(embedding) {
7386
10581
  if (!embedding) return false;
7387
10582
  const vectorDimensions = getVectorDimensions();
@@ -7429,7 +10624,7 @@ async function withFreshObservations(fn) {
7429
10624
  return fn();
7430
10625
  }
7431
10626
  async function storeObservation(input) {
7432
- const now = (/* @__PURE__ */ new Date()).toISOString();
10627
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7433
10628
  input = { ...input, title: sanitizeCredentials(input.title), narrative: sanitizeCredentials(input.narrative), facts: input.facts?.map(sanitizeCredentials) };
7434
10629
  await ensureFreshObservations();
7435
10630
  if (input.topicKey) {
@@ -7437,7 +10632,7 @@ async function storeObservation(input) {
7437
10632
  (o) => o.topicKey === input.topicKey && o.projectId === input.projectId
7438
10633
  );
7439
10634
  if (existing) {
7440
- return { observation: await upsertObservation(existing, input, now), upserted: true };
10635
+ return { observation: await upsertObservation(existing, input, now3), upserted: true };
7441
10636
  }
7442
10637
  }
7443
10638
  const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -7489,7 +10684,7 @@ async function storeObservation(input) {
7489
10684
  filesModified: enrichedFiles,
7490
10685
  concepts: enrichedConcepts,
7491
10686
  tokens,
7492
- createdAt: now,
10687
+ createdAt: now3,
7493
10688
  projectId: input.projectId,
7494
10689
  hasCausalLanguage: extracted.hasCausalLanguage,
7495
10690
  topicKey: input.topicKey,
@@ -7535,7 +10730,7 @@ async function storeObservation(input) {
7535
10730
  filesModified: enrichedFiles,
7536
10731
  concepts: enrichedConcepts,
7537
10732
  tokens,
7538
- createdAt: now,
10733
+ createdAt: now3,
7539
10734
  projectId: input.projectId,
7540
10735
  hasCausalLanguage: extracted.hasCausalLanguage,
7541
10736
  topicKey: input.topicKey,
@@ -7565,7 +10760,7 @@ async function storeObservation(input) {
7565
10760
  filesModified: enrichedFiles.join("\n"),
7566
10761
  concepts: enrichedConcepts.map((c) => c.replace(/-/g, " ")).join(", "),
7567
10762
  tokens,
7568
- createdAt: now,
10763
+ createdAt: now3,
7569
10764
  projectId: input.projectId,
7570
10765
  accessCount: 0,
7571
10766
  lastAccessedAt: "",
@@ -7578,9 +10773,10 @@ async function storeObservation(input) {
7578
10773
  };
7579
10774
  await assignAndPersist();
7580
10775
  if (upsertedInsideLock) {
7581
- return { observation: await upsertObservation(observation, input, now), upserted: true };
10776
+ return { observation: await upsertObservation(observation, input, now3), upserted: true };
7582
10777
  }
7583
10778
  await bindObservationCodeRefsBestEffort(observation);
10779
+ queueClaimDerivation(observation);
7584
10780
  const obsId = observation.id;
7585
10781
  vectorMissingIds.add(obsId);
7586
10782
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
@@ -7625,7 +10821,7 @@ async function storeObservation(input) {
7625
10821
  });
7626
10822
  return { observation, upserted: false };
7627
10823
  }
7628
- async function upsertObservation(existing, input, now) {
10824
+ async function upsertObservation(existing, input, now3) {
7629
10825
  input = { ...input, title: sanitizeCredentials(input.title), narrative: sanitizeCredentials(input.narrative), facts: input.facts?.map(sanitizeCredentials) };
7630
10826
  const contentForExtraction = [input.title, input.narrative, ...input.facts ?? []].join(" ");
7631
10827
  const extracted = extractEntities(contentForExtraction);
@@ -7645,7 +10841,7 @@ async function upsertObservation(existing, input, now) {
7645
10841
  existing.filesModified = enrichedFiles;
7646
10842
  existing.concepts = enrichedConcepts;
7647
10843
  existing.tokens = tokens;
7648
- existing.updatedAt = now;
10844
+ existing.updatedAt = now3;
7649
10845
  existing.hasCausalLanguage = extracted.hasCausalLanguage;
7650
10846
  existing.revisionCount = (existing.revisionCount ?? 1) + 1;
7651
10847
  existing.status = "active";
@@ -7694,6 +10890,7 @@ async function upsertObservation(existing, input, now) {
7694
10890
  await store.update(existing);
7695
10891
  }
7696
10892
  await bindObservationCodeRefsBestEffort(existing);
10893
+ queueClaimDerivation(existing);
7697
10894
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
7698
10895
  const obsId = existing.id;
7699
10896
  vectorMissingIds.add(obsId);
@@ -7736,7 +10933,7 @@ async function resolveObservations(ids, status = "resolved") {
7736
10933
  const resolved = [];
7737
10934
  const notFound = [];
7738
10935
  const changed = [];
7739
- const now = (/* @__PURE__ */ new Date()).toISOString();
10936
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7740
10937
  for (const id of ids) {
7741
10938
  const obs = observations.find((o) => o.id === id);
7742
10939
  if (!obs) {
@@ -7744,7 +10941,7 @@ async function resolveObservations(ids, status = "resolved") {
7744
10941
  continue;
7745
10942
  }
7746
10943
  obs.status = status;
7747
- obs.updatedAt = now;
10944
+ obs.updatedAt = now3;
7748
10945
  if (obs.progress) {
7749
10946
  obs.progress.status = status === "resolved" ? "completed" : obs.progress.status;
7750
10947
  }
@@ -7838,9 +11035,9 @@ function suggestTopicKey(type, title) {
7838
11035
  break;
7839
11036
  }
7840
11037
  }
7841
- const slug = title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60);
7842
- if (!slug) return "";
7843
- return `${family}/${slug}`;
11038
+ const slug2 = title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 60);
11039
+ if (!slug2) return "";
11040
+ return `${family}/${slug2}`;
7844
11041
  }
7845
11042
  async function reindexObservations() {
7846
11043
  if (observations.length === 0) return 0;
@@ -8078,6 +11275,7 @@ var init_observations = __esm({
8078
11275
  init_entity_extractor();
8079
11276
  init_provider2();
8080
11277
  init_secret_filter();
11278
+ init_lifecycle();
8081
11279
  observations = [];
8082
11280
  nextId = 1;
8083
11281
  projectDir = null;
@@ -8110,16 +11308,23 @@ var MAINTENANCE_RESULT_PREFIX = "__MEMORIX_MAINTENANCE_RESULT__";
8110
11308
  var ISOLATED_MAINTENANCE_JOB_KINDS = [
8111
11309
  "retention-archive",
8112
11310
  "consolidation",
8113
- "codegraph-refresh"
11311
+ "codegraph-refresh",
11312
+ "claim-derive",
11313
+ "claim-requalification",
11314
+ "knowledge-compile",
11315
+ "knowledge-lint",
11316
+ "workflow-index"
8114
11317
  ];
8115
11318
  var DEFAULT_TIMEOUT_MS = 5 * 6e4;
8116
11319
 
8117
11320
  // src/runtime/project-maintenance.ts
8118
11321
  init_esm_shims();
11322
+ init_lifecycle();
8119
11323
  var DEFAULT_VECTOR_BATCH_SIZE = 12;
8120
11324
  var DEFAULT_RETENTION_BATCH_SIZE = 100;
8121
11325
  var DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
8122
11326
  var DEFAULT_CODEGRAPH_MAX_FILES = 5e3;
11327
+ var DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
8123
11328
  var VECTOR_RETRY_DELAY_MS = 5e3;
8124
11329
  var DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
8125
11330
  function vectorBatchSize(payload) {
@@ -8150,6 +11355,31 @@ function codeGraphMaxFiles(payload) {
8150
11355
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CODEGRAPH_MAX_FILES;
8151
11356
  return Math.min(2e4, Math.max(1, Math.floor(value)));
8152
11357
  }
11358
+ function claimDerivationBatchSize(payload) {
11359
+ const value = payload.limit;
11360
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CLAIM_DERIVATION_BATCH_SIZE;
11361
+ return Math.min(500, Math.max(1, Math.floor(value)));
11362
+ }
11363
+ function claimDerivationCursor(payload) {
11364
+ const value = payload.cursor;
11365
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
11366
+ }
11367
+ function observationId(payload) {
11368
+ const value = payload.observationId;
11369
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
11370
+ }
11371
+ function workspaceMode(payload) {
11372
+ return payload.workspaceMode === "local" || payload.workspaceMode === "versioned" ? payload.workspaceMode : void 0;
11373
+ }
11374
+ async function loadWorkspaceForMaintenance(projectId, projectDir2, mode) {
11375
+ const { loadKnowledgeWorkspace: loadKnowledgeWorkspace2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
11376
+ if (mode) return loadKnowledgeWorkspace2({ projectId, dataDir: projectDir2, mode });
11377
+ const [versioned, local] = await Promise.all([
11378
+ loadKnowledgeWorkspace2({ projectId, dataDir: projectDir2, mode: "versioned" }),
11379
+ loadKnowledgeWorkspace2({ projectId, dataDir: projectDir2, mode: "local" })
11380
+ ]);
11381
+ return versioned ?? local;
11382
+ }
8153
11383
  function withTimeout(promise, ms, label) {
8154
11384
  return new Promise((resolve, reject) => {
8155
11385
  const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
@@ -8236,7 +11466,7 @@ async function runAutomaticConsolidation(projectId, projectDir2, options) {
8236
11466
  }
8237
11467
  return nextCursor === void 0 ? {} : { nextCursor };
8238
11468
  }
8239
- function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot) {
11469
+ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, options = {}) {
8240
11470
  return async (job) => {
8241
11471
  if (job.projectId !== projectId) {
8242
11472
  return { action: "reschedule", delayMs: VECTOR_RETRY_DELAY_MS };
@@ -8304,6 +11534,144 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot) {
8304
11534
  });
8305
11535
  const activeObservations = await getObservationStore2().loadByProject(projectId, { status: "active" });
8306
11536
  await backfillMissingObservationCodeRefs2(store, activeObservations);
11537
+ const snapshot = store.latestSnapshot(projectId);
11538
+ enqueueClaimRequalification({
11539
+ projectId,
11540
+ dataDir: projectDir2,
11541
+ source: "codegraph-refresh",
11542
+ ...snapshot?.id ? { snapshotId: snapshot.id } : {},
11543
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
11544
+ });
11545
+ return { action: "complete" };
11546
+ }
11547
+ if (job.kind === "claim-derive") {
11548
+ const id = observationId(job.payload);
11549
+ if (!id) throw new Error("Claim derivation requires a positive observationId");
11550
+ const [
11551
+ { CodeGraphStore: CodeGraphStore2 },
11552
+ { ClaimStore: ClaimStore2 },
11553
+ { bindObservationToCode: bindObservationToCode2 },
11554
+ { deriveLowRiskClaimsFromObservation: deriveLowRiskClaimsFromObservation2 },
11555
+ { getObservationStore: getObservationStore2 }
11556
+ ] = await Promise.all([
11557
+ Promise.resolve().then(() => (init_store(), store_exports)),
11558
+ Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
11559
+ Promise.resolve().then(() => (init_binder(), binder_exports)),
11560
+ Promise.resolve().then(() => (init_claims(), claims_exports)),
11561
+ Promise.resolve().then(() => (init_obs_store(), obs_store_exports))
11562
+ ]);
11563
+ const observation = await getObservationStore2().getById(id);
11564
+ if (!observation || observation.projectId !== projectId) return { action: "complete" };
11565
+ const codeStore = new CodeGraphStore2();
11566
+ const claimStore = new ClaimStore2();
11567
+ await Promise.all([codeStore.init(projectDir2), claimStore.init(projectDir2)]);
11568
+ await bindObservationToCode2(codeStore, observation);
11569
+ const derived = deriveLowRiskClaimsFromObservation2(claimStore, observation, codeStore);
11570
+ if (derived.length > 0) {
11571
+ enqueueKnowledgeFollowups({
11572
+ projectId,
11573
+ dataDir: projectDir2,
11574
+ source: "claim-derive:" + id,
11575
+ includeCompile: true,
11576
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
11577
+ });
11578
+ }
11579
+ return { action: "complete" };
11580
+ }
11581
+ if (job.kind === "claim-requalification") {
11582
+ const [
11583
+ { CodeGraphStore: CodeGraphStore2 },
11584
+ { ClaimStore: ClaimStore2 },
11585
+ { bindObservationToCode: bindObservationToCode2 },
11586
+ { deriveLowRiskClaimsFromObservation: deriveLowRiskClaimsFromObservation2, requalifyClaimsForCodeState: requalifyClaimsForCodeState2 },
11587
+ { getObservationStore: getObservationStore2 }
11588
+ ] = await Promise.all([
11589
+ Promise.resolve().then(() => (init_store(), store_exports)),
11590
+ Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
11591
+ Promise.resolve().then(() => (init_binder(), binder_exports)),
11592
+ Promise.resolve().then(() => (init_claims(), claims_exports)),
11593
+ Promise.resolve().then(() => (init_obs_store(), obs_store_exports))
11594
+ ]);
11595
+ const codeStore = new CodeGraphStore2();
11596
+ const claimStore = new ClaimStore2();
11597
+ await Promise.all([codeStore.init(projectDir2), claimStore.init(projectDir2)]);
11598
+ const limit = claimDerivationBatchSize(job.payload);
11599
+ const observations2 = await getObservationStore2().loadByProject(projectId, {
11600
+ status: "active",
11601
+ afterId: claimDerivationCursor(job.payload),
11602
+ limit
11603
+ });
11604
+ let derivedCount = 0;
11605
+ for (const observation of observations2) {
11606
+ await bindObservationToCode2(codeStore, observation);
11607
+ derivedCount += deriveLowRiskClaimsFromObservation2(claimStore, observation, codeStore).length;
11608
+ }
11609
+ requalifyClaimsForCodeState2(claimStore, codeStore, projectId);
11610
+ if (observations2.length === limit) {
11611
+ return {
11612
+ action: "reschedule",
11613
+ delayMs: 0,
11614
+ resetAttempts: true,
11615
+ payload: {
11616
+ ...job.payload,
11617
+ cursor: observations2[observations2.length - 1].id,
11618
+ limit
11619
+ }
11620
+ };
11621
+ }
11622
+ enqueueKnowledgeFollowups({
11623
+ projectId,
11624
+ dataDir: projectDir2,
11625
+ source: "claim-requalification",
11626
+ includeCompile: derivedCount > 0,
11627
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
11628
+ });
11629
+ return { action: "complete" };
11630
+ }
11631
+ if (job.kind === "knowledge-compile") {
11632
+ const workspace = await loadWorkspaceForMaintenance(projectId, projectDir2, workspaceMode(job.payload));
11633
+ if (!workspace) return { action: "complete" };
11634
+ if (workspace.mode === "versioned" && job.payload.allowVersionedWrite !== true) {
11635
+ return { action: "complete" };
11636
+ }
11637
+ const [{ ClaimStore: ClaimStore2 }, { compileKnowledgeWorkspace: compileKnowledgeWorkspace2 }] = await Promise.all([
11638
+ Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
11639
+ Promise.resolve().then(() => (init_wiki(), wiki_exports))
11640
+ ]);
11641
+ const claims = new ClaimStore2();
11642
+ await claims.init(projectDir2);
11643
+ await compileKnowledgeWorkspace2({ workspace, claims });
11644
+ enqueueKnowledgeFollowups({
11645
+ projectId,
11646
+ dataDir: projectDir2,
11647
+ source: "knowledge-compile",
11648
+ ...options.maintenanceQueue ? { queue: options.maintenanceQueue } : {}
11649
+ });
11650
+ return { action: "complete" };
11651
+ }
11652
+ if (job.kind === "workflow-index") {
11653
+ const workspace = await loadWorkspaceForMaintenance(projectId, projectDir2, workspaceMode(job.payload));
11654
+ if (!workspace) return { action: "complete" };
11655
+ const { syncCanonicalWorkflows: syncCanonicalWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), workflows_exports));
11656
+ await syncCanonicalWorkflows2(workspace);
11657
+ return { action: "complete" };
11658
+ }
11659
+ if (job.kind === "knowledge-lint") {
11660
+ const workspace = await loadWorkspaceForMaintenance(projectId, projectDir2, workspaceMode(job.payload));
11661
+ if (!workspace) return { action: "complete" };
11662
+ const [
11663
+ { ClaimStore: ClaimStore2 },
11664
+ { CodeGraphStore: CodeGraphStore2 },
11665
+ { lintKnowledgeWorkspace: lintKnowledgeWorkspace2 }
11666
+ ] = await Promise.all([
11667
+ Promise.resolve().then(() => (init_claim_store(), claim_store_exports)),
11668
+ Promise.resolve().then(() => (init_store(), store_exports)),
11669
+ Promise.resolve().then(() => (init_wiki(), wiki_exports))
11670
+ ]);
11671
+ const claims = new ClaimStore2();
11672
+ const codeStore = new CodeGraphStore2();
11673
+ await Promise.all([claims.init(projectDir2), codeStore.init(projectDir2)]);
11674
+ await lintKnowledgeWorkspace2({ workspace, claims, codeStore });
8307
11675
  return { action: "complete" };
8308
11676
  }
8309
11677
  if (job.kind !== "vector-backfill") {