linksee-memory 0.16.1 → 0.17.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 +1081 -1044
- package/dist/bin/setup.js +14 -14
- package/dist/bin/share-draft.d.ts +2 -0
- package/dist/bin/share-draft.js +90 -0
- package/dist/db/migrate.js +28 -28
- package/dist/lib/drift-anchors.js +3 -3
- package/dist/lib/drift-detection.js +20 -20
- package/dist/lib/drift-view.js +16 -16
- package/dist/lib/map-import.js +17 -17
- package/dist/lib/share-draft.d.ts +56 -0
- package/dist/lib/share-draft.js +234 -0
- package/dist/mcp/prompts.js +58 -58
- package/dist/mcp/resources.js +7 -7
- package/dist/mcp/sampling.js +20 -20
- package/dist/mcp/server.js +183 -136
- package/package.json +107 -106
package/dist/bin/setup.js
CHANGED
|
@@ -49,20 +49,20 @@ const noGuard = args.includes('--no-guard');
|
|
|
49
49
|
const projectGuard = args.includes('--project-guard');
|
|
50
50
|
const showHelp = args.includes('--help') || args.includes('-h');
|
|
51
51
|
if (showHelp) {
|
|
52
|
-
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
53
|
-
|
|
54
|
-
Usage:
|
|
55
|
-
npx linksee-memory setup Interactive setup
|
|
56
|
-
npx linksee-memory setup --yes Accept all defaults, no prompts
|
|
57
|
-
npx linksee-memory setup --dry-run Show what would happen
|
|
58
|
-
|
|
59
|
-
What it does:
|
|
60
|
-
1. Registers linksee-memory MCP server with Claude Code
|
|
61
|
-
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
62
|
-
3. Configures Stop hook (auto-captures every session)
|
|
63
|
-
4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
64
|
-
|
|
65
|
-
After setup, just chat with Claude Code normally.
|
|
52
|
+
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
npx linksee-memory setup Interactive setup
|
|
56
|
+
npx linksee-memory setup --yes Accept all defaults, no prompts
|
|
57
|
+
npx linksee-memory setup --dry-run Show what would happen
|
|
58
|
+
|
|
59
|
+
What it does:
|
|
60
|
+
1. Registers linksee-memory MCP server with Claude Code
|
|
61
|
+
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
62
|
+
3. Configures Stop hook (auto-captures every session)
|
|
63
|
+
4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
64
|
+
|
|
65
|
+
After setup, just chat with Claude Code normally.
|
|
66
66
|
Add "Use Linksee" to any prompt to trigger memory recall.`);
|
|
67
67
|
process.exit(0);
|
|
68
68
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory share-draft — print a link to KanseiLINK's reviewed draft page for a field note.
|
|
3
|
+
// Same checks and the same builder as the MCP tool `share_draft` (src/lib/share-draft.ts).
|
|
4
|
+
// Makes no network call and writes nothing: it prints a URL, and you open it yourself.
|
|
5
|
+
//
|
|
6
|
+
// npx -y linksee-memory share-draft --help
|
|
7
|
+
// npx -y linksee-memory share-draft --service freee --task "..." --symptom "..." \
|
|
8
|
+
// --tried "..." --result "..." --kind execution [--conditions "..."] [--runtime claude] [--json]
|
|
9
|
+
import { buildShareDraft, ALLOWED_KEYS, KINDS, RUNTIMES, NOTICE, NOTICE_EN, DRAFT_PAGE, URL_MAX } from '../lib/share-draft.js';
|
|
10
|
+
const HELP = `linksee-memory share-draft — make a link to a reviewed draft of a fix, for other agents
|
|
11
|
+
|
|
12
|
+
Linksee sends nothing. This prints a URL to ${DRAFT_PAGE}
|
|
13
|
+
The page opens with the fields filled; you read them there, and only your click sends.
|
|
14
|
+
If you never run this (or the MCP tool share_draft), nothing happens.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
npx -y linksee-memory share-draft --service <id> --task <text> --symptom <text> \\
|
|
18
|
+
--tried <text> --result <text> --kind <${KINDS.join('|')}> \\
|
|
19
|
+
[--conditions <text>] [--runtime <${RUNTIMES.join('|')}>] [--json]
|
|
20
|
+
|
|
21
|
+
Fields (nothing else is accepted):
|
|
22
|
+
--service KanseiLINK service_id, lowercase: ^[a-z0-9_.-]{1,160}$ (required)
|
|
23
|
+
--task what you were trying to do, ≤300 chars (required)
|
|
24
|
+
--symptom the KIND of error, no values, ≤600 chars (required)
|
|
25
|
+
--tried steps you tried, ≤2,000 chars (required)
|
|
26
|
+
--result how it was solved / that it was not, ≤1,000 chars (required)
|
|
27
|
+
--conditions tool + version, kind of environment, ≤600 chars
|
|
28
|
+
--kind execution | documentation | hypothesis (required)
|
|
29
|
+
--runtime claude | codex | gpt | gemini | other
|
|
30
|
+
|
|
31
|
+
Stops (no link) when a field looks like a token / API key / JWT / Bearer value, an e-mail,
|
|
32
|
+
a tenant URL (acme.slack.com …), an IP address, a local path (C:\\, /Users/, /home/), or a
|
|
33
|
+
long hex / base64 run (32+), and says which field. The whole URL must be ≤${URL_MAX} chars
|
|
34
|
+
(one Japanese character takes 9) — it asks you to shorten; it never cuts.
|
|
35
|
+
|
|
36
|
+
${NOTICE[0]}
|
|
37
|
+
${NOTICE[1]}
|
|
38
|
+
`;
|
|
39
|
+
function parse(argv) {
|
|
40
|
+
const fields = {};
|
|
41
|
+
const bad = [];
|
|
42
|
+
let json = false;
|
|
43
|
+
let help = false;
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a === '-h' || a === '--help') {
|
|
47
|
+
help = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (a === '--json') {
|
|
51
|
+
json = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const m = /^--([a-z_]+)(?:=(.*))?$/s.exec(a);
|
|
55
|
+
if (!m) {
|
|
56
|
+
bad.push(a);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const key = m[1];
|
|
60
|
+
const value = m[2] ?? argv[++i];
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
bad.push(a);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
fields[key] = value; // unknown keys are reported by the builder
|
|
66
|
+
}
|
|
67
|
+
return { fields, json, help, bad };
|
|
68
|
+
}
|
|
69
|
+
const { fields, json, help, bad } = parse(process.argv.slice(2));
|
|
70
|
+
if (help || (Object.keys(fields).length === 0 && bad.length === 0)) {
|
|
71
|
+
process.stdout.write(HELP);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
if (bad.length) {
|
|
75
|
+
process.stderr.write(`Could not read: ${bad.join(' ')}\nFields: ${ALLOWED_KEYS.map((k) => '--' + k).join(' ')}\n`);
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
const r = buildShareDraft(fields);
|
|
79
|
+
if (json) {
|
|
80
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
81
|
+
process.exit(r.ok ? 0 : 1);
|
|
82
|
+
}
|
|
83
|
+
if (!r.ok) {
|
|
84
|
+
process.stderr.write('No link made.\n');
|
|
85
|
+
for (const p of r.problems)
|
|
86
|
+
process.stderr.write(` ${p.field}: ${p.reason}\n`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(`${r.url}\n\n${NOTICE[0]}\n${NOTICE[1]}\n${NOTICE_EN[0]}\n${NOTICE_EN[1]}\n`);
|
|
90
|
+
//# sourceMappingURL=share-draft.js.map
|
package/dist/db/migrate.js
CHANGED
|
@@ -70,11 +70,11 @@ export function runMigrations(db) {
|
|
|
70
70
|
// v3 → v4: rebuild memories_fts with trigram tokenizer for JP/CJK support.
|
|
71
71
|
// Only runs when upgrading an existing DB from schema v1-3.
|
|
72
72
|
if (currentVersion > 0 && currentVersion < 4) {
|
|
73
|
-
db.exec(`
|
|
74
|
-
DROP TRIGGER IF EXISTS trg_memories_fts_ai;
|
|
75
|
-
DROP TRIGGER IF EXISTS trg_memories_fts_ad;
|
|
76
|
-
DROP TRIGGER IF EXISTS trg_memories_fts_au;
|
|
77
|
-
DROP TABLE IF EXISTS memories_fts;
|
|
73
|
+
db.exec(`
|
|
74
|
+
DROP TRIGGER IF EXISTS trg_memories_fts_ai;
|
|
75
|
+
DROP TRIGGER IF EXISTS trg_memories_fts_ad;
|
|
76
|
+
DROP TRIGGER IF EXISTS trg_memories_fts_au;
|
|
77
|
+
DROP TABLE IF EXISTS memories_fts;
|
|
78
78
|
`);
|
|
79
79
|
}
|
|
80
80
|
// v4 → v5: add normalized_name column BEFORE schema.sql runs,
|
|
@@ -118,9 +118,9 @@ export function runMigrations(db) {
|
|
|
118
118
|
db.exec('ALTER TABLE memories ADD COLUMN thread_id TEXT');
|
|
119
119
|
}
|
|
120
120
|
// Backfill thread_id from content JSON session_id for existing memories
|
|
121
|
-
db.exec(`
|
|
122
|
-
UPDATE memories SET thread_id = json_extract(content, '$.session_id')
|
|
123
|
-
WHERE thread_id IS NULL AND json_valid(content) AND json_extract(content, '$.session_id') IS NOT NULL
|
|
121
|
+
db.exec(`
|
|
122
|
+
UPDATE memories SET thread_id = json_extract(content, '$.session_id')
|
|
123
|
+
WHERE thread_id IS NULL AND json_valid(content) AND json_extract(content, '$.session_id') IS NOT NULL
|
|
124
124
|
`);
|
|
125
125
|
}
|
|
126
126
|
// v8 → v9: ProjectCoreNode — extend drift_anchors into the Current Truth Map node.
|
|
@@ -221,12 +221,12 @@ function migrateV5EntityNormalization(db) {
|
|
|
221
221
|
}
|
|
222
222
|
})();
|
|
223
223
|
// 4. Auto-merge duplicate entities (same kind + normalized_name)
|
|
224
|
-
const dupes = db.prepare(`
|
|
225
|
-
SELECT kind, normalized_name, GROUP_CONCAT(id) as ids
|
|
226
|
-
FROM entities
|
|
227
|
-
WHERE normalized_name IS NOT NULL
|
|
228
|
-
GROUP BY kind, normalized_name
|
|
229
|
-
HAVING COUNT(*) > 1
|
|
224
|
+
const dupes = db.prepare(`
|
|
225
|
+
SELECT kind, normalized_name, GROUP_CONCAT(id) as ids
|
|
226
|
+
FROM entities
|
|
227
|
+
WHERE normalized_name IS NOT NULL
|
|
228
|
+
GROUP BY kind, normalized_name
|
|
229
|
+
HAVING COUNT(*) > 1
|
|
230
230
|
`).all();
|
|
231
231
|
if (dupes.length > 0) {
|
|
232
232
|
console.log(`[linksee-memory] v5 migration: merging ${dupes.length} duplicate entity clusters`);
|
|
@@ -247,12 +247,12 @@ function mergeEntityCluster(db, ids) {
|
|
|
247
247
|
if (ids.length < 2)
|
|
248
248
|
return;
|
|
249
249
|
// Score each entity: prefer most memories, then has canonical_key, then lowest id
|
|
250
|
-
const rows = db.prepare(`
|
|
251
|
-
SELECT e.id, e.name, e.canonical_key, COUNT(m.id) as mem_count
|
|
252
|
-
FROM entities e LEFT JOIN memories m ON m.entity_id = e.id
|
|
253
|
-
WHERE e.id IN (${ids.map(() => '?').join(',')})
|
|
254
|
-
GROUP BY e.id
|
|
255
|
-
ORDER BY mem_count DESC, (e.canonical_key IS NOT NULL) DESC, e.id ASC
|
|
250
|
+
const rows = db.prepare(`
|
|
251
|
+
SELECT e.id, e.name, e.canonical_key, COUNT(m.id) as mem_count
|
|
252
|
+
FROM entities e LEFT JOIN memories m ON m.entity_id = e.id
|
|
253
|
+
WHERE e.id IN (${ids.map(() => '?').join(',')})
|
|
254
|
+
GROUP BY e.id
|
|
255
|
+
ORDER BY mem_count DESC, (e.canonical_key IS NOT NULL) DESC, e.id ASC
|
|
256
256
|
`).all(...ids);
|
|
257
257
|
const keep = rows[0];
|
|
258
258
|
const mergeIds = rows.slice(1).map(r => r.id);
|
|
@@ -273,16 +273,16 @@ function mergeEntityCluster(db, ids) {
|
|
|
273
273
|
db.prepare('UPDATE events SET entity_id = ? WHERE entity_id = ?').run(keep.id, mid);
|
|
274
274
|
// Reassign edges (both directions)
|
|
275
275
|
// Handle UNIQUE constraint: delete duplicates first
|
|
276
|
-
db.prepare(`
|
|
277
|
-
DELETE FROM edges WHERE from_id = ? AND EXISTS (
|
|
278
|
-
SELECT 1 FROM edges e2 WHERE e2.from_id = ? AND e2.to_id = edges.to_id AND e2.relation = edges.relation
|
|
279
|
-
)
|
|
276
|
+
db.prepare(`
|
|
277
|
+
DELETE FROM edges WHERE from_id = ? AND EXISTS (
|
|
278
|
+
SELECT 1 FROM edges e2 WHERE e2.from_id = ? AND e2.to_id = edges.to_id AND e2.relation = edges.relation
|
|
279
|
+
)
|
|
280
280
|
`).run(mid, keep.id);
|
|
281
281
|
db.prepare('UPDATE edges SET from_id = ? WHERE from_id = ?').run(keep.id, mid);
|
|
282
|
-
db.prepare(`
|
|
283
|
-
DELETE FROM edges WHERE to_id = ? AND EXISTS (
|
|
284
|
-
SELECT 1 FROM edges e2 WHERE e2.to_id = ? AND e2.from_id = edges.from_id AND e2.relation = edges.relation
|
|
285
|
-
)
|
|
282
|
+
db.prepare(`
|
|
283
|
+
DELETE FROM edges WHERE to_id = ? AND EXISTS (
|
|
284
|
+
SELECT 1 FROM edges e2 WHERE e2.to_id = ? AND e2.from_id = edges.from_id AND e2.relation = edges.relation
|
|
285
|
+
)
|
|
286
286
|
`).run(mid, keep.id);
|
|
287
287
|
db.prepare('UPDATE edges SET to_id = ? WHERE to_id = ?').run(keep.id, mid);
|
|
288
288
|
// Reassign consolidations
|
|
@@ -74,9 +74,9 @@ export function declareAnchor(db, input) {
|
|
|
74
74
|
throw new Error(`${input.kind} anchors need at least one violation_signal term (the forbidden act / rejected alternative) — otherwise the detector can never deduce a contradiction`);
|
|
75
75
|
}
|
|
76
76
|
const info = db
|
|
77
|
-
.prepare(`INSERT INTO drift_anchors
|
|
78
|
-
(kind, statement, rationale, affects, detect_terms, violation_signal, tier, source, source_memory_id)
|
|
79
|
-
VALUES
|
|
77
|
+
.prepare(`INSERT INTO drift_anchors
|
|
78
|
+
(kind, statement, rationale, affects, detect_terms, violation_signal, tier, source, source_memory_id)
|
|
79
|
+
VALUES
|
|
80
80
|
(@kind, @statement, @rationale, @affects, @detect_terms, @violation_signal, @tier, @source, @source_memory_id)`)
|
|
81
81
|
.run({
|
|
82
82
|
kind: input.kind,
|
|
@@ -110,7 +110,7 @@ export function detectDrift(db, opts = {}) {
|
|
|
110
110
|
samples: [],
|
|
111
111
|
};
|
|
112
112
|
const anchors = db
|
|
113
|
-
.prepare(`SELECT id, kind, statement, affects, detect_terms, violation_signal, tier, created_at
|
|
113
|
+
.prepare(`SELECT id, kind, statement, affects, detect_terms, violation_signal, tier, created_at
|
|
114
114
|
FROM drift_anchors WHERE status = 'active'`)
|
|
115
115
|
.all();
|
|
116
116
|
res.anchorsScanned = anchors.length;
|
|
@@ -120,8 +120,8 @@ export function detectDrift(db, opts = {}) {
|
|
|
120
120
|
// the "why this edit" text, not just path + snippet. (session_file_edits.memory_id →
|
|
121
121
|
// memories.id; memories_fts.rowid = memories.id.)
|
|
122
122
|
const edits = db
|
|
123
|
-
.prepare(`SELECT e.id, e.file_path, e.context_snippet, e.memory_id, m.content AS memory_content, e.occurred_at
|
|
124
|
-
FROM session_file_edits e
|
|
123
|
+
.prepare(`SELECT e.id, e.file_path, e.context_snippet, e.memory_id, m.content AS memory_content, e.occurred_at
|
|
124
|
+
FROM session_file_edits e
|
|
125
125
|
LEFT JOIN memories m ON m.id = e.memory_id`)
|
|
126
126
|
.all();
|
|
127
127
|
res.editsScanned = edits.length;
|
|
@@ -137,19 +137,19 @@ export function detectDrift(db, opts = {}) {
|
|
|
137
137
|
// Write paths prepared only when persisting — keeps dryRun safe on a readonly connection.
|
|
138
138
|
const upsertContradicts = opts.dryRun
|
|
139
139
|
? null
|
|
140
|
-
: db.prepare(`
|
|
141
|
-
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
142
|
-
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
143
|
-
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
144
|
-
confidence = excluded.confidence,
|
|
145
|
-
evidence = excluded.evidence
|
|
146
|
-
WHERE drift_edges.status = 'open'
|
|
140
|
+
: db.prepare(`
|
|
141
|
+
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
142
|
+
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
143
|
+
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
144
|
+
confidence = excluded.confidence,
|
|
145
|
+
evidence = excluded.evidence
|
|
146
|
+
WHERE drift_edges.status = 'open'
|
|
147
147
|
`);
|
|
148
148
|
const insertAbsent = opts.dryRun
|
|
149
149
|
? null
|
|
150
|
-
: db.prepare(`
|
|
151
|
-
INSERT OR IGNORE INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
152
|
-
VALUES (@anchor_id, NULL, 'absent', @confidence, @evidence, 'open')
|
|
150
|
+
: db.prepare(`
|
|
151
|
+
INSERT OR IGNORE INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
152
|
+
VALUES (@anchor_id, NULL, 'absent', @confidence, @evidence, 'open')
|
|
153
153
|
`);
|
|
154
154
|
const apply = () => {
|
|
155
155
|
for (const a of anchors) {
|
|
@@ -351,13 +351,13 @@ export function detectFileViolations(db, opts = {}) {
|
|
|
351
351
|
.all();
|
|
352
352
|
const upsert = opts.dryRun
|
|
353
353
|
? null
|
|
354
|
-
: db.prepare(`
|
|
355
|
-
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
356
|
-
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
357
|
-
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
358
|
-
confidence = excluded.confidence,
|
|
359
|
-
evidence = excluded.evidence
|
|
360
|
-
WHERE drift_edges.status = 'open'
|
|
354
|
+
: db.prepare(`
|
|
355
|
+
INSERT INTO drift_edges (anchor_id, edit_id, verdict, confidence, evidence, status)
|
|
356
|
+
VALUES (@anchor_id, @edit_id, 'contradicts', @confidence, @evidence, 'open')
|
|
357
|
+
ON CONFLICT(anchor_id, edit_id, verdict) DO UPDATE SET
|
|
358
|
+
confidence = excluded.confidence,
|
|
359
|
+
evidence = excluded.evidence
|
|
360
|
+
WHERE drift_edges.status = 'open'
|
|
361
361
|
`);
|
|
362
362
|
const apply = () => {
|
|
363
363
|
for (const a of anchors) {
|
package/dist/lib/drift-view.js
CHANGED
|
@@ -27,14 +27,14 @@ function parseEvidence(s) {
|
|
|
27
27
|
export function getDriftHeadline(db, opts = {}) {
|
|
28
28
|
const limit = opts.limit ?? 3;
|
|
29
29
|
const rows = db
|
|
30
|
-
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
31
|
-
e.file_path, e.context_snippet, e.occurred_at,
|
|
32
|
-
d.confidence, d.evidence
|
|
33
|
-
FROM drift_edges d
|
|
34
|
-
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
35
|
-
JOIN session_file_edits e ON e.id = d.edit_id
|
|
36
|
-
WHERE d.verdict = 'contradicts' AND d.status = 'open' AND a.status = 'active'
|
|
37
|
-
ORDER BY d.confidence DESC, (a.tier = 'human') DESC, e.occurred_at DESC
|
|
30
|
+
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
31
|
+
e.file_path, e.context_snippet, e.occurred_at,
|
|
32
|
+
d.confidence, d.evidence
|
|
33
|
+
FROM drift_edges d
|
|
34
|
+
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
35
|
+
JOIN session_file_edits e ON e.id = d.edit_id
|
|
36
|
+
WHERE d.verdict = 'contradicts' AND d.status = 'open' AND a.status = 'active'
|
|
37
|
+
ORDER BY d.confidence DESC, (a.tier = 'human') DESC, e.occurred_at DESC
|
|
38
38
|
LIMIT ?`)
|
|
39
39
|
.all(limit);
|
|
40
40
|
return rows.map((r) => {
|
|
@@ -61,12 +61,12 @@ export function getDriftHeadline(db, opts = {}) {
|
|
|
61
61
|
export function getDriftAbsences(db, opts = {}) {
|
|
62
62
|
const limit = opts.limit ?? 10;
|
|
63
63
|
const rows = db
|
|
64
|
-
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
65
|
-
d.confidence, d.evidence, d.detected_at
|
|
66
|
-
FROM drift_edges d
|
|
67
|
-
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
68
|
-
WHERE d.verdict = 'absent' AND d.status = 'open' AND a.status = 'active'
|
|
69
|
-
ORDER BY a.created_at ASC
|
|
64
|
+
.prepare(`SELECT d.id AS edge_id, d.anchor_id, a.kind, a.statement, a.rationale,
|
|
65
|
+
d.confidence, d.evidence, d.detected_at
|
|
66
|
+
FROM drift_edges d
|
|
67
|
+
JOIN drift_anchors a ON a.id = d.anchor_id
|
|
68
|
+
WHERE d.verdict = 'absent' AND d.status = 'open' AND a.status = 'active'
|
|
69
|
+
ORDER BY a.created_at ASC
|
|
70
70
|
LIMIT ?`)
|
|
71
71
|
.all(limit);
|
|
72
72
|
return rows.map((r) => {
|
|
@@ -85,8 +85,8 @@ export function getDriftAbsences(db, opts = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
function countOpen(db, verdict) {
|
|
87
87
|
return db
|
|
88
|
-
.prepare(`SELECT COUNT(*) AS n
|
|
89
|
-
FROM drift_edges d JOIN drift_anchors a ON a.id = d.anchor_id
|
|
88
|
+
.prepare(`SELECT COUNT(*) AS n
|
|
89
|
+
FROM drift_edges d JOIN drift_anchors a ON a.id = d.anchor_id
|
|
90
90
|
WHERE d.verdict = ? AND d.status = 'open' AND a.status = 'active'`)
|
|
91
91
|
.get(verdict).n;
|
|
92
92
|
}
|
package/dist/lib/map-import.js
CHANGED
|
@@ -71,13 +71,13 @@ export function importMap(db, map) {
|
|
|
71
71
|
const anchorExists = db.prepare('SELECT 1 FROM drift_anchors WHERE id = ?');
|
|
72
72
|
let linked = 0;
|
|
73
73
|
const tx = db.transaction(() => {
|
|
74
|
-
db.prepare(`
|
|
75
|
-
INSERT INTO map_projects (project, job, audience, product_status, template, stages, related_projects, updated_at)
|
|
76
|
-
VALUES (@project, @job, @audience, @product_status, @template, @stages, @related_projects, unixepoch())
|
|
77
|
-
ON CONFLICT(project) DO UPDATE SET
|
|
78
|
-
job=excluded.job, audience=excluded.audience, product_status=excluded.product_status,
|
|
79
|
-
template=excluded.template, stages=excluded.stages, related_projects=excluded.related_projects,
|
|
80
|
-
updated_at=unixepoch()
|
|
74
|
+
db.prepare(`
|
|
75
|
+
INSERT INTO map_projects (project, job, audience, product_status, template, stages, related_projects, updated_at)
|
|
76
|
+
VALUES (@project, @job, @audience, @product_status, @template, @stages, @related_projects, unixepoch())
|
|
77
|
+
ON CONFLICT(project) DO UPDATE SET
|
|
78
|
+
job=excluded.job, audience=excluded.audience, product_status=excluded.product_status,
|
|
79
|
+
template=excluded.template, stages=excluded.stages, related_projects=excluded.related_projects,
|
|
80
|
+
updated_at=unixepoch()
|
|
81
81
|
`).run({
|
|
82
82
|
project: map.project,
|
|
83
83
|
job: map.job ?? null,
|
|
@@ -89,13 +89,13 @@ export function importMap(db, map) {
|
|
|
89
89
|
});
|
|
90
90
|
db.prepare('DELETE FROM map_edges WHERE project = ?').run(map.project);
|
|
91
91
|
db.prepare('DELETE FROM map_nodes WHERE project = ?').run(map.project);
|
|
92
|
-
const insNode = db.prepare(`
|
|
93
|
-
INSERT INTO map_nodes
|
|
94
|
-
(id, project, layer, stage, statement, status, facets, role, note, due,
|
|
95
|
-
paused_reason, related_project, spinout_candidate, anchor_id, review_by, revival_condition, reality, extra, updated_at)
|
|
96
|
-
VALUES
|
|
97
|
-
(@id, @project, @layer, @stage, @statement, @status, @facets, @role, @note, @due,
|
|
98
|
-
@paused_reason, @related_project, @spinout_candidate, @anchor_id, @review_by, @revival_condition, @reality, @extra, unixepoch())
|
|
92
|
+
const insNode = db.prepare(`
|
|
93
|
+
INSERT INTO map_nodes
|
|
94
|
+
(id, project, layer, stage, statement, status, facets, role, note, due,
|
|
95
|
+
paused_reason, related_project, spinout_candidate, anchor_id, review_by, revival_condition, reality, extra, updated_at)
|
|
96
|
+
VALUES
|
|
97
|
+
(@id, @project, @layer, @stage, @statement, @status, @facets, @role, @note, @due,
|
|
98
|
+
@paused_reason, @related_project, @spinout_candidate, @anchor_id, @review_by, @revival_condition, @reality, @extra, unixepoch())
|
|
99
99
|
`);
|
|
100
100
|
for (const n of map.nodes) {
|
|
101
101
|
if (!n.id)
|
|
@@ -134,9 +134,9 @@ export function importMap(db, map) {
|
|
|
134
134
|
extra: JSON.stringify(extra),
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
const insEdge = db.prepare(`
|
|
138
|
-
INSERT OR IGNORE INTO map_edges (project, from_id, to_id, type, strength, note)
|
|
139
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
137
|
+
const insEdge = db.prepare(`
|
|
138
|
+
INSERT OR IGNORE INTO map_edges (project, from_id, to_id, type, strength, note)
|
|
139
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
140
140
|
`);
|
|
141
141
|
for (const e of map.edges)
|
|
142
142
|
insEdge.run(map.project, e.from, e.to, e.type, e.strength ?? null, e.note ?? null);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const DRAFT_PAGE = "https://kansei-link.com/agents/note/new.html";
|
|
2
|
+
export declare const URL_MAX = 2000;
|
|
3
|
+
export declare const KINDS: readonly ["execution", "documentation", "hypothesis"];
|
|
4
|
+
export declare const RUNTIMES: readonly ["claude", "codex", "gpt", "gemini", "other"];
|
|
5
|
+
/** Text fields in URL order, with the page's caps (same numbers as the page's maxlength). */
|
|
6
|
+
export declare const TEXT_FIELDS: readonly [{
|
|
7
|
+
readonly key: "task";
|
|
8
|
+
readonly max: 300;
|
|
9
|
+
readonly required: true;
|
|
10
|
+
}, {
|
|
11
|
+
readonly key: "symptom";
|
|
12
|
+
readonly max: 600;
|
|
13
|
+
readonly required: true;
|
|
14
|
+
}, {
|
|
15
|
+
readonly key: "tried";
|
|
16
|
+
readonly max: 2000;
|
|
17
|
+
readonly required: true;
|
|
18
|
+
}, {
|
|
19
|
+
readonly key: "result";
|
|
20
|
+
readonly max: 1000;
|
|
21
|
+
readonly required: true;
|
|
22
|
+
}, {
|
|
23
|
+
readonly key: "conditions";
|
|
24
|
+
readonly max: 600;
|
|
25
|
+
readonly required: false;
|
|
26
|
+
}];
|
|
27
|
+
export declare const ALLOWED_KEYS: readonly ["service", ...("task" | "symptom" | "tried" | "result" | "conditions")[], "kind", "runtime"];
|
|
28
|
+
export declare const SERVICE_RE: RegExp;
|
|
29
|
+
export declare const NOTICE: readonly ["この内容は、開いた画面で確認してから送られます。送るまでどこにも出ていません。", "URL の中身は KanseiLINK の通信の記録(Cloudflare)を通ります。秘密は入れないでください。"];
|
|
30
|
+
export declare const NOTICE_EN: readonly ["Nothing is sent yet: the page opens with these fields filled, and only your click there sends them.", "The URL passes through KanseiLINK's request logs (Cloudflare). Do not put secrets in it."];
|
|
31
|
+
/** Which secret shapes a text contains (kinds, in check order). Empty = clean. */
|
|
32
|
+
export declare function findSecretShapes(text: string): Array<{
|
|
33
|
+
kind: string;
|
|
34
|
+
ja: string;
|
|
35
|
+
}>;
|
|
36
|
+
export interface ShareDraftProblem {
|
|
37
|
+
field: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
kinds?: string[];
|
|
40
|
+
}
|
|
41
|
+
export type ShareDraftResult = {
|
|
42
|
+
ok: true;
|
|
43
|
+
url: string;
|
|
44
|
+
length: number;
|
|
45
|
+
notice: readonly string[];
|
|
46
|
+
notice_en: readonly string[];
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
problems: ShareDraftProblem[];
|
|
50
|
+
hint: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Check the fields and build the draft URL. Pure: no I/O, no network, no DB.
|
|
54
|
+
* Returns problems (field + reason) instead of a URL when anything is off.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildShareDraft(input: unknown): ShareDraftResult;
|