laminark 0.1.0 → 2.21.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +15 -0
- package/README.md +71 -36
- package/package.json +7 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/dist/hooks/handler.d.ts +1 -3
- package/plugin/dist/hooks/handler.d.ts.map +1 -1
- package/plugin/dist/hooks/handler.js +22 -310
- package/plugin/dist/hooks/handler.js.map +1 -1
- package/plugin/dist/index.d.ts +1 -3
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +392 -1895
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/{observations-CorAAc1A.d.mts → observations-Ch0nc47i.d.mts} +1 -23
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
- package/plugin/dist/{tool-registry-e710BvXq.mjs → tool-registry-CZ3mJ4iR.mjs} +13 -932
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
- package/plugin/hooks/hooks.json +6 -6
- package/plugin/scripts/README.md +1 -19
- package/plugin/scripts/bump-version.sh +3 -1
- package/plugin/scripts/ensure-deps.sh +2 -5
- package/plugin/scripts/install.sh +39 -115
- package/plugin/scripts/local-install.sh +58 -93
- package/plugin/scripts/setup-tmpdir.sh +65 -0
- package/plugin/scripts/uninstall.sh +38 -76
- package/plugin/scripts/update.sh +69 -20
- package/plugin/scripts/verify-install.sh +25 -69
- package/plugin/ui/activity.js +0 -12
- package/plugin/ui/app.js +54 -24
- package/plugin/ui/graph.js +186 -413
- package/plugin/ui/help.js +172 -876
- package/plugin/ui/index.html +242 -506
- package/plugin/ui/settings.js +17 -781
- package/plugin/ui/styles.css +44 -990
- package/plugin/ui/timeline.js +2 -2
- package/plugin/CLAUDE.md +0 -10
- package/plugin/commands/recall.md +0 -55
- package/plugin/commands/remember.md +0 -34
- package/plugin/commands/resume.md +0 -45
- package/plugin/commands/stash.md +0 -34
- package/plugin/commands/status.md +0 -33
- package/plugin/dist/observations-CorAAc1A.d.mts.map +0 -1
- package/plugin/dist/tool-registry-e710BvXq.mjs.map +0 -1
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +0 -17
- package/plugin/scripts/dev-sync.sh +0 -58
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/tools.js +0 -826
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as isDebugEnabled
|
|
1
|
+
import { a as isDebugEnabled } from "./config-t8LZeB-u.mjs";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import * as sqliteVec from "sqlite-vec";
|
|
4
|
-
import { mkdirSync
|
|
5
|
-
import { dirname
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
@@ -81,8 +81,6 @@ function debugTimed(category, message, fn) {
|
|
|
81
81
|
* Migration 018: Tool registry FTS5 + vec0 tables for hybrid search on tool descriptions.
|
|
82
82
|
* Migration 019: Add status column (active/stale/demoted) to tool_registry for staleness management.
|
|
83
83
|
* Migration 020: Debug path tables (debug_paths + path_waypoints) for resolution path tracking.
|
|
84
|
-
* Migration 021: Thought branch tables for coherent work unit tracking.
|
|
85
|
-
* Migration 022: Add trigger_hints column to tool_registry for proactive suggestion matching.
|
|
86
84
|
*/
|
|
87
85
|
const MIGRATIONS = [
|
|
88
86
|
{
|
|
@@ -579,60 +577,6 @@ const MIGRATIONS = [
|
|
|
579
577
|
|
|
580
578
|
CREATE INDEX IF NOT EXISTS idx_path_waypoints_path_order
|
|
581
579
|
ON path_waypoints(path_id, sequence_order);
|
|
582
|
-
`
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
version: 21,
|
|
586
|
-
name: "create_thought_branch_tables",
|
|
587
|
-
up: `
|
|
588
|
-
CREATE TABLE IF NOT EXISTS thought_branches (
|
|
589
|
-
id TEXT PRIMARY KEY,
|
|
590
|
-
project_hash TEXT NOT NULL,
|
|
591
|
-
session_id TEXT,
|
|
592
|
-
status TEXT NOT NULL DEFAULT 'active'
|
|
593
|
-
CHECK(status IN ('active', 'completed', 'abandoned', 'merged')),
|
|
594
|
-
branch_type TEXT NOT NULL DEFAULT 'unknown'
|
|
595
|
-
CHECK(branch_type IN ('investigation', 'bug_fix', 'feature', 'refactor', 'research', 'unknown')),
|
|
596
|
-
arc_stage TEXT NOT NULL DEFAULT 'investigation'
|
|
597
|
-
CHECK(arc_stage IN ('investigation', 'diagnosis', 'planning', 'execution', 'verification', 'completed')),
|
|
598
|
-
title TEXT,
|
|
599
|
-
summary TEXT,
|
|
600
|
-
parent_branch_id TEXT REFERENCES thought_branches(id),
|
|
601
|
-
linked_debug_path_id TEXT,
|
|
602
|
-
trigger_source TEXT,
|
|
603
|
-
trigger_observation_id TEXT,
|
|
604
|
-
observation_count INTEGER NOT NULL DEFAULT 0,
|
|
605
|
-
tool_pattern TEXT DEFAULT '{}',
|
|
606
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
607
|
-
ended_at TEXT,
|
|
608
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
CREATE TABLE IF NOT EXISTS branch_observations (
|
|
612
|
-
branch_id TEXT NOT NULL REFERENCES thought_branches(id) ON DELETE CASCADE,
|
|
613
|
-
observation_id TEXT NOT NULL,
|
|
614
|
-
sequence_order INTEGER NOT NULL,
|
|
615
|
-
tool_name TEXT,
|
|
616
|
-
arc_stage_at_add TEXT,
|
|
617
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
618
|
-
PRIMARY KEY (branch_id, observation_id)
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
CREATE INDEX IF NOT EXISTS idx_thought_branches_project_status
|
|
622
|
-
ON thought_branches(project_hash, status);
|
|
623
|
-
CREATE INDEX IF NOT EXISTS idx_thought_branches_session
|
|
624
|
-
ON thought_branches(session_id);
|
|
625
|
-
CREATE INDEX IF NOT EXISTS idx_thought_branches_started
|
|
626
|
-
ON thought_branches(started_at DESC);
|
|
627
|
-
CREATE INDEX IF NOT EXISTS idx_branch_observations_obs
|
|
628
|
-
ON branch_observations(observation_id);
|
|
629
|
-
`
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
version: 22,
|
|
633
|
-
name: "add_tool_registry_trigger_hints",
|
|
634
|
-
up: `
|
|
635
|
-
ALTER TABLE tool_registry ADD COLUMN trigger_hints TEXT;
|
|
636
580
|
`
|
|
637
581
|
}
|
|
638
582
|
];
|
|
@@ -861,32 +805,11 @@ var ObservationRepository = class {
|
|
|
861
805
|
return rowToObservation(row);
|
|
862
806
|
}
|
|
863
807
|
/**
|
|
864
|
-
* Resolves a full or prefix ID to the full 32-char ID.
|
|
865
|
-
* Observation IDs are 32-char hex strings. Search results display only the
|
|
866
|
-
* first 8 chars via shortId(). This method allows callers to pass either
|
|
867
|
-
* a full ID or an 8-char (or any-length) prefix and get the full ID back.
|
|
868
|
-
* Returns null if no unique match is found.
|
|
869
|
-
*/
|
|
870
|
-
resolveId(id) {
|
|
871
|
-
if (id.length === 32) return id;
|
|
872
|
-
const rows = this.db.prepare("SELECT id FROM observations WHERE project_hash = ? AND id LIKE ? ESCAPE '\\' LIMIT 2").all(this.projectHash, id.replace(/[%_\\]/g, "\\$&") + "%");
|
|
873
|
-
if (rows.length === 1) return rows[0].id;
|
|
874
|
-
if (rows.length > 1) debug("obs", "Ambiguous ID prefix - multiple matches", {
|
|
875
|
-
prefix: id,
|
|
876
|
-
count: rows.length
|
|
877
|
-
});
|
|
878
|
-
return null;
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
808
|
* Gets an observation by ID, scoped to this project.
|
|
882
|
-
* Accepts full 32-char IDs or shorter prefix strings (e.g. the 8-char
|
|
883
|
-
* display IDs shown in search results).
|
|
884
809
|
* Returns null if not found or soft-deleted.
|
|
885
810
|
*/
|
|
886
811
|
getById(id) {
|
|
887
|
-
const
|
|
888
|
-
if (!resolvedId) return null;
|
|
889
|
-
const row = this.stmtGetById.get(resolvedId, this.projectHash);
|
|
812
|
+
const row = this.stmtGetById.get(id, this.projectHash);
|
|
890
813
|
return row ? rowToObservation(row) : null;
|
|
891
814
|
}
|
|
892
815
|
/**
|
|
@@ -955,26 +878,20 @@ var ObservationRepository = class {
|
|
|
955
878
|
}
|
|
956
879
|
/**
|
|
957
880
|
* Soft-deletes an observation by setting deleted_at.
|
|
958
|
-
* Accepts full 32-char IDs or shorter prefix strings (e.g. the 8-char
|
|
959
|
-
* display IDs shown in search results).
|
|
960
881
|
* Returns true if the observation was found and deleted.
|
|
961
882
|
*/
|
|
962
883
|
softDelete(id) {
|
|
963
884
|
debug("obs", "Soft-deleting observation", { id });
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
debug("obs", result.changes > 0 ? "Observation soft-deleted" : "Observation not found for delete", { id: resolvedId });
|
|
885
|
+
const result = this.stmtSoftDelete.run(id, this.projectHash);
|
|
886
|
+
debug("obs", result.changes > 0 ? "Observation soft-deleted" : "Observation not found for delete", { id });
|
|
967
887
|
return result.changes > 0;
|
|
968
888
|
}
|
|
969
889
|
/**
|
|
970
890
|
* Restores a soft-deleted observation by clearing deleted_at.
|
|
971
|
-
* Accepts full 32-char IDs or shorter prefix strings (e.g. the 8-char
|
|
972
|
-
* display IDs shown in search results).
|
|
973
891
|
* Returns true if the observation was found and restored.
|
|
974
892
|
*/
|
|
975
893
|
restore(id) {
|
|
976
|
-
|
|
977
|
-
return this.stmtRestore.run(resolvedId, this.projectHash).changes > 0;
|
|
894
|
+
return this.stmtRestore.run(id, this.projectHash).changes > 0;
|
|
978
895
|
}
|
|
979
896
|
/**
|
|
980
897
|
* Updates the classification of an observation.
|
|
@@ -1011,18 +928,6 @@ var ObservationRepository = class {
|
|
|
1011
928
|
ORDER BY created_at ASC
|
|
1012
929
|
LIMIT ?
|
|
1013
930
|
`).all(this.projectHash, limit).map(rowToObservation);
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Lists unclassified observations across ALL projects.
|
|
1017
|
-
* Used by HaikuProcessor to avoid missing observations from other projects.
|
|
1018
|
-
*/
|
|
1019
|
-
static listAllUnclassified(db, limit = 20) {
|
|
1020
|
-
return db.prepare(`
|
|
1021
|
-
SELECT * FROM observations
|
|
1022
|
-
WHERE classification IS NULL AND deleted_at IS NULL
|
|
1023
|
-
ORDER BY created_at ASC
|
|
1024
|
-
LIMIT ?
|
|
1025
|
-
`).all(limit).map(rowToObservation);
|
|
1026
931
|
}
|
|
1027
932
|
/**
|
|
1028
933
|
* Fetches observations surrounding a given timestamp for classification context.
|
|
@@ -1057,28 +962,11 @@ var ObservationRepository = class {
|
|
|
1057
962
|
}
|
|
1058
963
|
/**
|
|
1059
964
|
* Gets an observation by ID, including soft-deleted observations.
|
|
1060
|
-
* Accepts full 32-char IDs or shorter prefix strings (e.g. the 8-char
|
|
1061
|
-
* display IDs shown in search results).
|
|
1062
965
|
* Used by the recall tool for restore operations (must find purged items).
|
|
1063
966
|
*/
|
|
1064
967
|
getByIdIncludingDeleted(id) {
|
|
1065
968
|
debug("obs", "Getting observation including deleted", { id });
|
|
1066
|
-
const
|
|
1067
|
-
let resolvedId;
|
|
1068
|
-
if (id.length === FULL_ID_LENGTH) resolvedId = id;
|
|
1069
|
-
else {
|
|
1070
|
-
const rows = this.db.prepare("SELECT id FROM observations WHERE project_hash = ? AND id LIKE ? ESCAPE '\\' LIMIT 2").all(this.projectHash, id.replace(/[%_\\]/g, "\\$&") + "%");
|
|
1071
|
-
if (rows.length === 0) return null;
|
|
1072
|
-
if (rows.length > 1) {
|
|
1073
|
-
debug("obs", "Ambiguous ID prefix for getByIdIncludingDeleted", {
|
|
1074
|
-
prefix: id,
|
|
1075
|
-
count: rows.length
|
|
1076
|
-
});
|
|
1077
|
-
return null;
|
|
1078
|
-
}
|
|
1079
|
-
resolvedId = rows[0].id;
|
|
1080
|
-
}
|
|
1081
|
-
const row = this.stmtGetByIdIncludingDeleted.get(resolvedId, this.projectHash);
|
|
969
|
+
const row = this.stmtGetByIdIncludingDeleted.get(id, this.projectHash);
|
|
1082
970
|
return row ? rowToObservation(row) : null;
|
|
1083
971
|
}
|
|
1084
972
|
/**
|
|
@@ -1857,570 +1745,6 @@ function insertEdge(db, edge) {
|
|
|
1857
1745
|
return rowToEdge(db.prepare("SELECT * FROM graph_edges WHERE source_id = ? AND target_id = ? AND type = ?").get(edge.source_id, edge.target_id, edge.type));
|
|
1858
1746
|
}
|
|
1859
1747
|
|
|
1860
|
-
//#endregion
|
|
1861
|
-
//#region src/graph/staleness.ts
|
|
1862
|
-
/**
|
|
1863
|
-
* Negation patterns: newer observation negates older one.
|
|
1864
|
-
* Matches when newer text contains negation keywords absent in older text
|
|
1865
|
-
* and both discuss similar subjects.
|
|
1866
|
-
*/
|
|
1867
|
-
const NEGATION_KEYWORDS = [
|
|
1868
|
-
"not",
|
|
1869
|
-
"don't",
|
|
1870
|
-
"no longer",
|
|
1871
|
-
"stopped",
|
|
1872
|
-
"never",
|
|
1873
|
-
"doesn't",
|
|
1874
|
-
"won't",
|
|
1875
|
-
"isn't",
|
|
1876
|
-
"aren't",
|
|
1877
|
-
"discontinued"
|
|
1878
|
-
];
|
|
1879
|
-
/**
|
|
1880
|
-
* Replacement patterns: newer observation explicitly replaces older approach.
|
|
1881
|
-
*/
|
|
1882
|
-
const REPLACEMENT_PATTERNS = [
|
|
1883
|
-
/switched\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1884
|
-
/migrated\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1885
|
-
/replaced\s+(?:\S+\s+)?with\b/i,
|
|
1886
|
-
/changed\s+from\b/i,
|
|
1887
|
-
/moved\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1888
|
-
/upgraded\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1889
|
-
/swapped\s+(?:\S+\s+)?(?:for|with)\b/i
|
|
1890
|
-
];
|
|
1891
|
-
/**
|
|
1892
|
-
* Status change patterns: newer observation marks something as inactive.
|
|
1893
|
-
*/
|
|
1894
|
-
const STATUS_CHANGE_KEYWORDS = [
|
|
1895
|
-
"removed",
|
|
1896
|
-
"deleted",
|
|
1897
|
-
"deprecated",
|
|
1898
|
-
"archived",
|
|
1899
|
-
"dropped",
|
|
1900
|
-
"disabled",
|
|
1901
|
-
"decommissioned",
|
|
1902
|
-
"sunset",
|
|
1903
|
-
"abandoned"
|
|
1904
|
-
];
|
|
1905
|
-
/**
|
|
1906
|
-
* Creates the staleness_flags table if it doesn't exist.
|
|
1907
|
-
* Uses a separate table rather than modifying the observations table,
|
|
1908
|
-
* keeping staleness metadata decoupled from core observation storage.
|
|
1909
|
-
*/
|
|
1910
|
-
function initStalenessSchema(db) {
|
|
1911
|
-
db.exec(`
|
|
1912
|
-
CREATE TABLE IF NOT EXISTS staleness_flags (
|
|
1913
|
-
observation_id TEXT PRIMARY KEY,
|
|
1914
|
-
flagged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1915
|
-
reason TEXT NOT NULL,
|
|
1916
|
-
resolved INTEGER NOT NULL DEFAULT 0
|
|
1917
|
-
);
|
|
1918
|
-
CREATE INDEX IF NOT EXISTS idx_staleness_resolved ON staleness_flags(resolved);
|
|
1919
|
-
`);
|
|
1920
|
-
}
|
|
1921
|
-
/**
|
|
1922
|
-
* Detects potential staleness (contradictions) between observations
|
|
1923
|
-
* linked to a specific entity.
|
|
1924
|
-
*
|
|
1925
|
-
* Compares consecutive observation pairs chronologically and checks for:
|
|
1926
|
-
* 1. Negation patterns (newer negates older)
|
|
1927
|
-
* 2. Replacement patterns (newer replaces older approach)
|
|
1928
|
-
* 3. Status change patterns (newer marks something as inactive)
|
|
1929
|
-
*
|
|
1930
|
-
* This is DETECTION ONLY -- no data is modified.
|
|
1931
|
-
*
|
|
1932
|
-
* @param db - better-sqlite3 Database handle
|
|
1933
|
-
* @param entityId - Graph node ID to check observations for
|
|
1934
|
-
* @returns Array of StalenessReport for each detected contradiction
|
|
1935
|
-
*/
|
|
1936
|
-
function detectStaleness(db, entityId) {
|
|
1937
|
-
const node = db.prepare("SELECT id, name, type, observation_ids FROM graph_nodes WHERE id = ?").get(entityId);
|
|
1938
|
-
if (!node) return [];
|
|
1939
|
-
const obsIds = JSON.parse(node.observation_ids);
|
|
1940
|
-
if (obsIds.length < 2) return [];
|
|
1941
|
-
const placeholders = obsIds.map(() => "?").join(", ");
|
|
1942
|
-
const observations = db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) AND deleted_at IS NULL ORDER BY created_at ASC`).all(...obsIds).map(rowToObservation);
|
|
1943
|
-
if (observations.length < 2) return [];
|
|
1944
|
-
const reports = [];
|
|
1945
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1946
|
-
for (let i = 0; i < observations.length - 1; i++) {
|
|
1947
|
-
const older = observations[i];
|
|
1948
|
-
const newer = observations[i + 1];
|
|
1949
|
-
const reason = detectContradiction(older.content, newer.content);
|
|
1950
|
-
if (reason) reports.push({
|
|
1951
|
-
entityId: node.id,
|
|
1952
|
-
entityName: node.name,
|
|
1953
|
-
entityType: node.type,
|
|
1954
|
-
newerObservation: {
|
|
1955
|
-
id: newer.id,
|
|
1956
|
-
text: newer.content,
|
|
1957
|
-
created_at: newer.createdAt
|
|
1958
|
-
},
|
|
1959
|
-
olderObservation: {
|
|
1960
|
-
id: older.id,
|
|
1961
|
-
text: older.content,
|
|
1962
|
-
created_at: older.createdAt
|
|
1963
|
-
},
|
|
1964
|
-
reason,
|
|
1965
|
-
detectedAt: now
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
return reports;
|
|
1969
|
-
}
|
|
1970
|
-
/**
|
|
1971
|
-
* Detects contradiction between two observation texts.
|
|
1972
|
-
* Returns a human-readable reason string, or null if no contradiction found.
|
|
1973
|
-
*/
|
|
1974
|
-
function detectContradiction(olderText, newerText) {
|
|
1975
|
-
const olderLower = olderText.toLowerCase();
|
|
1976
|
-
const newerLower = newerText.toLowerCase();
|
|
1977
|
-
const negationResult = detectNegation(olderLower, newerLower);
|
|
1978
|
-
if (negationResult) return negationResult;
|
|
1979
|
-
const replacementResult = detectReplacement(newerLower);
|
|
1980
|
-
if (replacementResult) return replacementResult;
|
|
1981
|
-
const statusResult = detectStatusChange(olderLower, newerLower);
|
|
1982
|
-
if (statusResult) return statusResult;
|
|
1983
|
-
return null;
|
|
1984
|
-
}
|
|
1985
|
-
/**
|
|
1986
|
-
* Detects negation: newer text contains negation keywords that are absent
|
|
1987
|
-
* in the older text, suggesting the newer observation contradicts the older.
|
|
1988
|
-
*/
|
|
1989
|
-
function detectNegation(olderLower, newerLower) {
|
|
1990
|
-
for (const keyword of NEGATION_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation contains negation ("${keyword}") not present in older observation`;
|
|
1991
|
-
return null;
|
|
1992
|
-
}
|
|
1993
|
-
/**
|
|
1994
|
-
* Detects replacement: newer text explicitly mentions switching/replacing.
|
|
1995
|
-
*/
|
|
1996
|
-
function detectReplacement(newerLower) {
|
|
1997
|
-
for (const pattern of REPLACEMENT_PATTERNS) {
|
|
1998
|
-
const match = newerLower.match(pattern);
|
|
1999
|
-
if (match) return `Newer observation indicates replacement ("${match[0].trim()}")`;
|
|
2000
|
-
}
|
|
2001
|
-
return null;
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Detects status change: newer text marks something as removed/deprecated
|
|
2005
|
-
* when the older text described it as active/present.
|
|
2006
|
-
*/
|
|
2007
|
-
function detectStatusChange(olderLower, newerLower) {
|
|
2008
|
-
for (const keyword of STATUS_CHANGE_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation indicates status change ("${keyword}")`;
|
|
2009
|
-
return null;
|
|
2010
|
-
}
|
|
2011
|
-
/**
|
|
2012
|
-
* Flags an observation as stale with an advisory reason.
|
|
2013
|
-
*
|
|
2014
|
-
* This flag is advisory -- search can use it to deprioritize but never hide
|
|
2015
|
-
* the observation. The observation remains fully queryable.
|
|
2016
|
-
*
|
|
2017
|
-
* Uses INSERT OR REPLACE to allow re-flagging with an updated reason.
|
|
2018
|
-
*
|
|
2019
|
-
* @param db - better-sqlite3 Database handle
|
|
2020
|
-
* @param observationId - ID of the observation to flag
|
|
2021
|
-
* @param reason - Human-readable explanation of why it's stale
|
|
2022
|
-
*/
|
|
2023
|
-
function flagStaleObservation(db, observationId, reason) {
|
|
2024
|
-
initStalenessSchema(db);
|
|
2025
|
-
db.prepare(`INSERT OR REPLACE INTO staleness_flags (observation_id, reason, resolved)
|
|
2026
|
-
VALUES (?, ?, 0)`).run(observationId, reason);
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
//#endregion
|
|
2030
|
-
//#region src/config/hygiene-config.ts
|
|
2031
|
-
/**
|
|
2032
|
-
* Database Hygiene Configuration
|
|
2033
|
-
*
|
|
2034
|
-
* Controls signal weights and tier thresholds used by the hygiene
|
|
2035
|
-
* analyzer to score observations for deletion candidacy.
|
|
2036
|
-
*
|
|
2037
|
-
* Configuration is loaded from .laminark/hygiene.json with
|
|
2038
|
-
* a 5-second cache to avoid repeated disk reads.
|
|
2039
|
-
*/
|
|
2040
|
-
const DEFAULT_AUTO_CLEANUP = {
|
|
2041
|
-
enabled: true,
|
|
2042
|
-
tier: "high",
|
|
2043
|
-
maxOrphanNodes: 500
|
|
2044
|
-
};
|
|
2045
|
-
const DEFAULTS = {
|
|
2046
|
-
signalWeights: {
|
|
2047
|
-
orphaned: .3,
|
|
2048
|
-
islandNode: .25,
|
|
2049
|
-
noiseClassified: .25,
|
|
2050
|
-
shortContent: .1,
|
|
2051
|
-
autoCaptured: .1,
|
|
2052
|
-
stale: .1
|
|
2053
|
-
},
|
|
2054
|
-
tierThresholds: {
|
|
2055
|
-
high: .7,
|
|
2056
|
-
medium: .5
|
|
2057
|
-
},
|
|
2058
|
-
shortContentThreshold: 50,
|
|
2059
|
-
autoCleanup: { ...DEFAULT_AUTO_CLEANUP }
|
|
2060
|
-
};
|
|
2061
|
-
const CACHE_TTL_MS = 5e3;
|
|
2062
|
-
let cachedConfig = null;
|
|
2063
|
-
let cachedAt = 0;
|
|
2064
|
-
function clamp(value, min, max) {
|
|
2065
|
-
return Math.max(min, Math.min(max, value));
|
|
2066
|
-
}
|
|
2067
|
-
function validate(raw) {
|
|
2068
|
-
const config = { ...DEFAULTS };
|
|
2069
|
-
if (raw.signalWeights && typeof raw.signalWeights === "object" && !Array.isArray(raw.signalWeights)) {
|
|
2070
|
-
const sw = raw.signalWeights;
|
|
2071
|
-
const weights = { ...DEFAULTS.signalWeights };
|
|
2072
|
-
for (const key of Object.keys(DEFAULTS.signalWeights)) if (typeof sw[key] === "number") weights[key] = clamp(sw[key], 0, 1);
|
|
2073
|
-
config.signalWeights = weights;
|
|
2074
|
-
}
|
|
2075
|
-
if (raw.tierThresholds && typeof raw.tierThresholds === "object" && !Array.isArray(raw.tierThresholds)) {
|
|
2076
|
-
const tt = raw.tierThresholds;
|
|
2077
|
-
let high = typeof tt.high === "number" ? clamp(tt.high, 0, 1) : DEFAULTS.tierThresholds.high;
|
|
2078
|
-
let medium = typeof tt.medium === "number" ? clamp(tt.medium, 0, 1) : DEFAULTS.tierThresholds.medium;
|
|
2079
|
-
if (medium >= high) medium = Math.max(0, high - .1);
|
|
2080
|
-
config.tierThresholds = {
|
|
2081
|
-
high,
|
|
2082
|
-
medium
|
|
2083
|
-
};
|
|
2084
|
-
}
|
|
2085
|
-
if (typeof raw.shortContentThreshold === "number") config.shortContentThreshold = Math.max(0, Math.round(raw.shortContentThreshold));
|
|
2086
|
-
if (raw.autoCleanup && typeof raw.autoCleanup === "object" && !Array.isArray(raw.autoCleanup)) {
|
|
2087
|
-
const ac = raw.autoCleanup;
|
|
2088
|
-
const cleanup = { ...DEFAULT_AUTO_CLEANUP };
|
|
2089
|
-
if (typeof ac.enabled === "boolean") cleanup.enabled = ac.enabled;
|
|
2090
|
-
if (ac.tier === "high" || ac.tier === "medium" || ac.tier === "all") cleanup.tier = ac.tier;
|
|
2091
|
-
if (typeof ac.maxOrphanNodes === "number") cleanup.maxOrphanNodes = Math.max(0, Math.round(ac.maxOrphanNodes));
|
|
2092
|
-
config.autoCleanup = cleanup;
|
|
2093
|
-
}
|
|
2094
|
-
return config;
|
|
2095
|
-
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Loads hygiene configuration from disk with a 5-second cache.
|
|
2098
|
-
*/
|
|
2099
|
-
function loadHygieneConfig() {
|
|
2100
|
-
const now = Date.now();
|
|
2101
|
-
if (cachedConfig && now - cachedAt < CACHE_TTL_MS) return cachedConfig;
|
|
2102
|
-
const configPath = join(getConfigDir(), "hygiene.json");
|
|
2103
|
-
try {
|
|
2104
|
-
const content = readFileSync(configPath, "utf-8");
|
|
2105
|
-
cachedConfig = validate(JSON.parse(content));
|
|
2106
|
-
debug("config", "Loaded hygiene config", cachedConfig);
|
|
2107
|
-
} catch {
|
|
2108
|
-
cachedConfig = {
|
|
2109
|
-
...DEFAULTS,
|
|
2110
|
-
signalWeights: { ...DEFAULTS.signalWeights },
|
|
2111
|
-
tierThresholds: { ...DEFAULTS.tierThresholds }
|
|
2112
|
-
};
|
|
2113
|
-
}
|
|
2114
|
-
cachedAt = now;
|
|
2115
|
-
return cachedConfig;
|
|
2116
|
-
}
|
|
2117
|
-
/**
|
|
2118
|
-
* Saves hygiene configuration to disk and invalidates cache.
|
|
2119
|
-
*/
|
|
2120
|
-
function saveHygieneConfig(config) {
|
|
2121
|
-
writeFileSync(join(getConfigDir(), "hygiene.json"), JSON.stringify(config, null, 2), "utf-8");
|
|
2122
|
-
cachedConfig = config;
|
|
2123
|
-
cachedAt = Date.now();
|
|
2124
|
-
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Resets hygiene config to defaults by invalidating cache.
|
|
2127
|
-
*/
|
|
2128
|
-
function resetHygieneConfig() {
|
|
2129
|
-
cachedConfig = null;
|
|
2130
|
-
cachedAt = 0;
|
|
2131
|
-
return {
|
|
2132
|
-
...DEFAULTS,
|
|
2133
|
-
signalWeights: { ...DEFAULTS.signalWeights },
|
|
2134
|
-
tierThresholds: { ...DEFAULTS.tierThresholds },
|
|
2135
|
-
autoCleanup: { ...DEFAULT_AUTO_CLEANUP }
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
//#endregion
|
|
2140
|
-
//#region src/graph/hygiene-analyzer.ts
|
|
2141
|
-
function buildSignalLookups(db) {
|
|
2142
|
-
const linkedObsIds = /* @__PURE__ */ new Set();
|
|
2143
|
-
const islandObsIds = /* @__PURE__ */ new Set();
|
|
2144
|
-
const allNodes = db.prepare("SELECT id, type, name, observation_ids FROM graph_nodes").all();
|
|
2145
|
-
const edgeCounts = /* @__PURE__ */ new Map();
|
|
2146
|
-
const edgeRows = db.prepare(`SELECT source_id AS nid, COUNT(*) AS cnt FROM graph_edges GROUP BY source_id
|
|
2147
|
-
UNION ALL
|
|
2148
|
-
SELECT target_id AS nid, COUNT(*) AS cnt FROM graph_edges GROUP BY target_id`).all();
|
|
2149
|
-
for (const row of edgeRows) edgeCounts.set(row.nid, (edgeCounts.get(row.nid) ?? 0) + row.cnt);
|
|
2150
|
-
for (const node of allNodes) {
|
|
2151
|
-
let obsIds;
|
|
2152
|
-
try {
|
|
2153
|
-
obsIds = JSON.parse(node.observation_ids);
|
|
2154
|
-
} catch {
|
|
2155
|
-
continue;
|
|
2156
|
-
}
|
|
2157
|
-
const degree = edgeCounts.get(node.id) ?? 0;
|
|
2158
|
-
for (const oid of obsIds) {
|
|
2159
|
-
linkedObsIds.add(oid);
|
|
2160
|
-
if (degree === 0) islandObsIds.add(oid);
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
const staleIds = /* @__PURE__ */ new Set();
|
|
2164
|
-
try {
|
|
2165
|
-
initStalenessSchema(db);
|
|
2166
|
-
const staleRows = db.prepare("SELECT observation_id FROM staleness_flags WHERE resolved = 0").all();
|
|
2167
|
-
for (const row of staleRows) staleIds.add(row.observation_id);
|
|
2168
|
-
} catch {}
|
|
2169
|
-
return {
|
|
2170
|
-
linkedObsIds,
|
|
2171
|
-
islandObsIds,
|
|
2172
|
-
staleIds,
|
|
2173
|
-
allNodes,
|
|
2174
|
-
edgeCounts
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
function scoreObservation(obs, lookups, config) {
|
|
2178
|
-
const weights = config.signalWeights;
|
|
2179
|
-
const thresholds = config.tierThresholds;
|
|
2180
|
-
const signals = {
|
|
2181
|
-
orphaned: !lookups.linkedObsIds.has(obs.id),
|
|
2182
|
-
islandNode: lookups.islandObsIds.has(obs.id),
|
|
2183
|
-
noiseClassified: obs.classification === "noise",
|
|
2184
|
-
shortContent: obs.content.length < config.shortContentThreshold,
|
|
2185
|
-
autoCaptured: obs.source.startsWith("hook:"),
|
|
2186
|
-
stale: lookups.staleIds.has(obs.id)
|
|
2187
|
-
};
|
|
2188
|
-
const confidence = (signals.orphaned ? weights.orphaned : 0) + (signals.islandNode ? weights.islandNode : 0) + (signals.noiseClassified ? weights.noiseClassified : 0) + (signals.shortContent ? weights.shortContent : 0) + (signals.autoCaptured ? weights.autoCaptured : 0) + (signals.stale ? weights.stale : 0);
|
|
2189
|
-
const tier = confidence >= thresholds.high ? "high" : confidence >= thresholds.medium ? "medium" : "low";
|
|
2190
|
-
return {
|
|
2191
|
-
signals,
|
|
2192
|
-
confidence: Math.round(confidence * 100) / 100,
|
|
2193
|
-
tier
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
/**
|
|
2197
|
-
* Analyzes all active observations and scores each on deletion signals.
|
|
2198
|
-
* Pure read-only — no data is modified.
|
|
2199
|
-
*/
|
|
2200
|
-
function analyzeObservations(db, projectHash, opts) {
|
|
2201
|
-
const limit = opts?.limit ?? 50;
|
|
2202
|
-
const minTier = opts?.minTier ?? "medium";
|
|
2203
|
-
const config = opts?.config ?? loadHygieneConfig();
|
|
2204
|
-
debug("hygiene", "Starting analysis", {
|
|
2205
|
-
projectHash,
|
|
2206
|
-
sessionId: opts?.sessionId
|
|
2207
|
-
});
|
|
2208
|
-
let obsSql = `
|
|
2209
|
-
SELECT id, content, title, source, kind, session_id, classification, created_at
|
|
2210
|
-
FROM observations
|
|
2211
|
-
WHERE project_hash = ? AND deleted_at IS NULL
|
|
2212
|
-
`;
|
|
2213
|
-
const obsParams = [projectHash];
|
|
2214
|
-
if (opts?.sessionId) {
|
|
2215
|
-
obsSql += " AND session_id = ?";
|
|
2216
|
-
obsParams.push(opts.sessionId);
|
|
2217
|
-
}
|
|
2218
|
-
obsSql += " ORDER BY created_at DESC";
|
|
2219
|
-
const observations = db.prepare(obsSql).all(...obsParams);
|
|
2220
|
-
const lookups = buildSignalLookups(db);
|
|
2221
|
-
const allCandidates = [];
|
|
2222
|
-
for (const obs of observations) {
|
|
2223
|
-
const { signals, confidence, tier } = scoreObservation(obs, lookups, config);
|
|
2224
|
-
if (minTier === "high" && tier !== "high") continue;
|
|
2225
|
-
if (minTier === "medium" && tier === "low") continue;
|
|
2226
|
-
const preview = obs.content.length > 80 ? obs.content.substring(0, 80) + "..." : obs.content;
|
|
2227
|
-
allCandidates.push({
|
|
2228
|
-
id: obs.id,
|
|
2229
|
-
shortId: obs.id.substring(0, 8),
|
|
2230
|
-
sessionId: obs.session_id,
|
|
2231
|
-
kind: obs.kind,
|
|
2232
|
-
source: obs.source,
|
|
2233
|
-
contentPreview: preview,
|
|
2234
|
-
createdAt: obs.created_at,
|
|
2235
|
-
signals,
|
|
2236
|
-
confidence,
|
|
2237
|
-
tier
|
|
2238
|
-
});
|
|
2239
|
-
}
|
|
2240
|
-
allCandidates.sort((a, b) => b.confidence - a.confidence);
|
|
2241
|
-
const activeObsIds = new Set(observations.map((o) => o.id));
|
|
2242
|
-
const orphanNodes = [];
|
|
2243
|
-
for (const node of lookups.allNodes) {
|
|
2244
|
-
if ((lookups.edgeCounts.get(node.id) ?? 0) > 0) continue;
|
|
2245
|
-
let obsIds;
|
|
2246
|
-
try {
|
|
2247
|
-
obsIds = JSON.parse(node.observation_ids);
|
|
2248
|
-
} catch {
|
|
2249
|
-
continue;
|
|
2250
|
-
}
|
|
2251
|
-
const allDead = obsIds.length === 0 || obsIds.every((oid) => !activeObsIds.has(oid));
|
|
2252
|
-
orphanNodes.push({
|
|
2253
|
-
id: node.id,
|
|
2254
|
-
type: node.type,
|
|
2255
|
-
name: node.name,
|
|
2256
|
-
reason: allDead ? "zero edges, dead observation refs" : "zero edges (island node)"
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
const limited = allCandidates.slice(0, limit);
|
|
2260
|
-
const highCount = allCandidates.filter((c) => c.tier === "high").length;
|
|
2261
|
-
const mediumCount = allCandidates.filter((c) => c.tier === "medium").length;
|
|
2262
|
-
const lowCount = allCandidates.filter((c) => c.tier === "low").length;
|
|
2263
|
-
debug("hygiene", "Analysis complete", {
|
|
2264
|
-
total: observations.length,
|
|
2265
|
-
high: highCount,
|
|
2266
|
-
medium: mediumCount,
|
|
2267
|
-
orphanNodes: orphanNodes.length
|
|
2268
|
-
});
|
|
2269
|
-
return {
|
|
2270
|
-
analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2271
|
-
totalObservations: observations.length,
|
|
2272
|
-
candidates: limited,
|
|
2273
|
-
orphanNodes: orphanNodes.slice(0, limit),
|
|
2274
|
-
summary: {
|
|
2275
|
-
high: highCount,
|
|
2276
|
-
medium: mediumCount,
|
|
2277
|
-
low: lowCount,
|
|
2278
|
-
orphanNodeCount: orphanNodes.length
|
|
2279
|
-
}
|
|
2280
|
-
};
|
|
2281
|
-
}
|
|
2282
|
-
/**
|
|
2283
|
-
* Produces a score distribution report across all observations.
|
|
2284
|
-
* Shows signal counts, confidence histogram, and island node summary
|
|
2285
|
-
* so users can tune thresholds to catch the right candidates.
|
|
2286
|
-
*/
|
|
2287
|
-
function findAnalysis(db, projectHash, config) {
|
|
2288
|
-
const cfg = config ?? loadHygieneConfig();
|
|
2289
|
-
const observations = db.prepare(`
|
|
2290
|
-
SELECT id, content, title, source, kind, session_id, classification, created_at
|
|
2291
|
-
FROM observations
|
|
2292
|
-
WHERE project_hash = ? AND deleted_at IS NULL
|
|
2293
|
-
ORDER BY created_at DESC
|
|
2294
|
-
`).all(projectHash);
|
|
2295
|
-
const lookups = buildSignalLookups(db);
|
|
2296
|
-
const bySignal = {
|
|
2297
|
-
orphaned: 0,
|
|
2298
|
-
islandNode: 0,
|
|
2299
|
-
noiseClassified: 0,
|
|
2300
|
-
shortContent: 0,
|
|
2301
|
-
autoCaptured: 0,
|
|
2302
|
-
stale: 0
|
|
2303
|
-
};
|
|
2304
|
-
const buckets = new Array(10).fill(0);
|
|
2305
|
-
const islandConfidences = [];
|
|
2306
|
-
for (const obs of observations) {
|
|
2307
|
-
const { signals, confidence } = scoreObservation(obs, lookups, cfg);
|
|
2308
|
-
if (signals.orphaned) bySignal.orphaned++;
|
|
2309
|
-
if (signals.islandNode) bySignal.islandNode++;
|
|
2310
|
-
if (signals.noiseClassified) bySignal.noiseClassified++;
|
|
2311
|
-
if (signals.shortContent) bySignal.shortContent++;
|
|
2312
|
-
if (signals.autoCaptured) bySignal.autoCaptured++;
|
|
2313
|
-
if (signals.stale) bySignal.stale++;
|
|
2314
|
-
const bucketIdx = Math.min(Math.floor(confidence * 10), 9);
|
|
2315
|
-
buckets[bucketIdx]++;
|
|
2316
|
-
if (signals.islandNode) islandConfidences.push(confidence);
|
|
2317
|
-
}
|
|
2318
|
-
const distribution = buckets.map((count, i) => ({
|
|
2319
|
-
range: `${(i / 10).toFixed(1)}-${((i + 1) / 10).toFixed(1)}`,
|
|
2320
|
-
count
|
|
2321
|
-
}));
|
|
2322
|
-
islandConfidences.sort((a, b) => a - b);
|
|
2323
|
-
const islandTotal = islandConfidences.length;
|
|
2324
|
-
const minConf = islandTotal > 0 ? islandConfidences[0] : 0;
|
|
2325
|
-
const maxConf = islandTotal > 0 ? islandConfidences[islandTotal - 1] : 0;
|
|
2326
|
-
const medianConf = islandTotal > 0 ? islandConfidences[Math.floor(islandTotal / 2)] : 0;
|
|
2327
|
-
const capturedHigh = islandConfidences.filter((c) => c >= cfg.tierThresholds.high).length;
|
|
2328
|
-
const capturedMedium = islandConfidences.filter((c) => c >= cfg.tierThresholds.medium).length;
|
|
2329
|
-
return {
|
|
2330
|
-
total: observations.length,
|
|
2331
|
-
bySignal,
|
|
2332
|
-
distribution,
|
|
2333
|
-
islandNodes: {
|
|
2334
|
-
total: islandTotal,
|
|
2335
|
-
minConfidence: Math.round(minConf * 100) / 100,
|
|
2336
|
-
maxConfidence: Math.round(maxConf * 100) / 100,
|
|
2337
|
-
medianConfidence: Math.round(medianConf * 100) / 100,
|
|
2338
|
-
capturedAtCurrentThresholds: {
|
|
2339
|
-
high: capturedHigh,
|
|
2340
|
-
medium: capturedMedium,
|
|
2341
|
-
all: islandTotal
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
};
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Runs automatic hygiene cleanup at session end.
|
|
2348
|
-
*
|
|
2349
|
-
* Analyzes observations and purges candidates matching the configured tier.
|
|
2350
|
-
* Orphan graph node removal is capped by autoCleanup.maxOrphanNodes.
|
|
2351
|
-
* Safe to call on every session end — skips quickly if disabled.
|
|
2352
|
-
*/
|
|
2353
|
-
function runAutoCleanup(db, projectHash, config) {
|
|
2354
|
-
const cfg = config ?? loadHygieneConfig();
|
|
2355
|
-
const auto = cfg.autoCleanup;
|
|
2356
|
-
if (!auto.enabled) return {
|
|
2357
|
-
skipped: true,
|
|
2358
|
-
reason: "disabled",
|
|
2359
|
-
observationsPurged: 0,
|
|
2360
|
-
orphanNodesRemoved: 0
|
|
2361
|
-
};
|
|
2362
|
-
debug("hygiene", "Auto-cleanup starting", {
|
|
2363
|
-
tier: auto.tier,
|
|
2364
|
-
maxOrphanNodes: auto.maxOrphanNodes
|
|
2365
|
-
});
|
|
2366
|
-
const report = analyzeObservations(db, projectHash, {
|
|
2367
|
-
limit: 200,
|
|
2368
|
-
minTier: auto.tier === "all" ? "low" : auto.tier,
|
|
2369
|
-
config: cfg
|
|
2370
|
-
});
|
|
2371
|
-
if (report.orphanNodes.length > auto.maxOrphanNodes) report.orphanNodes = report.orphanNodes.slice(0, auto.maxOrphanNodes);
|
|
2372
|
-
if (report.candidates.length + report.orphanNodes.length === 0) {
|
|
2373
|
-
debug("hygiene", "Auto-cleanup: nothing to clean");
|
|
2374
|
-
return {
|
|
2375
|
-
skipped: false,
|
|
2376
|
-
observationsPurged: 0,
|
|
2377
|
-
orphanNodesRemoved: 0
|
|
2378
|
-
};
|
|
2379
|
-
}
|
|
2380
|
-
const result = executePurge(db, projectHash, report, auto.tier);
|
|
2381
|
-
debug("hygiene", "Auto-cleanup complete", {
|
|
2382
|
-
observationsPurged: result.observationsPurged,
|
|
2383
|
-
orphanNodesRemoved: result.orphanNodesRemoved
|
|
2384
|
-
});
|
|
2385
|
-
return {
|
|
2386
|
-
skipped: false,
|
|
2387
|
-
...result
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
function executePurge(db, projectHash, report, tier) {
|
|
2391
|
-
const candidateIds = report.candidates.filter((c) => {
|
|
2392
|
-
if (tier === "high") return c.tier === "high";
|
|
2393
|
-
if (tier === "medium") return c.tier === "high" || c.tier === "medium";
|
|
2394
|
-
return true;
|
|
2395
|
-
}).map((c) => c.id);
|
|
2396
|
-
debug("hygiene", "Executing purge", {
|
|
2397
|
-
tier,
|
|
2398
|
-
candidates: candidateIds.length
|
|
2399
|
-
});
|
|
2400
|
-
let observationsPurged = 0;
|
|
2401
|
-
const softDeleteStmt = db.prepare(`
|
|
2402
|
-
UPDATE observations
|
|
2403
|
-
SET deleted_at = datetime('now'), updated_at = datetime('now')
|
|
2404
|
-
WHERE id = ? AND project_hash = ? AND deleted_at IS NULL
|
|
2405
|
-
`);
|
|
2406
|
-
return db.transaction(() => {
|
|
2407
|
-
for (const id of candidateIds) {
|
|
2408
|
-
const result = softDeleteStmt.run(id, projectHash);
|
|
2409
|
-
observationsPurged += result.changes;
|
|
2410
|
-
}
|
|
2411
|
-
let orphanNodesRemoved = 0;
|
|
2412
|
-
const deleteNodeStmt = db.prepare("DELETE FROM graph_nodes WHERE id = ?");
|
|
2413
|
-
for (const node of report.orphanNodes) {
|
|
2414
|
-
const result = deleteNodeStmt.run(node.id);
|
|
2415
|
-
orphanNodesRemoved += result.changes;
|
|
2416
|
-
}
|
|
2417
|
-
return {
|
|
2418
|
-
observationsPurged,
|
|
2419
|
-
orphanNodesRemoved
|
|
2420
|
-
};
|
|
2421
|
-
})();
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
1748
|
//#endregion
|
|
2425
1749
|
//#region src/hooks/tool-name-parser.ts
|
|
2426
1750
|
/**
|
|
@@ -2466,248 +1790,6 @@ function extractServerName(toolName) {
|
|
|
2466
1790
|
return null;
|
|
2467
1791
|
}
|
|
2468
1792
|
|
|
2469
|
-
//#endregion
|
|
2470
|
-
//#region src/branches/branch-repository.ts
|
|
2471
|
-
var BranchRepository = class {
|
|
2472
|
-
db;
|
|
2473
|
-
projectHash;
|
|
2474
|
-
stmtCreate;
|
|
2475
|
-
stmtComplete;
|
|
2476
|
-
stmtAbandon;
|
|
2477
|
-
stmtGetActive;
|
|
2478
|
-
stmtGetById;
|
|
2479
|
-
stmtList;
|
|
2480
|
-
stmtListByStatus;
|
|
2481
|
-
stmtListByType;
|
|
2482
|
-
stmtUpdateArcStage;
|
|
2483
|
-
stmtUpdateToolPattern;
|
|
2484
|
-
stmtUpdateClassification;
|
|
2485
|
-
stmtUpdateSummary;
|
|
2486
|
-
stmtIncrementObsCount;
|
|
2487
|
-
stmtLinkDebugPath;
|
|
2488
|
-
stmtAddObservation;
|
|
2489
|
-
stmtGetObservations;
|
|
2490
|
-
stmtMaxSequence;
|
|
2491
|
-
stmtFindStale;
|
|
2492
|
-
stmtFindUnclassified;
|
|
2493
|
-
stmtFindRecentCompleted;
|
|
2494
|
-
stmtFindRecentActive;
|
|
2495
|
-
stmtListRecent;
|
|
2496
|
-
constructor(db, projectHash) {
|
|
2497
|
-
this.db = db;
|
|
2498
|
-
this.projectHash = projectHash;
|
|
2499
|
-
this.stmtCreate = db.prepare(`
|
|
2500
|
-
INSERT INTO thought_branches
|
|
2501
|
-
(id, project_hash, session_id, status, trigger_source, trigger_observation_id, started_at)
|
|
2502
|
-
VALUES (?, ?, ?, 'active', ?, ?, datetime('now'))
|
|
2503
|
-
`);
|
|
2504
|
-
this.stmtComplete = db.prepare(`
|
|
2505
|
-
UPDATE thought_branches
|
|
2506
|
-
SET status = 'completed', arc_stage = 'completed', ended_at = datetime('now')
|
|
2507
|
-
WHERE id = ? AND project_hash = ?
|
|
2508
|
-
`);
|
|
2509
|
-
this.stmtAbandon = db.prepare(`
|
|
2510
|
-
UPDATE thought_branches
|
|
2511
|
-
SET status = 'abandoned', ended_at = datetime('now')
|
|
2512
|
-
WHERE id = ? AND project_hash = ?
|
|
2513
|
-
`);
|
|
2514
|
-
this.stmtGetActive = db.prepare(`
|
|
2515
|
-
SELECT * FROM thought_branches
|
|
2516
|
-
WHERE project_hash = ? AND status = 'active'
|
|
2517
|
-
ORDER BY started_at DESC
|
|
2518
|
-
LIMIT 1
|
|
2519
|
-
`);
|
|
2520
|
-
this.stmtGetById = db.prepare(`
|
|
2521
|
-
SELECT * FROM thought_branches
|
|
2522
|
-
WHERE id = ? AND project_hash = ?
|
|
2523
|
-
`);
|
|
2524
|
-
this.stmtList = db.prepare(`
|
|
2525
|
-
SELECT * FROM thought_branches
|
|
2526
|
-
WHERE project_hash = ?
|
|
2527
|
-
ORDER BY started_at DESC
|
|
2528
|
-
LIMIT ?
|
|
2529
|
-
`);
|
|
2530
|
-
this.stmtListByStatus = db.prepare(`
|
|
2531
|
-
SELECT * FROM thought_branches
|
|
2532
|
-
WHERE project_hash = ? AND status = ?
|
|
2533
|
-
ORDER BY started_at DESC
|
|
2534
|
-
LIMIT ?
|
|
2535
|
-
`);
|
|
2536
|
-
this.stmtListByType = db.prepare(`
|
|
2537
|
-
SELECT * FROM thought_branches
|
|
2538
|
-
WHERE project_hash = ? AND branch_type = ?
|
|
2539
|
-
ORDER BY started_at DESC
|
|
2540
|
-
LIMIT ?
|
|
2541
|
-
`);
|
|
2542
|
-
this.stmtUpdateArcStage = db.prepare(`
|
|
2543
|
-
UPDATE thought_branches SET arc_stage = ? WHERE id = ? AND project_hash = ?
|
|
2544
|
-
`);
|
|
2545
|
-
this.stmtUpdateToolPattern = db.prepare(`
|
|
2546
|
-
UPDATE thought_branches SET tool_pattern = ? WHERE id = ? AND project_hash = ?
|
|
2547
|
-
`);
|
|
2548
|
-
this.stmtUpdateClassification = db.prepare(`
|
|
2549
|
-
UPDATE thought_branches SET branch_type = ?, title = ? WHERE id = ? AND project_hash = ?
|
|
2550
|
-
`);
|
|
2551
|
-
this.stmtUpdateSummary = db.prepare(`
|
|
2552
|
-
UPDATE thought_branches SET summary = ? WHERE id = ? AND project_hash = ?
|
|
2553
|
-
`);
|
|
2554
|
-
this.stmtIncrementObsCount = db.prepare(`
|
|
2555
|
-
UPDATE thought_branches SET observation_count = observation_count + 1 WHERE id = ? AND project_hash = ?
|
|
2556
|
-
`);
|
|
2557
|
-
this.stmtLinkDebugPath = db.prepare(`
|
|
2558
|
-
UPDATE thought_branches SET linked_debug_path_id = ? WHERE id = ? AND project_hash = ?
|
|
2559
|
-
`);
|
|
2560
|
-
this.stmtAddObservation = db.prepare(`
|
|
2561
|
-
INSERT OR IGNORE INTO branch_observations
|
|
2562
|
-
(branch_id, observation_id, sequence_order, tool_name, arc_stage_at_add)
|
|
2563
|
-
VALUES (?, ?, ?, ?, ?)
|
|
2564
|
-
`);
|
|
2565
|
-
this.stmtGetObservations = db.prepare(`
|
|
2566
|
-
SELECT * FROM branch_observations
|
|
2567
|
-
WHERE branch_id = ?
|
|
2568
|
-
ORDER BY sequence_order ASC
|
|
2569
|
-
`);
|
|
2570
|
-
this.stmtMaxSequence = db.prepare(`
|
|
2571
|
-
SELECT COALESCE(MAX(sequence_order), 0) AS max_seq FROM branch_observations
|
|
2572
|
-
WHERE branch_id = ?
|
|
2573
|
-
`);
|
|
2574
|
-
this.stmtFindStale = db.prepare(`
|
|
2575
|
-
SELECT * FROM thought_branches
|
|
2576
|
-
WHERE project_hash = ? AND status = 'active'
|
|
2577
|
-
AND started_at < datetime('now', '-24 hours')
|
|
2578
|
-
`);
|
|
2579
|
-
this.stmtFindUnclassified = db.prepare(`
|
|
2580
|
-
SELECT * FROM thought_branches
|
|
2581
|
-
WHERE project_hash = ? AND branch_type = 'unknown'
|
|
2582
|
-
AND observation_count >= 3
|
|
2583
|
-
ORDER BY started_at DESC
|
|
2584
|
-
LIMIT ?
|
|
2585
|
-
`);
|
|
2586
|
-
this.stmtFindRecentCompleted = db.prepare(`
|
|
2587
|
-
SELECT * FROM thought_branches
|
|
2588
|
-
WHERE project_hash = ? AND status = 'completed' AND summary IS NULL
|
|
2589
|
-
AND ended_at > datetime('now', '-1 hour')
|
|
2590
|
-
ORDER BY ended_at DESC
|
|
2591
|
-
LIMIT ?
|
|
2592
|
-
`);
|
|
2593
|
-
this.stmtFindRecentActive = db.prepare(`
|
|
2594
|
-
SELECT * FROM thought_branches
|
|
2595
|
-
WHERE project_hash = ? AND status = 'active'
|
|
2596
|
-
AND started_at > datetime('now', '-24 hours')
|
|
2597
|
-
ORDER BY started_at DESC
|
|
2598
|
-
LIMIT 1
|
|
2599
|
-
`);
|
|
2600
|
-
this.stmtListRecent = db.prepare(`
|
|
2601
|
-
SELECT * FROM thought_branches
|
|
2602
|
-
WHERE project_hash = ?
|
|
2603
|
-
AND started_at > datetime('now', ? || ' hours')
|
|
2604
|
-
ORDER BY started_at DESC
|
|
2605
|
-
`);
|
|
2606
|
-
}
|
|
2607
|
-
createBranch(sessionId, triggerSource, triggerObservationId) {
|
|
2608
|
-
const id = randomBytes(16).toString("hex");
|
|
2609
|
-
this.stmtCreate.run(id, this.projectHash, sessionId, triggerSource, triggerObservationId ?? null);
|
|
2610
|
-
return this.getBranch(id);
|
|
2611
|
-
}
|
|
2612
|
-
completeBranch(branchId) {
|
|
2613
|
-
this.stmtComplete.run(branchId, this.projectHash);
|
|
2614
|
-
}
|
|
2615
|
-
abandonBranch(branchId) {
|
|
2616
|
-
this.stmtAbandon.run(branchId, this.projectHash);
|
|
2617
|
-
}
|
|
2618
|
-
getActiveBranch() {
|
|
2619
|
-
const row = this.stmtGetActive.get(this.projectHash);
|
|
2620
|
-
return row ? rowToBranch(row) : null;
|
|
2621
|
-
}
|
|
2622
|
-
getBranch(branchId) {
|
|
2623
|
-
const row = this.stmtGetById.get(branchId, this.projectHash);
|
|
2624
|
-
return row ? rowToBranch(row) : null;
|
|
2625
|
-
}
|
|
2626
|
-
listBranches(limit = 20) {
|
|
2627
|
-
return this.stmtList.all(this.projectHash, limit).map(rowToBranch);
|
|
2628
|
-
}
|
|
2629
|
-
listByStatus(status, limit = 20) {
|
|
2630
|
-
return this.stmtListByStatus.all(this.projectHash, status, limit).map(rowToBranch);
|
|
2631
|
-
}
|
|
2632
|
-
listByType(branchType, limit = 20) {
|
|
2633
|
-
return this.stmtListByType.all(this.projectHash, branchType, limit).map(rowToBranch);
|
|
2634
|
-
}
|
|
2635
|
-
updateArcStage(branchId, stage) {
|
|
2636
|
-
this.stmtUpdateArcStage.run(stage, branchId, this.projectHash);
|
|
2637
|
-
}
|
|
2638
|
-
updateToolPattern(branchId, pattern) {
|
|
2639
|
-
this.stmtUpdateToolPattern.run(JSON.stringify(pattern), branchId, this.projectHash);
|
|
2640
|
-
}
|
|
2641
|
-
updateClassification(branchId, branchType, title) {
|
|
2642
|
-
this.stmtUpdateClassification.run(branchType, title, branchId, this.projectHash);
|
|
2643
|
-
}
|
|
2644
|
-
updateSummary(branchId, summary) {
|
|
2645
|
-
this.stmtUpdateSummary.run(summary, branchId, this.projectHash);
|
|
2646
|
-
}
|
|
2647
|
-
linkDebugPath(branchId, debugPathId) {
|
|
2648
|
-
this.stmtLinkDebugPath.run(debugPathId, branchId, this.projectHash);
|
|
2649
|
-
}
|
|
2650
|
-
addObservation(branchId, observationId, toolName, arcStage) {
|
|
2651
|
-
const { max_seq } = this.stmtMaxSequence.get(branchId);
|
|
2652
|
-
this.stmtAddObservation.run(branchId, observationId, max_seq + 1, toolName, arcStage);
|
|
2653
|
-
this.stmtIncrementObsCount.run(branchId, this.projectHash);
|
|
2654
|
-
}
|
|
2655
|
-
getObservations(branchId) {
|
|
2656
|
-
return this.stmtGetObservations.all(branchId).map(rowToBranchObservation);
|
|
2657
|
-
}
|
|
2658
|
-
findStaleBranches() {
|
|
2659
|
-
return this.stmtFindStale.all(this.projectHash).map(rowToBranch);
|
|
2660
|
-
}
|
|
2661
|
-
findUnclassifiedBranches(limit = 5) {
|
|
2662
|
-
return this.stmtFindUnclassified.all(this.projectHash, limit).map(rowToBranch);
|
|
2663
|
-
}
|
|
2664
|
-
findRecentCompletedUnsummarized(limit = 3) {
|
|
2665
|
-
return this.stmtFindRecentCompleted.all(this.projectHash, limit).map(rowToBranch);
|
|
2666
|
-
}
|
|
2667
|
-
findRecentActiveBranch() {
|
|
2668
|
-
const row = this.stmtFindRecentActive.get(this.projectHash);
|
|
2669
|
-
return row ? rowToBranch(row) : null;
|
|
2670
|
-
}
|
|
2671
|
-
listRecentBranches(hours) {
|
|
2672
|
-
return this.stmtListRecent.all(this.projectHash, `-${hours}`).map(rowToBranch);
|
|
2673
|
-
}
|
|
2674
|
-
};
|
|
2675
|
-
function rowToBranch(row) {
|
|
2676
|
-
let toolPattern = {};
|
|
2677
|
-
try {
|
|
2678
|
-
toolPattern = JSON.parse(row.tool_pattern);
|
|
2679
|
-
} catch {}
|
|
2680
|
-
return {
|
|
2681
|
-
id: row.id,
|
|
2682
|
-
project_hash: row.project_hash,
|
|
2683
|
-
session_id: row.session_id,
|
|
2684
|
-
status: row.status,
|
|
2685
|
-
branch_type: row.branch_type,
|
|
2686
|
-
arc_stage: row.arc_stage,
|
|
2687
|
-
title: row.title,
|
|
2688
|
-
summary: row.summary,
|
|
2689
|
-
parent_branch_id: row.parent_branch_id,
|
|
2690
|
-
linked_debug_path_id: row.linked_debug_path_id,
|
|
2691
|
-
trigger_source: row.trigger_source,
|
|
2692
|
-
trigger_observation_id: row.trigger_observation_id,
|
|
2693
|
-
observation_count: row.observation_count,
|
|
2694
|
-
tool_pattern: toolPattern,
|
|
2695
|
-
started_at: row.started_at,
|
|
2696
|
-
ended_at: row.ended_at,
|
|
2697
|
-
created_at: row.created_at
|
|
2698
|
-
};
|
|
2699
|
-
}
|
|
2700
|
-
function rowToBranchObservation(row) {
|
|
2701
|
-
return {
|
|
2702
|
-
branch_id: row.branch_id,
|
|
2703
|
-
observation_id: row.observation_id,
|
|
2704
|
-
sequence_order: row.sequence_order,
|
|
2705
|
-
tool_name: row.tool_name,
|
|
2706
|
-
arc_stage_at_add: row.arc_stage_at_add,
|
|
2707
|
-
created_at: row.created_at
|
|
2708
|
-
};
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
1793
|
//#endregion
|
|
2712
1794
|
//#region src/storage/research-buffer.ts
|
|
2713
1795
|
/**
|
|
@@ -3091,12 +2173,11 @@ var ToolRegistryRepository = class {
|
|
|
3091
2173
|
this.db = db;
|
|
3092
2174
|
try {
|
|
3093
2175
|
this.stmtUpsert = db.prepare(`
|
|
3094
|
-
INSERT INTO tool_registry (name, tool_type, scope, source, project_hash, description, server_name,
|
|
3095
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
2176
|
+
INSERT INTO tool_registry (name, tool_type, scope, source, project_hash, description, server_name, discovered_at)
|
|
2177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
3096
2178
|
ON CONFLICT (name, COALESCE(project_hash, ''))
|
|
3097
2179
|
DO UPDATE SET
|
|
3098
2180
|
description = COALESCE(excluded.description, tool_registry.description),
|
|
3099
|
-
trigger_hints = COALESCE(excluded.trigger_hints, tool_registry.trigger_hints),
|
|
3100
2181
|
source = excluded.source,
|
|
3101
2182
|
status = 'active',
|
|
3102
2183
|
updated_at = datetime('now')
|
|
@@ -3227,7 +2308,7 @@ var ToolRegistryRepository = class {
|
|
|
3227
2308
|
*/
|
|
3228
2309
|
upsert(tool) {
|
|
3229
2310
|
try {
|
|
3230
|
-
this.stmtUpsert.run(tool.name, tool.toolType, tool.scope, tool.source, tool.projectHash, tool.description, tool.serverName
|
|
2311
|
+
this.stmtUpsert.run(tool.name, tool.toolType, tool.scope, tool.source, tool.projectHash, tool.description, tool.serverName);
|
|
3231
2312
|
debug("tool-registry", "Upserted tool", {
|
|
3232
2313
|
name: tool.name,
|
|
3233
2314
|
scope: tool.scope
|
|
@@ -3570,5 +2651,5 @@ var ToolRegistryRepository = class {
|
|
|
3570
2651
|
};
|
|
3571
2652
|
|
|
3572
2653
|
//#endregion
|
|
3573
|
-
export {
|
|
3574
|
-
//# sourceMappingURL=tool-registry-
|
|
2654
|
+
export { rowToObservation as C, debug as D, runMigrations as E, debugTimed as O, ObservationRepository as S, MIGRATIONS as T, SaveGuard as _, ResearchBufferRepository as a, SearchEngine as b, inferToolType as c, getNodeByNameAndType as d, getNodesByType as f, upsertNode as g, traverseFrom as h, NotificationStore as i, countEdgesForNode as l, insertEdge as m, PathRepository as n, extractServerName as o, initGraphSchema as p, initPathSchema as r, inferScope as s, ToolRegistryRepository as t, getEdgesForNode as u, jaccardSimilarity as v, openDatabase as w, SessionRepository as x, hybridSearch as y };
|
|
2655
|
+
//# sourceMappingURL=tool-registry-CZ3mJ4iR.mjs.map
|