scrumrun 2.1.0 → 2.1.1
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/CHANGELOG.md +23 -0
- package/CORE.md +6 -2
- package/MIGRATION-1-to-2.md +2 -2
- package/README.md +12 -4
- package/SPEC.md +5 -2
- package/bin/scrumrun.js +56 -0
- package/docs/COMMANDS.md +6 -2
- package/docs/RELEASE-SCORECARD.md +11 -11
- package/docs/RELEASE.md +7 -7
- package/docs/SCHEMA.md +3 -1
- package/docs/SEMANTIC-MEMORY.md +1 -1
- package/docs/TROUBLESHOOTING.md +1 -1
- package/lib/commands/manifest.js +4 -1
- package/lib/commands/render.js +1 -0
- package/lib/memory/index.js +135 -34
- package/lib/memory/service.js +3 -0
- package/lib/runtime/mutation-gateway.js +434 -0
- package/lib/runtime/orchestrator.js +35 -3
- package/lib/runtime/policy-engine.js +85 -2
- package/lib/runtime/request-engine.js +4 -0
- package/lib/runtime/review-service.js +92 -0
- package/lib/runtime/run-ledger.js +228 -6
- package/lib/runtime/workspace-state.js +146 -0
- package/lib/security/secrets.js +15 -1
- package/lib/v2/artifacts.js +3 -0
- package/lib/v2/conformance.js +20 -1
- package/lib/v2/migration.js +6 -2
- package/lib/v2/run-ledger-migration.js +30 -2
- package/lib/v2/schema.js +10 -3
- package/package.json +1 -1
- package/templates/project/.scrumrun/method.json +4 -1
- package/templates/project/AGENTS.md +2 -1
- package/templates/project-lean/AGENTS.md +3 -1
- package/templates/shared/skills/scrumrun/SKILL.md +6 -2
package/lib/memory/index.js
CHANGED
|
@@ -12,8 +12,10 @@ const { containsSecret } = require("../security/secrets");
|
|
|
12
12
|
const { canonicalWatchSnapshot, fileWatchSnapshot } = require("../runtime/canonical-snapshot");
|
|
13
13
|
|
|
14
14
|
const CACHE_RELATIVE = path.join(".cache", "semantic-index.sqlite");
|
|
15
|
-
const INDEX_SCHEMA_VERSION =
|
|
15
|
+
const INDEX_SCHEMA_VERSION = 4;
|
|
16
16
|
const INACTIVE = new Set(["rejected", "invalidated", "deprecated", "archived"]);
|
|
17
|
+
const SEARCH_BACKENDS = new Set(["auto", "fts5", "like"]);
|
|
18
|
+
const MAX_SEARCH_TOKENS = 32;
|
|
17
19
|
|
|
18
20
|
function indexPath(projectRoot) {
|
|
19
21
|
return path.join(projectRoot, ".scrumrun", CACHE_RELATIVE);
|
|
@@ -23,6 +25,10 @@ function titleFromBody(body, fallback) {
|
|
|
23
25
|
return ((String(body || "").match(/^#\s+([^\r\n]+)/m) || [])[1] || fallback).trim();
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function normalizeSearchText(...values) {
|
|
29
|
+
return values.map((value) => String(value || "")).join(" ").normalize("NFKC").toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
function canonicalArtifacts(scrumDir) {
|
|
27
33
|
const repository = new ArtifactRepository(scrumDir);
|
|
28
34
|
const artifacts = [];
|
|
@@ -104,7 +110,8 @@ function explicitEdges(artifact) {
|
|
|
104
110
|
return [...unique.values()];
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
function createSchema(database) {
|
|
113
|
+
function createSchema(database, { searchBackend = "auto" } = {}) {
|
|
114
|
+
if (!SEARCH_BACKENDS.has(searchBackend)) throw new Error(`Unknown semantic search backend: ${searchBackend}`);
|
|
108
115
|
database.exec(`
|
|
109
116
|
PRAGMA journal_mode = DELETE;
|
|
110
117
|
PRAGMA synchronous = FULL;
|
|
@@ -121,6 +128,7 @@ function createSchema(database) {
|
|
|
121
128
|
valid_until TEXT,
|
|
122
129
|
last_verified_commit TEXT,
|
|
123
130
|
review_trigger TEXT,
|
|
131
|
+
search_text TEXT NOT NULL,
|
|
124
132
|
active INTEGER NOT NULL CHECK (active IN (0, 1))
|
|
125
133
|
);
|
|
126
134
|
CREATE TABLE edges (
|
|
@@ -143,7 +151,8 @@ function createSchema(database) {
|
|
|
143
151
|
status TEXT NOT NULL CHECK (status IN ('current', 'orphaned')),
|
|
144
152
|
language TEXT NOT NULL,
|
|
145
153
|
exported INTEGER NOT NULL CHECK (exported IN (0, 1)),
|
|
146
|
-
remapped_from TEXT
|
|
154
|
+
remapped_from TEXT,
|
|
155
|
+
search_text TEXT NOT NULL
|
|
147
156
|
);
|
|
148
157
|
CREATE TABLE evidence_snapshots (
|
|
149
158
|
artifact_id TEXT NOT NULL,
|
|
@@ -167,9 +176,18 @@ function createSchema(database) {
|
|
|
167
176
|
CREATE INDEX edges_from ON edges(from_id);
|
|
168
177
|
CREATE INDEX edges_to ON edges(to_id);
|
|
169
178
|
CREATE INDEX artifacts_status ON artifacts(status);
|
|
170
|
-
CREATE VIRTUAL TABLE artifacts_fts USING fts5(id UNINDEXED, title, content, tokenize='unicode61');
|
|
171
|
-
CREATE VIRTUAL TABLE code_fts USING fts5(id UNINDEXED, name, qualified_name, path, tokenize='unicode61');
|
|
172
179
|
`);
|
|
180
|
+
if (searchBackend === "like") return "like";
|
|
181
|
+
try {
|
|
182
|
+
database.exec(`
|
|
183
|
+
CREATE VIRTUAL TABLE artifacts_fts USING fts5(id UNINDEXED, title, content, tokenize='unicode61');
|
|
184
|
+
CREATE VIRTUAL TABLE code_fts USING fts5(id UNINDEXED, name, qualified_name, path, tokenize='unicode61');
|
|
185
|
+
`);
|
|
186
|
+
return "fts5";
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (searchBackend === "fts5" || !/no such module:\s*fts5/i.test(error.message)) throw error;
|
|
189
|
+
return "like";
|
|
190
|
+
}
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
function tableExists(database, name) {
|
|
@@ -342,7 +360,7 @@ function derivedInvalidations(projectRoot, artifacts, codeNodes, prior, evidence
|
|
|
342
360
|
return { invalidations: [...invalidations.values()], subjects };
|
|
343
361
|
}
|
|
344
362
|
|
|
345
|
-
function rebuildIndex(projectRoot) {
|
|
363
|
+
function rebuildIndex(projectRoot, { searchBackend = "auto" } = {}) {
|
|
346
364
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
347
365
|
if (!fs.existsSync(scrumDir)) throw new Error("Not a ScrumRun project: .scrumrun/ is missing.");
|
|
348
366
|
const cacheDir = path.join(scrumDir, ".cache");
|
|
@@ -358,14 +376,19 @@ function rebuildIndex(projectRoot) {
|
|
|
358
376
|
const evidence = evidenceSnapshots(projectRoot, artifacts);
|
|
359
377
|
const derived = derivedInvalidations(projectRoot, artifacts, remapped.nodes, prior, evidence);
|
|
360
378
|
let database;
|
|
379
|
+
let selectedSearchBackend;
|
|
361
380
|
try {
|
|
362
381
|
database = new DatabaseSync(temp);
|
|
363
|
-
createSchema(database);
|
|
364
|
-
const insertArtifact = database.prepare("INSERT INTO artifacts(id, kind, status, title, path, hash, content, valid_from, valid_until, last_verified_commit, review_trigger, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
365
|
-
const insertFts =
|
|
382
|
+
selectedSearchBackend = createSchema(database, { searchBackend });
|
|
383
|
+
const insertArtifact = database.prepare("INSERT INTO artifacts(id, kind, status, title, path, hash, content, valid_from, valid_until, last_verified_commit, review_trigger, search_text, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
384
|
+
const insertFts = selectedSearchBackend === "fts5"
|
|
385
|
+
? database.prepare("INSERT INTO artifacts_fts(id, title, content) VALUES (?, ?, ?)")
|
|
386
|
+
: null;
|
|
366
387
|
const insertEdge = database.prepare("INSERT OR IGNORE INTO edges(from_id, relation, to_id, evidence, confidence) VALUES (?, ?, ?, ?, ?)");
|
|
367
|
-
const insertCode = database.prepare("INSERT INTO code_nodes(id, kind, name, qualified_name, path, line, fingerprint, commit_hash, status, language, exported, remapped_from) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
368
|
-
const insertCodeFts =
|
|
388
|
+
const insertCode = database.prepare("INSERT INTO code_nodes(id, kind, name, qualified_name, path, line, fingerprint, commit_hash, status, language, exported, remapped_from, search_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
389
|
+
const insertCodeFts = selectedSearchBackend === "fts5"
|
|
390
|
+
? database.prepare("INSERT INTO code_fts(id, name, qualified_name, path) VALUES (?, ?, ?, ?)")
|
|
391
|
+
: null;
|
|
369
392
|
database.exec("BEGIN IMMEDIATE");
|
|
370
393
|
try {
|
|
371
394
|
for (const artifact of artifacts) {
|
|
@@ -381,14 +404,29 @@ function rebuildIndex(projectRoot) {
|
|
|
381
404
|
artifact.record.valid_until || null,
|
|
382
405
|
artifact.record.last_verified_commit || null,
|
|
383
406
|
artifact.record.review_trigger || null,
|
|
407
|
+
normalizeSearchText(artifact.title, artifact.content),
|
|
384
408
|
INACTIVE.has(artifact.record.status) ? 0 : 1
|
|
385
409
|
);
|
|
386
|
-
insertFts.run(artifact.record.id, artifact.title, artifact.content);
|
|
410
|
+
if (insertFts) insertFts.run(artifact.record.id, artifact.title, artifact.content);
|
|
387
411
|
for (const edge of explicitEdges(artifact)) insertEdge.run(artifact.record.id, edge.relation, edge.target, edge.evidence, "canonical");
|
|
388
412
|
}
|
|
389
413
|
for (const node of remapped.nodes) {
|
|
390
|
-
insertCode.run(
|
|
391
|
-
|
|
414
|
+
insertCode.run(
|
|
415
|
+
node.id,
|
|
416
|
+
node.kind,
|
|
417
|
+
node.name,
|
|
418
|
+
node.qualifiedName,
|
|
419
|
+
node.path,
|
|
420
|
+
node.line,
|
|
421
|
+
node.fingerprint,
|
|
422
|
+
node.commit || null,
|
|
423
|
+
node.status,
|
|
424
|
+
node.language,
|
|
425
|
+
node.exported ? 1 : 0,
|
|
426
|
+
node.remappedFrom || null,
|
|
427
|
+
normalizeSearchText(node.name, node.qualifiedName, node.path)
|
|
428
|
+
);
|
|
429
|
+
if (insertCodeFts) insertCodeFts.run(node.id, node.name, node.qualifiedName, node.path);
|
|
392
430
|
}
|
|
393
431
|
for (const edge of snapshot.code.edges) insertEdge.run(edge.from, edge.relation, edge.to, edge.evidence, edge.confidence);
|
|
394
432
|
for (const edge of prior.edges.filter((item) => remapped.orphanIds.has(item.from_id))) {
|
|
@@ -403,6 +441,7 @@ function rebuildIndex(projectRoot) {
|
|
|
403
441
|
const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");
|
|
404
442
|
metadata.run("schema_version", String(INDEX_SCHEMA_VERSION));
|
|
405
443
|
metadata.run("method_version", "2.0.0");
|
|
444
|
+
metadata.run("search_backend", selectedSearchBackend);
|
|
406
445
|
metadata.run("source_fingerprint", snapshot.fingerprint);
|
|
407
446
|
metadata.run("source_watch_fingerprint", snapshot.watch.fingerprint);
|
|
408
447
|
metadata.run("artifact_count", String(artifacts.length));
|
|
@@ -422,7 +461,24 @@ function rebuildIndex(projectRoot) {
|
|
|
422
461
|
if (fs.existsSync(temp)) fs.rmSync(temp, { force: true });
|
|
423
462
|
throw error;
|
|
424
463
|
}
|
|
425
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
file: target,
|
|
466
|
+
fingerprint: snapshot.fingerprint,
|
|
467
|
+
watchFingerprint: snapshot.watch.fingerprint,
|
|
468
|
+
searchBackend: selectedSearchBackend,
|
|
469
|
+
artifacts: artifacts.length,
|
|
470
|
+
codeNodes: remapped.nodes.length,
|
|
471
|
+
warnings: [...snapshot.code.warnings, ...snapshot.watch.warnings]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function runtimeSupportsFts5(database) {
|
|
476
|
+
try {
|
|
477
|
+
database.prepare("SELECT 1 FROM artifacts_fts LIMIT 1").get();
|
|
478
|
+
return true;
|
|
479
|
+
} catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
426
482
|
}
|
|
427
483
|
|
|
428
484
|
function indexStatus(projectRoot) {
|
|
@@ -434,12 +490,20 @@ function indexStatus(projectRoot) {
|
|
|
434
490
|
database = new DatabaseSync(file, { readOnly: true });
|
|
435
491
|
const metadata = new Map(database.prepare("SELECT key, value FROM metadata").all().map((row) => [row.key, row.value]));
|
|
436
492
|
const stored = metadata.get("source_fingerprint") || null;
|
|
437
|
-
|
|
493
|
+
const searchBackend = metadata.get("search_backend") || null;
|
|
494
|
+
if (
|
|
495
|
+
metadata.get("schema_version") !== String(INDEX_SCHEMA_VERSION)
|
|
496
|
+
|| !metadata.get("source_watch_fingerprint")
|
|
497
|
+
|| !["fts5", "like"].includes(searchBackend)
|
|
498
|
+
) {
|
|
438
499
|
return { exists: true, stale: true, file, stored, check: "schema", reason: `semantic index schema ${INDEX_SCHEMA_VERSION} rebuild required` };
|
|
439
500
|
}
|
|
501
|
+
if (searchBackend === "fts5" && !runtimeSupportsFts5(database)) {
|
|
502
|
+
return { exists: true, stale: true, file, stored, searchBackend, check: "schema", reason: "semantic index requires unavailable FTS5; fallback rebuild required" };
|
|
503
|
+
}
|
|
440
504
|
const watch = sourceWatchSnapshot(projectRoot);
|
|
441
505
|
if (metadata.get("source_watch_fingerprint") === watch.fingerprint) {
|
|
442
|
-
return { exists: true, stale: false, file, stored, actual: stored, watch: watch.fingerprint, check: "metadata" };
|
|
506
|
+
return { exists: true, stale: false, file, stored, actual: stored, watch: watch.fingerprint, searchBackend, check: "metadata" };
|
|
443
507
|
}
|
|
444
508
|
const actual = sourceSnapshot(projectRoot, { watch }).fingerprint;
|
|
445
509
|
return {
|
|
@@ -449,6 +513,7 @@ function indexStatus(projectRoot) {
|
|
|
449
513
|
stored,
|
|
450
514
|
actual,
|
|
451
515
|
watch: watch.fingerprint,
|
|
516
|
+
searchBackend,
|
|
452
517
|
check: "hash",
|
|
453
518
|
reason: stored === actual ? "file metadata changed but semantic content is unchanged" : "canonical or source content changed"
|
|
454
519
|
};
|
|
@@ -459,11 +524,40 @@ function indexStatus(projectRoot) {
|
|
|
459
524
|
}
|
|
460
525
|
}
|
|
461
526
|
|
|
527
|
+
function searchTokens(query) {
|
|
528
|
+
return (String(query || "").normalize("NFKC").match(/[\p{L}\p{N}_-]+/gu) || []).slice(0, MAX_SEARCH_TOKENS);
|
|
529
|
+
}
|
|
530
|
+
|
|
462
531
|
function ftsQuery(query) {
|
|
463
|
-
const tokens =
|
|
532
|
+
const tokens = searchTokens(query);
|
|
464
533
|
return tokens.map((token) => `"${token.replace(/"/g, "")}"`).join(" AND ");
|
|
465
534
|
}
|
|
466
535
|
|
|
536
|
+
function fallbackArtifactRows(database, tokens, activeClause, cap) {
|
|
537
|
+
if (!tokens.length) return [];
|
|
538
|
+
const clauses = tokens.map(() => "instr(a.search_text, ?) > 0").join(" AND ");
|
|
539
|
+
const parameters = tokens.map((token) => normalizeSearchText(token));
|
|
540
|
+
return database.prepare(`
|
|
541
|
+
SELECT a.id, a.kind, a.status, a.title, a.path, a.active, a.valid_until, a.review_trigger
|
|
542
|
+
FROM artifacts a
|
|
543
|
+
WHERE ${clauses} ${activeClause}
|
|
544
|
+
ORDER BY a.id LIMIT ?
|
|
545
|
+
`).all(...parameters, cap);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function fallbackCodeRows(database, tokens, cap) {
|
|
549
|
+
if (!tokens.length) return [];
|
|
550
|
+
const clauses = tokens.map(() => "instr(c.search_text, ?) > 0").join(" AND ");
|
|
551
|
+
const parameters = tokens.map((token) => normalizeSearchText(token));
|
|
552
|
+
return database.prepare(`
|
|
553
|
+
SELECT c.id, c.kind, c.name AS title, c.qualified_name, c.path, c.line, c.fingerprint,
|
|
554
|
+
c.commit_hash, c.status, c.language, c.remapped_from
|
|
555
|
+
FROM code_nodes c
|
|
556
|
+
WHERE ${clauses}
|
|
557
|
+
ORDER BY CASE c.status WHEN 'current' THEN 0 ELSE 1 END, c.id LIMIT ?
|
|
558
|
+
`).all(...parameters, cap);
|
|
559
|
+
}
|
|
560
|
+
|
|
467
561
|
function queryIndex(projectRoot, query, { includeInactive = false, limit = DEFAULT_CONTEXT_RESULTS, relationLimit = DEFAULT_RELATIONS_PER_RESULT, rebuild = true } = {}) {
|
|
468
562
|
const text = String(query || "").trim();
|
|
469
563
|
if (!text) throw new Error("A non-empty memory query is required.");
|
|
@@ -476,18 +570,23 @@ function queryIndex(projectRoot, query, { includeInactive = false, limit = DEFAU
|
|
|
476
570
|
}
|
|
477
571
|
const database = new DatabaseSync(indexPath(projectRoot), { readOnly: true });
|
|
478
572
|
try {
|
|
573
|
+
const metadata = new Map(database.prepare("SELECT key, value FROM metadata").all().map((row) => [row.key, row.value]));
|
|
574
|
+
const searchBackend = metadata.get("search_backend");
|
|
479
575
|
const cap = Math.max(1, Math.min(Number(limit) || DEFAULT_CONTEXT_RESULTS, MAX_CONTEXT_RESULTS));
|
|
480
576
|
const relationCap = Math.max(1, Math.min(Number(relationLimit) || DEFAULT_RELATIONS_PER_RESULT, MAX_RELATIONS_PER_RESULT));
|
|
481
577
|
const activeClause = includeInactive ? "" : "AND a.active = 1";
|
|
482
578
|
const matches = new Map();
|
|
579
|
+
const tokens = searchTokens(text);
|
|
483
580
|
const match = ftsQuery(text);
|
|
484
|
-
if (
|
|
485
|
-
const rows =
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
581
|
+
if (tokens.length) {
|
|
582
|
+
const rows = searchBackend === "fts5"
|
|
583
|
+
? database.prepare(`
|
|
584
|
+
SELECT a.id, a.kind, a.status, a.title, a.path, a.active, a.valid_until, a.review_trigger, bm25(artifacts_fts) AS rank
|
|
585
|
+
FROM artifacts_fts JOIN artifacts a ON a.id = artifacts_fts.id
|
|
586
|
+
WHERE artifacts_fts MATCH ? ${activeClause}
|
|
587
|
+
ORDER BY rank, a.id LIMIT ?
|
|
588
|
+
`).all(match, cap)
|
|
589
|
+
: fallbackArtifactRows(database, tokens, activeClause, cap);
|
|
491
590
|
for (const row of rows) matches.set(`artifact:${row.id}`, { ...row, nodeType: "artifact", match: "content" });
|
|
492
591
|
}
|
|
493
592
|
const like = `%${text.toLowerCase()}%`;
|
|
@@ -504,14 +603,16 @@ function queryIndex(projectRoot, query, { includeInactive = false, limit = DEFAU
|
|
|
504
603
|
if (existing) existing.match = "content+relation";
|
|
505
604
|
else matches.set(key, { ...row, nodeType: "artifact", match: "relation" });
|
|
506
605
|
}
|
|
507
|
-
if (
|
|
508
|
-
const codeRows =
|
|
509
|
-
|
|
510
|
-
c.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
606
|
+
if (tokens.length) {
|
|
607
|
+
const codeRows = searchBackend === "fts5"
|
|
608
|
+
? database.prepare(`
|
|
609
|
+
SELECT c.id, c.kind, c.name AS title, c.qualified_name, c.path, c.line, c.fingerprint,
|
|
610
|
+
c.commit_hash, c.status, c.language, c.remapped_from, bm25(code_fts) AS rank
|
|
611
|
+
FROM code_fts JOIN code_nodes c ON c.id = code_fts.id
|
|
612
|
+
WHERE code_fts MATCH ?
|
|
613
|
+
ORDER BY CASE c.status WHEN 'current' THEN 0 ELSE 1 END, rank, c.id LIMIT ?
|
|
614
|
+
`).all(match, cap)
|
|
615
|
+
: fallbackCodeRows(database, tokens, cap);
|
|
515
616
|
for (const row of codeRows) matches.set(`code:${row.id}`, { ...row, nodeType: "code", match: "code" });
|
|
516
617
|
}
|
|
517
618
|
const codeRelationRows = database.prepare(`
|
|
@@ -575,7 +676,7 @@ function queryIndex(projectRoot, query, { includeInactive = false, limit = DEFAU
|
|
|
575
676
|
relations
|
|
576
677
|
};
|
|
577
678
|
});
|
|
578
|
-
return { indexFile: indexPath(projectRoot), rebuilt, query: text, results };
|
|
679
|
+
return { indexFile: indexPath(projectRoot), rebuilt, searchBackend, query: text, results };
|
|
579
680
|
} finally {
|
|
580
681
|
database.close();
|
|
581
682
|
}
|
package/lib/memory/service.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
} = require("../v2/artifacts");
|
|
16
16
|
const { extractEvidence, renderBullets } = require("./markdown");
|
|
17
17
|
const { assertNoSecret } = require("../security/secrets");
|
|
18
|
+
const { assertCanonicalWrite } = require("../runtime/mutation-gateway");
|
|
18
19
|
|
|
19
20
|
const MEMORY_KINDS = new Set(["knowledge", "decision", "insight", "dossier"]);
|
|
20
21
|
const DEFAULT_STATUS = Object.freeze({ knowledge: "candidate", decision: "open", insight: "candidate", dossier: "active" });
|
|
@@ -203,6 +204,7 @@ function createMemoryUnlocked(projectRoot, kind, options = {}) {
|
|
|
203
204
|
"",
|
|
204
205
|
`- ${created}: ${record.status} — proposed by ${sourceType}.`
|
|
205
206
|
].join("\n");
|
|
207
|
+
assertCanonicalWrite(projectRoot, `create-${kind}`, [title, content, body]);
|
|
206
208
|
repository.write(record, body);
|
|
207
209
|
return repository.read(kind, record.id);
|
|
208
210
|
}
|
|
@@ -267,6 +269,7 @@ function transitionMemoryUnlocked(projectRoot, kind, id, action, options = {}) {
|
|
|
267
269
|
const parsed = parseArtifact(next);
|
|
268
270
|
const errors = [...parsed.errors, ...validateArtifact(parsed.record, kind)];
|
|
269
271
|
if (errors.length) throw new Error(`Memory transition validation failed: ${errors.join("; ")}`);
|
|
272
|
+
assertCanonicalWrite(projectRoot, `transition-${kind}`, [next]);
|
|
270
273
|
atomicWrite(artifact.file, next);
|
|
271
274
|
return repository.read(kind, id);
|
|
272
275
|
}
|