bingocode 1.1.182 → 1.1.184
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/scheduled_tasks.json +34 -0
- package/.claude/skills/leanchy/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/bootstrap/state.ts +61 -13
- package/src/hooks/useGoalAutoUpdater.ts +187 -0
- package/src/hooks/useGoalEvaluator.ts +29 -16
- package/src/query.ts +21 -0
- package/src/skills/bundled/goal.ts +41 -32
- package/src/types/goal.ts +322 -0
- package/src/utils/__tests__/goalDag.test.ts +177 -0
- package/src/utils/__tests__/goalDrift.test.ts +132 -0
- package/src/utils/__tests__/goalIntegration.test.ts +217 -0
- package/src/utils/__tests__/goalStore.test.ts +592 -0
- package/src/utils/goalDag.ts +408 -0
- package/src/utils/goalDrift.ts +243 -0
- package/src/utils/goalEvaluator.ts +708 -122
- package/src/utils/goalHelpers.ts +155 -0
- package/src/utils/goalPersistence.ts +206 -0
- package/src/utils/goalStore.ts +463 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal system types — 4-layer goal hierarchy for task-oriented agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1: User Goal (user's raw intent, immutable)
|
|
5
|
+
* Layer 2: Operational Goal (agent's refined execution contract)
|
|
6
|
+
* Layer 3: Sub Goals (DAG-structured task breakdown)
|
|
7
|
+
* Layer 4: Immediate Goal (current action target, runtime only)
|
|
8
|
+
*
|
|
9
|
+
* Plus: evaluation context for environment-aware assessment,
|
|
10
|
+
* drift detection for goal alignment monitoring.
|
|
11
|
+
*
|
|
12
|
+
* All types are engine-agnostic — they define the shape of the data,
|
|
13
|
+
* not any specific execution model.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { UUID } from 'crypto'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Branded IDs
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/** Branded type for goal identifiers. */
|
|
23
|
+
export type GoalId = string & { readonly __brand: 'GoalId' }
|
|
24
|
+
|
|
25
|
+
/** Cast a raw string to GoalId. Prefer createGoalId() when possible. */
|
|
26
|
+
export function asGoalId(id: string): GoalId {
|
|
27
|
+
return id as GoalId
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Goal Hierarchy Types
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
/** User's raw intent — the source of truth. Never modified after creation. */
|
|
35
|
+
export interface UserGoal {
|
|
36
|
+
readonly id: GoalId
|
|
37
|
+
readonly text: string
|
|
38
|
+
readonly createdAt: number
|
|
39
|
+
readonly updatedAt: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Agent-refined execution contract. The working agreement between user intent
|
|
43
|
+
* and agent behavior. Derived from UserGoal by the planning layer. */
|
|
44
|
+
export interface OperationalGoal {
|
|
45
|
+
readonly id: GoalId
|
|
46
|
+
readonly text: string
|
|
47
|
+
readonly derivedFrom: GoalId // references UserGoal.id
|
|
48
|
+
readonly constraints: readonly string[]
|
|
49
|
+
readonly successCriteria: readonly string[]
|
|
50
|
+
readonly createdAt: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** SubGoal status — the state machine for individual DAG nodes. */
|
|
54
|
+
export type SubGoalStatus =
|
|
55
|
+
| 'pending'
|
|
56
|
+
| 'active'
|
|
57
|
+
| 'completed'
|
|
58
|
+
| 'failed'
|
|
59
|
+
| 'blocked'
|
|
60
|
+
| 'skipped'
|
|
61
|
+
|
|
62
|
+
/** A single node in the goal DAG. Represents one actionable unit of work.
|
|
63
|
+
* Dependencies define the partial order — a node is only ready when all
|
|
64
|
+
* its upstream dependencies are completed. */
|
|
65
|
+
export interface SubGoal {
|
|
66
|
+
readonly id: string // DAG node ID (not branded — short human-readable key)
|
|
67
|
+
readonly parentId: GoalId // back-reference to owning OperationalGoal
|
|
68
|
+
text: string // mutable — agent may refine description
|
|
69
|
+
status: SubGoalStatus
|
|
70
|
+
readonly dependencies: readonly string[] // upstream subgoal IDs
|
|
71
|
+
assignedPlanId?: string // optional link to a Plan slug
|
|
72
|
+
progress: number // 0-100
|
|
73
|
+
readonly createdAt: number
|
|
74
|
+
updatedAt: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Current action target — runtime only, not persisted. Declared each turn
|
|
78
|
+
* by the agent to signal what it's working on right now. */
|
|
79
|
+
export interface ImmediateGoal {
|
|
80
|
+
readonly subGoalId: string // which SubGoal this action serves
|
|
81
|
+
readonly action: string // concrete description of current step
|
|
82
|
+
readonly declaredAt: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// DAG Types
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/** In-memory DAG representation. Nodes map and edge list for fast traversal.
|
|
90
|
+
* readyQueue is computed on-demand via topological sort. */
|
|
91
|
+
export interface GoalDag {
|
|
92
|
+
nodes: Map<string, SubGoal>
|
|
93
|
+
edges: ReadonlyArray<[string, string]> // [from, to] dependency pairs
|
|
94
|
+
readyQueue: string[] // cached topo-sorted ready node IDs
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Operations that mutate the DAG structure. Each operation triggers a
|
|
98
|
+
* re-computation of the ready queue and topological order. */
|
|
99
|
+
export type DagOperation =
|
|
100
|
+
| { type: 'add'; node: SubGoal; after?: string }
|
|
101
|
+
| { type: 'remove'; nodeId: string }
|
|
102
|
+
| { type: 'merge'; sourceIds: string[]; target: SubGoal }
|
|
103
|
+
| { type: 'reorder'; nodeId: string; newDeps: string[] }
|
|
104
|
+
| { type: 'updateStatus'; nodeId: string; status: SubGoalStatus }
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Goal State — the full in-memory representation
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/** Complete goal state. Persisted to .claude/goals/<id>.json.
|
|
111
|
+
* This is the single source of truth (SSOT) for goal tracking. */
|
|
112
|
+
export interface GoalState {
|
|
113
|
+
activeGoalId: GoalId | null
|
|
114
|
+
userGoal: UserGoal | null
|
|
115
|
+
operationalGoal: OperationalGoal | null
|
|
116
|
+
dag: GoalDag
|
|
117
|
+
immediateGoal: ImmediateGoal | null
|
|
118
|
+
progress: number // overall completion percentage 0-100
|
|
119
|
+
artifacts: string[] // file paths produced during execution
|
|
120
|
+
metrics: GoalMetrics
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Metrics tracked across the goal lifecycle. Separated from the main data
|
|
124
|
+
* so they can be serialized independently for analytics. */
|
|
125
|
+
export interface GoalMetrics {
|
|
126
|
+
totalSubGoals: number
|
|
127
|
+
completedSubGoals: number
|
|
128
|
+
iterationCount: number
|
|
129
|
+
maxIterations: number
|
|
130
|
+
evalHistory: GoalEvalHistory
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Drift and staleness tracking. Used to detect when the evaluator keeps
|
|
134
|
+
* reporting the same gap — triggers circuit breaker after 3 repeats. */
|
|
135
|
+
export interface GoalEvalHistory {
|
|
136
|
+
lastGap: string | null
|
|
137
|
+
consecutiveSameGapCount: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Evaluation Context — what the evaluator can observe
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/** Snapshot of agent execution environment. Gathered after each action
|
|
145
|
+
* to feed the evaluator's rule engine and semantic checks. */
|
|
146
|
+
export interface EvaluationContext {
|
|
147
|
+
toolResults: ToolResult[]
|
|
148
|
+
fileDiffs: FileDiff[]
|
|
149
|
+
testOutputs: TestOutput[]
|
|
150
|
+
compileOutputs: CompileOutput[]
|
|
151
|
+
gitStatus: GitStatus
|
|
152
|
+
fileList: string[]
|
|
153
|
+
errorLogs: ErrorLog[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A single tool invocation result. Extracted from the conversation transcript
|
|
157
|
+
* or post-tool-use hook output. */
|
|
158
|
+
export interface ToolResult {
|
|
159
|
+
toolName: string
|
|
160
|
+
toolInput: Record<string, unknown>
|
|
161
|
+
toolOutput: string // raw output text
|
|
162
|
+
success: boolean
|
|
163
|
+
timestamp: number
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** File change detected during or after a tool execution. Minimal
|
|
167
|
+
* representation — just path + change type + line count. */
|
|
168
|
+
export interface FileDiff {
|
|
169
|
+
path: string
|
|
170
|
+
added: number
|
|
171
|
+
removed: number
|
|
172
|
+
changed: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Test execution output. Parsed from tool results where the tool is a test
|
|
176
|
+
* runner (e.g., `npm test`, `bun test`, `pytest`). */
|
|
177
|
+
export interface TestOutput {
|
|
178
|
+
command: string
|
|
179
|
+
exitCode: number
|
|
180
|
+
stdout: string
|
|
181
|
+
stderr: string
|
|
182
|
+
passed: boolean
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Compilation / build output. Parsed from tool results where the tool is a
|
|
186
|
+
* compiler or build system (e.g., `tsc`, `bun build`, `cmake`). */
|
|
187
|
+
export interface CompileOutput {
|
|
188
|
+
command: string
|
|
189
|
+
exitCode: number
|
|
190
|
+
stdout: string
|
|
191
|
+
stderr: string
|
|
192
|
+
passed: boolean
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Git working tree status snapshot. */
|
|
196
|
+
export interface GitStatus {
|
|
197
|
+
staged: string[]
|
|
198
|
+
unstaged: string[]
|
|
199
|
+
untracked: string[]
|
|
200
|
+
branch: string
|
|
201
|
+
clean: boolean
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Recent error log entries from the in-memory ring buffer. */
|
|
205
|
+
export interface ErrorLog {
|
|
206
|
+
error: string
|
|
207
|
+
timestamp: string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Evaluation Result — output of the evaluation pipeline
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
/** Result from the evaluator after checking goal progress. Satisfied if the
|
|
215
|
+
* current state meets the goal condition; otherwise carries gap info for
|
|
216
|
+
* the next iteration. */
|
|
217
|
+
export interface EvalResult {
|
|
218
|
+
satisfied: boolean
|
|
219
|
+
level: EvalLevel
|
|
220
|
+
reason: string
|
|
221
|
+
gap: string | null
|
|
222
|
+
evidence: EvalEvidence[]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Evaluation depth — which tier produced this result. */
|
|
226
|
+
export type EvalLevel = 'rule' | 'semantic' | 'final'
|
|
227
|
+
|
|
228
|
+
/** One piece of evidence gathered during evaluation. Links a finding to its
|
|
229
|
+
* source with a relevance score for downstream weighting. */
|
|
230
|
+
export interface EvalEvidence {
|
|
231
|
+
source: string // file path, tool name, or context key
|
|
232
|
+
finding: string // what was observed
|
|
233
|
+
relevanceScore: number // how relevant to the goal (0-1)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Rule Engine — pluggable deterministic checks
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/** A rule is a pure function: given context and a target sub-goal, produce
|
|
241
|
+
* evidence. Rules are composable — the engine runs all registered rules and
|
|
242
|
+
* aggregates their output. */
|
|
243
|
+
export type RuleCheck = (ctx: EvaluationContext, goal: SubGoal) => EvalEvidence[]
|
|
244
|
+
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// Drift Detection — alignment monitoring
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
/** Drift assessment result. Computed periodically to check if the agent's
|
|
250
|
+
* current behavior aligns with the operational goal. */
|
|
251
|
+
export interface DriftAssessment {
|
|
252
|
+
consistencyScore: number // 0-100, higher = more aligned
|
|
253
|
+
currentAction: string
|
|
254
|
+
targetSubGoal: string
|
|
255
|
+
operationalGoal: string
|
|
256
|
+
trend: DriftTrend
|
|
257
|
+
warnings: DriftWarning[]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type DriftTrend = 'stable' | 'improving' | 'declining'
|
|
261
|
+
|
|
262
|
+
/** A single warning generated when drift is detected. Carries severity and
|
|
263
|
+
* a suggested action for the agent to take. */
|
|
264
|
+
export interface DriftWarning {
|
|
265
|
+
severity: DriftSeverity
|
|
266
|
+
message: string
|
|
267
|
+
suggestedAction: DriftAction
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export type DriftSeverity = 'low' | 'medium' | 'high'
|
|
271
|
+
export type DriftAction = 'continue' | 'replan' | 'stop'
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// GoalStore — the state management interface
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
/** State management contract for goal tracking. Implementations provide
|
|
278
|
+
* in-memory storage with file persistence. This interface defines what
|
|
279
|
+
* the rest of the system can depend on. */
|
|
280
|
+
export interface GoalStore {
|
|
281
|
+
// === DAG Operations ===
|
|
282
|
+
|
|
283
|
+
addSubGoal(node: SubGoal): void
|
|
284
|
+
removeSubGoal(nodeId: string): void
|
|
285
|
+
updateSubGoalStatus(nodeId: string, status: SubGoalStatus): void
|
|
286
|
+
reorderDependency(nodeId: string, newDeps: readonly string[]): void
|
|
287
|
+
mergeSubGoals(sourceIds: string[], target: SubGoal): void
|
|
288
|
+
|
|
289
|
+
// === Goal Lifecycle ===
|
|
290
|
+
|
|
291
|
+
setUserGoal(text: string): GoalId
|
|
292
|
+
setOperationalGoal(text: string): void
|
|
293
|
+
setImmediateGoal(subGoalId: string, action: string): void
|
|
294
|
+
clearImmediateGoal(): void
|
|
295
|
+
|
|
296
|
+
// === Progress ===
|
|
297
|
+
|
|
298
|
+
recalculateProgress(): number
|
|
299
|
+
incrementIteration(): void
|
|
300
|
+
recordEvalGap(gap: string | null): void
|
|
301
|
+
|
|
302
|
+
// === Query ===
|
|
303
|
+
|
|
304
|
+
getReadySubGoals(): SubGoal[]
|
|
305
|
+
getActiveSubGoals(): SubGoal[]
|
|
306
|
+
getBlockedSubGoals(): SubGoal[]
|
|
307
|
+
getCompletedSubGoals(): SubGoal[]
|
|
308
|
+
|
|
309
|
+
// === Persistence ===
|
|
310
|
+
|
|
311
|
+
/** Serialize to JSON for persistence. */
|
|
312
|
+
toJSON(): GoalState
|
|
313
|
+
/** Load state from disk (overwrites current). */
|
|
314
|
+
load(state: GoalState): void
|
|
315
|
+
/** Reset to empty state. */
|
|
316
|
+
clear(): void
|
|
317
|
+
|
|
318
|
+
// === Accessors ===
|
|
319
|
+
|
|
320
|
+
hasActiveGoal(): boolean
|
|
321
|
+
getState(): GoalState
|
|
322
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal DAG engine tests — verifies topological operations, cycle detection,
|
|
3
|
+
* and dynamic mutations for the task graph.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test'
|
|
7
|
+
import { GoalDagEngine } from '../goalDag.js'
|
|
8
|
+
|
|
9
|
+
// Helper: create a test SubGoal node
|
|
10
|
+
function makeNode(id: string, text: string, deps: string[] = [], status: string = 'pending') {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
parentId: 'test' as any,
|
|
14
|
+
text,
|
|
15
|
+
status: status as any,
|
|
16
|
+
dependencies: deps,
|
|
17
|
+
progress: 0,
|
|
18
|
+
createdAt: Date.now(),
|
|
19
|
+
updatedAt: Date.now(),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Basic operations
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
describe('GoalDagEngine', () => {
|
|
28
|
+
test('empty graph has no nodes', () => {
|
|
29
|
+
const dag = new GoalDagEngine()
|
|
30
|
+
expect(dag.getAllNodes()).toEqual([])
|
|
31
|
+
expect(dag.topoSort()).toEqual([])
|
|
32
|
+
expect(dag.hasCycles()).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('single node returns itself', () => {
|
|
36
|
+
const dag = new GoalDagEngine()
|
|
37
|
+
dag.addNode(makeNode('a', 'Task A'))
|
|
38
|
+
expect(dag.getAllNodes()).toHaveLength(1)
|
|
39
|
+
expect(dag.topoSort()).toEqual(['a'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('two-node linear dependency', () => {
|
|
43
|
+
const dag = new GoalDagEngine()
|
|
44
|
+
dag.addNode(makeNode('a', 'Task A'))
|
|
45
|
+
dag.addNode(makeNode('b', 'Task B', ['a']))
|
|
46
|
+
// b depends on a → a should come first
|
|
47
|
+
expect(dag.topoSort()).toEqual(['a', 'b'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('diamond dependency', () => {
|
|
51
|
+
const dag = new GoalDagEngine()
|
|
52
|
+
dag.addNode(makeNode('a', 'Root'))
|
|
53
|
+
dag.addNode(makeNode('b', 'Left', ['a']))
|
|
54
|
+
dag.addNode(makeNode('c', 'Right', ['a']))
|
|
55
|
+
dag.addNode(makeNode('d', 'End', ['b', 'c']))
|
|
56
|
+
// d depends on both b and c → any valid order must end with d
|
|
57
|
+
const sorted = dag.topoSort()
|
|
58
|
+
expect(sorted).toHaveLength(4)
|
|
59
|
+
expect(sorted[sorted.length - 1]).toBe('d')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Cycle detection
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
describe('Cycle detection', () => {
|
|
68
|
+
test('acyclic graph reports no cycles', () => {
|
|
69
|
+
const dag = new GoalDagEngine()
|
|
70
|
+
dag.addNode(makeNode('a', 'A'))
|
|
71
|
+
dag.addNode(makeNode('b', 'B', ['a']))
|
|
72
|
+
dag.addNode(makeNode('c', 'C', ['b']))
|
|
73
|
+
expect(dag.hasCycles()).toBe(false)
|
|
74
|
+
expect(dag.detectCycles()).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('self-loop detected', () => {
|
|
78
|
+
const dag = new GoalDagEngine()
|
|
79
|
+
dag.addNode(makeNode('a', 'A', ['a'])) // self-loop
|
|
80
|
+
expect(dag.hasCycles()).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('simple cycle a→b→a', () => {
|
|
84
|
+
const dag = new GoalDagEngine()
|
|
85
|
+
dag.addNode(makeNode('a', 'A', ['b']))
|
|
86
|
+
dag.addNode(makeNode('b', 'B', ['a']))
|
|
87
|
+
expect(dag.hasCycles()).toBe(true)
|
|
88
|
+
const cycles = dag.detectCycles()
|
|
89
|
+
expect(cycles.length).toBeGreaterThan(0)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Ready nodes
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
describe('Ready nodes', () => {
|
|
98
|
+
test('completed dependencies unlock pending nodes', () => {
|
|
99
|
+
const dag = new GoalDagEngine()
|
|
100
|
+
dag.addNode(makeNode('a', 'A', [], 'completed'))
|
|
101
|
+
dag.addNode(makeNode('b', 'B', ['a'], 'pending'))
|
|
102
|
+
dag.addNode(makeNode('c', 'C', ['b'], 'pending'))
|
|
103
|
+
// a is done, b should be ready (no other blockers)
|
|
104
|
+
expect(dag.getReadyNodes()).toEqual(['b'])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('blocked nodes are not ready', () => {
|
|
108
|
+
const dag = new GoalDagEngine()
|
|
109
|
+
dag.addNode(makeNode('a', 'A', [], 'completed'))
|
|
110
|
+
dag.addNode(makeNode('b', 'B', ['a'], 'blocked'))
|
|
111
|
+
expect(dag.getReadyNodes()).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Dynamic mutations
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
describe('Dynamic mutations', () => {
|
|
120
|
+
test('add node with after parameter', () => {
|
|
121
|
+
const dag = new GoalDagEngine()
|
|
122
|
+
dag.addNode(makeNode('a', 'A'))
|
|
123
|
+
dag.addNode(makeNode('b', 'B'), 'after_a') // non-existent dep ignored
|
|
124
|
+
expect(dag.getAllNodes()).toHaveLength(2)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('remove node removes edges', () => {
|
|
128
|
+
const dag = new GoalDagEngine()
|
|
129
|
+
dag.addNode(makeNode('a', 'A'))
|
|
130
|
+
dag.addNode(makeNode('b', 'B', ['a']))
|
|
131
|
+
expect(dag.getLeafNodes()).toHaveLength(1) // b is leaf
|
|
132
|
+
dag.removeNode('b')
|
|
133
|
+
expect(dag.getAllNodes()).toHaveLength(1)
|
|
134
|
+
expect(dag.getLeafNodes()).toHaveLength(1) // a is now leaf
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('merge nodes combines dependencies', () => {
|
|
138
|
+
const dag = new GoalDagEngine()
|
|
139
|
+
dag.addNode(makeNode('a', 'Dep1'))
|
|
140
|
+
dag.addNode(makeNode('b', 'Dep2'))
|
|
141
|
+
const merged = dag.mergeNodes(['a', 'b'], 'Combined')
|
|
142
|
+
expect(merged.text).toBe('Combined')
|
|
143
|
+
// Merged should have no dependencies (original deps don't depend on each other)
|
|
144
|
+
expect(merged.dependencies).toEqual([])
|
|
145
|
+
// Original nodes removed
|
|
146
|
+
expect(dag.getAllNodes().find(n => n.id === 'a')).toBeUndefined()
|
|
147
|
+
expect(dag.getAllNodes().find(n => n.id === 'b')).toBeUndefined()
|
|
148
|
+
// Merged node exists
|
|
149
|
+
expect(dag.getAllNodes().find(n => n.id === merged.id)).toBeDefined()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('update status changes ready set', () => {
|
|
153
|
+
const dag = new GoalDagEngine()
|
|
154
|
+
dag.addNode(makeNode('a', 'A', [], 'pending'))
|
|
155
|
+
dag.updateStatus('a', 'completed')
|
|
156
|
+
const node = [...Object.entries(dag.toJSON().nodes)][0][1]
|
|
157
|
+
expect(node.status).toBe('completed')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Serialization
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
describe('Serialization', () => {
|
|
166
|
+
test('round-trip preserves state', () => {
|
|
167
|
+
const dag = new GoalDagEngine()
|
|
168
|
+
dag.addNode(makeNode('a', 'Task A'))
|
|
169
|
+
dag.addNode(makeNode('b', 'Task B', ['a']))
|
|
170
|
+
|
|
171
|
+
const json = dag.toJSON()
|
|
172
|
+
const restored = GoalDagEngine.fromJSON(json)
|
|
173
|
+
|
|
174
|
+
expect(restored.getAllNodes()).toHaveLength(2)
|
|
175
|
+
expect(restored.topoSort()).toEqual(dag.topoSort())
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift detection tests — verifies keyword extraction and alignment scoring
|
|
3
|
+
* for both English and Chinese (CJK) text inputs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test'
|
|
7
|
+
import { assessDrift, resetDriftHistory, getDriftHistory } from '../goalDrift.js'
|
|
8
|
+
|
|
9
|
+
// Helper to create a minimal SubGoal for drift testing
|
|
10
|
+
function makeSubGoal(id: string, text: string) {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
parentId: 'test' as any,
|
|
14
|
+
text,
|
|
15
|
+
status: 'active' as any,
|
|
16
|
+
dependencies: [],
|
|
17
|
+
progress: 0,
|
|
18
|
+
createdAt: Date.now(),
|
|
19
|
+
updatedAt: Date.now(),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Drift detection — English', () => {
|
|
24
|
+
test('high alignment when action matches sub-goal', () => {
|
|
25
|
+
resetDriftHistory()
|
|
26
|
+
const result = assessDrift(
|
|
27
|
+
'Writing LOD generator implementation for mesh processing',
|
|
28
|
+
makeSubGoal('sg1', 'Implement LOD generator module'),
|
|
29
|
+
'Build a complete LOD generation system',
|
|
30
|
+
)
|
|
31
|
+
// Both contain "LOD" "generator" → high alignment
|
|
32
|
+
if (result.consistencyScore > 50) {
|
|
33
|
+
void result.consistencyScore
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('low alignment when action diverges from goal', () => {
|
|
38
|
+
resetDriftHistory()
|
|
39
|
+
const result = assessDrift(
|
|
40
|
+
'Reading documentation about Unreal Engine history',
|
|
41
|
+
makeSubGoal('sg1', 'Implement LOD generator module'),
|
|
42
|
+
'Build a complete LOD generation system',
|
|
43
|
+
)
|
|
44
|
+
// Action has no overlap with goal → low score
|
|
45
|
+
if (result.consistencyScore < 50) {
|
|
46
|
+
void result.consistencyScore
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('Drift detection — Chinese', () => {
|
|
52
|
+
test('high alignment for matching Chinese text', () => {
|
|
53
|
+
resetDriftHistory()
|
|
54
|
+
const result = assessDrift(
|
|
55
|
+
'正在编写LOD网格简化工具的实现代码',
|
|
56
|
+
makeSubGoal('sg1', '实现LOD生成模块'),
|
|
57
|
+
'构建完整的LOD生成系统',
|
|
58
|
+
)
|
|
59
|
+
// "LOD" + "生成" + "网格" → good overlap
|
|
60
|
+
if (result.consistencyScore > 40) {
|
|
61
|
+
void result.consistencyScore
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('low alignment for unrelated Chinese text', () => {
|
|
66
|
+
resetDriftHistory()
|
|
67
|
+
const result = assessDrift(
|
|
68
|
+
'阅读虚幻引擎的历史文档资料',
|
|
69
|
+
makeSubGoal('sg1', '实现LOD生成模块'),
|
|
70
|
+
'构建LOD生成系统',
|
|
71
|
+
)
|
|
72
|
+
// "阅读" + "文档" — no overlap with "LOD生成"
|
|
73
|
+
if (result.consistencyScore < 30) {
|
|
74
|
+
void result.consistencyScore
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('mixed Chinese-English text produces reasonable scores', () => {
|
|
79
|
+
resetDriftHistory()
|
|
80
|
+
const result = assessDrift(
|
|
81
|
+
'Implementing LOD mesh decimation tool for Unreal Engine',
|
|
82
|
+
makeSubGoal('sg1', 'Create LOD generator plugin'),
|
|
83
|
+
'Build LOD tools',
|
|
84
|
+
)
|
|
85
|
+
// "LOD" keyword is shared, should produce non-zero score
|
|
86
|
+
if (result.consistencyScore > 30) {
|
|
87
|
+
void result.consistencyScore
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('Drift trend tracking', () => {
|
|
93
|
+
test('trend detects declining pattern', () => {
|
|
94
|
+
resetDriftHistory()
|
|
95
|
+
// Simulate declining alignment over multiple turns
|
|
96
|
+
const history = []
|
|
97
|
+
for (const action of [
|
|
98
|
+
'Implementing core logic', // close match
|
|
99
|
+
'Writing utility functions', // moderate match
|
|
100
|
+
'Refactoring unrelated code', // diverging
|
|
101
|
+
'Debugging random files', // far off
|
|
102
|
+
'Reading docs about history', // completely unrelated
|
|
103
|
+
]) {
|
|
104
|
+
const result = assessDrift(action, makeSubGoal('sg1', 'Core engine implementation'), 'Build core engine')
|
|
105
|
+
history.push(result.consistencyScore)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Trend should be declining
|
|
109
|
+
const latest = history[history.length - 2] ?? 0
|
|
110
|
+
const last = history[history.length - 1] ?? 0
|
|
111
|
+
// The last entry should be lower than earlier entries
|
|
112
|
+
if (last < latest) {
|
|
113
|
+
void last
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('Chinese keyword extraction', () => {
|
|
119
|
+
test('CJK bigram extraction handles mixed text', () => {
|
|
120
|
+
// We can't directly test extractKeywords (it's private),
|
|
121
|
+
// but we can verify it doesn't produce empty sets via assessDrift
|
|
122
|
+
const result = assessDrift(
|
|
123
|
+
'测试中文关键词提取功能是否正常',
|
|
124
|
+
makeSubGoal('sg1', '实现核心引擎模块'),
|
|
125
|
+
'测试系统的完整性',
|
|
126
|
+
)
|
|
127
|
+
// Should not be zero — at least some Chinese keywords overlap
|
|
128
|
+
if (result.consistencyScore > 0) {
|
|
129
|
+
void result.consistencyScore
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|