fluffy-context 0.5.0 → 0.6.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 +25 -4
- package/dist/src/agent/api.d.ts +5 -1
- package/dist/src/agent/api.js +41 -12
- package/dist/src/agent/index.d.ts +1 -1
- package/dist/src/agent/types.d.ts +21 -0
- package/dist/src/cli/main.js +84 -10
- package/dist/src/cognition/feedback.d.ts +9 -0
- package/dist/src/cognition/feedback.js +30 -0
- package/dist/src/cognition/migration-v1.js +12 -10
- package/dist/src/cognition/orient.d.ts +4 -0
- package/dist/src/cognition/orient.js +10 -1
- package/dist/src/cognition/projections.js +6 -3
- package/dist/src/cognition/retrieval.d.ts +26 -0
- package/dist/src/cognition/retrieval.js +273 -0
- package/dist/src/cognition/types.d.ts +39 -1
- package/dist/src/cognition/v1-bridge.js +10 -2
- package/dist/src/runtime/knowledge.d.ts +2 -0
- package/dist/src/runtime/knowledge.js +63 -0
- package/dist/src/runtime/types.d.ts +19 -0
- package/dist/src/storage/layout.d.ts +1 -0
- package/dist/src/storage/layout.js +3 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
Git 仍然负责代码和真实文件变更,Issue 系统仍然负责任务,项目文档仍然负责设计;`fluffy-context` 补齐的是持续演进的项目认知层。最终架构是 `project → knowledge runtime → LLM`:CLI、MCP、Claude Hook 和未来 IDE/Agent 集成只是事件入口与信息注入适配器,而不是认知本体。
|
|
6
6
|
|
|
7
|
-
0.
|
|
7
|
+
0.6.0 在 branch-aware 注入闭环上补齐了可治理的确定性检索:迁移并启用 Runtime 后,Orient 从 v2 journal 的可重建索引检索已验证、适用的 Knowledge 与 Deadend,并返回有界的选择、排除与截断说明。v1 的 checkpoint、Note、Knowledge 与 Deadend 写入仍会最佳努力镜像为 v2 事件;Runtime 也会协调检测到的外部 v1 变更。自动产物默认仍仅进入 Candidate,Verified Knowledge 仍需治理确认。
|
|
8
|
+
|
|
9
|
+
## 版本演进
|
|
10
|
+
|
|
11
|
+
| 版本 | 主要能力 | 关键特点 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| 0.1.0 | 本地 Context CLI、baseline/patch Snapshot、resume、Note、Candidate Knowledge/Deadend | Git 继续作为代码权威;Context 只保存有界任务状态,不复制项目文件;checkpoint 显式触发且支持 no-op、限流与敏感路径过滤。 |
|
|
14
|
+
| 0.2.0 | 确定性 Knowledge Discovery、Agent TypeScript API、可安装 Skill | Candidate 与 Verified 分离;默认只注入已验证共识;不依赖向量库、模型或远端服务。 |
|
|
15
|
+
| 0.3.x | MCP `context_orient`、Claude Code Hook 与集成配置生成 | 通过 stdio MCP 和 fail-open Hook 降低 Agent 主动调用成本;集成安装仅在 `--apply` 时写入项目配置。 |
|
|
16
|
+
| 0.4.0 | v2 事件 journal、v1 显式迁移、可重建投影、本地 Runtime、Git 观察 | v2 与 v1 并存;NDJSON hash chain 是 v2 权威;Runtime 仅监听 loopback 并记录分支、ref 与 HEAD 元数据。 |
|
|
17
|
+
| 0.5.0 | branch-aware Orient、v1 写入镜像、Runtime 协调、宿主事件捕获 | Context 按当前 worktree/ref 选择,避免跨分支任务状态泄漏;稳定宿主 ID 才会写入限长、脱敏后的 session/prompt intent。 |
|
|
18
|
+
| 0.6.0 | journal 派生的确定性检索、治理终态、可解释 Orient、显式使用反馈 | 启用 v2 后直接从可重建索引检索已验证且适用的共识;使用反馈只影响稳定排序,绝不自动验证或提升 Candidate。 |
|
|
19
|
+
|
|
20
|
+
所有版本均保持本地优先、显式治理与 fail-open 集成原则。v1 数据不会在迁移过程中被重写;当 v2 未启用、不可用、损坏或尚未同步时,读取接口会回退到兼容的 v1 行为。
|
|
8
21
|
|
|
9
22
|
## 适用场景
|
|
10
23
|
|
|
@@ -93,7 +106,11 @@ ctx note add "测试环境缺少回调凭据" --kind problem --context <context-
|
|
|
93
106
|
ctx activity --context <context-id> --open
|
|
94
107
|
ctx knowledge discover "支付回调"
|
|
95
108
|
ctx knowledge verify <knowledge-id>
|
|
109
|
+
ctx knowledge deprecate <knowledge-id> --reason "已被新的结论取代"
|
|
96
110
|
ctx deadend verify <deadend-id>
|
|
111
|
+
ctx deadend obsolete <deadend-id> --reason "当前架构已不再适用"
|
|
112
|
+
ctx feedback use knowledge <knowledge-id> --event-id <stable-id>
|
|
113
|
+
ctx journal verify
|
|
97
114
|
ctx migrate v1 --dry-run
|
|
98
115
|
ctx migrate v1 --apply
|
|
99
116
|
ctx migrate verify
|
|
@@ -127,7 +144,7 @@ ctx runtime disable
|
|
|
127
144
|
|
|
128
145
|
Runtime 是单项目、按需启动的本地守护进程。它只绑定 `127.0.0.1` 的动态端口,并要求存储在项目本地 `.context/v2/runtime/secret` 中的能力令牌;令牌不会显示在 CLI 状态输出或写入事件日志。启动后,Runtime 持续观察当前 worktree 的 Git 状态:初始状态会记录 `git.state.observed`,分支/ref 切换记录 `git.branch.changed`,HEAD 更新记录 `git.head.changed`。这些事件会重建 branch workspace 投影,但不会自动创建 checkpoint 或将候选信息提升为已验证共识。
|
|
129
146
|
|
|
130
|
-
完成迁移并显式 `ctx runtime enable` 后,`ctx orient`、MCP `context_orient` 与 Claude Hook 会优先选择当前 worktree/ref 的 v2 workspace;它不会借用其他分支的 Context
|
|
147
|
+
完成迁移并显式 `ctx runtime enable` 后,`ctx orient`、MCP `context_orient` 与 Claude Hook 会优先选择当前 worktree/ref 的 v2 workspace;它不会借用其他分支的 Context。v2 可用时,Knowledge 与 Deadend 由 journal 重放出的确定性倒排索引直接检索,默认仅返回已验证且适用的记录;无匹配 workspace、v2 状态不可用、journal 损坏、v1 尚未同步或 Runtime 已禁用时,接口会安全回退到既有 v1 选择逻辑。Runtime 停止时仍可读取已验证的 v2 状态,但运行中的 Runtime 负责持续 Git 观察和 v1 写入镜像。
|
|
131
148
|
|
|
132
149
|
Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.session.started` 或 `host.prompt.submitted`。提示只保存按 `promptIntentMaxChars` 截断并经过敏感值脱敏后的 intent,绝不保存原始提示;缺少稳定标识时不写入事件。默认 Hook 不启动守护进程;将 v2 policy 的 `allowHookStartup` 设为 `true` 后,SessionStart 才会在严格超时内尽力启动 Runtime,失败仍会 fail open。UserPromptSubmit 不会启动 Runtime。
|
|
133
150
|
|
|
@@ -141,7 +158,11 @@ Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.
|
|
|
141
158
|
|
|
142
159
|
### Knowledge 与 Deadend
|
|
143
160
|
|
|
144
|
-
`ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all`
|
|
161
|
+
`ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all` 审查。验证后的 Knowledge 可用 `ctx knowledge deprecate` 退役,候选项可用 `ctx knowledge reject` 拒绝;验证后的 Deadend 可用 `ctx deadend obsolete` 退役,候选项可用 `ctx deadend reject` 拒绝。终态记录保留证据与治理原因,但不会被默认注入。
|
|
162
|
+
|
|
163
|
+
新记录默认具有 `global` 适用性。需要限制到当前 worktree/ref 时,可使用 `--branch-mode exact`;`--paths` 只保存精确项目相对路径约束,当前版本不会推断目录前缀、Git 祖先关系、依赖关系或语义相似路径。`ctx orient` 的 `explanation` 字段会有界地给出 v2/v1 选择方式、排除计数和各类别截断原因。
|
|
164
|
+
|
|
165
|
+
`ctx feedback use knowledge|deadend ... --event-id <stable-id>` 只记录一个幂等的 v2 使用信号。它只作为同等词法相关性结果的确定性排序因素,永不改变验证状态、confidence、evidence 或适用性;Orient、MCP 与 Hook 不会隐式写入该信号。
|
|
145
166
|
|
|
146
167
|
```bash
|
|
147
168
|
ctx learn "订单取消后不能再次进入支付中状态" --scope project
|
|
@@ -222,6 +243,6 @@ npm pack --dry-run --json
|
|
|
222
243
|
|
|
223
244
|
## 当前范围
|
|
224
245
|
|
|
225
|
-
0.
|
|
246
|
+
0.6.0 聚焦单项目、本地优先、可治理的 branch-aware Knowledge Runtime MVP:有界且可解释的 Orient、显式 checkpoint、journal 派生的确定性 Knowledge/Deadend 检索、终态治理、显式使用反馈、MCP `context_orient`、可选 Claude Code Hook,以及 v1 到 v2 的显式事件日志迁移和 Git-aware 本地 Runtime。完成迁移并启用 Runtime 后,Orient、MCP 与 Hook 使用当前 worktree/ref 的 workspace 选择分支正确的 Context,并在 v2 不可用时保持 v1 回退;Runtime 负责可靠捕获生命周期、Git 元数据与后续 v1 写入镜像。带稳定宿主标识的 Hook 事件仅以脱敏、限长 intent 进入 journal。
|
|
226
247
|
|
|
227
248
|
远程同步、多人协作、复杂语义检索、自动模型总结、自动 checkpoint、Context merge、自动验证候选共识,以及更多状态变更型 MCP 工具不属于当前版本范围。
|
package/dist/src/agent/api.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
|
|
2
|
-
import type { ContextOrientOptions, ContextOrientResult } from './types.js';
|
|
2
|
+
import type { ContextOrientOptions, ContextOrientResult, ContextUseInput } from './types.js';
|
|
3
3
|
export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
|
|
4
4
|
minSaveIntervalMs?: number;
|
|
5
5
|
}): Promise<AgentSaveResult>;
|
|
6
6
|
export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions): Promise<AgentLoadResult>;
|
|
7
7
|
export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions): Promise<ContextOrientResult>;
|
|
8
|
+
export declare function recordContextUse(startPath: string | undefined, input: ContextUseInput): Promise<{
|
|
9
|
+
recorded: boolean;
|
|
10
|
+
eventId: string;
|
|
11
|
+
}>;
|
|
8
12
|
export declare function searchContext(startPath: string | undefined, query: string, options?: ContextSearchOptions): Promise<ContextSearchResult>;
|
package/dist/src/agent/api.js
CHANGED
|
@@ -3,7 +3,9 @@ import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
|
3
3
|
import { contextsRoot, contextMetadataPath } from '../storage/layout.js';
|
|
4
4
|
import { isRecord, readJson } from '../storage/json-store.js';
|
|
5
5
|
import { checkpoint, rebuildSnapshot, resume, resumeSnapshot } from '../runtime/runtime.js';
|
|
6
|
+
import { recordUse } from '../cognition/feedback.js';
|
|
6
7
|
import { selectV2Orientation } from '../cognition/orient.js';
|
|
8
|
+
import { discoverV2Deadends, discoverV2Knowledge } from '../cognition/retrieval.js';
|
|
7
9
|
import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
|
|
8
10
|
import { listNotes } from '../runtime/notes.js';
|
|
9
11
|
function normalized(value) {
|
|
@@ -176,6 +178,12 @@ export async function contextOrient(startPath, options = {}) {
|
|
|
176
178
|
deadends: emptyDeadends(query ?? ''),
|
|
177
179
|
notes: [],
|
|
178
180
|
truncated: false,
|
|
181
|
+
explanation: {
|
|
182
|
+
mode: 'v1-fallback',
|
|
183
|
+
selection: 'v1',
|
|
184
|
+
retrieval: { knowledge: {}, deadends: {} },
|
|
185
|
+
truncation: { summary: false, knowledge: false, deadends: false, notes: false },
|
|
186
|
+
},
|
|
179
187
|
};
|
|
180
188
|
}
|
|
181
189
|
throw error;
|
|
@@ -187,26 +195,30 @@ export async function contextOrient(startPath, options = {}) {
|
|
|
187
195
|
limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
|
|
188
196
|
maxChars: options.maxChars ?? 4000,
|
|
189
197
|
})).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
|
|
190
|
-
const
|
|
198
|
+
const v2Knowledge = query === null || !v2 ? null : discoverV2Knowledge(v2.retrieval, query, v2.currentGit, {
|
|
199
|
+
scope: options.scope,
|
|
200
|
+
limit: options.knowledgeLimit ?? 10,
|
|
201
|
+
maxChars: options.maxChars ?? 4000,
|
|
202
|
+
});
|
|
203
|
+
const v2Deadends = query === null || !v2 ? null : discoverV2Deadends(v2.retrieval, query, v2.currentGit, {
|
|
204
|
+
scope: options.scope,
|
|
205
|
+
limit: options.deadendLimit ?? 10,
|
|
206
|
+
maxChars: options.maxChars ?? 4000,
|
|
207
|
+
});
|
|
208
|
+
const applicableKnowledge = query === null
|
|
191
209
|
? emptyKnowledge('')
|
|
192
|
-
: await discoverKnowledge(projectRoot, query, {
|
|
210
|
+
: v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
|
|
193
211
|
scope: options.scope,
|
|
194
|
-
limit:
|
|
212
|
+
limit: options.knowledgeLimit ?? 10,
|
|
195
213
|
maxChars: options.maxChars ?? 4000,
|
|
196
214
|
});
|
|
197
|
-
const
|
|
215
|
+
const applicableDeadends = query === null
|
|
198
216
|
? emptyDeadends('')
|
|
199
|
-
: await discoverDeadends(projectRoot, query, {
|
|
217
|
+
: v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
|
|
200
218
|
scope: options.scope,
|
|
201
|
-
limit:
|
|
219
|
+
limit: options.deadendLimit ?? 10,
|
|
202
220
|
maxChars: options.maxChars ?? 4000,
|
|
203
221
|
});
|
|
204
|
-
const applicableKnowledge = v2
|
|
205
|
-
? { ...knowledge, total: knowledge.hits.filter((hit) => v2.knowledgeIds.has(hit.knowledge.knowledgeId)).length, hits: knowledge.hits.filter((hit) => v2.knowledgeIds.has(hit.knowledge.knowledgeId)).slice(0, options.knowledgeLimit ?? 10) }
|
|
206
|
-
: knowledge;
|
|
207
|
-
const applicableDeadends = v2
|
|
208
|
-
? { ...deadends, total: deadends.hits.filter((hit) => v2.deadendIds.has(hit.deadend.deadendId)).length, hits: deadends.hits.filter((hit) => v2.deadendIds.has(hit.deadend.deadendId)).slice(0, options.deadendLimit ?? 10) }
|
|
209
|
-
: deadends;
|
|
210
222
|
const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
|
|
211
223
|
const resumeSummary = {
|
|
212
224
|
progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
|
|
@@ -252,8 +264,25 @@ export async function contextOrient(startPath, options = {}) {
|
|
|
252
264
|
deadends: limitedDeadendResult.result,
|
|
253
265
|
notes: limitedNotesResult.notes,
|
|
254
266
|
truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
|
|
267
|
+
explanation: {
|
|
268
|
+
mode: v2 ? 'v2' : 'v1-fallback',
|
|
269
|
+
selection: v2?.selection ?? 'v1',
|
|
270
|
+
retrieval: {
|
|
271
|
+
knowledge: v2Knowledge?.exclusions ?? {},
|
|
272
|
+
deadends: v2Deadends?.exclusions ?? {},
|
|
273
|
+
},
|
|
274
|
+
truncation: {
|
|
275
|
+
summary: resumeTruncated,
|
|
276
|
+
knowledge: limitedKnowledgeResult.truncated,
|
|
277
|
+
deadends: limitedDeadendResult.truncated,
|
|
278
|
+
notes: limitedNotesResult.truncated,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
255
281
|
};
|
|
256
282
|
}
|
|
283
|
+
export async function recordContextUse(startPath, input) {
|
|
284
|
+
return recordUse(startPath, input);
|
|
285
|
+
}
|
|
257
286
|
export async function searchContext(startPath, query, options = {}) {
|
|
258
287
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
259
288
|
const cleanQuery = query.trim();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './api.js';
|
|
2
|
-
export type { ContextOrientContext, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
|
|
2
|
+
export type { ContextOrientContext, ContextUseInput, ContextOrientExplanation, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
|
|
3
3
|
export type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, ContextSearchHit, ContextSearchOptions, ContextSearchResult, } from '../runtime/types.js';
|
|
@@ -23,6 +23,25 @@ export interface ContextOrientSnapshot {
|
|
|
23
23
|
branch: string | null;
|
|
24
24
|
commit: string | null;
|
|
25
25
|
}
|
|
26
|
+
export interface ContextUseInput {
|
|
27
|
+
entity: 'knowledge' | 'deadend';
|
|
28
|
+
entityId: string;
|
|
29
|
+
eventId: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ContextOrientExplanation {
|
|
32
|
+
mode: 'v2' | 'v1-fallback';
|
|
33
|
+
selection: 'requested-context' | 'exact-worktree-ref' | 'legacy-ref-branch' | 'v1';
|
|
34
|
+
retrieval: {
|
|
35
|
+
knowledge: Record<'status_not_verified' | 'applicability_mismatch' | 'scope_mismatch' | 'query_no_match' | 'result_limit', number> | {};
|
|
36
|
+
deadends: Record<'status_not_verified' | 'applicability_mismatch' | 'scope_mismatch' | 'query_no_match' | 'result_limit', number> | {};
|
|
37
|
+
};
|
|
38
|
+
truncation: {
|
|
39
|
+
summary: boolean;
|
|
40
|
+
knowledge: boolean;
|
|
41
|
+
deadends: boolean;
|
|
42
|
+
notes: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
26
45
|
export interface ContextOrientReadyResult {
|
|
27
46
|
status: 'ready';
|
|
28
47
|
projectRoot: string;
|
|
@@ -34,6 +53,7 @@ export interface ContextOrientReadyResult {
|
|
|
34
53
|
deadends: DeadendDiscoveryResult;
|
|
35
54
|
notes: Note[];
|
|
36
55
|
truncated: boolean;
|
|
56
|
+
explanation: ContextOrientExplanation;
|
|
37
57
|
}
|
|
38
58
|
export interface ContextOrientNoContextResult {
|
|
39
59
|
status: 'no_context';
|
|
@@ -46,5 +66,6 @@ export interface ContextOrientNoContextResult {
|
|
|
46
66
|
deadends: DeadendDiscoveryResult;
|
|
47
67
|
notes: Note[];
|
|
48
68
|
truncated: false;
|
|
69
|
+
explanation: ContextOrientExplanation;
|
|
49
70
|
}
|
|
50
71
|
export type ContextOrientResult = ContextOrientReadyResult | ContextOrientNoContextResult;
|
package/dist/src/cli/main.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import { checkpoint, resume } from '../runtime/runtime.js';
|
|
3
3
|
import { contextOrient } from '../agent/api.js';
|
|
4
4
|
import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
|
|
5
|
+
import { recordUse } from '../cognition/feedback.js';
|
|
6
|
+
import { readJournal } from '../cognition/journal.js';
|
|
7
|
+
import { projectEvents } from '../cognition/projections.js';
|
|
5
8
|
import { disableRuntime, enableRuntime, runtimeStatus, serveRuntime, startRuntime, stopRuntime } from '../cognition/runtime.js';
|
|
6
9
|
import { migrationReadiness } from '../cognition/readiness.js';
|
|
7
10
|
import { runClaudeCodeHook } from '../hooks/claude-code.js';
|
|
11
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
8
12
|
import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
|
|
9
13
|
import { doctor, status } from '../runtime/diagnostics.js';
|
|
10
14
|
import { initProject } from '../runtime/init.js';
|
|
11
|
-
import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
|
|
15
|
+
import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend, transitionKnowledge, transitionDeadend } from '../runtime/knowledge.js';
|
|
12
16
|
import { addNote, listActivity, listNotes } from '../runtime/notes.js';
|
|
13
17
|
import { VERSION } from '../version.js';
|
|
14
18
|
function option(args, name) {
|
|
@@ -158,16 +162,23 @@ Options:
|
|
|
158
162
|
--evidence <items> Comma-separated evidence references`,
|
|
159
163
|
knowledge: `usage: ctx knowledge [--all] [--path <path>]
|
|
160
164
|
ctx knowledge verify <knowledge-id> [--path <path>]
|
|
165
|
+
ctx knowledge deprecate|reject <knowledge-id> --reason <text> [--path <path>]
|
|
161
166
|
ctx knowledge discover <query> [options]
|
|
162
167
|
|
|
163
|
-
List
|
|
168
|
+
List, govern, or discover project knowledge.
|
|
164
169
|
|
|
165
170
|
Options:
|
|
166
171
|
--all Include unverified items
|
|
167
172
|
--path <path> Project path`,
|
|
168
173
|
'knowledge verify': `usage: ctx knowledge verify <knowledge-id> [--path <path>]
|
|
169
174
|
|
|
170
|
-
Mark a knowledge item as verified.`,
|
|
175
|
+
Mark a candidate knowledge item as verified.`,
|
|
176
|
+
'knowledge deprecate': `usage: ctx knowledge deprecate <knowledge-id> --reason <text> [--path <path>]
|
|
177
|
+
|
|
178
|
+
Retire a verified knowledge item with an explicit governance reason.`,
|
|
179
|
+
'knowledge reject': `usage: ctx knowledge reject <knowledge-id> --reason <text> [--path <path>]
|
|
180
|
+
|
|
181
|
+
Reject a candidate knowledge item with an explicit governance reason.`,
|
|
171
182
|
'knowledge discover': `usage: ctx knowledge discover <query> [options]
|
|
172
183
|
|
|
173
184
|
Find matching project knowledge using deterministic lexical and alias rules.
|
|
@@ -181,8 +192,9 @@ Options:
|
|
|
181
192
|
--max-chars <number> Maximum text characters per match`,
|
|
182
193
|
deadend: `usage: ctx deadend [attempt] [options]
|
|
183
194
|
ctx deadend verify <deadend-id> [--path <path>]
|
|
195
|
+
ctx deadend obsolete|reject <deadend-id> --reason <text> [--path <path>]
|
|
184
196
|
|
|
185
|
-
Record or
|
|
197
|
+
Record or govern a candidate deadend.
|
|
186
198
|
|
|
187
199
|
Options:
|
|
188
200
|
--path <path> Project path
|
|
@@ -195,7 +207,25 @@ Options:
|
|
|
195
207
|
--evidence <items> Comma-separated evidence references`,
|
|
196
208
|
'deadend verify': `usage: ctx deadend verify <deadend-id> [--path <path>]
|
|
197
209
|
|
|
198
|
-
Mark a deadend as verified.`,
|
|
210
|
+
Mark a candidate deadend as verified.`,
|
|
211
|
+
'deadend obsolete': `usage: ctx deadend obsolete <deadend-id> --reason <text> [--path <path>]
|
|
212
|
+
|
|
213
|
+
Retire a verified deadend with an explicit governance reason.`,
|
|
214
|
+
'deadend reject': `usage: ctx deadend reject <deadend-id> --reason <text> [--path <path>]
|
|
215
|
+
|
|
216
|
+
Reject a candidate deadend with an explicit governance reason.`,
|
|
217
|
+
feedback: `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
|
|
218
|
+
|
|
219
|
+
Record explicit use of verified v2 retrieval material.`,
|
|
220
|
+
'feedback use': `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
|
|
221
|
+
|
|
222
|
+
Record an idempotent use signal that affects deterministic rank only.`,
|
|
223
|
+
journal: `usage: ctx journal verify [--path <path>]
|
|
224
|
+
|
|
225
|
+
Validate the v2 journal hash chain and replay projections in memory.`,
|
|
226
|
+
'journal verify': `usage: ctx journal verify [--path <path>]
|
|
227
|
+
|
|
228
|
+
Validate journal integrity without writing derived state.`,
|
|
199
229
|
deadends: `usage: ctx deadends [--all] [--path <path>]
|
|
200
230
|
|
|
201
231
|
List recorded deadends.`,
|
|
@@ -282,7 +312,7 @@ Stop the local runtime through authenticated loopback IPC.`,
|
|
|
282
312
|
Internal runtime daemon entry point.`,
|
|
283
313
|
};
|
|
284
314
|
function usage() {
|
|
285
|
-
return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime [options]
|
|
315
|
+
return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|feedback|journal [options]
|
|
286
316
|
|
|
287
317
|
Run \"ctx <command> --help\" for command details.`;
|
|
288
318
|
}
|
|
@@ -304,8 +334,10 @@ function printHelp(args) {
|
|
|
304
334
|
const nested = (command === 'agent' && args[1] === 'serve')
|
|
305
335
|
|| (command === 'hook' && args[1] === 'claude-code')
|
|
306
336
|
|| (command === 'integrate' && args[1] === 'claude')
|
|
307
|
-
|| (command === 'knowledge' && ['verify', 'discover'].includes(args[1]))
|
|
308
|
-
|| (command === 'deadend' &&
|
|
337
|
+
|| (command === 'knowledge' && ['verify', 'discover', 'deprecate', 'reject'].includes(args[1]))
|
|
338
|
+
|| (command === 'deadend' && ['verify', 'obsolete', 'reject'].includes(args[1]))
|
|
339
|
+
|| (command === 'feedback' && args[1] === 'use')
|
|
340
|
+
|| (command === 'journal' && args[1] === 'verify')
|
|
309
341
|
|| (command === 'note' && ['add', 'list'].includes(args[1]))
|
|
310
342
|
|| (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
|
|
311
343
|
|| (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]));
|
|
@@ -423,17 +455,20 @@ async function run(args) {
|
|
|
423
455
|
print(await doctor(target));
|
|
424
456
|
return;
|
|
425
457
|
case 'learn': {
|
|
426
|
-
const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence'];
|
|
458
|
+
const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
|
|
427
459
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
428
460
|
const statement = positionals(args.slice(1), valueOptions);
|
|
429
461
|
if (statement.length === 0)
|
|
430
462
|
throw new Error(HELP.learn);
|
|
463
|
+
const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
|
|
464
|
+
const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
|
|
431
465
|
const input = {
|
|
432
466
|
kind: option(args, '--kind'),
|
|
433
467
|
scope: option(args, '--scope'),
|
|
434
468
|
sourceContextId: option(args, '--context'),
|
|
435
469
|
sourceSnapshotId: option(args, '--snapshot'),
|
|
436
470
|
evidence: listOption(args, '--evidence'),
|
|
471
|
+
applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
|
|
437
472
|
};
|
|
438
473
|
print(await learnKnowledge(target, statement.join(' '), input));
|
|
439
474
|
return;
|
|
@@ -466,6 +501,28 @@ async function run(args) {
|
|
|
466
501
|
}
|
|
467
502
|
return;
|
|
468
503
|
}
|
|
504
|
+
case 'feedback': {
|
|
505
|
+
if (args[1] !== 'use' || !['knowledge', 'deadend'].includes(args[2]))
|
|
506
|
+
throw new Error(HELP.feedback);
|
|
507
|
+
validateOptions(args.slice(3), ['--path', '--event-id'], ['--path', '--event-id']);
|
|
508
|
+
const values = positionals(args.slice(3), ['--path', '--event-id']);
|
|
509
|
+
if (values.length !== 1)
|
|
510
|
+
throw new Error(HELP['feedback use']);
|
|
511
|
+
print(await recordUse(target, { entity: args[2], entityId: values[0], eventId: option(args, '--event-id') ?? '' }));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
case 'journal': {
|
|
515
|
+
if (args[1] !== 'verify')
|
|
516
|
+
throw new Error(HELP.journal);
|
|
517
|
+
validateOptions(args.slice(2), ['--path'], ['--path']);
|
|
518
|
+
validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['journal verify']);
|
|
519
|
+
const root = target ?? process.cwd();
|
|
520
|
+
const events = await readJournal(root);
|
|
521
|
+
const projection = projectEvents(events);
|
|
522
|
+
const last = events.at(-1);
|
|
523
|
+
print({ verified: true, eventCount: events.length, cursor: { eventId: last?.eventId ?? null, hash: last?.hash ?? null }, counts: { contexts: Object.keys(projection.contexts).length, notes: Object.keys(projection.notes).length, knowledge: Object.keys(projection.knowledge).length, deadends: Object.keys(projection.deadends).length } });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
469
526
|
case 'migrate': {
|
|
470
527
|
if (args[1] === 'v1') {
|
|
471
528
|
validateOptions(args.slice(2), ['--dry-run', '--apply', '--path'], ['--path']);
|
|
@@ -545,6 +602,13 @@ async function run(args) {
|
|
|
545
602
|
throw new Error(HELP['knowledge verify']);
|
|
546
603
|
print(await verifyKnowledge(target, values[0]));
|
|
547
604
|
}
|
|
605
|
+
else if (args[1] === 'deprecate' || args[1] === 'reject') {
|
|
606
|
+
validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
|
|
607
|
+
const values = positionals(args.slice(2), ['--path', '--reason']);
|
|
608
|
+
if (values.length !== 1)
|
|
609
|
+
throw new Error(HELP[`knowledge ${args[1]}`]);
|
|
610
|
+
print(await transitionKnowledge(target, values[0], args[1] === 'deprecate' ? 'deprecated' : 'rejected', option(args, '--reason') ?? ''));
|
|
611
|
+
}
|
|
548
612
|
else {
|
|
549
613
|
validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
|
|
550
614
|
validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.knowledge);
|
|
@@ -560,16 +624,26 @@ async function run(args) {
|
|
|
560
624
|
throw new Error(HELP['deadend verify']);
|
|
561
625
|
print(await verifyDeadend(target, values[0]));
|
|
562
626
|
}
|
|
627
|
+
else if (args[1] === 'obsolete' || args[1] === 'reject') {
|
|
628
|
+
validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
|
|
629
|
+
const values = positionals(args.slice(2), ['--path', '--reason']);
|
|
630
|
+
if (values.length !== 1)
|
|
631
|
+
throw new Error(HELP[`deadend ${args[1]}`]);
|
|
632
|
+
print(await transitionDeadend(target, values[0], args[1] === 'obsolete' ? 'obsolete' : 'rejected', option(args, '--reason') ?? ''));
|
|
633
|
+
}
|
|
563
634
|
else {
|
|
564
|
-
const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence'];
|
|
635
|
+
const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
|
|
565
636
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
566
637
|
const values = positionals(args.slice(1), valueOptions);
|
|
567
638
|
validatePositionals(values, 1, HELP.deadend);
|
|
639
|
+
const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
|
|
640
|
+
const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
|
|
568
641
|
const input = {
|
|
569
642
|
scope: option(args, '--scope'),
|
|
570
643
|
sourceContextId: option(args, '--context'),
|
|
571
644
|
sourceSnapshotId: option(args, '--snapshot'),
|
|
572
645
|
evidence: listOption(args, '--evidence'),
|
|
646
|
+
applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
|
|
573
647
|
};
|
|
574
648
|
print(await recordDeadend(target, option(args, '--attempt') ?? values[0], option(args, '--reason') ?? option(args, '-m') ?? '', input));
|
|
575
649
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
2
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
3
|
+
import { appendEvent, readJournal } from './journal.js';
|
|
4
|
+
import { v2Enabled } from './orient.js';
|
|
5
|
+
import { rebuildProjections } from './projections.js';
|
|
6
|
+
import { buildRetrievalProjection } from './retrieval.js';
|
|
7
|
+
export async function recordUse(startPath, input) {
|
|
8
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
9
|
+
if (!input.eventId.trim())
|
|
10
|
+
throw new Error('feedback event ID must not be empty');
|
|
11
|
+
if (!await v2Enabled(projectRoot))
|
|
12
|
+
throw new Error('record use requires an enabled v2 runtime');
|
|
13
|
+
const events = await readJournal(projectRoot);
|
|
14
|
+
const entry = buildRetrievalProjection(events).entries[`${input.entity}:${input.entityId}`];
|
|
15
|
+
if (!entry)
|
|
16
|
+
throw new Error(`${input.entity} not found in v2 retrieval: ${input.entityId}`);
|
|
17
|
+
if (entry.status !== 'verified')
|
|
18
|
+
throw new Error(`record use requires verified ${input.entity}: ${input.entityId}`);
|
|
19
|
+
const git = await readGitWorktreeState(projectRoot);
|
|
20
|
+
const result = await appendEvent(projectRoot, {
|
|
21
|
+
type: 'record.used',
|
|
22
|
+
source: { kind: 'cli' },
|
|
23
|
+
idempotencyKey: `record-use:${input.entity}:${input.entityId}:${input.eventId.trim()}`,
|
|
24
|
+
git: { worktreeId: git.worktreeId, branch: git.branch, ref: git.ref, head: git.commit },
|
|
25
|
+
payload: { feedbackId: input.eventId.trim(), entity: input.entity, entityId: input.entityId, outcome: 'used' },
|
|
26
|
+
});
|
|
27
|
+
if (result.appended)
|
|
28
|
+
await rebuildProjections(projectRoot);
|
|
29
|
+
return { recorded: result.appended, eventId: result.event.eventId };
|
|
30
|
+
}
|
|
@@ -21,6 +21,16 @@ function sha256(value) {
|
|
|
21
21
|
function isNotFound(error) {
|
|
22
22
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
23
23
|
}
|
|
24
|
+
function eventType(record, live = false) {
|
|
25
|
+
const status = record.record.status;
|
|
26
|
+
if (record.kind === 'context')
|
|
27
|
+
return live ? 'context.state.recorded' : 'context.checkpoint.imported';
|
|
28
|
+
if (record.kind === 'note')
|
|
29
|
+
return record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded';
|
|
30
|
+
if (record.kind === 'knowledge')
|
|
31
|
+
return status === 'verified' ? 'knowledge.verified' : status === 'deprecated' ? 'knowledge.deprecated' : status === 'rejected' ? 'knowledge.rejected' : 'knowledge.proposed';
|
|
32
|
+
return status === 'verified' ? 'deadend.verified' : status === 'obsolete' ? 'deadend.obsoleted' : status === 'rejected' ? 'deadend.rejected' : 'deadend.proposed';
|
|
33
|
+
}
|
|
24
34
|
async function readOptional(filePath) {
|
|
25
35
|
try {
|
|
26
36
|
return await readFile(filePath, 'utf8');
|
|
@@ -129,12 +139,8 @@ export async function importV1(startPath) {
|
|
|
129
139
|
await writeV2Metadata(projectRoot);
|
|
130
140
|
const importedEventIds = [];
|
|
131
141
|
for (const record of records) {
|
|
132
|
-
const status = record.record.status;
|
|
133
142
|
const event = await appendEvent(projectRoot, {
|
|
134
|
-
type: record
|
|
135
|
-
: record.kind === 'note' ? 'note.recorded'
|
|
136
|
-
: record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
|
|
137
|
-
: status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
|
|
143
|
+
type: eventType(record),
|
|
138
144
|
occurredAt: record.occurredAt,
|
|
139
145
|
source: { kind: 'migration' },
|
|
140
146
|
idempotencyKey: `v1:${record.sourceHash}:${record.kind}:${record.id}`,
|
|
@@ -158,12 +164,8 @@ export async function reconcileV1State(startPath) {
|
|
|
158
164
|
return;
|
|
159
165
|
const { records, sourceHash } = await sourceRecords(projectRoot);
|
|
160
166
|
for (const record of records) {
|
|
161
|
-
const status = record.record.status;
|
|
162
167
|
await appendEvent(projectRoot, {
|
|
163
|
-
type: record
|
|
164
|
-
: record.kind === 'note' ? record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded'
|
|
165
|
-
: record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
|
|
166
|
-
: status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
|
|
168
|
+
type: eventType(record, true),
|
|
167
169
|
occurredAt: record.occurredAt,
|
|
168
170
|
source: { kind: 'migration' },
|
|
169
171
|
idempotencyKey: `v1-live-sync:${sourceHash}:${record.kind}:${record.id}`,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
|
|
1
2
|
export interface V2OrientationPlan {
|
|
2
3
|
contextId: string;
|
|
3
4
|
snapshotId: string;
|
|
4
5
|
knowledgeIds: ReadonlySet<string>;
|
|
5
6
|
deadendIds: ReadonlySet<string>;
|
|
6
7
|
noteIds: ReadonlySet<string>;
|
|
8
|
+
retrieval: CognitionRetrievalProjection;
|
|
9
|
+
currentGit: CognitionEvent['git'];
|
|
10
|
+
selection: 'requested-context' | 'exact-worktree-ref' | 'legacy-ref-branch';
|
|
7
11
|
}
|
|
8
12
|
export declare function selectV2Orientation(projectRoot: string, contextId?: string): Promise<V2OrientationPlan | null>;
|
|
9
13
|
export declare function v2Enabled(projectRoot: string): Promise<boolean>;
|
|
@@ -5,6 +5,7 @@ import { readJson } from '../storage/json-store.js';
|
|
|
5
5
|
import { loadManifest } from '../runtime/init.js';
|
|
6
6
|
import { readJournal } from './journal.js';
|
|
7
7
|
import { projectEvents, workspaceKey } from './projections.js';
|
|
8
|
+
import { buildRetrievalProjection } from './retrieval.js';
|
|
8
9
|
import { v1Synced } from './v1-bridge.js';
|
|
9
10
|
function contextReference(value) {
|
|
10
11
|
const boundary = value.lastIndexOf(':');
|
|
@@ -56,6 +57,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
56
57
|
return null;
|
|
57
58
|
const projection = projectEvents(events);
|
|
58
59
|
let selected = null;
|
|
60
|
+
let selection = 'requested-context';
|
|
59
61
|
if (contextId) {
|
|
60
62
|
const matching = [...Object.keys(projection.contexts)].reverse().find((reference) => contextReference(reference)?.contextId === contextId);
|
|
61
63
|
selected = matching ? contextReference(matching) : null;
|
|
@@ -65,10 +67,17 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
65
67
|
const legacy = Object.values(projection.workspaces).find((workspace) => workspace.worktreeId === null && (workspace.ref === current.ref || workspace.branch === current.branch));
|
|
66
68
|
const reference = (exact?.activeContextRef ? exact : legacy)?.activeContextRef;
|
|
67
69
|
selected = reference ? contextReference(reference) : null;
|
|
70
|
+
selection = exact?.activeContextRef ? 'exact-worktree-ref' : 'legacy-ref-branch';
|
|
68
71
|
}
|
|
69
72
|
if (!selected || !projection.contexts[`${selected.contextId}:${selected.snapshotId}`])
|
|
70
73
|
return null;
|
|
71
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
...selected,
|
|
76
|
+
...latestRecords(events, current, selected.contextId),
|
|
77
|
+
retrieval: buildRetrievalProjection(events),
|
|
78
|
+
currentGit: { worktreeId: current.worktreeId, branch: current.branch, ref: current.ref, head: current.commit },
|
|
79
|
+
selection,
|
|
80
|
+
};
|
|
72
81
|
}
|
|
73
82
|
catch {
|
|
74
83
|
return null;
|
|
@@ -2,8 +2,9 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { readdir, rm } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
5
|
-
import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, } from '../storage/layout.js';
|
|
5
|
+
import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, cognitionRetrievalProjectionPath, } from '../storage/layout.js';
|
|
6
6
|
import { readJournal } from './journal.js';
|
|
7
|
+
import { buildRetrievalProjection } from './retrieval.js';
|
|
7
8
|
export function workspaceKey(anchor) {
|
|
8
9
|
return crypto.createHash('sha256')
|
|
9
10
|
.update(`${anchor.worktreeId ?? 'default'}\0${anchor.ref ?? anchor.branch ?? 'detached'}`)
|
|
@@ -74,12 +75,12 @@ export function projectEvents(events) {
|
|
|
74
75
|
if (noteId)
|
|
75
76
|
projection.notes[noteId] = payload;
|
|
76
77
|
}
|
|
77
|
-
if (
|
|
78
|
+
if (['knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected'].includes(event.type) && record) {
|
|
78
79
|
const knowledgeId = recordId(record, 'knowledge');
|
|
79
80
|
if (knowledgeId)
|
|
80
81
|
projection.knowledge[knowledgeId] = payload;
|
|
81
82
|
}
|
|
82
|
-
if (
|
|
83
|
+
if (['deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected'].includes(event.type) && record) {
|
|
83
84
|
const deadendId = recordId(record, 'deadend');
|
|
84
85
|
if (deadendId)
|
|
85
86
|
projection.deadends[deadendId] = payload;
|
|
@@ -107,6 +108,7 @@ async function removeObsoleteWorkspaceFiles(projectRoot, workspaceKeys) {
|
|
|
107
108
|
export async function rebuildProjections(projectRoot) {
|
|
108
109
|
const events = await readJournal(projectRoot);
|
|
109
110
|
const projection = projectEvents(events);
|
|
111
|
+
const retrieval = buildRetrievalProjection(events);
|
|
110
112
|
const last = events.at(-1);
|
|
111
113
|
const cursor = {
|
|
112
114
|
schemaVersion: 2,
|
|
@@ -120,6 +122,7 @@ export async function rebuildProjections(projectRoot) {
|
|
|
120
122
|
atomicWriteJson(cognitionNotesProjectionPath(projectRoot), projection.notes),
|
|
121
123
|
atomicWriteJson(cognitionKnowledgeProjectionPath(projectRoot), projection.knowledge),
|
|
122
124
|
atomicWriteJson(cognitionDeadendsProjectionPath(projectRoot), projection.deadends),
|
|
125
|
+
atomicWriteJson(cognitionRetrievalProjectionPath(projectRoot), retrieval),
|
|
123
126
|
...Object.values(projection.workspaces).map((workspace) => atomicWriteJson(`${cognitionBranchesDirectory(projectRoot)}/${workspace.workspaceKey}.json`, workspace)),
|
|
124
127
|
]);
|
|
125
128
|
await atomicWriteJson(cognitionProjectionCursorPath(projectRoot), cursor);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DeadendDiscoveryResult, KnowledgeDiscoveryResult } from '../runtime/types.js';
|
|
2
|
+
import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
|
|
3
|
+
export declare function normalized(value: string): string;
|
|
4
|
+
export declare function queryTerms(query: string): string[];
|
|
5
|
+
export declare function buildRetrievalProjection(events: CognitionEvent[]): CognitionRetrievalProjection;
|
|
6
|
+
export interface RetrievalExclusions {
|
|
7
|
+
status_not_verified: number;
|
|
8
|
+
applicability_mismatch: number;
|
|
9
|
+
scope_mismatch: number;
|
|
10
|
+
query_no_match: number;
|
|
11
|
+
result_limit: number;
|
|
12
|
+
}
|
|
13
|
+
export interface V2Discovery<T> {
|
|
14
|
+
result: T;
|
|
15
|
+
exclusions: RetrievalExclusions;
|
|
16
|
+
}
|
|
17
|
+
export declare function discoverV2Knowledge(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
|
|
18
|
+
scope?: string;
|
|
19
|
+
limit: number;
|
|
20
|
+
maxChars: number;
|
|
21
|
+
}): V2Discovery<KnowledgeDiscoveryResult>;
|
|
22
|
+
export declare function discoverV2Deadends(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
|
|
23
|
+
scope?: string;
|
|
24
|
+
limit: number;
|
|
25
|
+
maxChars: number;
|
|
26
|
+
}): V2Discovery<DeadendDiscoveryResult>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const aliasGroups = [
|
|
2
|
+
['投保人认证', '投保人身份认证', '身份核验', '实名认证', 'applicant authentication', 'identity verification'],
|
|
3
|
+
['短信验证码', '验证码', 'sms verification', 'otp'],
|
|
4
|
+
['证件ocr', '证件识别', 'identity ocr', 'document ocr'],
|
|
5
|
+
['人脸识别', '人脸认证', 'face recognition', 'facial verification'],
|
|
6
|
+
];
|
|
7
|
+
export function normalized(value) {
|
|
8
|
+
return value.trim().toLocaleLowerCase();
|
|
9
|
+
}
|
|
10
|
+
export function queryTerms(query) {
|
|
11
|
+
const clean = normalized(query);
|
|
12
|
+
const terms = new Set(clean.split(/\s+/).filter(Boolean));
|
|
13
|
+
for (const group of aliasGroups) {
|
|
14
|
+
if (group.some((alias) => clean.includes(normalized(alias)))) {
|
|
15
|
+
for (const alias of group)
|
|
16
|
+
terms.add(normalized(alias));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...terms].sort();
|
|
20
|
+
}
|
|
21
|
+
function strings(record, entity) {
|
|
22
|
+
return entity === 'knowledge'
|
|
23
|
+
? {
|
|
24
|
+
statement: typeof record.statement === 'string' ? record.statement : '',
|
|
25
|
+
kind: typeof record.kind === 'string' ? record.kind : '',
|
|
26
|
+
scope: typeof record.scope === 'string' ? record.scope : '',
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
attempt: typeof record.attempt === 'string' ? record.attempt : '',
|
|
30
|
+
reason: typeof record.reason === 'string' ? record.reason : '',
|
|
31
|
+
scope: typeof record.scope === 'string' ? record.scope : '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function defaultApplicability(event) {
|
|
35
|
+
if (event.git.ref === null && event.git.branch === null)
|
|
36
|
+
return { project: true, branchMode: 'global' };
|
|
37
|
+
return {
|
|
38
|
+
project: true,
|
|
39
|
+
branchMode: 'exact',
|
|
40
|
+
branches: [event.git.ref ?? event.git.branch ?? 'detached'],
|
|
41
|
+
sourceCommit: event.git.head,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function applicability(record, event) {
|
|
45
|
+
const value = record.applicability;
|
|
46
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
47
|
+
const candidate = value;
|
|
48
|
+
if (candidate.project === true && (candidate.branchMode === 'global' || candidate.branchMode === 'exact')) {
|
|
49
|
+
return {
|
|
50
|
+
project: true,
|
|
51
|
+
branchMode: candidate.branchMode,
|
|
52
|
+
...(Array.isArray(candidate.branches) ? { branches: candidate.branches.filter((item) => typeof item === 'string').sort() } : {}),
|
|
53
|
+
...(Array.isArray(candidate.paths) ? { paths: candidate.paths.filter((item) => typeof item === 'string').sort() } : {}),
|
|
54
|
+
...(typeof candidate.sourceCommit === 'string' || candidate.sourceCommit === null ? { sourceCommit: candidate.sourceCommit } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return defaultApplicability(event);
|
|
59
|
+
}
|
|
60
|
+
function recordPayload(event) {
|
|
61
|
+
const payload = event.payload;
|
|
62
|
+
return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record)
|
|
63
|
+
? payload.record
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
function lifecycleEntity(event, record) {
|
|
67
|
+
if (event.type.startsWith('knowledge.') && typeof record.knowledgeId === 'string')
|
|
68
|
+
return { entity: 'knowledge', entityId: record.knowledgeId };
|
|
69
|
+
if (event.type.startsWith('deadend.') && typeof record.deadendId === 'string')
|
|
70
|
+
return { entity: 'deadend', entityId: record.deadendId };
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function lifecycleEvent(event) {
|
|
74
|
+
return [
|
|
75
|
+
'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
|
|
76
|
+
'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected',
|
|
77
|
+
].includes(event.type);
|
|
78
|
+
}
|
|
79
|
+
export function buildRetrievalProjection(events) {
|
|
80
|
+
const entries = new Map();
|
|
81
|
+
const feedback = new Map();
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
if (event.type === 'record.used') {
|
|
84
|
+
const payload = event.payload;
|
|
85
|
+
const entity = payload.entity === 'knowledge' || payload.entity === 'deadend' ? payload.entity : null;
|
|
86
|
+
const entityId = typeof payload.entityId === 'string' ? payload.entityId : null;
|
|
87
|
+
if (!entity || !entityId)
|
|
88
|
+
continue;
|
|
89
|
+
const key = `${entity}:${entityId}`;
|
|
90
|
+
const prior = feedback.get(key) ?? { usedCount: 0, latestUsedAt: event.occurredAt };
|
|
91
|
+
feedback.set(key, { usedCount: prior.usedCount + 1, latestUsedAt: event.occurredAt });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!lifecycleEvent(event))
|
|
95
|
+
continue;
|
|
96
|
+
const record = recordPayload(event);
|
|
97
|
+
if (!record)
|
|
98
|
+
continue;
|
|
99
|
+
const identity = lifecycleEntity(event, record);
|
|
100
|
+
if (!identity)
|
|
101
|
+
continue;
|
|
102
|
+
const fields = strings(record, identity.entity);
|
|
103
|
+
const normalizedTerms = [...new Set(Object.values(fields).flatMap((value) => {
|
|
104
|
+
const normalizedValue = normalized(value);
|
|
105
|
+
return normalizedValue ? [normalizedValue, ...normalizedValue.split(/\s+/).filter(Boolean)] : [];
|
|
106
|
+
}))].sort();
|
|
107
|
+
const key = `${identity.entity}:${identity.entityId}`;
|
|
108
|
+
entries.set(key, {
|
|
109
|
+
entity: identity.entity,
|
|
110
|
+
entityId: identity.entityId,
|
|
111
|
+
status: typeof record.status === 'string' ? record.status : 'candidate',
|
|
112
|
+
record,
|
|
113
|
+
applicability: applicability(record, event),
|
|
114
|
+
searchable: Object.fromEntries(Object.entries(fields).map(([field, value]) => [field, normalized(value)])),
|
|
115
|
+
normalizedTerms,
|
|
116
|
+
provenance: {
|
|
117
|
+
eventId: event.eventId,
|
|
118
|
+
eventType: event.type,
|
|
119
|
+
occurredAt: event.occurredAt,
|
|
120
|
+
git: event.git,
|
|
121
|
+
...(typeof event.payload.anchorProvenance === 'string' ? { anchorProvenance: event.payload.anchorProvenance } : {}),
|
|
122
|
+
},
|
|
123
|
+
feedback: { usedCount: 0, latestUsedAt: null },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
for (const [key, entry] of entries) {
|
|
127
|
+
const value = feedback.get(key);
|
|
128
|
+
if (value)
|
|
129
|
+
entry.feedback = value;
|
|
130
|
+
}
|
|
131
|
+
const ordered = Object.fromEntries([...entries.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
132
|
+
const terms = new Map();
|
|
133
|
+
for (const [key, entry] of Object.entries(ordered)) {
|
|
134
|
+
for (const term of entry.normalizedTerms)
|
|
135
|
+
terms.set(term, [...(terms.get(term) ?? []), key]);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
schemaVersion: 2,
|
|
139
|
+
entries: ordered,
|
|
140
|
+
terms: Object.fromEntries([...terms.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([term, ids]) => [term, ids.sort()])),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function emptyExclusions() {
|
|
144
|
+
return { status_not_verified: 0, applicability_mismatch: 0, scope_mismatch: 0, query_no_match: 0, result_limit: 0 };
|
|
145
|
+
}
|
|
146
|
+
function applicable(entry, current) {
|
|
147
|
+
if (entry.applicability.branchMode === 'global')
|
|
148
|
+
return true;
|
|
149
|
+
const anchor = entry.provenance.git;
|
|
150
|
+
return anchor.ref === current.ref
|
|
151
|
+
&& anchor.branch === current.branch
|
|
152
|
+
&& (anchor.worktreeId === null || anchor.worktreeId === current.worktreeId);
|
|
153
|
+
}
|
|
154
|
+
function match(entry, terms, entity) {
|
|
155
|
+
const fields = new Set();
|
|
156
|
+
const matched = new Set();
|
|
157
|
+
let score = 0;
|
|
158
|
+
const phrase = terms.join(' ');
|
|
159
|
+
const statementField = entity === 'knowledge' ? 'statement' : 'attempt';
|
|
160
|
+
if (entry.searchable[statementField]?.includes(phrase))
|
|
161
|
+
score += entity === 'knowledge' ? 20 : 0;
|
|
162
|
+
for (const term of terms) {
|
|
163
|
+
for (const [field, value] of Object.entries(entry.searchable)) {
|
|
164
|
+
if (!value.includes(term))
|
|
165
|
+
continue;
|
|
166
|
+
fields.add(field);
|
|
167
|
+
matched.add(term);
|
|
168
|
+
score += field === statementField ? 5 : field === 'scope' ? 4 : 3;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (entry.searchable.scope === phrase)
|
|
172
|
+
score += 10;
|
|
173
|
+
return { score, matchedTerms: [...matched].sort(), matchedFields: [...fields].sort() };
|
|
174
|
+
}
|
|
175
|
+
function limitText(value, maxChars) {
|
|
176
|
+
return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
177
|
+
}
|
|
178
|
+
export function discoverV2Knowledge(index, query, current, options) {
|
|
179
|
+
const exclusions = emptyExclusions();
|
|
180
|
+
const terms = queryTerms(query);
|
|
181
|
+
const hits = Object.values(index.entries).flatMap((entry) => {
|
|
182
|
+
if (entry.entity !== 'knowledge')
|
|
183
|
+
return [];
|
|
184
|
+
if (entry.status !== 'verified') {
|
|
185
|
+
exclusions.status_not_verified += 1;
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
if (!applicable(entry, current)) {
|
|
189
|
+
exclusions.applicability_mismatch += 1;
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const knowledge = entry.record;
|
|
193
|
+
if (options.scope && normalized(knowledge.scope) !== normalized(options.scope)) {
|
|
194
|
+
exclusions.scope_mismatch += 1;
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const matched = match(entry, terms, 'knowledge');
|
|
198
|
+
if (matched.matchedTerms.length === 0) {
|
|
199
|
+
exclusions.query_no_match += 1;
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
return [{ entry, knowledge, ...matched }];
|
|
203
|
+
}).sort((left, right) => right.score - left.score
|
|
204
|
+
|| Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
|
|
205
|
+
|| right.knowledge.confidence - left.knowledge.confidence
|
|
206
|
+
|| right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
|
|
207
|
+
|| left.knowledge.knowledgeId.localeCompare(right.knowledge.knowledgeId));
|
|
208
|
+
const selected = hits.slice(0, options.limit);
|
|
209
|
+
exclusions.result_limit = hits.length - selected.length;
|
|
210
|
+
return {
|
|
211
|
+
result: {
|
|
212
|
+
query,
|
|
213
|
+
total: hits.length,
|
|
214
|
+
truncated: hits.length > selected.length,
|
|
215
|
+
hits: selected.map(({ knowledge, score, matchedTerms, matchedFields, entry }) => ({
|
|
216
|
+
knowledge: { ...knowledge, statement: limitText(knowledge.statement, options.maxChars), supportingEvidence: knowledge.supportingEvidence.map((value) => limitText(value, options.maxChars)) },
|
|
217
|
+
score,
|
|
218
|
+
matchedTerms,
|
|
219
|
+
matchedFields,
|
|
220
|
+
why: [...matchedFields.map((field) => `matched ${field}`), ...(entry.feedback.usedCount > 0 ? ['prior recorded use'] : [])],
|
|
221
|
+
})),
|
|
222
|
+
duplicateIds: [],
|
|
223
|
+
possibleConflicts: [],
|
|
224
|
+
},
|
|
225
|
+
exclusions,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function discoverV2Deadends(index, query, current, options) {
|
|
229
|
+
const exclusions = emptyExclusions();
|
|
230
|
+
const terms = queryTerms(query);
|
|
231
|
+
const hits = Object.values(index.entries).flatMap((entry) => {
|
|
232
|
+
if (entry.entity !== 'deadend')
|
|
233
|
+
return [];
|
|
234
|
+
if (entry.status !== 'verified') {
|
|
235
|
+
exclusions.status_not_verified += 1;
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
if (!applicable(entry, current)) {
|
|
239
|
+
exclusions.applicability_mismatch += 1;
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const deadend = entry.record;
|
|
243
|
+
if (options.scope && normalized(deadend.scope) !== normalized(options.scope)) {
|
|
244
|
+
exclusions.scope_mismatch += 1;
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
const matched = match(entry, terms, 'deadend');
|
|
248
|
+
if (matched.matchedTerms.length === 0) {
|
|
249
|
+
exclusions.query_no_match += 1;
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
return [{ entry, deadend, ...matched }];
|
|
253
|
+
}).sort((left, right) => right.score - left.score
|
|
254
|
+
|| Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
|
|
255
|
+
|| right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
|
|
256
|
+
|| left.deadend.deadendId.localeCompare(right.deadend.deadendId));
|
|
257
|
+
const selected = hits.slice(0, options.limit);
|
|
258
|
+
exclusions.result_limit = hits.length - selected.length;
|
|
259
|
+
return {
|
|
260
|
+
result: {
|
|
261
|
+
query,
|
|
262
|
+
total: hits.length,
|
|
263
|
+
truncated: hits.length > selected.length,
|
|
264
|
+
hits: selected.map(({ deadend, score, matchedTerms, matchedFields }) => ({
|
|
265
|
+
deadend: { ...deadend, attempt: limitText(deadend.attempt, options.maxChars), reason: limitText(deadend.reason, options.maxChars), observedEvidence: deadend.observedEvidence.map((value) => limitText(value, options.maxChars)) },
|
|
266
|
+
score,
|
|
267
|
+
matchedTerms,
|
|
268
|
+
matchedFields,
|
|
269
|
+
})),
|
|
270
|
+
},
|
|
271
|
+
exclusions,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
|
|
2
2
|
export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'git-observer' | 'fs-observer' | 'migration';
|
|
3
|
-
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'projection.advanced' | 'projection.failed';
|
|
3
|
+
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'projection.advanced' | 'projection.failed';
|
|
4
4
|
export interface CognitionGitAnchor {
|
|
5
5
|
worktreeId: string | null;
|
|
6
6
|
branch: string | null;
|
|
@@ -53,6 +53,44 @@ export interface Applicability {
|
|
|
53
53
|
sourceCommit?: string | null;
|
|
54
54
|
dependencyConstraints?: string[];
|
|
55
55
|
}
|
|
56
|
+
export interface CognitionLifecycleTransition {
|
|
57
|
+
entity: 'knowledge' | 'deadend';
|
|
58
|
+
entityId: string;
|
|
59
|
+
from: 'candidate' | 'verified';
|
|
60
|
+
to: 'verified' | 'deprecated' | 'rejected' | 'obsolete';
|
|
61
|
+
reason?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface CognitionUseFeedback {
|
|
64
|
+
feedbackId: string;
|
|
65
|
+
entity: 'knowledge' | 'deadend';
|
|
66
|
+
entityId: string;
|
|
67
|
+
outcome: 'used';
|
|
68
|
+
}
|
|
69
|
+
export interface CognitionRetrievalEntry {
|
|
70
|
+
entity: 'knowledge' | 'deadend';
|
|
71
|
+
entityId: string;
|
|
72
|
+
status: string;
|
|
73
|
+
record: Record<string, unknown>;
|
|
74
|
+
applicability: Applicability;
|
|
75
|
+
searchable: Record<string, string>;
|
|
76
|
+
normalizedTerms: string[];
|
|
77
|
+
provenance: {
|
|
78
|
+
eventId: string;
|
|
79
|
+
eventType: CognitionEventType;
|
|
80
|
+
occurredAt: string;
|
|
81
|
+
git: CognitionGitAnchor;
|
|
82
|
+
anchorProvenance?: CognitionRecordPayload['anchorProvenance'];
|
|
83
|
+
};
|
|
84
|
+
feedback: {
|
|
85
|
+
usedCount: number;
|
|
86
|
+
latestUsedAt: string | null;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export interface CognitionRetrievalProjection {
|
|
90
|
+
schemaVersion: 2;
|
|
91
|
+
entries: Record<string, CognitionRetrievalEntry>;
|
|
92
|
+
terms: Record<string, string[]>;
|
|
93
|
+
}
|
|
56
94
|
export interface BranchWorkspace {
|
|
57
95
|
schemaVersion: 2;
|
|
58
96
|
workspaceKey: string;
|
|
@@ -67,8 +67,16 @@ export async function mirrorNote(projectRoot, note, absorbed = false) {
|
|
|
67
67
|
await mirror(projectRoot, absorbed ? 'note.absorbed' : 'note.recorded', note, note.noteId);
|
|
68
68
|
}
|
|
69
69
|
export async function mirrorKnowledge(projectRoot, knowledge) {
|
|
70
|
-
|
|
70
|
+
const type = knowledge.status === 'verified' ? 'knowledge.verified'
|
|
71
|
+
: knowledge.status === 'deprecated' ? 'knowledge.deprecated'
|
|
72
|
+
: knowledge.status === 'rejected' ? 'knowledge.rejected'
|
|
73
|
+
: 'knowledge.proposed';
|
|
74
|
+
await mirror(projectRoot, type, knowledge, knowledge.knowledgeId);
|
|
71
75
|
}
|
|
72
76
|
export async function mirrorDeadend(projectRoot, deadend) {
|
|
73
|
-
|
|
77
|
+
const type = deadend.status === 'verified' ? 'deadend.verified'
|
|
78
|
+
: deadend.status === 'obsolete' ? 'deadend.obsoleted'
|
|
79
|
+
: deadend.status === 'rejected' ? 'deadend.rejected'
|
|
80
|
+
: 'deadend.proposed';
|
|
81
|
+
await mirror(projectRoot, type, deadend, deadend.deadendId);
|
|
74
82
|
}
|
|
@@ -3,7 +3,9 @@ export declare function learnKnowledge(startPath: string | undefined, statement:
|
|
|
3
3
|
export declare function listKnowledge(startPath?: string, includeUnverified?: boolean): Promise<Knowledge[]>;
|
|
4
4
|
export declare function discoverKnowledge(startPath: string | undefined, query: string, options?: KnowledgeDiscoverOptions): Promise<KnowledgeDiscoveryResult>;
|
|
5
5
|
export declare function verifyKnowledge(startPath: string | undefined, knowledgeId: string): Promise<Knowledge>;
|
|
6
|
+
export declare function transitionKnowledge(startPath: string | undefined, knowledgeId: string, status: 'deprecated' | 'rejected', reason: string): Promise<Knowledge>;
|
|
6
7
|
export declare function recordDeadend(startPath: string | undefined, attempt: string, reason: string, scopeOrInput?: string | DeadendInput): Promise<Deadend>;
|
|
7
8
|
export declare function listDeadends(startPath?: string, includeUnverified?: boolean): Promise<Deadend[]>;
|
|
8
9
|
export declare function discoverDeadends(startPath: string | undefined, query: string, options?: DeadendDiscoverOptions): Promise<DeadendDiscoveryResult>;
|
|
10
|
+
export declare function transitionDeadend(startPath: string | undefined, deadendId: string, status: 'obsolete' | 'rejected', reason: string): Promise<Deadend>;
|
|
9
11
|
export declare function verifyDeadend(startPath: string | undefined, deadendId: string): Promise<Deadend>;
|
|
@@ -35,6 +35,23 @@ function isKnowledgeFile(value) {
|
|
|
35
35
|
function cleanList(values) {
|
|
36
36
|
return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
|
|
37
37
|
}
|
|
38
|
+
function cleanApplicability(input) {
|
|
39
|
+
if (!input)
|
|
40
|
+
return { project: true, branchMode: 'global' };
|
|
41
|
+
if (input.project !== true || !['global', 'exact'].includes(input.branchMode))
|
|
42
|
+
throw new Error('invalid record applicability');
|
|
43
|
+
const branches = cleanList(input.branches);
|
|
44
|
+
const paths = cleanList(input.paths);
|
|
45
|
+
if (input.branchMode === 'exact' && branches.length === 0)
|
|
46
|
+
throw new Error('exact applicability requires a branch');
|
|
47
|
+
return {
|
|
48
|
+
project: true,
|
|
49
|
+
branchMode: input.branchMode,
|
|
50
|
+
...(branches.length > 0 ? { branches } : {}),
|
|
51
|
+
...(paths.length > 0 ? { paths } : {}),
|
|
52
|
+
...(input.sourceCommit === undefined ? {} : { sourceCommit: input.sourceCommit }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
38
55
|
function normalized(value) {
|
|
39
56
|
return value.trim().toLocaleLowerCase();
|
|
40
57
|
}
|
|
@@ -116,6 +133,7 @@ export async function learnKnowledge(startPath, statement, scopeOrInput = 'proje
|
|
|
116
133
|
sourceContextIds: cleanList(input.sourceContextId ? [input.sourceContextId] : []),
|
|
117
134
|
sourceSnapshotIds: cleanList(input.sourceSnapshotId ? [input.sourceSnapshotId] : []),
|
|
118
135
|
supportingEvidence: cleanList(input.evidence),
|
|
136
|
+
applicability: cleanApplicability(input.applicability),
|
|
119
137
|
createdAt: now,
|
|
120
138
|
updatedAt: now,
|
|
121
139
|
};
|
|
@@ -208,6 +226,8 @@ export async function verifyKnowledge(startPath, knowledgeId) {
|
|
|
208
226
|
const item = file.items.find((candidate) => candidate.knowledgeId === knowledgeId);
|
|
209
227
|
if (!item)
|
|
210
228
|
throw new Error(`knowledge not found: ${knowledgeId}`);
|
|
229
|
+
if (item.status !== 'candidate')
|
|
230
|
+
throw new Error(`knowledge must be a candidate to verify: ${knowledgeId}`);
|
|
211
231
|
item.status = 'verified';
|
|
212
232
|
item.confidence = Math.max(item.confidence, 0.8);
|
|
213
233
|
item.updatedAt = new Date().toISOString();
|
|
@@ -216,6 +236,26 @@ export async function verifyKnowledge(startPath, knowledgeId) {
|
|
|
216
236
|
return item;
|
|
217
237
|
});
|
|
218
238
|
}
|
|
239
|
+
export async function transitionKnowledge(startPath, knowledgeId, status, reason) {
|
|
240
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
241
|
+
if (!reason.trim())
|
|
242
|
+
throw new Error('knowledge transition reason must not be empty');
|
|
243
|
+
return withLock(`${locksDirectory(projectRoot)}/knowledge.lock`, async () => {
|
|
244
|
+
const file = await loadKnowledge(projectRoot);
|
|
245
|
+
const item = file.items.find((candidate) => candidate.knowledgeId === knowledgeId);
|
|
246
|
+
if (!item)
|
|
247
|
+
throw new Error(`knowledge not found: ${knowledgeId}`);
|
|
248
|
+
if ((status === 'deprecated' && item.status !== 'verified') || (status === 'rejected' && item.status !== 'candidate')) {
|
|
249
|
+
throw new Error(`invalid knowledge transition: ${item.status} to ${status}`);
|
|
250
|
+
}
|
|
251
|
+
item.status = status;
|
|
252
|
+
item.updatedAt = new Date().toISOString();
|
|
253
|
+
item.governance = { reason: reason.trim(), transitionedAt: item.updatedAt };
|
|
254
|
+
await atomicWriteJson(knowledgePath(projectRoot), file);
|
|
255
|
+
await mirrorKnowledge(projectRoot, item);
|
|
256
|
+
return item;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
219
259
|
export async function recordDeadend(startPath, attempt, reason, scopeOrInput = 'project') {
|
|
220
260
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
221
261
|
const input = typeof scopeOrInput === 'string' ? { scope: scopeOrInput } : scopeOrInput;
|
|
@@ -232,6 +272,7 @@ export async function recordDeadend(startPath, attempt, reason, scopeOrInput = '
|
|
|
232
272
|
status: 'candidate',
|
|
233
273
|
sourceContextId: input.sourceContextId ?? null,
|
|
234
274
|
sourceSnapshotId: input.sourceSnapshotId ?? null,
|
|
275
|
+
applicability: cleanApplicability(input.applicability),
|
|
235
276
|
createdAt: now,
|
|
236
277
|
updatedAt: now,
|
|
237
278
|
};
|
|
@@ -288,6 +329,26 @@ export async function discoverDeadends(startPath, query, options = {}) {
|
|
|
288
329
|
hits: selected.map((item) => ({ ...item, deadend: limitedDeadend(item.deadend, maxChars) })),
|
|
289
330
|
};
|
|
290
331
|
}
|
|
332
|
+
export async function transitionDeadend(startPath, deadendId, status, reason) {
|
|
333
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
334
|
+
if (!reason.trim())
|
|
335
|
+
throw new Error('deadend transition reason must not be empty');
|
|
336
|
+
return withLock(`${locksDirectory(projectRoot)}/deadends.lock`, async () => {
|
|
337
|
+
const file = await loadDeadends(projectRoot);
|
|
338
|
+
const item = file.items.find((candidate) => candidate.deadendId === deadendId);
|
|
339
|
+
if (!item)
|
|
340
|
+
throw new Error(`deadend not found: ${deadendId}`);
|
|
341
|
+
if ((status === 'obsolete' && item.status !== 'verified') || (status === 'rejected' && item.status !== 'candidate')) {
|
|
342
|
+
throw new Error(`invalid deadend transition: ${item.status} to ${status}`);
|
|
343
|
+
}
|
|
344
|
+
item.status = status;
|
|
345
|
+
item.updatedAt = new Date().toISOString();
|
|
346
|
+
item.governance = { reason: reason.trim(), transitionedAt: item.updatedAt };
|
|
347
|
+
await atomicWriteJson(deadendsPath(projectRoot), file);
|
|
348
|
+
await mirrorDeadend(projectRoot, item);
|
|
349
|
+
return item;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
291
352
|
export async function verifyDeadend(startPath, deadendId) {
|
|
292
353
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
293
354
|
return withLock(`${locksDirectory(projectRoot)}/deadends.lock`, async () => {
|
|
@@ -295,6 +356,8 @@ export async function verifyDeadend(startPath, deadendId) {
|
|
|
295
356
|
const item = file.items.find((candidate) => candidate.deadendId === deadendId);
|
|
296
357
|
if (!item)
|
|
297
358
|
throw new Error(`deadend not found: ${deadendId}`);
|
|
359
|
+
if (item.status !== 'candidate')
|
|
360
|
+
throw new Error(`deadend must be a candidate to verify: ${deadendId}`);
|
|
298
361
|
item.status = 'verified';
|
|
299
362
|
item.updatedAt = new Date().toISOString();
|
|
300
363
|
await atomicWriteJson(deadendsPath(projectRoot), file);
|
|
@@ -136,6 +136,13 @@ export interface ActivityResult {
|
|
|
136
136
|
}
|
|
137
137
|
export type KnowledgeStatus = 'candidate' | 'verified' | 'deprecated' | 'rejected';
|
|
138
138
|
export type DeadendStatus = 'candidate' | 'verified' | 'obsolete' | 'rejected';
|
|
139
|
+
export interface RecordApplicability {
|
|
140
|
+
project: true;
|
|
141
|
+
branches?: string[];
|
|
142
|
+
branchMode: 'global' | 'exact';
|
|
143
|
+
paths?: string[];
|
|
144
|
+
sourceCommit?: string | null;
|
|
145
|
+
}
|
|
139
146
|
export interface Knowledge {
|
|
140
147
|
knowledgeId: string;
|
|
141
148
|
kind: string;
|
|
@@ -146,6 +153,11 @@ export interface Knowledge {
|
|
|
146
153
|
sourceContextIds: string[];
|
|
147
154
|
sourceSnapshotIds: string[];
|
|
148
155
|
supportingEvidence: string[];
|
|
156
|
+
applicability?: RecordApplicability;
|
|
157
|
+
governance?: {
|
|
158
|
+
reason: string;
|
|
159
|
+
transitionedAt: string;
|
|
160
|
+
};
|
|
149
161
|
createdAt: string;
|
|
150
162
|
updatedAt: string;
|
|
151
163
|
}
|
|
@@ -155,6 +167,7 @@ export interface KnowledgeInput {
|
|
|
155
167
|
sourceContextId?: string;
|
|
156
168
|
sourceSnapshotId?: string;
|
|
157
169
|
evidence?: string[];
|
|
170
|
+
applicability?: RecordApplicability;
|
|
158
171
|
}
|
|
159
172
|
export interface KnowledgeDiscoverOptions {
|
|
160
173
|
scope?: string;
|
|
@@ -205,6 +218,7 @@ export interface DeadendInput {
|
|
|
205
218
|
sourceContextId?: string;
|
|
206
219
|
sourceSnapshotId?: string;
|
|
207
220
|
evidence?: string[];
|
|
221
|
+
applicability?: RecordApplicability;
|
|
208
222
|
}
|
|
209
223
|
export interface Deadend {
|
|
210
224
|
deadendId: string;
|
|
@@ -215,6 +229,11 @@ export interface Deadend {
|
|
|
215
229
|
status: DeadendStatus;
|
|
216
230
|
sourceContextId: string | null;
|
|
217
231
|
sourceSnapshotId: string | null;
|
|
232
|
+
applicability?: RecordApplicability;
|
|
233
|
+
governance?: {
|
|
234
|
+
reason: string;
|
|
235
|
+
transitionedAt: string;
|
|
236
|
+
};
|
|
218
237
|
createdAt: string;
|
|
219
238
|
updatedAt: string;
|
|
220
239
|
}
|
|
@@ -22,6 +22,7 @@ export declare function cognitionContextsProjectionPath(projectRoot: string): st
|
|
|
22
22
|
export declare function cognitionKnowledgeProjectionPath(projectRoot: string): string;
|
|
23
23
|
export declare function cognitionDeadendsProjectionPath(projectRoot: string): string;
|
|
24
24
|
export declare function cognitionNotesProjectionPath(projectRoot: string): string;
|
|
25
|
+
export declare function cognitionRetrievalProjectionPath(projectRoot: string): string;
|
|
25
26
|
export declare function cognitionMigrationsDirectory(projectRoot: string): string;
|
|
26
27
|
export declare function cognitionV1ImportPath(projectRoot: string): string;
|
|
27
28
|
export declare function cognitionRuntimeDirectory(projectRoot: string): string;
|
|
@@ -71,6 +71,9 @@ export function cognitionDeadendsProjectionPath(projectRoot) {
|
|
|
71
71
|
export function cognitionNotesProjectionPath(projectRoot) {
|
|
72
72
|
return path.join(cognitionProjectionsDirectory(projectRoot), 'notes.json');
|
|
73
73
|
}
|
|
74
|
+
export function cognitionRetrievalProjectionPath(projectRoot) {
|
|
75
|
+
return path.join(cognitionProjectionsDirectory(projectRoot), 'retrieval.json');
|
|
76
|
+
}
|
|
74
77
|
export function cognitionMigrationsDirectory(projectRoot) {
|
|
75
78
|
return path.join(cognitionRoot(projectRoot), 'migrations');
|
|
76
79
|
}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.6.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluffy-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Local context management CLI and MCP tools for AI coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "FluffyChi-Xing",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsc -p tsconfig.json",
|
|
57
57
|
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
58
|
-
"test": "tsc -p tsconfig.test.json && node --test dist/test/*.test.js",
|
|
58
|
+
"test": "tsc -p tsconfig.test.json && node --test --test-concurrency=1 dist/test/*.test.js",
|
|
59
59
|
"pack:check": "tsc -p tsconfig.test.json && node --test dist/test/package-smoke.test.js",
|
|
60
60
|
"prepack": "npm run build",
|
|
61
61
|
"prepublishOnly": "npm test && npm run pack:check",
|