vericify 0.1.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.
- package/README.md +389 -0
- package/package.json +57 -0
- package/src/adapters/index.js +37 -0
- package/src/adapters/local-state.js +86 -0
- package/src/adapters/registry.js +126 -0
- package/src/api.js +12 -0
- package/src/checkpoints/policy.js +96 -0
- package/src/compare/engine.js +220 -0
- package/src/core/fs.js +86 -0
- package/src/core/util.js +59 -0
- package/src/index.js +464 -0
- package/src/post/process-posts.js +72 -0
- package/src/projection/runs.js +809 -0
- package/src/publish/artifact.js +91 -0
- package/src/similarity/semantic-hash.js +95 -0
- package/src/store/adapter-attachments.js +47 -0
- package/src/store/common.js +38 -0
- package/src/store/handoffs.js +64 -0
- package/src/store/paths.js +40 -0
- package/src/store/run-ledger.js +46 -0
- package/src/store/status-events.js +39 -0
- package/src/store/todo-state.js +49 -0
- package/src/sync/outbox.js +29 -0
- package/src/tui/app.js +571 -0
- package/src/tui/commands.js +224 -0
- package/src/tui/panels.js +440 -0
- package/src/tui/runtime-activity.js +172 -0
package/src/api.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_ADAPTER,
|
|
3
|
+
attachAdapter,
|
|
4
|
+
detectAdapters,
|
|
5
|
+
listAvailableAdapters,
|
|
6
|
+
listDefaultWorkspacePaths,
|
|
7
|
+
loadWorkspaceState,
|
|
8
|
+
} from "./adapters/index.js";
|
|
9
|
+
export { buildRunComparison, findRunById } from "./compare/engine.js";
|
|
10
|
+
export { projectWorkspaceState } from "./projection/runs.js";
|
|
11
|
+
export { publishRunArtifact } from "./publish/artifact.js";
|
|
12
|
+
export { enqueueSyncOutboxItem } from "./sync/outbox.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const CHECKPOINT_POLICY_VERSION = "1.0.0";
|
|
2
|
+
|
|
3
|
+
const CHECKPOINT_TRIGGER_RULES = [
|
|
4
|
+
{ trigger_kind: "handoff", source: "handoff_history", capture_mode: "semantic" },
|
|
5
|
+
{ trigger_kind: "status_transition", source: "status_event", capture_mode: "semantic" },
|
|
6
|
+
{ trigger_kind: "process_milestone", source: "process_post", capture_mode: "semantic" },
|
|
7
|
+
{ trigger_kind: "ledger_update", source: "run_ledger", capture_mode: "semantic" },
|
|
8
|
+
{ trigger_kind: "operator_save", source: "workspace_rollup", capture_mode: "semantic" },
|
|
9
|
+
{ trigger_kind: "branch_fork", source: "explicit_branch_signal", capture_mode: "hybrid" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function checkpointMeta(triggerKind, triggerReason, captureMode = "semantic") {
|
|
13
|
+
return {
|
|
14
|
+
checkpoint_policy_version: CHECKPOINT_POLICY_VERSION,
|
|
15
|
+
trigger_kind: triggerKind,
|
|
16
|
+
trigger_reason: triggerReason,
|
|
17
|
+
capture_mode: captureMode,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function captureModeForRecord(record) {
|
|
22
|
+
return record?.git_commit_sha || record?.payload?.git_commit_sha || record?.metadata?.git_commit_sha ? "hybrid" : "semantic";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listCheckpointTriggerRules() {
|
|
26
|
+
return [...CHECKPOINT_TRIGGER_RULES];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function checkpointTriggerFromHandoff(handoff, entry) {
|
|
30
|
+
const status = entry?.status ?? handoff?.status ?? "open";
|
|
31
|
+
return checkpointMeta(
|
|
32
|
+
"handoff",
|
|
33
|
+
`Handoff ${status} between ${handoff?.from ?? "unknown"} and ${handoff?.to ?? "unknown"}.`,
|
|
34
|
+
captureModeForRecord(entry)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkpointTriggerFromWorkspaceSummary() {
|
|
39
|
+
return checkpointMeta(
|
|
40
|
+
"operator_save",
|
|
41
|
+
"Workspace rollup checkpoint summarizing current task, event, ledger, and process-post state."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkpointTriggerFromStatusEvent(event, previousEvent) {
|
|
46
|
+
const branchFork = Boolean(event?.payload?.branch_id) && previousEvent?.payload?.branch_id !== event?.payload?.branch_id;
|
|
47
|
+
if (branchFork) {
|
|
48
|
+
return checkpointMeta(
|
|
49
|
+
"branch_fork",
|
|
50
|
+
`Status event ${event?.event_type ?? "unknown"} moved execution onto branch ${event?.payload?.branch_id}.`,
|
|
51
|
+
captureModeForRecord(event)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return checkpointMeta(
|
|
55
|
+
"status_transition",
|
|
56
|
+
`Status event ${event?.event_type ?? "unknown"} recorded ${event?.status ?? "planned"}.`,
|
|
57
|
+
captureModeForRecord(event)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function checkpointTriggerFromProcessPost(post) {
|
|
62
|
+
if (post?.branch_id) {
|
|
63
|
+
return checkpointMeta(
|
|
64
|
+
"branch_fork",
|
|
65
|
+
`Process post ${post?.kind ?? "progress"} referenced branch ${post.branch_id}.`,
|
|
66
|
+
captureModeForRecord(post)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (post?.checkpoint_ref) {
|
|
70
|
+
return checkpointMeta(
|
|
71
|
+
"operator_save",
|
|
72
|
+
`Process post ${post?.kind ?? "progress"} pinned checkpoint ${post.checkpoint_ref}.`,
|
|
73
|
+
captureModeForRecord(post)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (post?.kind === "handoff_note") {
|
|
77
|
+
return checkpointMeta(
|
|
78
|
+
"handoff",
|
|
79
|
+
`Process post recorded a handoff note from ${post?.agent_id ?? "unknown"}.`,
|
|
80
|
+
captureModeForRecord(post)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return checkpointMeta(
|
|
84
|
+
"process_milestone",
|
|
85
|
+
`Process post ${post?.kind ?? "progress"} from ${post?.agent_id ?? "unknown"}.`,
|
|
86
|
+
captureModeForRecord(post)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function checkpointTriggerFromLedgerEntry(entry) {
|
|
91
|
+
return checkpointMeta(
|
|
92
|
+
entry?.category === "regression" ? "status_transition" : "ledger_update",
|
|
93
|
+
`Run-ledger ${entry?.category ?? "update"} from ${entry?.tool ?? "unknown"}.`,
|
|
94
|
+
captureModeForRecord(entry)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { calculateJaccard, similarityFromText } from "../similarity/semantic-hash.js";
|
|
2
|
+
import { isoNow, unique } from "../core/util.js";
|
|
3
|
+
|
|
4
|
+
function latestCheckpoint(run) {
|
|
5
|
+
return run?.recent_checkpoints?.at(-1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function latestDelta(run) {
|
|
9
|
+
return run?.deltas?.at(-1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function asSet(values) {
|
|
13
|
+
return new Set((values ?? []).filter(Boolean));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function overlap(leftValues, rightValues) {
|
|
17
|
+
const left = asSet(leftValues);
|
|
18
|
+
const right = asSet(rightValues);
|
|
19
|
+
const shared = [...left].filter((value) => right.has(value));
|
|
20
|
+
const leftOnly = [...left].filter((value) => !right.has(value));
|
|
21
|
+
const rightOnly = [...right].filter((value) => !left.has(value));
|
|
22
|
+
return {
|
|
23
|
+
score: calculateJaccard(left, right),
|
|
24
|
+
shared,
|
|
25
|
+
left_only: leftOnly,
|
|
26
|
+
right_only: rightOnly,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function activeNodeLabels(run) {
|
|
31
|
+
return unique((run?.nodes ?? []).filter((node) => node.status !== "done").map((node) => node.title));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function actorIds(run) {
|
|
35
|
+
return unique((run?.lanes ?? []).map((lane) => lane.agent_id));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function layerIds(run) {
|
|
39
|
+
return unique(latestDelta(run)?.layers ?? []);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkpointSummary(run) {
|
|
43
|
+
return latestCheckpoint(run)?.process_summary ?? "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function lineSummary(run) {
|
|
47
|
+
return checkpointSummary(run).split("\n")[0] ?? "-";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scoreRunSimilarity(metrics) {
|
|
51
|
+
return Math.max(
|
|
52
|
+
0,
|
|
53
|
+
Math.min(
|
|
54
|
+
1,
|
|
55
|
+
metrics.checkpoint_similarity * 0.5 +
|
|
56
|
+
metrics.actor_overlap * 0.2 +
|
|
57
|
+
metrics.node_overlap * 0.15 +
|
|
58
|
+
metrics.layer_overlap * 0.15
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function layerGrid(leftRun, rightRun) {
|
|
64
|
+
const layers = unique([...layerIds(leftRun), ...layerIds(rightRun)]);
|
|
65
|
+
return layers.map((layer) => ({
|
|
66
|
+
layer,
|
|
67
|
+
left_present: layerIds(leftRun).includes(layer),
|
|
68
|
+
right_present: layerIds(rightRun).includes(layer),
|
|
69
|
+
status: layerIds(leftRun).includes(layer) && layerIds(rightRun).includes(layer)
|
|
70
|
+
? "shared"
|
|
71
|
+
: layerIds(leftRun).includes(layer)
|
|
72
|
+
? "left_only"
|
|
73
|
+
: "right_only",
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildSimilarityExplanations(metrics, leftRun, rightRun) {
|
|
78
|
+
const rows = [];
|
|
79
|
+
if (metrics.checkpoint_similarity >= 0.35) {
|
|
80
|
+
rows.push(`Latest checkpoints read similarly at ${(metrics.checkpoint_similarity * 100).toFixed(0)}%.`);
|
|
81
|
+
}
|
|
82
|
+
if (metrics.actor.shared.length) {
|
|
83
|
+
rows.push(`Shared actors: ${metrics.actor.shared.join(", ")}.`);
|
|
84
|
+
}
|
|
85
|
+
if (metrics.nodes.shared.length) {
|
|
86
|
+
rows.push(`Shared active nodes: ${metrics.nodes.shared.join(", ")}.`);
|
|
87
|
+
}
|
|
88
|
+
if (metrics.layers.shared.length) {
|
|
89
|
+
rows.push(`Shared delta layers: ${metrics.layers.shared.join(", ")}.`);
|
|
90
|
+
}
|
|
91
|
+
if (!rows.length) {
|
|
92
|
+
rows.push(`Runs ${leftRun.run.run_id} and ${rightRun.run.run_id} share little surface area.`);
|
|
93
|
+
}
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildDivergenceExplanations(metrics, leftRun, rightRun) {
|
|
98
|
+
const rows = [];
|
|
99
|
+
if (leftRun.run.status !== rightRun.run.status) {
|
|
100
|
+
rows.push(`Run status diverged: ${leftRun.run.status} vs ${rightRun.run.status}.`);
|
|
101
|
+
}
|
|
102
|
+
if ((leftRun.branches?.length ?? 0) !== (rightRun.branches?.length ?? 0)) {
|
|
103
|
+
rows.push(`Branch topology diverged: ${(leftRun.branches?.length ?? 0)} vs ${(rightRun.branches?.length ?? 0)} branches.`);
|
|
104
|
+
}
|
|
105
|
+
if ((leftRun.lanes?.length ?? 0) !== (rightRun.lanes?.length ?? 0)) {
|
|
106
|
+
rows.push(`Lane load diverged: ${(leftRun.lanes?.length ?? 0)} vs ${(rightRun.lanes?.length ?? 0)} lanes.`);
|
|
107
|
+
}
|
|
108
|
+
if (metrics.nodes.left_only.length || metrics.nodes.right_only.length) {
|
|
109
|
+
rows.push(
|
|
110
|
+
`Active node focus differs: left-only ${metrics.nodes.left_only.join(", ") || "none"} | right-only ${metrics.nodes.right_only.join(", ") || "none"}.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (metrics.checkpoint_similarity < 0.35) {
|
|
114
|
+
rows.push(`Latest checkpoint narratives are materially different at only ${(metrics.checkpoint_similarity * 100).toFixed(0)}% similarity.`);
|
|
115
|
+
}
|
|
116
|
+
if (!rows.length) rows.push("Primary run shape is aligned.");
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildRecoveryExplanations(metrics, leftRun, rightRun) {
|
|
121
|
+
if (rightRun.run.status === "completed") {
|
|
122
|
+
return [
|
|
123
|
+
`Use ${rightRun.run.run_id} as a recovery template because it completed with ${(metrics.similarity_score * 100).toFixed(0)}% composite similarity.`,
|
|
124
|
+
`Replay from checkpoint: ${lineSummary(rightRun)}.`,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
if (rightRun.run.status === "blocked") {
|
|
128
|
+
return [
|
|
129
|
+
`Treat ${rightRun.run.run_id} as a failure echo and intervene before matching its blocked shape.`,
|
|
130
|
+
`Watch for: ${lineSummary(rightRun)}.`,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
return [
|
|
134
|
+
`Peer run ${rightRun.run.run_id} is still active; use it as a live comparison, not a template.`,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildRecommendedActions(metrics, leftRun, rightRun) {
|
|
139
|
+
const actions = [];
|
|
140
|
+
if (rightRun.run.status === "completed" && metrics.similarity_score >= 0.3) {
|
|
141
|
+
actions.push(`Compare the latest completed checkpoint from ${rightRun.run.run_id} against ${leftRun.run.run_id}.`);
|
|
142
|
+
}
|
|
143
|
+
if (rightRun.run.status === "blocked" && metrics.similarity_score >= 0.3) {
|
|
144
|
+
actions.push(`Review the divergence that precedes ${rightRun.run.run_id}'s blocked checkpoint.`);
|
|
145
|
+
}
|
|
146
|
+
if ((leftRun.lanes?.length ?? 0) < (rightRun.lanes?.length ?? 0)) {
|
|
147
|
+
actions.push(`Investigate whether ${leftRun.run.run_id} is under-provisioned relative to ${rightRun.run.run_id}.`);
|
|
148
|
+
}
|
|
149
|
+
if (metrics.nodes.left_only.length) {
|
|
150
|
+
actions.push(`Validate whether left-only active nodes are expected: ${metrics.nodes.left_only.join(", ")}.`);
|
|
151
|
+
}
|
|
152
|
+
if (!actions.length) actions.push("No urgent operator action. Monitor the latest checkpoint pulse.");
|
|
153
|
+
return actions;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildRunComparison(leftRun, rightRun) {
|
|
157
|
+
if (!leftRun || !rightRun) {
|
|
158
|
+
throw new Error("Run comparison requires two run details.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const actor = overlap(actorIds(leftRun), actorIds(rightRun));
|
|
162
|
+
const nodes = overlap(activeNodeLabels(leftRun), activeNodeLabels(rightRun));
|
|
163
|
+
const layers = overlap(layerIds(leftRun), layerIds(rightRun));
|
|
164
|
+
const checkpointSimilarity = similarityFromText(checkpointSummary(leftRun), checkpointSummary(rightRun));
|
|
165
|
+
const metrics = {
|
|
166
|
+
checkpoint_similarity: checkpointSimilarity,
|
|
167
|
+
actor_overlap: actor.score,
|
|
168
|
+
node_overlap: nodes.score,
|
|
169
|
+
layer_overlap: layers.score,
|
|
170
|
+
branch_delta: Math.abs((leftRun.branches?.length ?? 0) - (rightRun.branches?.length ?? 0)),
|
|
171
|
+
lane_delta: Math.abs((leftRun.lanes?.length ?? 0) - (rightRun.lanes?.length ?? 0)),
|
|
172
|
+
actor,
|
|
173
|
+
nodes,
|
|
174
|
+
layers,
|
|
175
|
+
};
|
|
176
|
+
metrics.similarity_score = scoreRunSimilarity(metrics);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
schema_version: "1.0.0",
|
|
180
|
+
compared_at: isoNow(),
|
|
181
|
+
left_run_id: leftRun.run.run_id,
|
|
182
|
+
right_run_id: rightRun.run.run_id,
|
|
183
|
+
similarity_score: metrics.similarity_score,
|
|
184
|
+
checkpoint_similarity: metrics.checkpoint_similarity,
|
|
185
|
+
actor_overlap: metrics.actor_overlap,
|
|
186
|
+
node_overlap: metrics.node_overlap,
|
|
187
|
+
layer_overlap: metrics.layer_overlap,
|
|
188
|
+
layer_grid: layerGrid(leftRun, rightRun),
|
|
189
|
+
shared_actors: metrics.actor.shared,
|
|
190
|
+
shared_nodes: metrics.nodes.shared,
|
|
191
|
+
shared_layers: metrics.layers.shared,
|
|
192
|
+
divergence_explanations: buildDivergenceExplanations(metrics, leftRun, rightRun),
|
|
193
|
+
similarity_explanations: buildSimilarityExplanations(metrics, leftRun, rightRun),
|
|
194
|
+
recovery_explanations: buildRecoveryExplanations(metrics, leftRun, rightRun),
|
|
195
|
+
recommended_actions: buildRecommendedActions(metrics, leftRun, rightRun),
|
|
196
|
+
left: {
|
|
197
|
+
run_id: leftRun.run.run_id,
|
|
198
|
+
title: leftRun.run.title,
|
|
199
|
+
status: leftRun.run.status,
|
|
200
|
+
branch_count: leftRun.branches?.length ?? 0,
|
|
201
|
+
lane_count: leftRun.lanes?.length ?? 0,
|
|
202
|
+
latest_checkpoint_id: latestCheckpoint(leftRun)?.checkpoint_id,
|
|
203
|
+
latest_checkpoint_summary: lineSummary(leftRun),
|
|
204
|
+
},
|
|
205
|
+
right: {
|
|
206
|
+
run_id: rightRun.run.run_id,
|
|
207
|
+
title: rightRun.run.title,
|
|
208
|
+
status: rightRun.run.status,
|
|
209
|
+
branch_count: rightRun.branches?.length ?? 0,
|
|
210
|
+
lane_count: rightRun.lanes?.length ?? 0,
|
|
211
|
+
latest_checkpoint_id: latestCheckpoint(rightRun)?.checkpoint_id,
|
|
212
|
+
latest_checkpoint_summary: lineSummary(rightRun),
|
|
213
|
+
},
|
|
214
|
+
summary: `${leftRun.run.run_id} vs ${rightRun.run.run_id} | ${(metrics.similarity_score * 100).toFixed(0)}% composite similarity`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function findRunById(projected, runId) {
|
|
219
|
+
return (projected?.runs ?? []).find((run) => run.run.run_id === runId);
|
|
220
|
+
}
|
package/src/core/fs.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export function readText(path, fallback = "") {
|
|
5
|
+
try {
|
|
6
|
+
return existsSync(path) ? readFileSync(path, "utf8") : fallback;
|
|
7
|
+
} catch {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readJson(path, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(path)) return fallback;
|
|
15
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readNdjson(path) {
|
|
22
|
+
const raw = readText(path, "");
|
|
23
|
+
if (!raw.trim()) return [];
|
|
24
|
+
return raw
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.flatMap((line) => {
|
|
29
|
+
try {
|
|
30
|
+
return [JSON.parse(line)];
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeJson(path, value) {
|
|
38
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
39
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDebouncedWatch(paths, onChange, debounceMs = 120) {
|
|
43
|
+
const timers = new Map();
|
|
44
|
+
const watchers = [];
|
|
45
|
+
const watchRoots = new Set();
|
|
46
|
+
for (const path of paths) {
|
|
47
|
+
if (existsSync(path)) {
|
|
48
|
+
try {
|
|
49
|
+
watchRoots.add(statSync(path).isDirectory() ? path : path);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore unstable paths
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(path);
|
|
55
|
+
if (existsSync(parent)) watchRoots.add(parent);
|
|
56
|
+
}
|
|
57
|
+
for (const path of watchRoots) {
|
|
58
|
+
try {
|
|
59
|
+
const watcher = watch(path, () => {
|
|
60
|
+
const existing = timers.get(path);
|
|
61
|
+
if (existing) clearTimeout(existing);
|
|
62
|
+
timers.set(
|
|
63
|
+
path,
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
timers.delete(path);
|
|
66
|
+
onChange(path);
|
|
67
|
+
}, debounceMs)
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
watchers.push(watcher);
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore unwatchable paths
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return () => {
|
|
76
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
77
|
+
timers.clear();
|
|
78
|
+
for (const watcher of watchers) {
|
|
79
|
+
try {
|
|
80
|
+
watcher.close();
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
package/src/core/util.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function slugify(value) {
|
|
5
|
+
return value
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
9
|
+
.replace(/^-+|-+$/g, "")
|
|
10
|
+
.slice(0, 48);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hashText(value) {
|
|
14
|
+
return createHash("sha256").update(value).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isoNow() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function workspaceRef(rootPath) {
|
|
22
|
+
return {
|
|
23
|
+
workspace_id: slugify(basename(rootPath) || "workspace"),
|
|
24
|
+
root_path: rootPath,
|
|
25
|
+
label: basename(rootPath) || rootPath,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function unique(values) {
|
|
30
|
+
return [...new Set(values.filter(Boolean))];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function asArray(value) {
|
|
34
|
+
return Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function firstDefined(...values) {
|
|
38
|
+
return values.find((value) => value !== undefined && value !== null && value !== "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatTimestamp(value) {
|
|
42
|
+
if (!value) return "-";
|
|
43
|
+
const date = new Date(value);
|
|
44
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
45
|
+
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function relativeTimeFromNow(value) {
|
|
49
|
+
if (!value) return "-";
|
|
50
|
+
const date = new Date(value);
|
|
51
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
52
|
+
const delta = date.getTime() - Date.now();
|
|
53
|
+
const abs = Math.abs(delta);
|
|
54
|
+
const minutes = Math.round(abs / 60000);
|
|
55
|
+
if (minutes < 1) return "now";
|
|
56
|
+
if (minutes < 60) return `${delta < 0 ? "" : "+"}${minutes}m`;
|
|
57
|
+
const hours = Math.round(minutes / 60);
|
|
58
|
+
return `${delta < 0 ? "" : "+"}${hours}h`;
|
|
59
|
+
}
|