principles-disciple 1.7.6 → 1.7.8

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 (106) hide show
  1. package/dist/commands/context.js +5 -15
  2. package/dist/commands/evolution-status.js +2 -9
  3. package/dist/commands/export.js +61 -8
  4. package/dist/commands/nocturnal-review.d.ts +24 -0
  5. package/dist/commands/nocturnal-review.js +265 -0
  6. package/dist/commands/nocturnal-rollout.d.ts +27 -0
  7. package/dist/commands/nocturnal-rollout.js +671 -0
  8. package/dist/commands/nocturnal-train.d.ts +25 -0
  9. package/dist/commands/nocturnal-train.js +919 -0
  10. package/dist/commands/pain.js +8 -21
  11. package/dist/constants/tools.d.ts +2 -2
  12. package/dist/constants/tools.js +1 -1
  13. package/dist/core/adaptive-thresholds.d.ts +186 -0
  14. package/dist/core/adaptive-thresholds.js +300 -0
  15. package/dist/core/config.d.ts +2 -38
  16. package/dist/core/config.js +6 -61
  17. package/dist/core/event-log.d.ts +1 -2
  18. package/dist/core/event-log.js +0 -3
  19. package/dist/core/evolution-engine.js +1 -21
  20. package/dist/core/evolution-reducer.d.ts +7 -1
  21. package/dist/core/evolution-reducer.js +56 -4
  22. package/dist/core/evolution-types.d.ts +61 -9
  23. package/dist/core/evolution-types.js +31 -9
  24. package/dist/core/external-training-contract.d.ts +276 -0
  25. package/dist/core/external-training-contract.js +269 -0
  26. package/dist/core/local-worker-routing.d.ts +175 -0
  27. package/dist/core/local-worker-routing.js +525 -0
  28. package/dist/core/model-deployment-registry.d.ts +218 -0
  29. package/dist/core/model-deployment-registry.js +503 -0
  30. package/dist/core/model-training-registry.d.ts +295 -0
  31. package/dist/core/model-training-registry.js +475 -0
  32. package/dist/core/nocturnal-arbiter.d.ts +159 -0
  33. package/dist/core/nocturnal-arbiter.js +534 -0
  34. package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
  35. package/dist/core/nocturnal-candidate-scoring.js +266 -0
  36. package/dist/core/nocturnal-compliance.d.ts +175 -0
  37. package/dist/core/nocturnal-compliance.js +824 -0
  38. package/dist/core/nocturnal-dataset.d.ts +224 -0
  39. package/dist/core/nocturnal-dataset.js +443 -0
  40. package/dist/core/nocturnal-executability.d.ts +85 -0
  41. package/dist/core/nocturnal-executability.js +331 -0
  42. package/dist/core/nocturnal-export.d.ts +124 -0
  43. package/dist/core/nocturnal-export.js +275 -0
  44. package/dist/core/nocturnal-paths.d.ts +124 -0
  45. package/dist/core/nocturnal-paths.js +214 -0
  46. package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
  47. package/dist/core/nocturnal-trajectory-extractor.js +307 -0
  48. package/dist/core/nocturnal-trinity.d.ts +311 -0
  49. package/dist/core/nocturnal-trinity.js +880 -0
  50. package/dist/core/paths.d.ts +6 -0
  51. package/dist/core/paths.js +6 -0
  52. package/dist/core/principle-training-state.d.ts +121 -0
  53. package/dist/core/principle-training-state.js +321 -0
  54. package/dist/core/promotion-gate.d.ts +238 -0
  55. package/dist/core/promotion-gate.js +529 -0
  56. package/dist/core/session-tracker.d.ts +10 -0
  57. package/dist/core/session-tracker.js +14 -0
  58. package/dist/core/shadow-observation-registry.d.ts +217 -0
  59. package/dist/core/shadow-observation-registry.js +308 -0
  60. package/dist/core/training-program.d.ts +233 -0
  61. package/dist/core/training-program.js +433 -0
  62. package/dist/core/trajectory.d.ts +95 -1
  63. package/dist/core/trajectory.js +220 -6
  64. package/dist/core/workspace-context.d.ts +0 -6
  65. package/dist/core/workspace-context.js +0 -12
  66. package/dist/hooks/bash-risk.d.ts +6 -6
  67. package/dist/hooks/bash-risk.js +8 -8
  68. package/dist/hooks/gate-block-helper.js +1 -1
  69. package/dist/hooks/gate.d.ts +1 -1
  70. package/dist/hooks/gate.js +2 -2
  71. package/dist/hooks/gfi-gate.d.ts +3 -3
  72. package/dist/hooks/gfi-gate.js +15 -14
  73. package/dist/hooks/pain.js +6 -9
  74. package/dist/hooks/progressive-trust-gate.d.ts +21 -49
  75. package/dist/hooks/progressive-trust-gate.js +51 -204
  76. package/dist/hooks/prompt.d.ts +11 -11
  77. package/dist/hooks/prompt.js +158 -72
  78. package/dist/hooks/subagent.js +43 -6
  79. package/dist/i18n/commands.js +8 -8
  80. package/dist/index.js +129 -28
  81. package/dist/service/evolution-worker.d.ts +42 -4
  82. package/dist/service/evolution-worker.js +321 -13
  83. package/dist/service/nocturnal-runtime.d.ts +183 -0
  84. package/dist/service/nocturnal-runtime.js +352 -0
  85. package/dist/service/nocturnal-service.d.ts +163 -0
  86. package/dist/service/nocturnal-service.js +787 -0
  87. package/dist/service/nocturnal-target-selector.d.ts +145 -0
  88. package/dist/service/nocturnal-target-selector.js +315 -0
  89. package/dist/service/phase3-input-filter.d.ts +2 -23
  90. package/dist/service/phase3-input-filter.js +3 -27
  91. package/dist/service/runtime-summary-service.d.ts +0 -10
  92. package/dist/service/runtime-summary-service.js +1 -54
  93. package/dist/tools/deep-reflect.js +2 -1
  94. package/dist/types/event-types.d.ts +2 -10
  95. package/dist/types/runtime-summary.d.ts +1 -8
  96. package/dist/types.d.ts +0 -3
  97. package/dist/types.js +0 -2
  98. package/openclaw.plugin.json +1 -1
  99. package/package.json +1 -1
  100. package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
  101. package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
  102. package/templates/pain_settings.json +0 -6
  103. package/dist/commands/trust.d.ts +0 -4
  104. package/dist/commands/trust.js +0 -78
  105. package/dist/core/trust-engine.d.ts +0 -96
  106. package/dist/core/trust-engine.js +0 -286
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Nocturnal Arbiter — Deterministic Validation of Reflection Artifacts
3
+ * ===================================================================
4
+ *
5
+ * PURPOSE: Validate that a reflection artifact passes all deterministic checks
6
+ * before being approved for persistence. This module is PURE FUNCTIONS —
7
+ * no side effects, no file I/O.
8
+ *
9
+ * VALIDATION RULES:
10
+ * 1. JSON is parseable and has required fields
11
+ * 2. principleId matches the target principle
12
+ * 3. sessionId matches the source snapshot
13
+ * 4. All required string fields are non-empty
14
+ * 5. No fields contain placeholder or dummy values
15
+ * 6. artifactId is a valid unique identifier
16
+ * 7. No raw/private content in any text field
17
+ *
18
+ * DESIGN CONSTRAINTS:
19
+ * - Pure functions only — no I/O, no side effects
20
+ * - Deterministic — same input always produces same output
21
+ * - Fail closed — invalid artifacts are rejected, never sanitized
22
+ * - No LLM involvement — all checks are algorithmic
23
+ */
24
+ // ---------------------------------------------------------------------------
25
+ // Validation Helpers
26
+ // ---------------------------------------------------------------------------
27
+ function isNonEmptyString(val) {
28
+ return typeof val === 'string' && val.trim().length > 0;
29
+ }
30
+ function isValidUUID(val) {
31
+ // Simple UUID v4 pattern check
32
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val);
33
+ }
34
+ function isISO8601(val) {
35
+ // ISO 8601 timestamp check
36
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(val);
37
+ }
38
+ /**
39
+ * Check if a string contains placeholder/dummy values.
40
+ */
41
+ function containsPlaceholder(val) {
42
+ const placeholders = [
43
+ '<placeholder>',
44
+ '<uuid>',
45
+ '<session-id>',
46
+ '<principle-id>',
47
+ '<reason>',
48
+ '<action>',
49
+ 'undefined',
50
+ 'null',
51
+ 'n/a',
52
+ 'tbd',
53
+ 'todo',
54
+ 'fixme',
55
+ ];
56
+ const lower = val.toLowerCase();
57
+ return placeholders.some((p) => lower === p || lower.startsWith(p + ' '));
58
+ }
59
+ /**
60
+ * Check if a string contains raw/private content patterns.
61
+ * This is a heuristic check — not foolproof.
62
+ */
63
+ function containsRawContent(val) {
64
+ // Detect file paths with actual code (not just path shapes)
65
+ const rawPatterns = [
66
+ /function\s+\w+\s*\(/, // function definitions
67
+ /class\s+\w+/, // class definitions
68
+ /const\s+\w+\s*=/, // variable declarations
69
+ /import\s+.*from/, // import statements
70
+ /export\s+(default\s+)?/, // export statements
71
+ /api_key|secret|password|token/, // credential patterns (case insensitive)
72
+ ];
73
+ return rawPatterns.some((p) => p.test(val));
74
+ }
75
+ /**
76
+ * Validate a Dreamer output contract.
77
+ * Ensures the output is well-formed before passing to Philosopher.
78
+ */
79
+ export function validateDreamerOutput(output) {
80
+ const failures = [];
81
+ if (output === null || output === undefined || typeof output !== 'object') {
82
+ return { valid: false, failures: ['Dreamer output must be a JSON object'] };
83
+ }
84
+ const obj = output;
85
+ // Check valid flag
86
+ if (obj.valid !== true) {
87
+ if (typeof obj.reason === 'string' && obj.reason.length > 0) {
88
+ failures.push(`Dreamer marked invalid: ${obj.reason}`);
89
+ }
90
+ else {
91
+ failures.push('Dreamer output marked invalid with no reason');
92
+ }
93
+ }
94
+ // Check candidates array
95
+ if (!Array.isArray(obj.candidates)) {
96
+ failures.push('Dreamer output must have a candidates array');
97
+ }
98
+ else {
99
+ // Validate each candidate
100
+ obj.candidates.forEach((candidate, idx) => {
101
+ if (candidate === null || candidate === undefined || typeof candidate !== 'object') {
102
+ failures.push(`Dreamer candidate at index ${idx} is not an object`);
103
+ return;
104
+ }
105
+ const c = candidate;
106
+ if (typeof c.candidateIndex !== 'number') {
107
+ failures.push(`Dreamer candidate ${idx} missing candidateIndex`);
108
+ }
109
+ if (typeof c.badDecision !== 'string' || c.badDecision.trim().length === 0) {
110
+ failures.push(`Dreamer candidate ${idx} missing non-empty badDecision`);
111
+ }
112
+ if (typeof c.betterDecision !== 'string' || c.betterDecision.trim().length === 0) {
113
+ failures.push(`Dreamer candidate ${idx} missing non-empty betterDecision`);
114
+ }
115
+ if (typeof c.rationale !== 'string' || c.rationale.trim().length === 0) {
116
+ failures.push(`Dreamer candidate ${idx} missing non-empty rationale`);
117
+ }
118
+ if (typeof c.confidence !== 'number' || c.confidence < 0 || c.confidence > 1) {
119
+ failures.push(`Dreamer candidate ${idx} has invalid confidence (must be 0-1)`);
120
+ }
121
+ // badDecision and betterDecision should not be identical
122
+ if (typeof c.badDecision === 'string' &&
123
+ typeof c.betterDecision === 'string' &&
124
+ c.badDecision.trim() === c.betterDecision.trim()) {
125
+ failures.push(`Dreamer candidate ${idx}: badDecision and betterDecision are identical`);
126
+ }
127
+ });
128
+ // Check for duplicate candidateIndices
129
+ const indices = obj.candidates
130
+ .map((c) => c.candidateIndex)
131
+ .filter((i) => typeof i === 'number');
132
+ const uniqueIndices = new Set(indices);
133
+ if (indices.length !== uniqueIndices.size) {
134
+ failures.push('Dreamer candidates have duplicate candidateIndex values');
135
+ }
136
+ }
137
+ // Check generatedAt
138
+ if (typeof obj.generatedAt !== 'string' || !isISO8601(obj.generatedAt)) {
139
+ failures.push('Dreamer output missing valid ISO 8601 generatedAt');
140
+ }
141
+ return { valid: failures.length === 0, failures };
142
+ }
143
+ /**
144
+ * Validate a Philosopher output contract.
145
+ * Ensures the output is well-formed before passing to Scribe.
146
+ */
147
+ export function validatePhilosopherOutput(output) {
148
+ const failures = [];
149
+ if (output === null || output === undefined || typeof output !== 'object') {
150
+ return { valid: false, failures: ['Philosopher output must be a JSON object'] };
151
+ }
152
+ const obj = output;
153
+ // Check valid flag
154
+ if (obj.valid !== true) {
155
+ if (typeof obj.reason === 'string' && obj.reason.length > 0) {
156
+ failures.push(`Philosopher marked invalid: ${obj.reason}`);
157
+ }
158
+ else {
159
+ failures.push('Philosopher output marked invalid with no reason');
160
+ }
161
+ }
162
+ // Check judgments array
163
+ if (!Array.isArray(obj.judgments)) {
164
+ failures.push('Philosopher output must have a judgments array');
165
+ }
166
+ else {
167
+ // Validate each judgment
168
+ obj.judgments.forEach((judgment, idx) => {
169
+ if (judgment === null || judgment === undefined || typeof judgment !== 'object') {
170
+ failures.push(`Philosopher judgment at index ${idx} is not an object`);
171
+ return;
172
+ }
173
+ const j = judgment;
174
+ if (typeof j.candidateIndex !== 'number') {
175
+ failures.push(`Philosopher judgment ${idx} missing candidateIndex`);
176
+ }
177
+ if (typeof j.critique !== 'string' || j.critique.trim().length === 0) {
178
+ failures.push(`Philosopher judgment ${idx} missing non-empty critique`);
179
+ }
180
+ if (typeof j.principleAligned !== 'boolean') {
181
+ failures.push(`Philosopher judgment ${idx} missing principleAligned boolean`);
182
+ }
183
+ if (typeof j.score !== 'number' || j.score < 0 || j.score > 1) {
184
+ failures.push(`Philosopher judgment ${idx} has invalid score (must be 0-1)`);
185
+ }
186
+ if (typeof j.rank !== 'number' || j.rank < 1) {
187
+ failures.push(`Philosopher judgment ${idx} has invalid rank (must be >= 1)`);
188
+ }
189
+ });
190
+ // Check ranks are unique and sequential (1, 2, 3...)
191
+ const ranks = obj.judgments
192
+ .map((j) => j.rank)
193
+ .filter((r) => typeof r === 'number')
194
+ .sort((a, b) => a - b);
195
+ for (let i = 0; i < ranks.length; i++) {
196
+ if (ranks[i] !== i + 1) {
197
+ failures.push('Philosopher judgments must have sequential ranks starting from 1');
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ // Check overallAssessment
203
+ if (typeof obj.overallAssessment !== 'string' || obj.overallAssessment.trim().length === 0) {
204
+ failures.push('Philosopher output missing non-empty overallAssessment');
205
+ }
206
+ // Check generatedAt
207
+ if (typeof obj.generatedAt !== 'string' || !isISO8601(obj.generatedAt)) {
208
+ failures.push('Philosopher output missing valid ISO 8601 generatedAt');
209
+ }
210
+ return { valid: failures.length === 0, failures };
211
+ }
212
+ /**
213
+ * Validate a TrinityDraftArtifact contract.
214
+ * This is the final artifact before arbiter approval.
215
+ */
216
+ export function validateTrinityDraft(draft) {
217
+ const failures = [];
218
+ if (draft === null || draft === undefined || typeof draft !== 'object') {
219
+ return { valid: false, failures: ['Trinity draft must be a JSON object'] };
220
+ }
221
+ const obj = draft;
222
+ // Required fields
223
+ if (typeof obj.selectedCandidateIndex !== 'number') {
224
+ failures.push('Trinity draft missing selectedCandidateIndex');
225
+ }
226
+ if (typeof obj.badDecision !== 'string' || obj.badDecision.trim().length === 0) {
227
+ failures.push('Trinity draft missing non-empty badDecision');
228
+ }
229
+ if (typeof obj.betterDecision !== 'string' || obj.betterDecision.trim().length === 0) {
230
+ failures.push('Trinity draft missing non-empty betterDecision');
231
+ }
232
+ if (typeof obj.rationale !== 'string' || obj.rationale.trim().length < 20) {
233
+ failures.push('Trinity draft rationale must be at least 20 characters');
234
+ }
235
+ if (typeof obj.sessionId !== 'string' || obj.sessionId.trim().length === 0) {
236
+ failures.push('Trinity draft missing non-empty sessionId');
237
+ }
238
+ if (typeof obj.principleId !== 'string' || obj.principleId.trim().length === 0) {
239
+ failures.push('Trinity draft missing non-empty principleId');
240
+ }
241
+ if (typeof obj.sourceSnapshotRef !== 'string') {
242
+ failures.push('Trinity draft missing sourceSnapshotRef');
243
+ }
244
+ // Semantic validation
245
+ if (typeof obj.badDecision === 'string' &&
246
+ typeof obj.betterDecision === 'string' &&
247
+ obj.badDecision.trim() === obj.betterDecision.trim()) {
248
+ failures.push('Trinity draft badDecision and betterDecision are identical');
249
+ }
250
+ // Validate telemetry
251
+ if (typeof obj.telemetry === 'object' && obj.telemetry !== null) {
252
+ const t = obj.telemetry;
253
+ if (t.chainMode !== 'trinity' && t.chainMode !== 'single-reflector') {
254
+ failures.push('Trinity draft telemetry must have valid chainMode');
255
+ }
256
+ if (typeof t.dreamerPassed !== 'boolean') {
257
+ failures.push('Trinity draft telemetry missing dreamerPassed boolean');
258
+ }
259
+ if (typeof t.philosopherPassed !== 'boolean') {
260
+ failures.push('Trinity draft telemetry missing philosopherPassed boolean');
261
+ }
262
+ if (typeof t.scribePassed !== 'boolean') {
263
+ failures.push('Trinity draft telemetry missing scribePassed boolean');
264
+ }
265
+ }
266
+ else {
267
+ failures.push('Trinity draft missing telemetry object');
268
+ }
269
+ return { valid: failures.length === 0, failures };
270
+ }
271
+ /**
272
+ * Parse and validate a Dreamer output from JSON string.
273
+ */
274
+ export function parseAndValidateDreamerOutput(jsonString) {
275
+ try {
276
+ const parsed = JSON.parse(jsonString);
277
+ return validateDreamerOutput(parsed);
278
+ }
279
+ catch (err) {
280
+ return {
281
+ valid: false,
282
+ failures: [`Failed to parse Dreamer output JSON: ${err instanceof Error ? err.message : String(err)}`],
283
+ };
284
+ }
285
+ }
286
+ /**
287
+ * Parse and validate a Philosopher output from JSON string.
288
+ */
289
+ export function parseAndValidatePhilosopherOutput(jsonString) {
290
+ try {
291
+ const parsed = JSON.parse(jsonString);
292
+ return validatePhilosopherOutput(parsed);
293
+ }
294
+ catch (err) {
295
+ return {
296
+ valid: false,
297
+ failures: [`Failed to parse Philosopher output JSON: ${err instanceof Error ? err.message : String(err)}`],
298
+ };
299
+ }
300
+ }
301
+ /**
302
+ * Parse and validate a Trinity draft artifact from JSON string.
303
+ */
304
+ export function parseAndValidateTrinityDraft(jsonString) {
305
+ try {
306
+ const parsed = JSON.parse(jsonString);
307
+ return validateTrinityDraft(parsed);
308
+ }
309
+ catch (err) {
310
+ return {
311
+ valid: false,
312
+ failures: [`Failed to parse Trinity draft JSON: ${err instanceof Error ? err.message : String(err)}`],
313
+ };
314
+ }
315
+ }
316
+ /**
317
+ * Validate a raw reflection artifact against all arbiter rules.
318
+ *
319
+ * @param raw - The raw artifact JSON (already parsed)
320
+ * @param options - Expected values for cross-validation
321
+ * @returns ArbiterResult with passed/failed status and details
322
+ */
323
+ export function validateArtifact(raw, options = {}) {
324
+ const failures = [];
325
+ // Rule 1: Must be an object
326
+ if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw)) {
327
+ return {
328
+ passed: false,
329
+ rawInput: raw,
330
+ failures: [{ reason: 'Artifact must be a JSON object' }],
331
+ };
332
+ }
333
+ const obj = raw;
334
+ // Rule 2: Check for invalid flag (reflector said it couldn't generate)
335
+ if (obj.invalid === true || obj.invalid === 'true') {
336
+ return {
337
+ passed: false,
338
+ rawInput: obj,
339
+ failures: [{ reason: `Reflector marked artifact as invalid: ${String(obj.reason ?? 'no reason provided')}` }],
340
+ };
341
+ }
342
+ // Rule 3: Required string fields must be present and non-empty
343
+ const requiredFields = [
344
+ { key: 'artifactId', label: 'artifactId' },
345
+ { key: 'sessionId', label: 'sessionId' },
346
+ { key: 'principleId', label: 'principleId' },
347
+ { key: 'badDecision', label: 'badDecision' },
348
+ { key: 'betterDecision', label: 'betterDecision' },
349
+ { key: 'rationale', label: 'rationale' },
350
+ { key: 'createdAt', label: 'createdAt' },
351
+ ];
352
+ for (const field of requiredFields) {
353
+ const val = obj[field.key];
354
+ if (!isNonEmptyString(val)) {
355
+ failures.push({ reason: `Field '${field.label}' is missing or empty`, field: field.label });
356
+ }
357
+ else {
358
+ // Additional checks for string fields
359
+ if (field.label === 'artifactId' && !isValidUUID(String(val))) {
360
+ failures.push({ reason: `Field '${field.label}' must be a valid UUID`, field: field.label });
361
+ }
362
+ if (field.label === 'createdAt' && !isISO8601(String(val))) {
363
+ failures.push({ reason: `Field '${field.label}' must be a valid ISO 8601 timestamp`, field: field.label });
364
+ }
365
+ }
366
+ }
367
+ // Rule 4: Cross-validate principleId
368
+ if (options.expectedPrincipleId !== undefined) {
369
+ const principleId = obj.principleId;
370
+ if (!isNonEmptyString(principleId)) {
371
+ failures.push({ reason: 'principleId is required but missing', field: 'principleId' });
372
+ }
373
+ else if (String(principleId) !== options.expectedPrincipleId) {
374
+ failures.push({
375
+ reason: `principleId mismatch: expected '${options.expectedPrincipleId}', got '${String(principleId)}'`,
376
+ field: 'principleId',
377
+ });
378
+ }
379
+ }
380
+ // Rule 5: Cross-validate sessionId
381
+ if (options.expectedSessionId !== undefined) {
382
+ const sessionId = obj.sessionId;
383
+ if (!isNonEmptyString(sessionId)) {
384
+ failures.push({ reason: 'sessionId is required but missing', field: 'sessionId' });
385
+ }
386
+ else if (String(sessionId) !== options.expectedSessionId) {
387
+ failures.push({
388
+ reason: `sessionId mismatch: expected '${options.expectedSessionId}', got '${String(sessionId)}'`,
389
+ field: 'sessionId',
390
+ });
391
+ }
392
+ }
393
+ // Rule 6: Check for placeholder values in text fields
394
+ const textFields = [
395
+ 'badDecision',
396
+ 'betterDecision',
397
+ 'rationale',
398
+ ];
399
+ for (const field of textFields) {
400
+ const val = obj[field];
401
+ if (isNonEmptyString(val)) {
402
+ const strVal = String(val);
403
+ if (containsPlaceholder(strVal)) {
404
+ failures.push({ reason: `Field '${String(field)}' contains a placeholder value`, field: String(field) });
405
+ }
406
+ if (containsRawContent(strVal)) {
407
+ failures.push({
408
+ reason: `Field '${String(field)}' may contain raw/private content — not allowed in nocturnal artifacts`,
409
+ field: String(field),
410
+ });
411
+ }
412
+ }
413
+ }
414
+ // Rule 7: Check sourceSnapshotRef if present
415
+ const sourceSnapshotRef = obj.sourceSnapshotRef;
416
+ if (sourceSnapshotRef !== undefined && !isNonEmptyString(sourceSnapshotRef)) {
417
+ failures.push({ reason: 'Field "sourceSnapshotRef" must be a non-empty string if present', field: 'sourceSnapshotRef' });
418
+ }
419
+ // Rule 8: badDecision should not be identical to betterDecision
420
+ const badDecision = obj.badDecision;
421
+ const betterDecision = obj.betterDecision;
422
+ if (isNonEmptyString(badDecision) && isNonEmptyString(betterDecision)) {
423
+ if (String(badDecision).trim() === String(betterDecision).trim()) {
424
+ failures.push({
425
+ reason: 'badDecision and betterDecision cannot be identical',
426
+ field: 'betterDecision',
427
+ });
428
+ }
429
+ }
430
+ // Rule 9: Rationale should not be too short (needs explanation)
431
+ const rationale = obj.rationale;
432
+ if (isNonEmptyString(rationale) && String(rationale).trim().length < 20) {
433
+ failures.push({
434
+ reason: 'rationale is too short — must provide meaningful explanation',
435
+ field: 'rationale',
436
+ });
437
+ }
438
+ // Rule 10: Validate optional reflection quality metrics (if present)
439
+ const thinkingModelDelta = obj.thinkingModelDelta;
440
+ if (thinkingModelDelta !== undefined && typeof thinkingModelDelta !== 'number') {
441
+ failures.push({
442
+ reason: 'thinkingModelDelta must be a number if present',
443
+ field: 'thinkingModelDelta',
444
+ });
445
+ }
446
+ else if (typeof thinkingModelDelta === 'number' && (thinkingModelDelta < -1 || thinkingModelDelta > 1)) {
447
+ failures.push({
448
+ reason: 'thinkingModelDelta must be between -1 and 1',
449
+ field: 'thinkingModelDelta',
450
+ });
451
+ }
452
+ const planningRatioGain = obj.planningRatioGain;
453
+ if (planningRatioGain !== undefined && typeof planningRatioGain !== 'number') {
454
+ failures.push({
455
+ reason: 'planningRatioGain must be a number if present',
456
+ field: 'planningRatioGain',
457
+ });
458
+ }
459
+ else if (typeof planningRatioGain === 'number' && (planningRatioGain < -1 || planningRatioGain > 1)) {
460
+ failures.push({
461
+ reason: 'planningRatioGain must be between -1 and 1',
462
+ field: 'planningRatioGain',
463
+ });
464
+ }
465
+ // Rule 11: Quality threshold gate — reject low-signal artifacts
466
+ // A reflection artifact must show positive cognitive improvement (thinkingModelDelta > 0).
467
+ // planningRatioGain must not show catastrophic regression (< -0.5).
468
+ if (options.qualityThresholds?.thinkingModelDeltaMin !== undefined &&
469
+ thinkingModelDelta !== undefined &&
470
+ typeof thinkingModelDelta === 'number' &&
471
+ thinkingModelDelta <= options.qualityThresholds.thinkingModelDeltaMin) {
472
+ failures.push({
473
+ reason: `thinkingModelDelta (${thinkingModelDelta}) does not meet minimum quality threshold (${options.qualityThresholds.thinkingModelDeltaMin}) — reflection shows no cognitive improvement`,
474
+ field: 'thinkingModelDelta',
475
+ });
476
+ }
477
+ if (options.qualityThresholds?.planningRatioGainMin !== undefined &&
478
+ planningRatioGain !== undefined &&
479
+ typeof planningRatioGain === 'number' &&
480
+ planningRatioGain < options.qualityThresholds.planningRatioGainMin) {
481
+ failures.push({
482
+ reason: `planningRatioGain (${planningRatioGain}) shows catastrophic planning regression — below minimum threshold (${options.qualityThresholds.planningRatioGainMin})`,
483
+ field: 'planningRatioGain',
484
+ });
485
+ }
486
+ // Final decision
487
+ if (failures.length > 0) {
488
+ return {
489
+ passed: false,
490
+ rawInput: obj,
491
+ failures,
492
+ };
493
+ }
494
+ // Construct validated artifact
495
+ const artifact = {
496
+ artifactId: String(obj.artifactId),
497
+ sessionId: String(obj.sessionId),
498
+ principleId: String(obj.principleId),
499
+ sourceSnapshotRef: isNonEmptyString(obj.sourceSnapshotRef) ? String(obj.sourceSnapshotRef) : '',
500
+ badDecision: String(obj.badDecision),
501
+ betterDecision: String(obj.betterDecision),
502
+ rationale: String(obj.rationale),
503
+ createdAt: String(obj.createdAt),
504
+ thinkingModelDelta: typeof obj.thinkingModelDelta === 'number' ? obj.thinkingModelDelta : undefined,
505
+ planningRatioGain: typeof obj.planningRatioGain === 'number' ? obj.planningRatioGain : undefined,
506
+ };
507
+ return {
508
+ passed: true,
509
+ artifact,
510
+ failures: [],
511
+ };
512
+ }
513
+ /**
514
+ * Parse and validate a JSON string as a reflection artifact.
515
+ *
516
+ * @param jsonString - Raw JSON string from reflector
517
+ * @param options - Expected values for cross-validation
518
+ * @returns ArbiterResult
519
+ */
520
+ export function parseAndValidateArtifact(jsonString, options = {}) {
521
+ // Step 1: Parse JSON
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(jsonString);
525
+ }
526
+ catch (err) {
527
+ return {
528
+ passed: false,
529
+ failures: [{ reason: `Failed to parse JSON: ${err instanceof Error ? err.message : String(err)}` }],
530
+ };
531
+ }
532
+ // Step 2: Validate
533
+ return validateArtifact(parsed, options);
534
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Nocturnal Candidate Scoring — Deterministic Tournament Selection
3
+ * ============================================================
4
+ *
5
+ * PURPOSE: Score Trinity candidates and run deterministic tournament selection
6
+ * to choose the best candidate for artifact generation.
7
+ *
8
+ * DESIGN CONSTRAINTS:
9
+ * - Scoring is deterministic: same inputs → same winner
10
+ * - Tie-break rules are stable and explicit
11
+ * - No randomness in ranking or selection
12
+ * - Winner is always the highest-scoring candidate
13
+ * - Thresholds provide minimum quality gates
14
+ * - Failed threshold candidates are excluded from tournament
15
+ *
16
+ * SCORING COMPONENTS:
17
+ * - schema completeness: candidate has all required fields
18
+ * - principle alignment: candidate aligns with target principle
19
+ * - executability: candidate describes an actionable next step
20
+ * - boundedness: candidate is specific and bounded
21
+ * - confidence/consistency: candidate's internal consistency
22
+ *
23
+ * PHASE 6 ONLY — No real training, no automatic deployment
24
+ */
25
+ import type { DreamerCandidate, PhilosopherJudgment } from './nocturnal-trinity.js';
26
+ import type { ThresholdValues } from './adaptive-thresholds.js';
27
+ /**
28
+ * Individual scoring dimensions for a candidate.
29
+ */
30
+ export interface CandidateScores {
31
+ /** Schema completeness (0-1) */
32
+ schemaCompleteness: number;
33
+ /** Principle alignment (0-1) */
34
+ principleAlignment: number;
35
+ /** Executability (0-1) */
36
+ executability: number;
37
+ /** Boundedness — specificity and constraint (0-1) */
38
+ boundedness: number;
39
+ /** Confidence/consistency (0-1) */
40
+ confidence: number;
41
+ /** Aggregate score (weighted average) */
42
+ aggregate: number;
43
+ }
44
+ /**
45
+ * Scored candidate with ranking.
46
+ */
47
+ export interface ScoredCandidate {
48
+ /** Original candidate index from Dreamer */
49
+ candidateIndex: number;
50
+ /** The Dreamer candidate */
51
+ candidate: DreamerCandidate;
52
+ /** The Philosopher judgment */
53
+ judgment: PhilosopherJudgment;
54
+ /** Individual dimension scores */
55
+ scores: CandidateScores;
56
+ /** Final tournament rank (1 = winner) */
57
+ rank: number;
58
+ /** Whether this candidate passed all thresholds */
59
+ thresholdPassed: boolean;
60
+ /** Which thresholds failed (if any) */
61
+ failedThresholds: string[];
62
+ }
63
+ /**
64
+ * Result of a tournament selection.
65
+ */
66
+ export interface TournamentResult {
67
+ /** Whether tournament produced a winner */
68
+ success: boolean;
69
+ /** The winning candidate (if success === true) */
70
+ winner: ScoredCandidate | null;
71
+ /** All ranked candidates (sorted by rank) */
72
+ rankedCandidates: ScoredCandidate[];
73
+ /** Trace of decisions for debugging/explainability */
74
+ trace: TournamentTraceEntry[];
75
+ /** Why no winner was selected (if success === false) */
76
+ failureReason?: string;
77
+ }
78
+ /**
79
+ * Single entry in the tournament trace.
80
+ */
81
+ export interface TournamentTraceEntry {
82
+ /** Description of this step */
83
+ step: string;
84
+ /** Details about the decision */
85
+ details: string;
86
+ }
87
+ /**
88
+ * Scoring weights for aggregate calculation.
89
+ */
90
+ export interface ScoringWeights {
91
+ schemaCompleteness: number;
92
+ principleAlignment: number;
93
+ executability: number;
94
+ boundedness: number;
95
+ confidence: number;
96
+ }
97
+ /**
98
+ * Default scoring weights (must sum to 1.0).
99
+ */
100
+ export declare const DEFAULT_SCORING_WEIGHTS: ScoringWeights;
101
+ /**
102
+ * Score a single Dreamer candidate + Philosopher judgment pair.
103
+ *
104
+ * @param candidate - Dreamer candidate
105
+ * @param judgment - Philosopher judgment
106
+ * @param weights - Scoring weights
107
+ * @returns Individual scores
108
+ */
109
+ export declare function scoreCandidate(candidate: DreamerCandidate, judgment: PhilosopherJudgment, weights?: ScoringWeights): CandidateScores;
110
+ /**
111
+ * Check if candidate passes minimum thresholds.
112
+ *
113
+ * @param scores - Candidate scores
114
+ * @param thresholds - Minimum threshold values
115
+ * @returns Tuple of [passed, failedThresholdNames]
116
+ */
117
+ export declare function checkThresholds(scores: CandidateScores, thresholds: ThresholdValues): [boolean, string[]];
118
+ /**
119
+ * Score and rank all candidates deterministically.
120
+ *
121
+ * @param candidates - Dreamer candidates
122
+ * @param judgments - Philosopher judgments (aligned by candidateIndex)
123
+ * @param thresholds - Minimum thresholds
124
+ * @param weights - Scoring weights
125
+ * @returns All scored and ranked candidates
126
+ */
127
+ export declare function rankCandidates(candidates: DreamerCandidate[], judgments: PhilosopherJudgment[], thresholds: ThresholdValues, weights?: ScoringWeights): ScoredCandidate[];
128
+ /**
129
+ * Run tournament selection to choose the best candidate.
130
+ *
131
+ * @param candidates - Dreamer candidates
132
+ * @param judgments - Philosopher judgments
133
+ * @param thresholds - Minimum thresholds
134
+ * @param weights - Scoring weights
135
+ * @returns Tournament result with winner
136
+ */
137
+ export declare function runTournament(candidates: DreamerCandidate[], judgments: PhilosopherJudgment[], thresholds: ThresholdValues, weights?: ScoringWeights): TournamentResult;