depa-codument 0.4.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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/package.json +63 -0
  4. package/src/cli/commands/archive.ts +519 -0
  5. package/src/cli/commands/decisions.ts +123 -0
  6. package/src/cli/commands/engineering.ts +105 -0
  7. package/src/cli/commands/init.ts +54 -0
  8. package/src/cli/commands/list.ts +73 -0
  9. package/src/cli/commands/modeling.ts +105 -0
  10. package/src/cli/commands/show.ts +238 -0
  11. package/src/cli/commands/status.ts +140 -0
  12. package/src/cli/commands/upgrade-track.ts +385 -0
  13. package/src/cli/commands/upgrade-workspace.ts +138 -0
  14. package/src/cli/commands/validate.ts +330 -0
  15. package/src/cli/engineering/config.ts +68 -0
  16. package/src/cli/engineering/lint.ts +58 -0
  17. package/src/cli/engineering/merge.ts +172 -0
  18. package/src/cli/engineering/registry.ts +230 -0
  19. package/src/cli/engineering/schema.ts +126 -0
  20. package/src/cli/engineering/validate.ts +286 -0
  21. package/src/cli/index.ts +136 -0
  22. package/src/cli/modeling/config.ts +68 -0
  23. package/src/cli/modeling/lint.ts +58 -0
  24. package/src/cli/modeling/merge.ts +172 -0
  25. package/src/cli/modeling/registry.ts +229 -0
  26. package/src/cli/modeling/schema.ts +160 -0
  27. package/src/cli/modeling/validate.ts +282 -0
  28. package/src/cli/utils/index.ts +941 -0
  29. package/src/cli/utils/install.ts +291 -0
  30. package/src/cli/utils/spec-xml.ts +673 -0
  31. package/src/cli/utils/track-time.ts +75 -0
  32. package/src/cli/utils/vfs.ts +102 -0
  33. package/src/templates/codument/README.md +59 -0
  34. package/src/templates/codument/attractors/product.md +17 -0
  35. package/src/templates/codument/attractors/project.md +10 -0
  36. package/src/templates/codument/backlog/README.md +33 -0
  37. package/src/templates/codument/config/attractor-profiles.xml +31 -0
  38. package/src/templates/codument/config/engineering.xml +22 -0
  39. package/src/templates/codument/config/modeling.xml +22 -0
  40. package/src/templates/codument/config/operation-hooks.xml +55 -0
  41. package/src/templates/codument/memory/README.md +13 -0
  42. package/src/templates/codument/missions/README.md +125 -0
  43. package/src/templates/codument/sop/README.md +14 -0
  44. package/src/templates/codument/std/AGENTS.md +82 -0
  45. package/src/templates/codument/std/attractors/depa-attractor.md +572 -0
  46. package/src/templates/codument/std/attractors/knowledge-tiers.md +128 -0
  47. package/src/templates/codument/std/attractors/model-driven-docs.md +293 -0
  48. package/src/templates/codument/std/attractors/project-memory.md +48 -0
  49. package/src/templates/codument/std/docs-impl-fractal/index.md +110 -0
  50. package/src/templates/codument/std/docs-modeling-fractal/index.md +156 -0
  51. package/src/templates/codument/std/kernel-pointer.md +19 -0
  52. package/src/templates/codument/std/operations/README.md +30 -0
  53. package/src/templates/codument/std/operations/_operation-spec.md +41 -0
  54. package/src/templates/codument/std/operations/archive-mission.md +66 -0
  55. package/src/templates/codument/std/operations/archive-track.md +238 -0
  56. package/src/templates/codument/std/operations/artifact-sync.md +172 -0
  57. package/src/templates/codument/std/operations/discuss-phase.md +214 -0
  58. package/src/templates/codument/std/operations/discuss.md +87 -0
  59. package/src/templates/codument/std/operations/docs-bootstrap.md +148 -0
  60. package/src/templates/codument/std/operations/gap-loop.md +301 -0
  61. package/src/templates/codument/std/operations/impl-mission.md +167 -0
  62. package/src/templates/codument/std/operations/impl-quick.md +79 -0
  63. package/src/templates/codument/std/operations/impl-track.md +537 -0
  64. package/src/templates/codument/std/operations/migrate.md +337 -0
  65. package/src/templates/codument/std/operations/plan-mission.md +230 -0
  66. package/src/templates/codument/std/operations/plan-track-wave.md +231 -0
  67. package/src/templates/codument/std/operations/plan-track.md +579 -0
  68. package/src/templates/codument/std/operations/revise-track.md +136 -0
  69. package/src/templates/codument/std/operations/validate.md +339 -0
  70. package/src/templates/codument/std/operations/verify.md +184 -0
  71. package/src/templates/codument/std/root-agents.md +39 -0
  72. package/src/templates/codument/std/sop/questioning.md +98 -0
  73. package/src/templates/codument/std/sop/tdd.md +26 -0
  74. package/src/templates/codument/std/sop/validation.md +25 -0
  75. package/src/templates/codument/std/sop/wave-exec.md +42 -0
  76. package/src/templates/codument/std/sop/workflow.md +35 -0
  77. package/src/templates/codument/std/spec/behavior-delta.md +36 -0
  78. package/src/templates/codument/std/spec/behavior-registry.md +42 -0
  79. package/src/templates/codument/std/spec/engineering-delta.md +68 -0
  80. package/src/templates/codument/std/spec/engineering-node-schema.md +86 -0
  81. package/src/templates/codument/std/spec/engineering-registry.md +82 -0
  82. package/src/templates/codument/std/spec/flow-notation.md +93 -0
  83. package/src/templates/codument/std/spec/folder-manifest.md +99 -0
  84. package/src/templates/codument/std/spec/mission-xml-spec.md +249 -0
  85. package/src/templates/codument/std/spec/modeling-delta.md +85 -0
  86. package/src/templates/codument/std/spec/modeling-node-schema.md +183 -0
  87. package/src/templates/codument/std/spec/modeling-registry.md +49 -0
  88. package/src/templates/codument/std/spec/track-xml-spec.md +272 -0
  89. package/src/templates/codument/std/spec/xnl-format.md +301 -0
  90. package/src/templates/codument/workflows/README.md +15 -0
  91. package/src/templates/manifest.ts +177 -0
  92. package/src/templates/skills/README.md +38 -0
  93. package/src/templates/skills/codument-archive/SKILL.md +17 -0
  94. package/src/templates/skills/codument-archive-mission/SKILL.md +17 -0
  95. package/src/templates/skills/codument-archive-track/SKILL.md +17 -0
  96. package/src/templates/skills/codument-artifact-sync/SKILL.md +17 -0
  97. package/src/templates/skills/codument-code-quality-score/SKILL.md +67 -0
  98. package/src/templates/skills/codument-decision-tree/SKILL.md +40 -0
  99. package/src/templates/skills/codument-discuss/SKILL.md +17 -0
  100. package/src/templates/skills/codument-discuss-phase/SKILL.md +17 -0
  101. package/src/templates/skills/codument-docs-bootstrap/SKILL.md +17 -0
  102. package/src/templates/skills/codument-gap-loop/SKILL.md +17 -0
  103. package/src/templates/skills/codument-impl-mission/SKILL.md +17 -0
  104. package/src/templates/skills/codument-impl-quick/SKILL.md +17 -0
  105. package/src/templates/skills/codument-impl-track/SKILL.md +17 -0
  106. package/src/templates/skills/codument-implement/SKILL.md +14 -0
  107. package/src/templates/skills/codument-migrate/SKILL.md +17 -0
  108. package/src/templates/skills/codument-modeling-engineering-e2e/SKILL.md +74 -0
  109. package/src/templates/skills/codument-plan-mission/SKILL.md +17 -0
  110. package/src/templates/skills/codument-plan-track/SKILL.md +17 -0
  111. package/src/templates/skills/codument-plan-track-wave/SKILL.md +17 -0
  112. package/src/templates/skills/codument-revise-track/SKILL.md +17 -0
  113. package/src/templates/skills/codument-track/SKILL.md +14 -0
  114. package/src/templates/skills/codument-validate/SKILL.md +17 -0
  115. package/src/templates/skills/codument-verify/SKILL.md +17 -0
  116. package/src/types/text-assets.d.ts +9 -0
  117. package/src/version.ts +1 -0
@@ -0,0 +1,519 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { execFileSync } from 'child_process';
5
+ import type { XnlNode } from 'xnl-core';
6
+ import {
7
+ ARCHIVE_DIR,
8
+ BEHAVIORS_DIR,
9
+ DECISIONS_DIR,
10
+ MEMORY_DIR,
11
+ SPECS_DIR,
12
+ TRACKS_DIR,
13
+ codumentExists,
14
+ getTrack,
15
+ parseOptions,
16
+ } from '../utils';
17
+ import { loadEngineeringConfig } from '../engineering/config';
18
+ import { loadEngineeringRegistry, saveEngineeringFile } from '../engineering/registry';
19
+ import { mergeEngineering, type MergeConflict } from '../engineering/merge';
20
+ import { applySpecXmlPatchToRegistry } from '../utils/spec-xml';
21
+ import { buildArchiveDestination, formatLocalMinutePrefix, resolveTrackUpdatedDate } from '../utils/track-time';
22
+
23
+ export async function archiveCommand(args: string[]) {
24
+ if (!codumentExists()) {
25
+ console.error('Codument is not initialized. Run codument init first.');
26
+ process.exit(1);
27
+ }
28
+
29
+ const { positional, options } = parseOptions(args);
30
+ const trackId = positional[0];
31
+ const skipSpecs = options['skip-specs'] === true;
32
+ const skipConfirm = options['yes'] === true || options['y'] === true;
33
+
34
+ if (!trackId) {
35
+ console.error('Please specify a track ID to archive.');
36
+ console.log('Usage: codument archive <track-id> [--skip-specs] [--yes]');
37
+ process.exit(1);
38
+ }
39
+
40
+ const track = getTrack(trackId);
41
+
42
+ if (!track) {
43
+ console.error(`Track not found: ${trackId}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // Check if track is completed
48
+ if (track.metadata.status !== 'completed') {
49
+ console.log(`Warning: Track "${trackId}" is not marked as completed (status: ${track.metadata.status})`);
50
+
51
+ if (!skipConfirm) {
52
+ console.warn('Warning: Archive requires explicit confirmation for non-completed tracks. Re-run with --yes/-y to archive anyway, or mark the track as completed first.');
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ const trackDir = path.join(TRACKS_DIR, trackId);
58
+ const updatedDate = resolveTrackUpdatedDate(trackDir);
59
+ const archiveDir = buildArchiveDestination(trackDir, trackId, ARCHIVE_DIR);
60
+ const archiveId = path.basename(archiveDir);
61
+
62
+ console.log(`\nArchiving track: ${trackId}`);
63
+ console.log(`Destination: ${archiveDir}`);
64
+
65
+ // Create archive directory
66
+ const archiveParentDir = path.dirname(archiveDir);
67
+ if (!fs.existsSync(archiveParentDir)) {
68
+ fs.mkdirSync(archiveParentDir, { recursive: true });
69
+ }
70
+ if (fs.existsSync(archiveDir)) {
71
+ console.error(`Archive destination already exists: ${archiveDir}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ const engineeringUpdate = prepareEngineeringDeltaMerge(trackDir);
76
+
77
+ // Apply behavior/spec registry updates BEFORE moving the track. The registry
78
+ // write is all-or-nothing (applySpecXmlPatchToRegistry stages every mutation in
79
+ // memory and only flushes on success), so a delta failure now leaves the track
80
+ // in place and re-runnable instead of stranding a half-archived, inconsistent state.
81
+ let updatedRegistries: string[] | null = null;
82
+ if (!skipSpecs) {
83
+ updatedRegistries = applyArchivedSpecDeltas(trackDir);
84
+ }
85
+
86
+ const updatedEngineeringFiles = engineeringUpdate?.apply() ?? [];
87
+
88
+ // Move track to archive
89
+ fs.renameSync(trackDir, archiveDir);
90
+ console.log('✓ Track moved to archive');
91
+ console.log(`✓ Archive ID: ${archiveId}`);
92
+
93
+ if (updatedRegistries === null) {
94
+ console.log(' Skipped behavior/spec updates (--skip-specs)');
95
+ } else if (updatedRegistries.length > 0) {
96
+ console.log(`✓ Updated behavior/spec registry: ${updatedRegistries.join(', ')}`);
97
+ } else {
98
+ console.log(' No behavior/spec updates needed');
99
+ }
100
+
101
+ if (updatedEngineeringFiles.length > 0) {
102
+ console.log(`✓ Updated engineering registry: ${updatedEngineeringFiles.join(', ')}`);
103
+ } else if (engineeringUpdate) {
104
+ console.log(' No engineering updates needed');
105
+ }
106
+
107
+ const promotedDecision = promoteDecisionRecord(archiveDir, archiveId, trackId, updatedDate);
108
+ if (promotedDecision) {
109
+ console.log(`✓ Promoted decision record: ${promotedDecision}`);
110
+ }
111
+
112
+ const summaryPath = generateArchiveSummary(archiveDir, trackId);
113
+ if (summaryPath) {
114
+ console.log(`✓ Generated archive summary: ${summaryPath}`);
115
+ }
116
+
117
+ // Promote memory candidates the track explicitly provided (no-op when none exist).
118
+ const promotedMemory = promoteMemoryRecords(archiveDir, archiveId, trackId, updatedDate);
119
+ if (promotedMemory.length > 0) {
120
+ console.log(`✓ Promoted memory records: ${promotedMemory.join(', ')}`);
121
+ }
122
+
123
+ console.log(`\n✓ Track "${trackId}" archived successfully!\n`);
124
+ }
125
+
126
+ function applyArchivedSpecDeltas(archiveDir: string): string[] {
127
+ const behaviorPatches = collectXmlPatches(archiveDir, ['behavior_deltas', 'behavior-deltas']);
128
+ if (behaviorPatches.length > 0) {
129
+ const updated = new Set<string>();
130
+ for (const patchPath of behaviorPatches) {
131
+ for (const capability of applySpecXmlPatchToRegistry(fs.readFileSync(patchPath, 'utf-8'), BEHAVIORS_DIR)) {
132
+ updated.add(capability);
133
+ }
134
+ }
135
+ return [...updated];
136
+ }
137
+
138
+ const legacyXmlPatches = collectXmlPatches(archiveDir, ['spec_deltas', 'spec-deltas']);
139
+ if (legacyXmlPatches.length > 0) {
140
+ const updated = new Set<string>();
141
+ for (const patchPath of legacyXmlPatches) {
142
+ for (const capability of applySpecXmlPatchToRegistry(fs.readFileSync(patchPath, 'utf-8'), SPECS_DIR)) {
143
+ updated.add(capability);
144
+ }
145
+ }
146
+ return [...updated];
147
+ }
148
+
149
+ const xmlPatchCandidates = ['spec.xml', 'spec.patch.xml', 'patch.xml'];
150
+ for (const candidate of xmlPatchCandidates) {
151
+ const patchPath = path.join(archiveDir, candidate);
152
+ if (fs.existsSync(patchPath)) {
153
+ const updated = applySpecXmlPatchToRegistry(fs.readFileSync(patchPath, 'utf-8'), SPECS_DIR);
154
+ if (updated.length > 0) {
155
+ return updated;
156
+ }
157
+ }
158
+ }
159
+
160
+ const specPath = path.join(archiveDir, 'spec.md');
161
+ if (fs.existsSync(specPath)) {
162
+ return applySpecDeltas(specPath);
163
+ }
164
+
165
+ return [];
166
+ }
167
+
168
+ interface PreparedEngineeringMerge {
169
+ apply(): string[];
170
+ }
171
+
172
+ interface MergedEngineeringFile {
173
+ relFile: string;
174
+ nodes: XnlNode[];
175
+ }
176
+
177
+ function prepareEngineeringDeltaMerge(trackDir: string): PreparedEngineeringMerge | null {
178
+ const config = loadEngineeringConfig();
179
+ if (!config.enabled) {
180
+ return null;
181
+ }
182
+
183
+ const deltaDir = path.join(trackDir, 'engineering_deltas');
184
+ if (!fs.existsSync(deltaDir)) {
185
+ return null;
186
+ }
187
+
188
+ const theirs = loadEngineeringRegistry(deltaDir);
189
+ if (theirs.files.size === 0) {
190
+ return { apply: () => [] };
191
+ }
192
+
193
+ const registryDir = config.registryDir;
194
+ const baseDir = materializeEngineeringBase(trackDir, registryDir);
195
+ const base = loadEngineeringRegistry(baseDir);
196
+ const ours = loadEngineeringRegistry(registryDir);
197
+ const mergedFiles: MergedEngineeringFile[] = [];
198
+ const conflicts: MergeConflict[] = [];
199
+
200
+ for (const relFile of theirs.files.keys()) {
201
+ const result = mergeEngineering(
202
+ base.files.get(relFile) ?? [],
203
+ ours.files.get(relFile) ?? [],
204
+ theirs.files.get(relFile) ?? [],
205
+ config.mergePolicy,
206
+ );
207
+ conflicts.push(...result.conflicts);
208
+ mergedFiles.push({ relFile, nodes: [...result.merged.values()] });
209
+ }
210
+
211
+ if (conflicts.length > 0) {
212
+ console.error('Engineering delta merge conflicts:');
213
+ for (const conflict of conflicts) {
214
+ console.error(` - ${conflict.id}: ${conflict.type}`);
215
+ }
216
+ console.error('Resolve conflicts manually or adjust codument/config/engineering.xml MergePolicy.');
217
+ process.exit(1);
218
+ }
219
+
220
+ return {
221
+ apply(): string[] {
222
+ if (!fs.existsSync(registryDir)) {
223
+ fs.mkdirSync(registryDir, { recursive: true });
224
+ }
225
+ const updated: string[] = [];
226
+ for (const file of mergedFiles) {
227
+ const abs = path.join(registryDir, file.relFile);
228
+ if (file.nodes.length === 0) {
229
+ if (fs.existsSync(abs)) {
230
+ fs.rmSync(abs);
231
+ updated.push(file.relFile);
232
+ }
233
+ continue;
234
+ }
235
+ saveEngineeringFile(registryDir, file.relFile, file.nodes);
236
+ updated.push(file.relFile);
237
+ }
238
+ return updated.sort();
239
+ },
240
+ };
241
+ }
242
+
243
+ function materializeEngineeringBase(trackDir: string, registryDir: string): string {
244
+ const commit = readEngineeringBaseCommit(trackDir);
245
+ if (!commit) {
246
+ return registryDir;
247
+ }
248
+
249
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codument-engineering-base-'));
250
+ const gitPath = registryDir.split(path.sep).join('/');
251
+ let files: string;
252
+ try {
253
+ files = execFileSync('git', ['ls-tree', '-r', '--name-only', commit, '--', gitPath], {
254
+ encoding: 'utf-8',
255
+ stdio: ['ignore', 'pipe', 'pipe'],
256
+ });
257
+ } catch (err) {
258
+ throw new Error(`Unable to read engineering base commit '${commit}': ${String(err)}`);
259
+ }
260
+
261
+ for (const fullPath of files.split('\n').filter(Boolean)) {
262
+ if (!fullPath.endsWith('.xnl')) {
263
+ continue;
264
+ }
265
+ const prefix = `${gitPath}/`;
266
+ if (!fullPath.startsWith(prefix)) {
267
+ continue;
268
+ }
269
+ const relFile = fullPath.slice(prefix.length).split('/').join(path.sep);
270
+ const content = execFileSync('git', ['show', `${commit}:${fullPath}`], {
271
+ encoding: 'utf-8',
272
+ stdio: ['ignore', 'pipe', 'pipe'],
273
+ });
274
+ const out = path.join(tempDir, relFile);
275
+ fs.mkdirSync(path.dirname(out), { recursive: true });
276
+ fs.writeFileSync(out, content, 'utf-8');
277
+ }
278
+
279
+ return tempDir;
280
+ }
281
+
282
+ function readEngineeringBaseCommit(trackDir: string): string | null {
283
+ const trackXml = path.join(trackDir, 'track.xml');
284
+ if (!fs.existsSync(trackXml)) {
285
+ return null;
286
+ }
287
+ const content = fs.readFileSync(trackXml, 'utf-8');
288
+ return content.match(/<EngineeringBaseCommit>([^<]+)<\/EngineeringBaseCommit>/)?.[1]?.trim() || null;
289
+ }
290
+
291
+ function collectXmlPatches(archiveDir: string, rootNames: string[]): string[] {
292
+ const results: string[] = [];
293
+ const roots = rootNames.map((name) => path.join(archiveDir, name));
294
+
295
+ const visit = (dir: string) => {
296
+ if (!fs.existsSync(dir)) {
297
+ return;
298
+ }
299
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
300
+ const entryPath = path.join(dir, entry.name);
301
+ if (entry.isDirectory()) {
302
+ visit(entryPath);
303
+ } else if (entry.isFile() && entry.name.endsWith('.xml')) {
304
+ results.push(entryPath);
305
+ }
306
+ }
307
+ };
308
+
309
+ for (const root of roots) {
310
+ visit(root);
311
+ }
312
+ return results.sort();
313
+ }
314
+
315
+ function ensureDir(dir: string): void {
316
+ if (!fs.existsSync(dir)) {
317
+ fs.mkdirSync(dir, { recursive: true });
318
+ }
319
+ }
320
+
321
+ function writePromotedArtifact(
322
+ rootDir: string,
323
+ fileName: string,
324
+ trackId: string,
325
+ updatedDate: Date,
326
+ content: string,
327
+ ): string {
328
+ const prefix = formatLocalMinutePrefix(updatedDate);
329
+ const dir = path.join(rootDir, prefix.monthBucket, `${prefix.minutePrefix}-${trackId}`);
330
+ ensureDir(dir);
331
+ const filePath = path.join(dir, fileName);
332
+ if (!fs.existsSync(filePath)) {
333
+ fs.writeFileSync(filePath, content, 'utf-8');
334
+ }
335
+ return filePath;
336
+ }
337
+
338
+ function decisionUriForSlug(slug: string): string {
339
+ return `decision://${slug}`;
340
+ }
341
+
342
+ function promoteDecisionRecord(archiveDir: string, archiveId: string, trackId: string, updatedDate: Date): string | null {
343
+ const decisionsDir = path.join(archiveDir, 'decisions');
344
+ if (!fs.existsSync(decisionsDir) || !fs.statSync(decisionsDir).isDirectory()) {
345
+ // legacy fallback: single decisions.md file
346
+ const legacyPath = path.join(archiveDir, 'decisions.md');
347
+ if (!fs.existsSync(legacyPath)) {
348
+ return null;
349
+ }
350
+ const source = fs.readFileSync(legacyPath, 'utf-8').trim();
351
+ if (!source || source === '# Decisions') {
352
+ return null;
353
+ }
354
+ if (!hasDurableDecision(source)) {
355
+ return null;
356
+ }
357
+ const content = [
358
+ `# Decision: ${trackId}`,
359
+ '',
360
+ `Decision URI: ${decisionUriForSlug(trackId)}`,
361
+ `Source: archive://${archiveId}`,
362
+ '',
363
+ source,
364
+ '',
365
+ ].join('\n');
366
+ return writePromotedArtifact(DECISIONS_DIR, 'decision.md', trackId, updatedDate, content);
367
+ }
368
+
369
+ const decisionFiles = fs.readdirSync(decisionsDir)
370
+ .filter(f => f.endsWith('.md') && f !== 'summary.md');
371
+
372
+ if (decisionFiles.length === 0) {
373
+ return null;
374
+ }
375
+
376
+ let promoted: string | null = null;
377
+ for (const fileName of decisionFiles) {
378
+ const source = fs.readFileSync(path.join(decisionsDir, fileName), 'utf-8').trim();
379
+ if (!source) {
380
+ continue;
381
+ }
382
+ if (!hasDurableDecision(source)) {
383
+ continue;
384
+ }
385
+ const slug = path.basename(fileName, '.md');
386
+ const content = [
387
+ `# Decision: ${slug}`,
388
+ '',
389
+ `Decision URI: ${decisionUriForSlug(slug)}`,
390
+ `Source: archive://${archiveId}`,
391
+ '',
392
+ source,
393
+ '',
394
+ ].join('\n');
395
+ promoted = writePromotedArtifact(DECISIONS_DIR, 'decision.md', slug, updatedDate, content);
396
+ }
397
+
398
+ return promoted;
399
+ }
400
+
401
+ function generateArchiveSummary(archiveDir: string, trackId: string): string | null {
402
+ const decisionsDir = path.join(archiveDir, 'decisions');
403
+ if (!fs.existsSync(decisionsDir) || !fs.statSync(decisionsDir).isDirectory()) {
404
+ const legacyPath = path.join(archiveDir, 'decisions.md');
405
+ if (!fs.existsSync(legacyPath)) {
406
+ return null;
407
+ }
408
+ const source = fs.readFileSync(legacyPath, 'utf-8').trim();
409
+ if (!source) {
410
+ return null;
411
+ }
412
+ const title = source.match(/^# (.+)$/m)?.[1] ?? trackId;
413
+ const lines = [`# Archive Summary: ${trackId}`, '', `- ${title}`, ''];
414
+ const summaryPath = path.join(archiveDir, 'summary.md');
415
+ fs.writeFileSync(summaryPath, lines.join('\n'), 'utf-8');
416
+ return summaryPath;
417
+ }
418
+
419
+ const decisionFiles = fs.readdirSync(decisionsDir)
420
+ .filter(f => f.endsWith('.md') && f !== 'summary.md');
421
+
422
+ if (decisionFiles.length === 0) {
423
+ return null;
424
+ }
425
+
426
+ const lines = [`# Archive Summary: ${trackId}`, ''];
427
+ for (const fileName of decisionFiles) {
428
+ const source = fs.readFileSync(path.join(decisionsDir, fileName), 'utf-8').trim();
429
+ const title = source.match(/^# (.+)$/m)?.[1] ?? fileName.replace('.md', '');
430
+ lines.push(`- ${title}`);
431
+ }
432
+ const summaryPath = path.join(archiveDir, 'summary.md');
433
+ fs.writeFileSync(summaryPath, lines.join('\n'), 'utf-8');
434
+ return summaryPath;
435
+ }
436
+
437
+ function hasDurableDecision(source: string): boolean {
438
+ return /\bdurable\b/i.test(source)
439
+ || /长期(项目)?决策/.test(source)
440
+ || /未来仍(然)?(需要|要|应当|必须)遵守/.test(source);
441
+ }
442
+
443
+ function promoteMemoryRecords(archiveDir: string, archiveId: string, trackId: string, updatedDate: Date): string[] {
444
+ const promoted: string[] = [];
445
+ const typeConfigs = [
446
+ { type: 'lessons', fileName: 'lesson.md' },
447
+ { type: 'incidents', fileName: 'incident.md' },
448
+ { type: 'patterns', fileName: 'pattern.md' },
449
+ { type: 'summaries', fileName: 'summary.md' },
450
+ ];
451
+
452
+ for (const { type, fileName } of typeConfigs) {
453
+ const sourceDir = path.join(archiveDir, 'memory', type);
454
+ if (!fs.existsSync(sourceDir)) {
455
+ continue;
456
+ }
457
+
458
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
459
+ if (!entry.isFile() || !entry.name.endsWith('.md')) {
460
+ continue;
461
+ }
462
+ const source = fs.readFileSync(path.join(sourceDir, entry.name), 'utf-8').trim();
463
+ if (!source) {
464
+ continue;
465
+ }
466
+ const slug = path.basename(entry.name, '.md');
467
+ const content = [
468
+ `# ${type.slice(0, -1)}: ${slug}`,
469
+ '',
470
+ `Memory URI: memory://${type}/${slug}`,
471
+ `Source: archive://${archiveId}`,
472
+ '',
473
+ source,
474
+ '',
475
+ ].join('\n');
476
+ promoted.push(writePromotedArtifact(path.join(MEMORY_DIR, type), fileName, slug, updatedDate, content));
477
+ }
478
+ }
479
+
480
+ return promoted;
481
+ }
482
+
483
+ function applySpecDeltas(specPath: string): string[] {
484
+ const content = fs.readFileSync(specPath, 'utf-8');
485
+ const updatedSpecs: string[] = [];
486
+
487
+ // Parse the spec file for delta operations
488
+ const addedMatch = content.match(/## ADDED Requirements\n([\s\S]*?)(?=## (?:MODIFIED|REMOVED|RENAMED) Requirements|$)/);
489
+ const modifiedMatch = content.match(/## MODIFIED Requirements\n([\s\S]*?)(?=## (?:ADDED|REMOVED|RENAMED) Requirements|$)/);
490
+ const removedMatch = content.match(/## REMOVED Requirements\n([\s\S]*?)(?=## (?:ADDED|MODIFIED|RENAMED) Requirements|$)/);
491
+
492
+ // Extract capability from first requirement
493
+ // This is simplified - in a real implementation, you'd parse the full structure
494
+ const requirements = content.match(/^### Requirement: (.+)$/gm) || [];
495
+
496
+ if (requirements.length === 0) {
497
+ return updatedSpecs;
498
+ }
499
+
500
+ // For simplicity, we'll just note which capabilities would be updated
501
+ // A full implementation would parse and apply the deltas
502
+
503
+ if (addedMatch && addedMatch[1].trim()) {
504
+ // Would add new requirements to specs
505
+ console.log(' Found ADDED requirements (would apply to specs)');
506
+ }
507
+
508
+ if (modifiedMatch && modifiedMatch[1].trim()) {
509
+ // Would modify existing requirements
510
+ console.log(' Found MODIFIED requirements (would apply to specs)');
511
+ }
512
+
513
+ if (removedMatch && removedMatch[1].trim()) {
514
+ // Would remove requirements
515
+ console.log(' Found REMOVED requirements (would apply to specs)');
516
+ }
517
+
518
+ return updatedSpecs;
519
+ }
@@ -0,0 +1,123 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { parseOptions, TRACKS_DIR } from '../utils';
4
+
5
+ export interface DecisionFinding {
6
+ severity: 'error' | 'warning';
7
+ file: string;
8
+ decision: string;
9
+ message: string;
10
+ }
11
+
12
+ const RESOLVED_STATUS = new Set(['accepted', 'resolved', 'deferred']);
13
+
14
+ function splitDecisionSections(content: string): Array<{ title: string; body: string }> {
15
+ const heading = /^###\s+(.+)$/gm;
16
+ const matches = [...content.matchAll(heading)];
17
+ if (matches.length === 0) return [{ title: '(document)', body: content }];
18
+
19
+ return matches.map((m, i) => {
20
+ const start = m.index! + m[0].length;
21
+ const end = i + 1 < matches.length ? matches[i + 1].index! : content.length;
22
+ return { title: m[1].trim(), body: content.slice(start, end) };
23
+ });
24
+ }
25
+
26
+ function field(section: string, name: string): string | undefined {
27
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ const re = new RegExp(`^-[ \\t]*${escaped}[ \\t]*[::][ \\t]*(.*)$`, 'im');
29
+ const match = section.match(re);
30
+ return match?.[1]?.trim();
31
+ }
32
+
33
+ function statusOf(section: string): string | undefined {
34
+ return field(section, '状态')?.toLowerCase() ?? field(section, 'Status')?.toLowerCase();
35
+ }
36
+
37
+ export function validateDecisionsFile(file: string): DecisionFinding[] {
38
+ const findings: DecisionFinding[] = [];
39
+ if (!fs.existsSync(file)) {
40
+ findings.push({ file, severity: 'error', decision: '(file)', message: 'decisions file not found' });
41
+ return findings;
42
+ }
43
+
44
+ const content = fs.readFileSync(file, 'utf-8');
45
+ for (const section of splitDecisionSections(content)) {
46
+ const status = statusOf(section.body);
47
+ const blocks = field(section.body, 'Blocks');
48
+ const durable = field(section.body, 'Durable candidate')?.toLowerCase();
49
+
50
+ if (status === 'pending') {
51
+ findings.push({
52
+ file,
53
+ severity: 'error',
54
+ decision: section.title,
55
+ message: 'decision is still pending',
56
+ });
57
+ }
58
+
59
+ if (blocks && blocks !== '-' && blocks.toLowerCase() !== 'none' && status && !RESOLVED_STATUS.has(status)) {
60
+ findings.push({
61
+ file,
62
+ severity: status === 'pending' ? 'error' : 'warning',
63
+ decision: section.title,
64
+ message: `blocking decision has unresolved status '${status}'`,
65
+ });
66
+ }
67
+
68
+ if (durable === 'yes') {
69
+ for (const required of ['Evidence', 'Confidence', 'Reversibility']) {
70
+ const value = field(section.body, required);
71
+ if (!value || value === '-') {
72
+ findings.push({
73
+ file,
74
+ severity: 'warning',
75
+ decision: section.title,
76
+ message: `durable candidate is missing ${required}`,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ return findings;
84
+ }
85
+
86
+ function resolveTarget(target: string | undefined): string {
87
+ if (!target) return path.join(process.cwd(), 'decisions.md');
88
+ if (target.endsWith('.md') || target.includes(path.sep)) return target;
89
+ return path.join(TRACKS_DIR, target, 'decisions.md');
90
+ }
91
+
92
+ function report(findings: DecisionFinding[], file: string): void {
93
+ if (findings.length === 0) {
94
+ console.log(`✓ decisions validate: no issues in ${file}`);
95
+ return;
96
+ }
97
+
98
+ console.log(`decisions validate: issues in ${file}:`);
99
+ for (const f of findings) {
100
+ console.log(` [${f.severity}] ${f.decision}: ${f.message}`);
101
+ }
102
+ const errors = findings.filter((f) => f.severity === 'error').length;
103
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
104
+ console.log(`${errors} error(s), ${warnings} warning(s)`);
105
+ if (errors > 0) process.exit(1);
106
+ }
107
+
108
+ export async function decisionsCommand(args: string[]) {
109
+ const sub = args[0];
110
+ const rest = args.slice(1);
111
+ switch (sub) {
112
+ case 'validate': {
113
+ const { positional } = parseOptions(rest);
114
+ const file = resolveTarget(positional[0]);
115
+ report(validateDecisionsFile(file), file);
116
+ return;
117
+ }
118
+ default:
119
+ console.error(`Unknown decisions subcommand: ${sub ?? '(none)'}`);
120
+ console.log('Usage: codument decisions validate [file|track-id]');
121
+ process.exit(1);
122
+ }
123
+ }