fluffy-context 0.5.0 → 0.7.2
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 +121 -9
- package/dist/src/agent/api.d.ts +5 -1
- package/dist/src/agent/api.js +41 -12
- package/dist/src/agent/index.d.ts +3 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/agent/types.d.ts +21 -0
- package/dist/src/cli/main.js +150 -12
- package/dist/src/cognition/feedback.d.ts +9 -0
- package/dist/src/cognition/feedback.js +30 -0
- package/dist/src/cognition/filesystem-observer.d.ts +41 -0
- package/dist/src/cognition/filesystem-observer.js +164 -0
- package/dist/src/cognition/git-observer.d.ts +1 -0
- package/dist/src/cognition/git-observer.js +3 -2
- package/dist/src/cognition/journal.d.ts +4 -1
- package/dist/src/cognition/journal.js +83 -4
- package/dist/src/cognition/migration-v1.js +13 -11
- package/dist/src/cognition/orient.d.ts +4 -0
- package/dist/src/cognition/orient.js +14 -4
- package/dist/src/cognition/policy.d.ts +4 -0
- package/dist/src/cognition/policy.js +49 -0
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/cognition/retrieval.d.ts +26 -0
- package/dist/src/cognition/retrieval.js +273 -0
- package/dist/src/cognition/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +55 -1
- package/dist/src/cognition/v1-bridge.js +13 -4
- package/dist/src/compiler/activity.d.ts +3 -0
- package/dist/src/compiler/activity.js +79 -0
- package/dist/src/compiler/compile.d.ts +2 -0
- package/dist/src/compiler/compile.js +330 -0
- package/dist/src/compiler/sources.d.ts +19 -0
- package/dist/src/compiler/sources.js +130 -0
- package/dist/src/compiler/types.d.ts +187 -0
- package/dist/src/compiler/types.js +1 -0
- package/dist/src/hooks/claude-code.js +7 -15
- package/dist/src/mcp/main.js +2 -0
- package/dist/src/mcp/server.js +2 -0
- 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 +2 -0
- package/dist/src/storage/layout.js +6 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
- package/skills/fluffy-context/SKILL.md +2 -1
|
@@ -5,6 +5,15 @@ import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
|
5
5
|
import { loadManifest } from '../runtime/init.js';
|
|
6
6
|
import { withLock } from '../storage/lock.js';
|
|
7
7
|
import { cognitionJournalDirectory, locksDirectory } from '../storage/layout.js';
|
|
8
|
+
const eventTypes = new Set([
|
|
9
|
+
'runtime.enabled', 'runtime.started', 'runtime.stopped', 'host.session.started', 'host.prompt.submitted',
|
|
10
|
+
'git.state.observed', 'git.branch.changed', 'git.head.changed', 'workspace.paths.changed',
|
|
11
|
+
'context.checkpoint.imported', 'context.state.recorded', 'note.recorded', 'note.resolved', 'note.absorbed',
|
|
12
|
+
'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
|
|
13
|
+
'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected', 'record.used',
|
|
14
|
+
'projection.advanced', 'projection.failed',
|
|
15
|
+
]);
|
|
16
|
+
const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'git-observer', 'fs-observer', 'migration']);
|
|
8
17
|
const segmentName = '00000001.ndjson';
|
|
9
18
|
function canonical(value) {
|
|
10
19
|
if (value === null || typeof value !== 'object')
|
|
@@ -33,6 +42,49 @@ async function journalFiles(projectRoot) {
|
|
|
33
42
|
throw error;
|
|
34
43
|
}
|
|
35
44
|
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function validGitAnchor(value) {
|
|
49
|
+
return isRecord(value)
|
|
50
|
+
&& (typeof value.worktreeId === 'string' || value.worktreeId === null)
|
|
51
|
+
&& (typeof value.branch === 'string' || value.branch === null)
|
|
52
|
+
&& (typeof value.ref === 'string' || value.ref === null)
|
|
53
|
+
&& (typeof value.head === 'string' || value.head === null);
|
|
54
|
+
}
|
|
55
|
+
function validIsoDate(value) {
|
|
56
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
57
|
+
}
|
|
58
|
+
function validateEventShape(event, projectId, file, line) {
|
|
59
|
+
if (!isRecord(event)
|
|
60
|
+
|| event.schemaVersion !== 2
|
|
61
|
+
|| typeof event.eventId !== 'string'
|
|
62
|
+
|| event.eventId.length === 0
|
|
63
|
+
|| typeof event.type !== 'string'
|
|
64
|
+
|| !eventTypes.has(event.type)
|
|
65
|
+
|| !validIsoDate(event.occurredAt)
|
|
66
|
+
|| !validIsoDate(event.observedAt)
|
|
67
|
+
|| event.projectId !== projectId
|
|
68
|
+
|| !isRecord(event.source)
|
|
69
|
+
|| typeof event.source.kind !== 'string'
|
|
70
|
+
|| !sourceKinds.has(event.source.kind)
|
|
71
|
+
|| (event.source.instanceId !== undefined && typeof event.source.instanceId !== 'string')
|
|
72
|
+
|| (event.correlationId !== undefined && typeof event.correlationId !== 'string')
|
|
73
|
+
|| typeof event.idempotencyKey !== 'string'
|
|
74
|
+
|| event.idempotencyKey.length === 0
|
|
75
|
+
|| !validGitAnchor(event.git)
|
|
76
|
+
|| !isRecord(event.privacy)
|
|
77
|
+
|| (event.privacy.classification !== 'metadata' && event.privacy.classification !== 'redacted-text')
|
|
78
|
+
|| typeof event.privacy.redactionCount !== 'number'
|
|
79
|
+
|| !Number.isSafeInteger(event.privacy.redactionCount)
|
|
80
|
+
|| event.privacy.redactionCount < 0
|
|
81
|
+
|| !isRecord(event.payload)
|
|
82
|
+
|| (event.previousHash !== null && typeof event.previousHash !== 'string')
|
|
83
|
+
|| typeof event.hash !== 'string'
|
|
84
|
+
|| event.hash.length === 0) {
|
|
85
|
+
throw new Error(`invalid journal record schema at ${file}:${line}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
36
88
|
async function recoverPartialTail(projectRoot) {
|
|
37
89
|
const files = await journalFiles(projectRoot);
|
|
38
90
|
const file = files.at(-1);
|
|
@@ -46,13 +98,15 @@ async function recoverPartialTail(projectRoot) {
|
|
|
46
98
|
const tail = content.slice(boundary + 1);
|
|
47
99
|
try {
|
|
48
100
|
JSON.parse(tail);
|
|
101
|
+
await appendFile(filePath, '\n', 'utf8');
|
|
49
102
|
}
|
|
50
103
|
catch {
|
|
51
104
|
await truncate(filePath, boundary < 0 ? 0 : boundary + 1);
|
|
52
105
|
}
|
|
53
106
|
}
|
|
54
|
-
export async function readJournal(startPath) {
|
|
107
|
+
export async function readJournal(startPath, options = {}) {
|
|
55
108
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
109
|
+
const projectId = (await loadManifest(projectRoot)).projectId;
|
|
56
110
|
const events = [];
|
|
57
111
|
let previousHash = null;
|
|
58
112
|
for (const file of await journalFiles(projectRoot)) {
|
|
@@ -67,12 +121,13 @@ export async function readJournal(startPath) {
|
|
|
67
121
|
event = JSON.parse(line);
|
|
68
122
|
}
|
|
69
123
|
catch (error) {
|
|
70
|
-
if (index === lines.length - 1 && !content.endsWith('\n'))
|
|
124
|
+
if (options.allowPartialTail !== false && index === lines.length - 1 && !content.endsWith('\n'))
|
|
71
125
|
break;
|
|
72
126
|
throw new Error(`invalid journal record ${file}:${index + 1}: ${error instanceof Error ? error.message : 'invalid JSON'}`);
|
|
73
127
|
}
|
|
128
|
+
validateEventShape(event, projectId, file, index + 1);
|
|
74
129
|
const { hash, ...withoutHash } = event;
|
|
75
|
-
if (event.
|
|
130
|
+
if (event.previousHash !== previousHash || hash !== eventHash(withoutHash)) {
|
|
76
131
|
throw new Error(`invalid journal hash chain at ${file}:${index + 1}`);
|
|
77
132
|
}
|
|
78
133
|
previousHash = event.hash;
|
|
@@ -81,14 +136,38 @@ export async function readJournal(startPath) {
|
|
|
81
136
|
}
|
|
82
137
|
return events;
|
|
83
138
|
}
|
|
139
|
+
export async function verifyJournal(startPath) {
|
|
140
|
+
return readJournal(startPath, { allowPartialTail: false });
|
|
141
|
+
}
|
|
84
142
|
export async function appendEvent(startPath, input) {
|
|
85
143
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
86
144
|
return withLock(`${locksDirectory(projectRoot)}/cognition-journal.lock`, async () => {
|
|
87
145
|
await recoverPartialTail(projectRoot);
|
|
88
146
|
const existing = await readJournal(projectRoot);
|
|
89
147
|
const duplicate = existing.find((event) => event.idempotencyKey === input.idempotencyKey);
|
|
90
|
-
if (duplicate)
|
|
148
|
+
if (duplicate) {
|
|
149
|
+
const candidate = {
|
|
150
|
+
type: input.type,
|
|
151
|
+
source: input.source ?? { kind: 'cli' },
|
|
152
|
+
correlationId: input.correlationId,
|
|
153
|
+
idempotencyKey: input.idempotencyKey,
|
|
154
|
+
git: input.git ?? { worktreeId: null, branch: null, ref: null, head: null },
|
|
155
|
+
privacy: input.privacy ?? { classification: 'metadata', redactionCount: 0 },
|
|
156
|
+
payload: input.payload,
|
|
157
|
+
};
|
|
158
|
+
const existingSemantic = {
|
|
159
|
+
type: duplicate.type,
|
|
160
|
+
source: duplicate.source,
|
|
161
|
+
correlationId: duplicate.correlationId,
|
|
162
|
+
idempotencyKey: duplicate.idempotencyKey,
|
|
163
|
+
git: duplicate.git,
|
|
164
|
+
privacy: duplicate.privacy,
|
|
165
|
+
payload: duplicate.payload,
|
|
166
|
+
};
|
|
167
|
+
if (canonical(candidate) !== canonical(existingSemantic))
|
|
168
|
+
throw new Error(`idempotency key conflict: ${input.idempotencyKey}`);
|
|
91
169
|
return { event: duplicate, appended: false };
|
|
170
|
+
}
|
|
92
171
|
const now = new Date().toISOString();
|
|
93
172
|
const eventWithoutHash = {
|
|
94
173
|
eventId: input.eventId ?? eventId(),
|
|
@@ -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');
|
|
@@ -95,7 +105,7 @@ async function sourceRecords(projectRoot) {
|
|
|
95
105
|
async function writeV2Metadata(projectRoot) {
|
|
96
106
|
const v1 = await loadManifest(projectRoot);
|
|
97
107
|
const manifest = { schemaVersion: 2, layoutVersion: COGNITION_LAYOUT_VERSION, projectId: v1.projectId, createdAt: new Date().toISOString(), policyVersion: 1 };
|
|
98
|
-
const policy = { schemaVersion: 2, enabled:
|
|
108
|
+
const policy = { schemaVersion: 2, enabled: true, autoStart: true, allowHookStartup: true, inactivityTimeoutMs: 300_000, promptIntentMaxChars: 500, filesystemEventsEnabled: false, retentionMaxBytes: 64 * 1024 * 1024 };
|
|
99
109
|
await Promise.all([atomicWriteJson(cognitionManifestPath(projectRoot), manifest), atomicWriteJson(cognitionPolicyPath(projectRoot), policy)]);
|
|
100
110
|
}
|
|
101
111
|
async function quarantineReadiness(projectRoot, readiness) {
|
|
@@ -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>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
3
|
-
import { cognitionManifestPath
|
|
3
|
+
import { cognitionManifestPath } from '../storage/layout.js';
|
|
4
4
|
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';
|
|
9
|
+
import { loadRuntimePolicy } from './policy.js';
|
|
8
10
|
import { v1Synced } from './v1-bridge.js';
|
|
9
11
|
function contextReference(value) {
|
|
10
12
|
const boundary = value.lastIndexOf(':');
|
|
@@ -47,7 +49,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
47
49
|
const [manifest, cognition, policy] = await Promise.all([
|
|
48
50
|
loadManifest(projectRoot),
|
|
49
51
|
readJson(cognitionManifestPath(projectRoot)),
|
|
50
|
-
|
|
52
|
+
loadRuntimePolicy(projectRoot),
|
|
51
53
|
]);
|
|
52
54
|
if (cognition.schemaVersion !== 2 || cognition.projectId !== manifest.projectId || !policy.enabled || !await v1Synced(projectRoot))
|
|
53
55
|
return null;
|
|
@@ -56,6 +58,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
56
58
|
return null;
|
|
57
59
|
const projection = projectEvents(events);
|
|
58
60
|
let selected = null;
|
|
61
|
+
let selection = 'requested-context';
|
|
59
62
|
if (contextId) {
|
|
60
63
|
const matching = [...Object.keys(projection.contexts)].reverse().find((reference) => contextReference(reference)?.contextId === contextId);
|
|
61
64
|
selected = matching ? contextReference(matching) : null;
|
|
@@ -65,10 +68,17 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
65
68
|
const legacy = Object.values(projection.workspaces).find((workspace) => workspace.worktreeId === null && (workspace.ref === current.ref || workspace.branch === current.branch));
|
|
66
69
|
const reference = (exact?.activeContextRef ? exact : legacy)?.activeContextRef;
|
|
67
70
|
selected = reference ? contextReference(reference) : null;
|
|
71
|
+
selection = exact?.activeContextRef ? 'exact-worktree-ref' : 'legacy-ref-branch';
|
|
68
72
|
}
|
|
69
73
|
if (!selected || !projection.contexts[`${selected.contextId}:${selected.snapshotId}`])
|
|
70
74
|
return null;
|
|
71
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
...selected,
|
|
77
|
+
...latestRecords(events, current, selected.contextId),
|
|
78
|
+
retrieval: buildRetrievalProjection(events),
|
|
79
|
+
currentGit: { worktreeId: current.worktreeId, branch: current.branch, ref: current.ref, head: current.commit },
|
|
80
|
+
selection,
|
|
81
|
+
};
|
|
72
82
|
}
|
|
73
83
|
catch {
|
|
74
84
|
return null;
|
|
@@ -77,7 +87,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
77
87
|
export async function v2Enabled(projectRoot) {
|
|
78
88
|
try {
|
|
79
89
|
await access(cognitionManifestPath(projectRoot));
|
|
80
|
-
return (await
|
|
90
|
+
return (await loadRuntimePolicy(projectRoot)).enabled;
|
|
81
91
|
}
|
|
82
92
|
catch {
|
|
83
93
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CognitionRuntimePolicy } from './types.js';
|
|
2
|
+
export declare function normalizeRuntimePolicy(value: Partial<CognitionRuntimePolicy>): CognitionRuntimePolicy;
|
|
3
|
+
export declare function loadRuntimePolicy(projectRoot: string): Promise<CognitionRuntimePolicy>;
|
|
4
|
+
export declare function writeRuntimePolicy(projectRoot: string, policy: CognitionRuntimePolicy): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
2
|
+
import { cognitionPolicyPath } from '../storage/layout.js';
|
|
3
|
+
import { readJson } from '../storage/json-store.js';
|
|
4
|
+
const defaults = {
|
|
5
|
+
schemaVersion: 2,
|
|
6
|
+
enabled: true,
|
|
7
|
+
autoStart: true,
|
|
8
|
+
allowHookStartup: true,
|
|
9
|
+
inactivityTimeoutMs: 300_000,
|
|
10
|
+
promptIntentMaxChars: 500,
|
|
11
|
+
filesystemEventsEnabled: false,
|
|
12
|
+
retentionMaxBytes: 64 * 1024 * 1024,
|
|
13
|
+
};
|
|
14
|
+
function booleanValue(value, fallback, field) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return fallback;
|
|
17
|
+
if (typeof value !== 'boolean')
|
|
18
|
+
throw new Error(`runtime policy field ${field} must be boolean`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function nonNegativeInteger(value, fallback, field) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return fallback;
|
|
24
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
25
|
+
throw new Error(`runtime policy field ${field} must be a non-negative integer`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeRuntimePolicy(value) {
|
|
29
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
throw new Error('runtime policy must be an object');
|
|
31
|
+
if (value.schemaVersion !== undefined && value.schemaVersion !== 2)
|
|
32
|
+
throw new Error('unsupported runtime policy schema');
|
|
33
|
+
return {
|
|
34
|
+
...defaults,
|
|
35
|
+
enabled: booleanValue(value.enabled, defaults.enabled, 'enabled'),
|
|
36
|
+
autoStart: booleanValue(value.autoStart, defaults.autoStart, 'autoStart'),
|
|
37
|
+
allowHookStartup: booleanValue(value.allowHookStartup, defaults.allowHookStartup, 'allowHookStartup'),
|
|
38
|
+
inactivityTimeoutMs: nonNegativeInteger(value.inactivityTimeoutMs, defaults.inactivityTimeoutMs, 'inactivityTimeoutMs'),
|
|
39
|
+
promptIntentMaxChars: nonNegativeInteger(value.promptIntentMaxChars, defaults.promptIntentMaxChars, 'promptIntentMaxChars'),
|
|
40
|
+
filesystemEventsEnabled: booleanValue(value.filesystemEventsEnabled, defaults.filesystemEventsEnabled, 'filesystemEventsEnabled'),
|
|
41
|
+
retentionMaxBytes: nonNegativeInteger(value.retentionMaxBytes, defaults.retentionMaxBytes, 'retentionMaxBytes'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function loadRuntimePolicy(projectRoot) {
|
|
45
|
+
return normalizeRuntimePolicy(await readJson(cognitionPolicyPath(projectRoot)));
|
|
46
|
+
}
|
|
47
|
+
export async function writeRuntimePolicy(projectRoot, policy) {
|
|
48
|
+
await atomicWriteJson(cognitionPolicyPath(projectRoot), normalizeRuntimePolicy(policy));
|
|
49
|
+
}
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
@@ -3,10 +3,29 @@ export interface RuntimeServer {
|
|
|
3
3
|
lease: RuntimeLease;
|
|
4
4
|
close(reason?: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
interface RuntimeServeOptions {
|
|
7
|
+
inactivityTimeoutMs?: number;
|
|
8
|
+
heartbeatIntervalMs?: number;
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface EnsureRuntimeOptions {
|
|
12
|
+
source?: 'claude-hook' | 'mcp' | 'cli';
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureRuntimeResult {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
started: boolean;
|
|
18
|
+
running: boolean;
|
|
19
|
+
skipped: 'disabled' | 'not-migrated' | 'already-running' | 'startup-in-progress' | 'startup-failed' | 'unavailable' | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function serveRuntime(startPath?: string, options?: RuntimeServeOptions): Promise<RuntimeServer>;
|
|
7
22
|
export declare function runtimeStatus(startPath?: string): Promise<RuntimeStatus>;
|
|
8
23
|
export declare function enableRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
9
24
|
export declare function disableRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
25
|
+
export declare function setFilesystemEvents(startPath: string | undefined, enabled: boolean): Promise<RuntimeStatus>;
|
|
10
26
|
export declare function startRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
27
|
+
export declare function ensureRuntimeForSession(startPath?: string, options?: EnsureRuntimeOptions): Promise<EnsureRuntimeResult>;
|
|
11
28
|
export declare function stopRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
12
29
|
export declare function observeRuntime(startPath?: string): Promise<boolean>;
|
|
30
|
+
export declare function touchRuntime(startPath?: string): Promise<boolean>;
|
|
31
|
+
export {};
|