@dommaker/harness 0.13.3 → 0.16.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/harness-knowledge-check.js +38 -23
  3. package/bin/harness-knowledge-track.js +30 -4
  4. package/bin/harness.js +24 -1
  5. package/dist/cli/commands/index.d.ts +1 -1
  6. package/dist/cli/commands/index.d.ts.map +1 -1
  7. package/dist/cli/commands/index.js +4 -1
  8. package/dist/cli/commands/index.js.map +1 -1
  9. package/dist/cli/commands/knowledge.d.ts +21 -0
  10. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  11. package/dist/cli/commands/knowledge.js +160 -9
  12. package/dist/cli/commands/knowledge.js.map +1 -1
  13. package/dist/cli/commands/release.d.ts.map +1 -1
  14. package/dist/cli/commands/release.js +43 -6
  15. package/dist/cli/commands/release.js.map +1 -1
  16. package/dist/core/constraints/checker.js +1 -1
  17. package/dist/core/constraints/checker.js.map +1 -1
  18. package/dist/core/constraints/definitions.d.ts.map +1 -1
  19. package/dist/core/constraints/definitions.js +34 -26
  20. package/dist/core/constraints/definitions.js.map +1 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/knowledge/audit.d.ts +76 -0
  26. package/dist/knowledge/audit.d.ts.map +1 -0
  27. package/dist/knowledge/audit.js +566 -0
  28. package/dist/knowledge/audit.js.map +1 -0
  29. package/dist/knowledge/doctor.d.ts +1 -1
  30. package/dist/knowledge/doctor.d.ts.map +1 -1
  31. package/dist/knowledge/import.d.ts +1 -1
  32. package/dist/knowledge/import.d.ts.map +1 -1
  33. package/dist/knowledge/import.js +3 -0
  34. package/dist/knowledge/import.js.map +1 -1
  35. package/dist/knowledge/index.d.ts +7 -2
  36. package/dist/knowledge/index.d.ts.map +1 -1
  37. package/dist/knowledge/index.js +7 -2
  38. package/dist/knowledge/index.js.map +1 -1
  39. package/dist/knowledge/ingest.d.ts +28 -1
  40. package/dist/knowledge/ingest.d.ts.map +1 -1
  41. package/dist/knowledge/ingest.js +150 -5
  42. package/dist/knowledge/ingest.js.map +1 -1
  43. package/dist/knowledge/lifecycle-hooks.d.ts +1 -1
  44. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  45. package/dist/knowledge/lifecycle-hooks.js +3 -0
  46. package/dist/knowledge/lifecycle-hooks.js.map +1 -1
  47. package/dist/knowledge/lifecycle.d.ts +60 -8
  48. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  49. package/dist/knowledge/lifecycle.js +213 -52
  50. package/dist/knowledge/lifecycle.js.map +1 -1
  51. package/dist/knowledge/lint.d.ts +1 -1
  52. package/dist/knowledge/lint.d.ts.map +1 -1
  53. package/dist/knowledge/lint.js +44 -3
  54. package/dist/knowledge/lint.js.map +1 -1
  55. package/dist/knowledge/migration.d.ts +25 -0
  56. package/dist/knowledge/migration.d.ts.map +1 -0
  57. package/dist/knowledge/migration.js +96 -0
  58. package/dist/knowledge/migration.js.map +1 -0
  59. package/dist/knowledge/query.d.ts +30 -2
  60. package/dist/knowledge/query.d.ts.map +1 -1
  61. package/dist/knowledge/query.js +58 -0
  62. package/dist/knowledge/query.js.map +1 -1
  63. package/dist/knowledge/reference-tracker.d.ts +1 -1
  64. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  65. package/dist/knowledge/store.d.ts +50 -3
  66. package/dist/knowledge/store.d.ts.map +1 -1
  67. package/dist/knowledge/store.js +111 -9
  68. package/dist/knowledge/store.js.map +1 -1
  69. package/dist/knowledge/types.d.ts +43 -1
  70. package/dist/knowledge/types.d.ts.map +1 -1
  71. package/dist/knowledge/types.js +1 -0
  72. package/dist/knowledge/types.js.map +1 -1
  73. package/dist/monitoring/knowledge-evolver.d.ts +1 -1
  74. package/dist/monitoring/knowledge-evolver.d.ts.map +1 -1
  75. package/dist/types/constraint.d.ts +1 -1
  76. package/dist/types/constraint.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/__tests__/checker.test.ts +4 -4
  79. package/src/cli/commands/__tests__/knowledge.test.ts +167 -0
  80. package/src/cli/commands/index.ts +1 -1
  81. package/src/cli/commands/knowledge.ts +176 -5
  82. package/src/cli/commands/release.ts +44 -6
  83. package/src/context/__tests__/knowledge-injector.test.ts +4 -1
  84. package/src/core/constraints/checker.ts +1 -1
  85. package/src/core/constraints/definitions.ts +35 -27
  86. package/src/dashboard/__tests__/data.test.ts +3 -0
  87. package/src/dashboard/__tests__/stats.test.ts +3 -0
  88. package/src/index.ts +3 -1
  89. package/src/knowledge/CONTEXT.md +29 -7
  90. package/src/knowledge/__tests__/audit.test.ts +598 -0
  91. package/src/knowledge/__tests__/doctor.test.ts +28 -1
  92. package/src/knowledge/__tests__/import.test.ts +1 -1
  93. package/src/knowledge/__tests__/ingest.test.ts +320 -1
  94. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +4 -1
  95. package/src/knowledge/__tests__/lifecycle.test.ts +656 -3
  96. package/src/knowledge/__tests__/lint.test.ts +52 -1
  97. package/src/knowledge/__tests__/migration.test.ts +115 -0
  98. package/src/knowledge/__tests__/query.test.ts +87 -1
  99. package/src/knowledge/__tests__/reference-tracker.test.ts +4 -1
  100. package/src/knowledge/__tests__/store.test.ts +208 -1
  101. package/src/knowledge/audit.ts +641 -0
  102. package/src/knowledge/doctor.ts +1 -1
  103. package/src/knowledge/import.ts +4 -1
  104. package/src/knowledge/index.ts +7 -2
  105. package/src/knowledge/ingest.ts +170 -6
  106. package/src/knowledge/lifecycle-hooks.ts +4 -1
  107. package/src/knowledge/lifecycle.ts +219 -50
  108. package/src/knowledge/lint.ts +45 -4
  109. package/src/knowledge/migration.ts +73 -0
  110. package/src/knowledge/query.ts +72 -1
  111. package/src/knowledge/reference-tracker.ts +1 -1
  112. package/src/knowledge/store.ts +125 -8
  113. package/src/knowledge/types.ts +50 -1
  114. package/src/monitoring/__tests__/knowledge-doctor.test.ts +3 -0
  115. package/src/monitoring/__tests__/knowledge-evolver.test.ts +3 -0
  116. package/src/monitoring/knowledge-evolver.ts +1 -1
  117. package/src/types/constraint.ts +1 -1
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { KnowledgeLifecycle } from '../lifecycle';
8
8
  import type { KnowledgeEntry } from '../types';
9
9
  import * as fs from 'fs';
@@ -29,6 +29,9 @@ describe('KnowledgeLifecycle', () => {
29
29
  applicablePhases: [],
30
30
  sourceReferences: [],
31
31
  referencedBy: [],
32
+ executionResults: [],
33
+ consumptionMode: 'reference',
34
+ origin: 'agent',
32
35
  ...overrides,
33
36
  });
34
37
 
@@ -80,9 +83,224 @@ describe('KnowledgeLifecycle', () => {
80
83
  });
81
84
  });
82
85
 
86
+ describe('recordReference with execution result', () => {
87
+ it('should record success execution result', () => {
88
+ store.save(makeEntry());
89
+ lifecycle.recordReference('DEC-001', 'alice', true);
90
+ const entry = store.get('DEC-001');
91
+ expect(entry!.executionResults).toHaveLength(1);
92
+ expect(entry!.executionResults[0].success).toBe(true);
93
+ expect(entry!.executionResults[0].contributor).toBe('alice');
94
+ });
95
+
96
+ it('should record failure execution result', () => {
97
+ store.save(makeEntry());
98
+ lifecycle.recordReference('DEC-001', 'bob', false);
99
+ const entry = store.get('DEC-001');
100
+ expect(entry!.executionResults).toHaveLength(1);
101
+ expect(entry!.executionResults[0].success).toBe(false);
102
+ });
103
+
104
+ it('should not record execution result when success is undefined', () => {
105
+ store.save(makeEntry());
106
+ lifecycle.recordReference('DEC-001', 'alice');
107
+ const entry = store.get('DEC-001');
108
+ expect(entry!.executionResults).toHaveLength(0);
109
+ });
110
+
111
+ it('should record execution result even on same-day dedup', () => {
112
+ store.save(makeEntry());
113
+ lifecycle.recordReference('DEC-001', 'alice', true);
114
+ lifecycle.recordReference('DEC-001', 'alice', false); // same day, but different result
115
+ const entry = store.get('DEC-001');
116
+ expect(entry!.referencedBy).toHaveLength(1); // deduped
117
+ expect(entry!.executionResults).toHaveLength(2); // both recorded
118
+ });
119
+ });
120
+
121
+ describe('getExecutionSuccessRate', () => {
122
+ it('should return undefined when no execution data', () => {
123
+ store.save(makeEntry());
124
+ expect(lifecycle.getExecutionSuccessRate('DEC-001')).toBeUndefined();
125
+ });
126
+
127
+ it('should compute success rate correctly', () => {
128
+ store.save(makeEntry());
129
+ lifecycle.recordReference('DEC-001', 'a', true);
130
+ lifecycle.recordReference('DEC-001', 'b', true);
131
+ lifecycle.recordReference('DEC-001', 'c', false);
132
+ const rate = lifecycle.getExecutionSuccessRate('DEC-001');
133
+ expect(rate).toBeDefined();
134
+ expect(rate!.total).toBe(3);
135
+ expect(rate!.rate).toBeCloseTo(2 / 3);
136
+ });
137
+
138
+ it('should return undefined for non-existent entry', () => {
139
+ expect(lifecycle.getExecutionSuccessRate('NON-EXISTENT')).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe('getHumanSuccessRate', () => {
144
+ it('should return undefined when no execution data', () => {
145
+ store.save(makeEntry());
146
+ expect(lifecycle.getHumanSuccessRate('DEC-001')).toBeUndefined();
147
+ });
148
+
149
+ it('should return undefined when no human-sourced results', () => {
150
+ store.save(makeEntry());
151
+ lifecycle.recordReference('DEC-001', 'auto-bot', true, 'auto');
152
+ lifecycle.recordReference('DEC-001', 'auto-bot', true, 'auto');
153
+ expect(lifecycle.getHumanSuccessRate('DEC-001')).toBeUndefined();
154
+ });
155
+
156
+ it('should compute human-only success rate', () => {
157
+ store.save(makeEntry());
158
+ lifecycle.recordReference('DEC-001', 'alice', true, 'human');
159
+ lifecycle.recordReference('DEC-001', 'bob', true, 'human');
160
+ lifecycle.recordReference('DEC-001', 'auto-bot', true, 'auto');
161
+ lifecycle.recordReference('DEC-001', 'charlie', false, 'human');
162
+ const rate = lifecycle.getHumanSuccessRate('DEC-001');
163
+ expect(rate).toBeDefined();
164
+ expect(rate!.total).toBe(3); // 3 human results
165
+ expect(rate!.rate).toBeCloseTo(2 / 3); // 2 success out of 3 human
166
+ });
167
+
168
+ it('should return undefined for non-existent entry', () => {
169
+ expect(lifecycle.getHumanSuccessRate('NON-EXISTENT')).toBeUndefined();
170
+ });
171
+ });
172
+
173
+ describe('recordReference with source', () => {
174
+ it('should store source in execution result', () => {
175
+ store.save(makeEntry());
176
+ lifecycle.recordReference('DEC-001', 'alice', true, 'human');
177
+ const entry = store.get('DEC-001');
178
+ expect(entry!.executionResults[0].source).toBe('human');
179
+ });
180
+
181
+ it('should default source to undefined when not provided', () => {
182
+ store.save(makeEntry());
183
+ lifecycle.recordReference('DEC-001', 'alice', true);
184
+ const entry = store.get('DEC-001');
185
+ expect(entry!.executionResults[0].source).toBeUndefined();
186
+ });
187
+
188
+ it('should auto-detect source for auto- prefixed contributors', () => {
189
+ store.save(makeEntry());
190
+ lifecycle.recordReference('DEC-001', 'auto-deploy', true);
191
+ const entry = store.get('DEC-001');
192
+ expect(entry!.executionResults[0].source).toBe('auto');
193
+ });
194
+ });
195
+
196
+ describe('onReference callback', () => {
197
+ it('should fire callback on recordReference()', () => {
198
+ store.save(makeEntry());
199
+ const events: any[] = [];
200
+ lifecycle.onReference((event) => events.push(event));
201
+
202
+ lifecycle.recordReference('DEC-001', 'alice');
203
+
204
+ expect(events).toHaveLength(1);
205
+ expect(events[0].entryId).toBe('DEC-001');
206
+ expect(events[0].contributor).toBe('alice');
207
+ expect(events[0].timestamp).toBeDefined();
208
+ });
209
+
210
+ it('should fire multiple callbacks', () => {
211
+ store.save(makeEntry());
212
+ const events1: any[] = [];
213
+ const events2: any[] = [];
214
+ lifecycle.onReference((event) => events1.push(event));
215
+ lifecycle.onReference((event) => events2.push(event));
216
+
217
+ lifecycle.recordReference('DEC-001', 'bob');
218
+
219
+ expect(events1).toHaveLength(1);
220
+ expect(events2).toHaveLength(1);
221
+ });
222
+
223
+ it('should not fire callback on same-day dedup', () => {
224
+ store.save(makeEntry());
225
+ const events: any[] = [];
226
+ lifecycle.onReference((event) => events.push(event));
227
+
228
+ lifecycle.recordReference('DEC-001', 'alice');
229
+ lifecycle.recordReference('DEC-001', 'alice'); // same day dedup
230
+
231
+ expect(events).toHaveLength(1); // only first call fires
232
+ });
233
+
234
+ it('should not fire callback for non-existent entry', () => {
235
+ const events: any[] = [];
236
+ lifecycle.onReference((event) => events.push(event));
237
+
238
+ lifecycle.recordReference('NONEXISTENT', 'alice');
239
+
240
+ expect(events).toHaveLength(0);
241
+ });
242
+ });
243
+
244
+ describe('recordReference same-day dedup (B4)', () => {
245
+ it('should skip file write when same contributor references on same day', () => {
246
+ store.save(makeEntry());
247
+ const result1 = lifecycle.recordReference('DEC-001', 'alice');
248
+ expect(result1).toBeDefined();
249
+ expect(result1!.referencedBy).toHaveLength(1);
250
+
251
+ // Second call same day, same contributor — should return entry unchanged
252
+ const result2 = lifecycle.recordReference('DEC-001', 'alice');
253
+ expect(result2).toBeDefined();
254
+ expect(result2!.referencedBy).toHaveLength(1); // no new refKey added
255
+ expect(result2!.lastReferenced).toBe(result1!.lastReferenced); // unchanged
256
+ });
257
+
258
+ it('should add ref for different contributor on same day', () => {
259
+ store.save(makeEntry());
260
+ lifecycle.recordReference('DEC-001', 'alice');
261
+ const result = lifecycle.recordReference('DEC-001', 'bob');
262
+ expect(result!.referencedBy).toHaveLength(2);
263
+ });
264
+ });
265
+
266
+ describe('referencedBy cap at 20 (B4)', () => {
267
+ it('should cap referencedBy at MAX_REFERENCED_BY (20)', () => {
268
+ // Pre-fill with 19 referencedBy entries
269
+ const refs = Array.from({ length: 19 }, (_, i) => `user-${i}:2026-05-${String(i + 1).padStart(2, '0')}`);
270
+ store.save(makeEntry({ referencedBy: refs }));
271
+
272
+ // Add a new unique ref
273
+ const result = lifecycle.recordReference('DEC-001', 'new-user');
274
+ expect(result!.referencedBy).toHaveLength(20);
275
+ expect(result!.referencedBy[19]).toMatch(/^new-user:/);
276
+ });
277
+
278
+ it('should drop oldest refs when exceeding cap', () => {
279
+ // Pre-fill with 25 referencedBy entries
280
+ const refs = Array.from({ length: 25 }, (_, i) => `user-${i}:2026-05-${String(i + 1).padStart(2, '0')}`);
281
+ store.save(makeEntry({ referencedBy: refs }));
282
+
283
+ // Add a new unique ref — total would be 26, should cap to 20
284
+ const result = lifecycle.recordReference('DEC-001', 'brand-new');
285
+ expect(result!.referencedBy).toHaveLength(20);
286
+ // Should have dropped the 6 oldest (indices 0-5)
287
+ expect(result!.referencedBy[0]).toBe('user-6:2026-05-07');
288
+ });
289
+ });
290
+
83
291
  describe('checkPromotion', () => {
84
292
  it('should promote draft to verified when referenced', () => {
85
- store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01' }));
293
+ store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01', content: 'a'.repeat(60) }));
294
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('verified');
295
+ });
296
+
297
+ it('should NOT promote draft with content < 50 chars', () => {
298
+ store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01', content: 'short' }));
299
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined();
300
+ });
301
+
302
+ it('should promote draft with exactly 50 chars content', () => {
303
+ store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01', content: 'a'.repeat(50) }));
86
304
  expect(lifecycle.checkPromotion('DEC-001')).toBe('verified');
87
305
  });
88
306
 
@@ -94,6 +312,7 @@ describe('KnowledgeLifecycle', () => {
94
312
  it('should promote verified to proven when criteria met', () => {
95
313
  store.save(makeEntry({
96
314
  maturity: 'verified',
315
+ content: 'a'.repeat(100),
97
316
  contributors: ['a', 'b', 'c'],
98
317
  projects: ['p1', 'p2'],
99
318
  }));
@@ -178,7 +397,7 @@ describe('KnowledgeLifecycle', () => {
178
397
 
179
398
  describe('tryPromote', () => {
180
399
  it('should promote eligible entry', () => {
181
- store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01' }));
400
+ store.save(makeEntry({ maturity: 'draft', lastReferenced: '2026-05-01', content: 'a'.repeat(60) }));
182
401
  const change = lifecycle.tryPromote('DEC-001');
183
402
  expect(change).toBeDefined();
184
403
  expect(change!.from).toBe('draft');
@@ -212,6 +431,181 @@ describe('KnowledgeLifecycle', () => {
212
431
  it('should return undefined for non-existent entry', () => {
213
432
  expect(lifecycle.checkPromotion('NON-EXISTENT')).toBeUndefined();
214
433
  });
434
+
435
+ // RC2: test ID exclusion
436
+ it('should NOT promote draft with test-* ID', () => {
437
+ store.save(makeEntry({ id: 'test-search-123', maturity: 'draft', lastReferenced: '2026-05-01', content: 'a'.repeat(60) }));
438
+ expect(lifecycle.checkPromotion('test-search-123')).toBeUndefined();
439
+ });
440
+
441
+ it('should NOT promote draft with inj-test-* ID', () => {
442
+ store.save(makeEntry({ id: 'inj-test-123', maturity: 'draft', lastReferenced: '2026-05-01', content: 'a'.repeat(60) }));
443
+ expect(lifecycle.checkPromotion('inj-test-123')).toBeUndefined();
444
+ });
445
+
446
+ // RC2: verified→proven content quality gate
447
+ it('should NOT promote verified to proven with content < 100 chars', () => {
448
+ store.save(makeEntry({
449
+ maturity: 'verified',
450
+ content: 'short content',
451
+ contributors: ['a', 'b', 'c'],
452
+ projects: ['p1', 'p2'],
453
+ }));
454
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined();
455
+ });
456
+
457
+ it('should promote verified to proven with content >= 100 chars', () => {
458
+ store.save(makeEntry({
459
+ maturity: 'verified',
460
+ content: 'a'.repeat(100),
461
+ contributors: ['a', 'b', 'c'],
462
+ projects: ['p1', 'p2'],
463
+ }));
464
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('proven');
465
+ });
466
+
467
+ it('should NOT promote verified via Path B with content < 100 chars', () => {
468
+ store.save(makeEntry({
469
+ maturity: 'verified',
470
+ content: 'short',
471
+ referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03'],
472
+ sourceReferences: [
473
+ { workflow: 'pattern:monitor', timestamp: '2026-05-01' },
474
+ { workflow: 'pattern:triage', timestamp: '2026-05-02' },
475
+ ],
476
+ }));
477
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined();
478
+ });
479
+
480
+ // Path C: execution success rate promotion
481
+ it('should promote verified to proven via execution success rate >= 80%', () => {
482
+ store.save(makeEntry({
483
+ maturity: 'verified',
484
+ content: 'a'.repeat(100),
485
+ executionResults: [
486
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
487
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
488
+ { contributor: 'c', success: true, timestamp: '2026-05-03' },
489
+ { contributor: 'd', success: true, timestamp: '2026-05-04' },
490
+ ],
491
+ }));
492
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('proven'); // 4/4 = 100%
493
+ });
494
+
495
+ it('should promote verified to proven with exactly 80% success rate', () => {
496
+ store.save(makeEntry({
497
+ maturity: 'verified',
498
+ content: 'a'.repeat(100),
499
+ executionResults: [
500
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
501
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
502
+ { contributor: 'c', success: true, timestamp: '2026-05-03' },
503
+ { contributor: 'd', success: true, timestamp: '2026-05-04' },
504
+ { contributor: 'e', success: false, timestamp: '2026-05-05' },
505
+ ],
506
+ }));
507
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('proven'); // 4/5 = 80%
508
+ });
509
+
510
+ it('should NOT promote verified with execution success rate just below 80%', () => {
511
+ store.save(makeEntry({
512
+ maturity: 'verified',
513
+ content: 'a'.repeat(100),
514
+ executionResults: [
515
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
516
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
517
+ { contributor: 'c', success: true, timestamp: '2026-05-03' },
518
+ { contributor: 'd', success: false, timestamp: '2026-05-04' },
519
+ ],
520
+ }));
521
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined(); // 3/4 = 75%
522
+ });
523
+
524
+ it('should NOT promote verified with execution success rate < 80%', () => {
525
+ store.save(makeEntry({
526
+ maturity: 'verified',
527
+ content: 'a'.repeat(100),
528
+ executionResults: [
529
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
530
+ { contributor: 'b', success: false, timestamp: '2026-05-02' },
531
+ { contributor: 'c', success: false, timestamp: '2026-05-03' },
532
+ ],
533
+ }));
534
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined(); // 1/3 = 33%
535
+ });
536
+
537
+ it('should NOT promote verified with < 3 execution results', () => {
538
+ store.save(makeEntry({
539
+ maturity: 'verified',
540
+ content: 'a'.repeat(100),
541
+ executionResults: [
542
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
543
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
544
+ ],
545
+ }));
546
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined(); // only 2 results
547
+ });
548
+
549
+ it('should NOT promote verified via Path C with content < 100 chars', () => {
550
+ store.save(makeEntry({
551
+ maturity: 'verified',
552
+ content: 'short',
553
+ executionResults: [
554
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
555
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
556
+ { contributor: 'c', success: true, timestamp: '2026-05-03' },
557
+ ],
558
+ }));
559
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined(); // content gate blocks
560
+ });
561
+
562
+ // Path C: human source preference
563
+ it('should promote verified via human success rate when mixed with auto', () => {
564
+ store.save(makeEntry({
565
+ maturity: 'verified',
566
+ content: 'a'.repeat(100),
567
+ executionResults: [
568
+ { contributor: 'alice', success: true, timestamp: '2026-05-01', source: 'human' },
569
+ { contributor: 'bob', success: true, timestamp: '2026-05-02', source: 'human' },
570
+ { contributor: 'charlie', success: true, timestamp: '2026-05-03', source: 'human' },
571
+ { contributor: 'auto-bot', success: false, timestamp: '2026-05-04', source: 'auto' },
572
+ ],
573
+ }));
574
+ // Human rate: 3/3 = 100%, auto fails but human succeeds → promote
575
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('proven');
576
+ });
577
+
578
+ it('should NOT promote when human rate < 80% even if overall rate >= 80%', () => {
579
+ store.save(makeEntry({
580
+ maturity: 'verified',
581
+ content: 'a'.repeat(100),
582
+ executionResults: [
583
+ { contributor: 'alice', success: false, timestamp: '2026-05-01', source: 'human' },
584
+ { contributor: 'bob', success: false, timestamp: '2026-05-02', source: 'human' },
585
+ { contributor: 'charlie', success: true, timestamp: '2026-05-03', source: 'human' },
586
+ { contributor: 'auto-1', success: true, timestamp: '2026-05-04', source: 'auto' },
587
+ { contributor: 'auto-2', success: true, timestamp: '2026-05-05', source: 'auto' },
588
+ { contributor: 'auto-3', success: true, timestamp: '2026-05-06', source: 'auto' },
589
+ ],
590
+ }));
591
+ // Human rate: 1/3 = 33%, overall: 4/6 = 67% — neither meets threshold
592
+ expect(lifecycle.checkPromotion('DEC-001')).toBeUndefined();
593
+ });
594
+
595
+ it('should fall back to overall rate when no human results', () => {
596
+ store.save(makeEntry({
597
+ maturity: 'verified',
598
+ content: 'a'.repeat(100),
599
+ executionResults: [
600
+ { contributor: 'auto-1', success: true, timestamp: '2026-05-01', source: 'auto' },
601
+ { contributor: 'auto-2', success: true, timestamp: '2026-05-02', source: 'auto' },
602
+ { contributor: 'auto-3', success: true, timestamp: '2026-05-03', source: 'auto' },
603
+ { contributor: 'auto-4', success: true, timestamp: '2026-05-04', source: 'auto' },
604
+ ],
605
+ }));
606
+ // No human results → fall back to overall: 4/4 = 100%
607
+ expect(lifecycle.checkPromotion('DEC-001')).toBe('proven');
608
+ });
215
609
  });
216
610
 
217
611
  describe('checkEntryDecay edge cases', () => {
@@ -245,4 +639,263 @@ describe('KnowledgeLifecycle', () => {
245
639
  expect(lifecycle.checkEntryDecay('DEC-001')).toBe('verified');
246
640
  });
247
641
  });
642
+
643
+ // ── decayAt hard expiry ────────────────────────────────────
644
+
645
+ describe('decayAt hard expiry', () => {
646
+ it('should archive entry when decayAt is in the past', () => {
647
+ store.save(makeEntry({
648
+ maturity: 'proven',
649
+ decayAt: '2026-01-01T00:00:00.000Z',
650
+ }));
651
+ expect(lifecycle.checkEntryDecay('DEC-001')).toBe('archived');
652
+ });
653
+
654
+ it('should NOT archive entry when decayAt is in the future', () => {
655
+ const future = new Date();
656
+ future.setFullYear(future.getFullYear() + 1);
657
+ store.save(makeEntry({
658
+ maturity: 'proven',
659
+ decayAt: future.toISOString(),
660
+ lastReferenced: new Date().toISOString(),
661
+ }));
662
+ expect(lifecycle.checkEntryDecay('DEC-001')).toBeUndefined();
663
+ });
664
+
665
+ it('should take precedence over mode-specific decay', () => {
666
+ // proven entry with decayAt in the past should go straight to archived
667
+ // even though proven→verified takes 12 months normally
668
+ store.save(makeEntry({
669
+ maturity: 'proven',
670
+ decayAt: '2026-01-01T00:00:00.000Z',
671
+ lastReferenced: new Date().toISOString(), // recent
672
+ }));
673
+ expect(lifecycle.checkEntryDecay('DEC-001')).toBe('archived');
674
+ });
675
+ });
676
+
677
+ // ── rule lifecycle ─────────────────────────────────────────
678
+
679
+ describe('rule lifecycle', () => {
680
+ it('should promote draft→active with 1 success execution', () => {
681
+ store.save(makeEntry({
682
+ id: 'RULE-001',
683
+ consumptionMode: 'rule',
684
+ maturity: 'draft',
685
+ content: 'API port must be 13101',
686
+ executionResults: [
687
+ { contributor: 'alice', success: true, timestamp: '2026-05-01T00:00:00.000Z', source: 'human' },
688
+ ],
689
+ }));
690
+ expect(lifecycle.checkPromotion('RULE-001')).toBe('active');
691
+ });
692
+
693
+ it('should NOT promote draft→active with 0 executions', () => {
694
+ store.save(makeEntry({
695
+ id: 'RULE-001',
696
+ consumptionMode: 'rule',
697
+ maturity: 'draft',
698
+ content: 'API port must be 13101',
699
+ executionResults: [],
700
+ }));
701
+ expect(lifecycle.checkPromotion('RULE-001')).toBeUndefined();
702
+ });
703
+
704
+ it('should NOT promote draft→active with failure execution', () => {
705
+ store.save(makeEntry({
706
+ id: 'RULE-001',
707
+ consumptionMode: 'rule',
708
+ maturity: 'draft',
709
+ content: 'API port must be 13101',
710
+ executionResults: [
711
+ { contributor: 'alice', success: false, timestamp: '2026-05-01T00:00:00.000Z', source: 'human' },
712
+ ],
713
+ }));
714
+ expect(lifecycle.checkPromotion('RULE-001')).toBeUndefined();
715
+ });
716
+
717
+ it('should decay active→deprecated when fail rate >= 50% with 3+ results', () => {
718
+ store.save(makeEntry({
719
+ id: 'RULE-001',
720
+ consumptionMode: 'rule',
721
+ maturity: 'active',
722
+ content: 'API port must be 13101',
723
+ executionResults: [
724
+ { contributor: 'alice', success: false, timestamp: '2026-05-01T00:00:00.000Z' },
725
+ { contributor: 'bob', success: false, timestamp: '2026-05-02T00:00:00.000Z' },
726
+ { contributor: 'charlie', success: true, timestamp: '2026-05-03T00:00:00.000Z' },
727
+ ],
728
+ }));
729
+ expect(lifecycle.checkEntryDecay('RULE-001')).toBe('deprecated');
730
+ });
731
+
732
+ it('should NOT decay active→deprecated when fail rate < 50%', () => {
733
+ store.save(makeEntry({
734
+ id: 'RULE-001',
735
+ consumptionMode: 'rule',
736
+ maturity: 'active',
737
+ content: 'API port must be 13101',
738
+ executionResults: [
739
+ { contributor: 'alice', success: true, timestamp: '2026-05-01T00:00:00.000Z' },
740
+ { contributor: 'bob', success: true, timestamp: '2026-05-02T00:00:00.000Z' },
741
+ { contributor: 'charlie', success: false, timestamp: '2026-05-03T00:00:00.000Z' },
742
+ ],
743
+ }));
744
+ expect(lifecycle.checkEntryDecay('RULE-001')).toBeUndefined();
745
+ });
746
+
747
+ it('should NOT decay active with < 3 results', () => {
748
+ store.save(makeEntry({
749
+ id: 'RULE-001',
750
+ consumptionMode: 'rule',
751
+ maturity: 'active',
752
+ content: 'API port must be 13101',
753
+ executionResults: [
754
+ { contributor: 'alice', success: false, timestamp: '2026-05-01T00:00:00.000Z' },
755
+ ],
756
+ }));
757
+ expect(lifecycle.checkEntryDecay('RULE-001')).toBeUndefined();
758
+ });
759
+
760
+ it('should NOT promote or decay deprecated', () => {
761
+ store.save(makeEntry({
762
+ id: 'RULE-001',
763
+ consumptionMode: 'rule',
764
+ maturity: 'deprecated',
765
+ content: 'Old rule',
766
+ }));
767
+ expect(lifecycle.checkPromotion('RULE-001')).toBeUndefined();
768
+ expect(lifecycle.checkEntryDecay('RULE-001')).toBeUndefined();
769
+ });
770
+ });
771
+
772
+ // ── context lifecycle ──────────────────────────────────────
773
+
774
+ describe('context lifecycle', () => {
775
+ it('should promote draft→active with 1 reference', () => {
776
+ store.save(makeEntry({
777
+ id: 'CTX-001',
778
+ consumptionMode: 'context',
779
+ maturity: 'draft',
780
+ content: 'Project uses TypeScript 5.0',
781
+ referencedBy: ['user:2026-05-01'],
782
+ }));
783
+ expect(lifecycle.checkPromotion('CTX-001')).toBe('active');
784
+ });
785
+
786
+ it('should NOT promote draft→active with 0 references', () => {
787
+ store.save(makeEntry({
788
+ id: 'CTX-001',
789
+ consumptionMode: 'context',
790
+ maturity: 'draft',
791
+ content: 'Project uses TypeScript 5.0',
792
+ referencedBy: [],
793
+ }));
794
+ expect(lifecycle.checkPromotion('CTX-001')).toBeUndefined();
795
+ });
796
+
797
+ it('should decay active→archived after 3 months unreferenced', () => {
798
+ const oldDate = new Date();
799
+ oldDate.setMonth(oldDate.getMonth() - 4);
800
+ store.save(makeEntry({
801
+ id: 'CTX-001',
802
+ consumptionMode: 'context',
803
+ maturity: 'active',
804
+ content: 'Project uses TypeScript 5.0',
805
+ lastReferenced: oldDate.toISOString(),
806
+ }));
807
+ expect(lifecycle.checkEntryDecay('CTX-001')).toBe('archived');
808
+ });
809
+
810
+ it('should NOT decay active when recently referenced', () => {
811
+ store.save(makeEntry({
812
+ id: 'CTX-001',
813
+ consumptionMode: 'context',
814
+ maturity: 'active',
815
+ content: 'Project uses TypeScript 5.0',
816
+ lastReferenced: new Date().toISOString(),
817
+ }));
818
+ expect(lifecycle.checkEntryDecay('CTX-001')).toBeUndefined();
819
+ });
820
+
821
+ it('should NOT promote active or archived', () => {
822
+ store.save(makeEntry({
823
+ id: 'CTX-001',
824
+ consumptionMode: 'context',
825
+ maturity: 'active',
826
+ content: 'Project uses TypeScript 5.0',
827
+ }));
828
+ expect(lifecycle.checkPromotion('CTX-001')).toBeUndefined();
829
+ });
830
+ });
831
+
832
+ // ── signal lifecycle ───────────────────────────────────────
833
+
834
+ describe('signal lifecycle', () => {
835
+ it('should NOT promote signal entries', () => {
836
+ store.save(makeEntry({
837
+ id: 'SIG-001',
838
+ consumptionMode: 'signal',
839
+ maturity: 'active',
840
+ content: 'CPU usage spike detected',
841
+ tags: ['monitoring'],
842
+ }));
843
+ expect(lifecycle.checkPromotion('SIG-001')).toBeUndefined();
844
+ });
845
+
846
+ it('should archive saturated signal with newer same-tag entry', () => {
847
+ store.save(makeEntry({
848
+ id: 'SIG-001',
849
+ consumptionMode: 'signal',
850
+ maturity: 'active',
851
+ content: 'Old monitoring alert',
852
+ tags: ['monitoring'],
853
+ created: '2026-05-01T00:00:00.000Z',
854
+ referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03'],
855
+ }));
856
+ store.save(makeEntry({
857
+ id: 'SIG-002',
858
+ consumptionMode: 'signal',
859
+ maturity: 'active',
860
+ content: 'New monitoring alert',
861
+ tags: ['monitoring'],
862
+ created: '2026-05-10T00:00:00.000Z',
863
+ }));
864
+ expect(lifecycle.checkEntryDecay('SIG-001')).toBe('archived');
865
+ });
866
+
867
+ it('should NOT archive saturated signal without newer same-tag entry', () => {
868
+ store.save(makeEntry({
869
+ id: 'SIG-001',
870
+ consumptionMode: 'signal',
871
+ maturity: 'active',
872
+ content: 'Latest monitoring alert',
873
+ tags: ['monitoring'],
874
+ created: '2026-05-10T00:00:00.000Z',
875
+ referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03'],
876
+ }));
877
+ expect(lifecycle.checkEntryDecay('SIG-001')).toBeUndefined();
878
+ });
879
+
880
+ it('should NOT archive unsaturated signal even with newer entry', () => {
881
+ store.save(makeEntry({
882
+ id: 'SIG-001',
883
+ consumptionMode: 'signal',
884
+ maturity: 'active',
885
+ content: 'Old monitoring alert',
886
+ tags: ['monitoring'],
887
+ created: '2026-05-01T00:00:00.000Z',
888
+ referencedBy: ['a:2026-05-01'], // only 1 reference, not saturated
889
+ }));
890
+ store.save(makeEntry({
891
+ id: 'SIG-002',
892
+ consumptionMode: 'signal',
893
+ maturity: 'active',
894
+ content: 'New monitoring alert',
895
+ tags: ['monitoring'],
896
+ created: '2026-05-10T00:00:00.000Z',
897
+ }));
898
+ expect(lifecycle.checkEntryDecay('SIG-001')).toBeUndefined();
899
+ });
900
+ });
248
901
  });