principles-disciple 1.16.0 → 1.18.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 (132) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +4 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +480 -158
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +221 -109
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +11 -4
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
  113. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
  114. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
  115. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  116. package/templates/pain_settings.json +1 -1
  117. package/tests/build-artifacts.test.ts +4 -58
  118. package/tests/commands/pd-reflect.test.ts +49 -0
  119. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  120. package/tests/core/pain-auto-repair.test.ts +96 -0
  121. package/tests/core/pain-integration.test.ts +483 -0
  122. package/tests/core/pain.test.ts +5 -4
  123. package/tests/core/workspace-dir-service.test.ts +68 -0
  124. package/tests/core/workspace-dir-validation.test.ts +56 -192
  125. package/tests/hooks/pain.test.ts +20 -0
  126. package/tests/http/principles-console-route.test.ts +42 -20
  127. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  128. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  129. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  130. package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
  131. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  132. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -12,15 +12,15 @@
12
12
 
13
13
 
14
14
  export enum EvolutionTier {
15
- // eslint-disable-next-line no-unused-vars -- Reason: enum members are used via EvolutionTier.Seed syntax - bare names unused but required for enum structure
15
+
16
16
  Seed = 1, // 起步:150行 + 3文件 + 子智能体(现代 AI 能力已足够强)
17
- // eslint-disable-next-line no-unused-vars -- Reason: enum members are used via EvolutionTier.Sprout syntax - bare names unused but required for enum structure
17
+
18
18
  Sprout = 2, // 成长:300行 + 5文件
19
- // eslint-disable-next-line no-unused-vars -- Reason: enum members are used via EvolutionTier.Sapling syntax - bare names unused but required for enum structure
19
+
20
20
  Sapling = 3, // 独当:500行 + 10文件 + 风险路径
21
- // eslint-disable-next-line no-unused-vars -- Reason: enum members are used via EvolutionTier.Tree syntax - bare names unused but required for enum structure
21
+
22
22
  Tree = 4, // 专家:1000行 + 20文件
23
- // eslint-disable-next-line no-unused-vars -- Reason: enum members are used via EvolutionTier.Forest syntax - bare names unused but required for enum structure
23
+
24
24
  Forest = 5 // 大师:完全自主
25
25
  }
26
26
 
@@ -403,7 +403,7 @@ export function computeConfigFingerprint(config: Partial<TrainingHyperparameters
403
403
  * If the file cannot be read, falls back to path+count hash (legacy behavior).
404
404
  */
405
405
  export function computeDatasetFingerprint(exportPath: string, sampleCount: number): string {
406
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch has fallback return
406
+
407
407
  let contentHash: string;
408
408
  try {
409
409
  const content = fs.readFileSync(exportPath, 'utf-8');
@@ -428,7 +428,7 @@ export function extractWorkingMemory(
428
428
  toolUse.name === 'write_file' || toolUse.name === 'create_file' ? 'created' : 'modified';
429
429
 
430
430
  // 尝试从文本中提取描述
431
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
431
+
432
432
  const description = extractDescription(text, filePath);
433
433
 
434
434
  snapshot.artifacts.push({
@@ -443,20 +443,20 @@ export function extractWorkingMemory(
443
443
  if (!text) continue;
444
444
 
445
445
  // 从文本中提取文件操作(备用方式)
446
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
446
+
447
447
  extractFileArtifacts(text, snapshot.artifacts, workspaceDir);
448
448
 
449
449
  // 提取问题
450
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
450
+
451
451
  extractProblems(text, snapshot.activeProblems);
452
452
 
453
453
  // 提取下一步
454
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
454
+
455
455
  extractNextActions(text, snapshot.nextActions);
456
456
  }
457
457
 
458
458
  // 去重和限制数量
459
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
459
+
460
460
  snapshot.artifacts = deduplicateArtifacts(snapshot.artifacts).slice(-MAX_ARTIFACTS);
461
461
  snapshot.activeProblems = snapshot.activeProblems.slice(-MAX_PROBLEMS);
462
462
  snapshot.nextActions = snapshot.nextActions.slice(-MAX_NEXT_ACTIONS);
@@ -476,7 +476,7 @@ function extractFileArtifacts(
476
476
  // 格式: file_path: "/path/to/file" 或 absolute_path: "/path/to/file"
477
477
  const filePathRegex = /(?:file_path|absolute_path)["']?\s*[:=]\s*["']([^"']+\.(ts|js|json|md|yaml|yml|py|sh|mjs|cjs))["']/gi;
478
478
 
479
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in while loop condition before use
479
+
480
480
  let match;
481
481
  while ((match = filePathRegex.exec(text)) !== null) {
482
482
  const [, filePath] = match;
@@ -506,7 +506,7 @@ function extractFileArtifacts(
506
506
  }
507
507
 
508
508
  // 尝试提取描述(从附近的文本)
509
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
509
+
510
510
  const description = extractDescription(text, filePath);
511
511
 
512
512
  artifacts.push({
@@ -539,7 +539,7 @@ function extractFileArtifacts(
539
539
  continue;
540
540
  }
541
541
 
542
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
542
+
543
543
  const description = extractDescription(text, filePath);
544
544
 
545
545
  artifacts.push({
@@ -587,7 +587,7 @@ function extractProblems(
587
587
  ): void {
588
588
  // 问题模式(匹配问题描述)
589
589
  const problemPattern = /(?:问题|problem|error|错误|失败|failed)[::]\s*([^\n]{5,100})/gi;
590
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in while loop condition before use
590
+
591
591
  let match;
592
592
  while ((match = problemPattern.exec(text)) !== null) {
593
593
  const content = match[1].trim();
@@ -630,7 +630,7 @@ function extractNextActions(text: string, actions: string[]): void {
630
630
  ];
631
631
 
632
632
  for (const pattern of patterns) {
633
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in while loop condition before use
633
+
634
634
  let match;
635
635
  while ((match = pattern.exec(text)) !== null) {
636
636
  const action = match[1].trim();
@@ -689,7 +689,7 @@ export function parseWorkingMemorySection(content: string): WorkingMemorySnapsho
689
689
  // 解析文件记录表格
690
690
  // | 文件路径 | 操作 | 描述 |
691
691
  const tableRegex = /\|\s*`?([^`|\n]+)`?\s*\|\s*(created|modified|deleted)\s*\|\s*([^|\n]*)\s*\|/gi;
692
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned immediately in while loop condition before use
692
+
693
693
  let match;
694
694
  while ((match = tableRegex.exec(wmContent)) !== null) {
695
695
  snapshot.artifacts.push({
@@ -728,7 +728,7 @@ export function mergeWorkingMemory(content: string, snapshot: WorkingMemorySnaps
728
728
  const wmIndex = content.indexOf(WORKING_MEMORY_SECTION);
729
729
 
730
730
  // 生成 Working Memory 章节
731
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: mutual recursion between helper functions - reordering would break logical grouping
731
+
732
732
  const wmSection = generateWorkingMemorySection(snapshot);
733
733
 
734
734
  if (wmIndex === -1) {
@@ -1405,9 +1405,9 @@ export function recoverFromTemplate(
1405
1405
  export function safeReadCurrentFocus(
1406
1406
  focusPath: string,
1407
1407
  extensionRoot: string,
1408
- /* eslint-disable no-unused-vars -- Reason: type-only callback parameter names in function type */
1408
+
1409
1409
  logger?: { warn?: (msg: string) => void; info?: (msg: string) => void }
1410
- /* eslint-enable no-unused-vars */
1410
+
1411
1411
  ): {
1412
1412
  content: string;
1413
1413
  recovered: boolean;
@@ -46,7 +46,7 @@ export class HygieneTracker {
46
46
  const backupPath = `${this.statsFile}.bak`;
47
47
  fs.renameSync(this.statsFile, backupPath);
48
48
  this.logger?.warn(`[PD] Corrupted hygiene stats backed up to ${backupPath}`);
49
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: catch parameter intentionally unused - error handling only
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: catch parameter intentionally unused - error handling only
50
50
  } catch (_renameErr) {
51
51
  // Empty - corrupted stats backup is non-fatal
52
52
  }
package/src/core/init.ts CHANGED
@@ -43,7 +43,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
43
43
  const commonTemplatesDir = path.resolve(__dirname, '..', '..', 'templates', 'workspace');
44
44
  if (fs.existsSync(commonTemplatesDir)) {
45
45
  api.logger.info(`[PD] Syncing workspace templates: ${workspaceDir}...`);
46
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: copyRecursiveSync is defined later in this file, called here for organizational reasons
46
+
47
47
  copyRecursiveSync(commonTemplatesDir, workspaceDir, api);
48
48
  }
49
49
 
@@ -85,7 +85,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
85
85
  if (!fs.existsSync(painDestDir)) {
86
86
  fs.mkdirSync(painDestDir, { recursive: true });
87
87
  }
88
- // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: copyRecursiveSync is defined later in this file, called here for organizational reasons
88
+
89
89
  copyRecursiveSync(painTemplatesDir, painDestDir, api);
90
90
  }
91
91
 
@@ -249,7 +249,7 @@ function writeRegistry(stateDir: string, registry: ModelDeploymentRegistry): voi
249
249
  /**
250
250
  * Execute a read-modify-write under an exclusive file lock.
251
251
  */
252
- /* eslint-disable no-unused-vars -- Reason: _registry is a type signature parameter */
252
+
253
253
  function withDeploymentRegistryLock<T>(
254
254
  stateDir: string,
255
255
  fn: (_registry: ModelDeploymentRegistry) => T
@@ -349,7 +349,7 @@ export function assertPromotionGatePassed(stateDir: string, checkpointId: string
349
349
  * @throws Error if checkpoint is not found or not deployable
350
350
  * @throws Error if checkpoint's targetModelFamily violates profile constraints
351
351
  */
352
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: checkpoint binding requires state + profile + checkpoint - refactoring would break API
352
+
353
353
  export function bindCheckpointToWorkerProfile(
354
354
  stateDir: string,
355
355
  workerProfile: WorkerProfile,
@@ -249,7 +249,7 @@ function writeRegistry(stateDir: string, registry: ModelTrainingRegistry): void
249
249
  */
250
250
  function withRegistryLock<T>(
251
251
  stateDir: string,
252
- // eslint-disable-next-line no-unused-vars -- parameter type annotation requires name
252
+
253
253
  fn: (_: ModelTrainingRegistry) => T
254
254
  ): T {
255
255
  const registryPath = getRegistryPath(stateDir);
@@ -321,7 +321,7 @@ export function registerTrainingRun(
321
321
  *
322
322
  * @throws Error if run not found or transition is invalid
323
323
  */
324
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: status update requires state + runId + status - refactoring would break API
324
+
325
325
  export function updateTrainingRunStatus(
326
326
  stateDir: string,
327
327
  trainRunId: string,
@@ -689,7 +689,7 @@ export function parseAndValidateArtifact(
689
689
  options: ArbiterOptions = {}
690
690
  ): ArbiterResult {
691
691
  // Step 1: Parse JSON
692
- // eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
692
+
693
693
  let parsed: unknown;
694
694
  try {
695
695
  parsed = JSON.parse(jsonString);
@@ -220,8 +220,8 @@ export function shouldRunArtificer(
220
220
 
221
221
  const signalCount =
222
222
  snapshot.stats.totalPainEvents +
223
- snapshot.stats.totalGateBlocks +
224
- snapshot.stats.failureCount;
223
+ (snapshot.stats.totalGateBlocks ?? 0) +
224
+ (snapshot.stats.failureCount ?? 0);
225
225
 
226
226
  return signalCount >= minimumSignalCount;
227
227
  }
@@ -241,7 +241,7 @@ export function checkThresholds(
241
241
  * @param weights - Scoring weights
242
242
  * @returns All scored and ranked candidates
243
243
  */
244
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: candidate ranking requires candidates + judgments + thresholds + weights - refactoring would break API
244
+
245
245
  export function rankCandidates(
246
246
  candidates: DreamerCandidate[],
247
247
  judgments: PhilosopherJudgment[],
@@ -328,7 +328,7 @@ export function rankCandidates(
328
328
  * @param weights - Scoring weights
329
329
  * @returns Tournament result with winner
330
330
  */
331
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: tournament requires candidates + judgments + thresholds + weights - refactoring would break API
331
+
332
332
  export function runTournament(
333
333
  candidates: DreamerCandidate[],
334
334
  judgments: PhilosopherJudgment[],
@@ -239,7 +239,7 @@ export function detectOpportunity(principleId: string, session: SessionEvents):
239
239
  }
240
240
 
241
241
  // T-xx principles — specific deterministic detection
242
- /* eslint-disable @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping */
242
+
243
243
  switch (principleId) {
244
244
  case 'T-01':
245
245
  return detectT01Opportunity(session);
@@ -537,7 +537,7 @@ export function detectViolation(principleId: string, session: SessionEvents): Vi
537
537
  }
538
538
 
539
539
  // T-xx principles — specific deterministic detection
540
- /* eslint-disable @typescript-eslint/no-use-before-define -- Reason: Mutual recursion between helper functions - reordering would break logical grouping */
540
+
541
541
  switch (principleId) {
542
542
  case 'T-01':
543
543
  return detectT01Violation(session);
@@ -558,6 +558,7 @@ export function detectViolation(principleId: string, session: SessionEvents): Vi
558
558
  case 'T-09':
559
559
  return detectT09Violation(session);
560
560
  default:
561
+ console.warn(`[PD:Compliance] Unknown principle ID: ${principleId} — treating as no violation. Check for typos (P-001 vs P_001).`);
561
562
  return { violated: false, reason: `Unknown principle: ${principleId}` };
562
563
  }
563
564
  }
@@ -986,7 +987,7 @@ function computeViolationTrend(
986
987
  /**
987
988
  * Builds a human-readable explanation for the compliance result.
988
989
  */
989
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: explanation builder requires all context parameters - refactoring would break API
990
+
990
991
  function buildExplanation(
991
992
  principleId: string,
992
993
  applicableOpportunityCount: number,
@@ -259,7 +259,7 @@ function writeRegistry(workspaceDir: string, records: NocturnalDatasetRecord[]):
259
259
  * Execute a read-modify-write on the registry under an exclusive lock.
260
260
  * This prevents concurrent writers from racing on the same file.
261
261
  */
262
- /* eslint-disable no-unused-vars -- Reason: _records is a type signature parameter, unused by design in function type definition */
262
+
263
263
  function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatasetRecord[]) => T): T {
264
264
  const registryPath = getRegistryPath(workspaceDir);
265
265
  return withLock(registryPath, () => {
@@ -283,7 +283,7 @@ function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatas
283
283
  * @param classification - Optional replay classification
284
284
  * @returns RegisterSampleResult
285
285
  */
286
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: sample registration requires workspace + artifact + path + family - refactoring would break API
286
+
287
287
  export function registerSample(
288
288
  workspaceDir: string,
289
289
  artifact: NocturnalArtifact,
@@ -425,7 +425,7 @@ const VALID_TRANSITIONS: Record<NocturnalReviewStatus, NocturnalReviewStatus[]>
425
425
  * @returns Updated record, or null if not found
426
426
  * @throws Error if transition is invalid
427
427
  */
428
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: status update requires workspace + fingerprint + status - refactoring would break API
428
+
429
429
  export function updateReviewStatus(
430
430
  workspaceDir: string,
431
431
  sampleFingerprint: string,
@@ -136,7 +136,7 @@ function computeDatasetFingerprint(sampleFingerprints: string[]): string {
136
136
  * Serialize a single dataset record + artifact to ORPO JSONL line.
137
137
  * Caller guarantees record.targetModelFamily is non-null.
138
138
  */
139
- // eslint-disable-next-line @typescript-eslint/max-params -- Reason: serialization requires record + artifact + export info - refactoring would break internal API
139
+
140
140
  function serializeORPOSample(
141
141
  record: NocturnalDatasetRecord,
142
142
  artifact: ReturnType<typeof readDatasetArtifact>,
@@ -183,7 +183,7 @@ function serializeORPOSample(
183
183
  export function exportORPOSamples(
184
184
  workspaceDir: string,
185
185
  targetModelFamily?: string | null,
186
- _options: Record<string, never> = {} // eslint-disable-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: options parameter intentionally unused
186
+ _options: Record<string, never> = {}
187
187
  ): ExportResult {
188
188
  const exportId = crypto.randomUUID();
189
189
  const now = new Date().toISOString();
@@ -195,7 +195,7 @@ export function exportORPOSamples(
195
195
  reviewStatus: 'approved_for_training',
196
196
  });
197
197
 
198
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
198
+
199
199
  let eligibleRecords: typeof allApprovedRecords;
200
200
 
201
201
  if (targetModelFamily !== undefined && targetModelFamily !== null) {
@@ -244,7 +244,7 @@ export function exportORPOSamples(
244
244
  }
245
245
 
246
246
  // Read artifact (throws on error — distinguishes read failure from missing artifact)
247
- // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in try, catch continues to next iteration
247
+
248
248
  let artifact;
249
249
  try {
250
250
  artifact = readDatasetArtifact(workspaceDir, record.sampleFingerprint);
@@ -201,7 +201,7 @@ export function validateRuleImplementationCandidate(
201
201
  try {
202
202
  const moduleExports = loadRuleImplementationModule(normalizedSource, 'nocturnal-candidate.js') as {
203
203
  meta?: unknown;
204
- /* eslint-disable no-unused-vars -- Reason: type signature parameters, unused by design in function type definition */
204
+
205
205
  evaluate?: (_input: RuleHostInput, _helpers: RuleHostHelpers) => unknown;
206
206
  };
207
207
 
@@ -0,0 +1,112 @@
1
+ import type { NocturnalSessionSnapshot } from './nocturnal-trajectory-extractor.js';
2
+
3
+ export interface NocturnalSnapshotContractResult {
4
+ status: 'valid' | 'invalid';
5
+ reasons: string[];
6
+ snapshot?: NocturnalSessionSnapshot;
7
+ }
8
+
9
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
10
+ return !!value && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+
13
+ function isNonEmptyString(value: unknown): value is string {
14
+ return typeof value === 'string' && value.trim().length > 0;
15
+ }
16
+
17
+ function isNumberOrNull(value: unknown): value is number | null {
18
+ return value === null || (typeof value === 'number' && Number.isFinite(value));
19
+ }
20
+
21
+ export function validateNocturnalSnapshotIngress(
22
+ value: unknown
23
+ ): NocturnalSnapshotContractResult {
24
+ const reasons: string[] = [];
25
+
26
+ if (!isObjectRecord(value)) {
27
+ return { status: 'invalid', reasons: ['snapshot must be an object'] };
28
+ }
29
+
30
+ if (!isNonEmptyString(value.sessionId)) {
31
+ reasons.push('snapshot.sessionId must be a non-empty string');
32
+ }
33
+
34
+ if (!isNonEmptyString(value.startedAt)) {
35
+ reasons.push('snapshot.startedAt must be a non-empty string');
36
+ }
37
+
38
+ if (!isNonEmptyString(value.updatedAt)) {
39
+ reasons.push('snapshot.updatedAt must be a non-empty string');
40
+ }
41
+
42
+ const arrayFields = [
43
+ 'assistantTurns',
44
+ 'userTurns',
45
+ 'toolCalls',
46
+ 'painEvents',
47
+ 'gateBlocks',
48
+ ] as const;
49
+ for (const field of arrayFields) {
50
+ if (!Array.isArray(value[field])) {
51
+ reasons.push(`snapshot.${field} must be an array`);
52
+ }
53
+ }
54
+
55
+ const stats = value.stats;
56
+ if (!isObjectRecord(stats)) {
57
+ reasons.push('snapshot.stats must be an object');
58
+ } else {
59
+ if (!isNumberOrNull(stats.totalAssistantTurns)) {
60
+ reasons.push('snapshot.stats.totalAssistantTurns must be a number or null');
61
+ }
62
+ if (!isNumberOrNull(stats.totalToolCalls)) {
63
+ reasons.push('snapshot.stats.totalToolCalls must be a number or null');
64
+ }
65
+ if (typeof stats.totalPainEvents !== 'number' || !Number.isFinite(stats.totalPainEvents)) {
66
+ reasons.push('snapshot.stats.totalPainEvents must be a finite number');
67
+ }
68
+ if (!isNumberOrNull(stats.totalGateBlocks)) {
69
+ reasons.push('snapshot.stats.totalGateBlocks must be a number or null');
70
+ }
71
+ if (!isNumberOrNull(stats.failureCount)) {
72
+ reasons.push('snapshot.stats.failureCount must be a number or null');
73
+ }
74
+ }
75
+
76
+ const isFallback = value._dataSource === 'pain_context_fallback';
77
+ if (value._dataSource !== undefined && !isFallback) {
78
+ reasons.push('snapshot._dataSource must be omitted or pain_context_fallback');
79
+ }
80
+
81
+ if (isFallback && isObjectRecord(stats) && Array.isArray(value.painEvents)) {
82
+ const hasPainSignal = value.painEvents.length > 0 || ((stats.totalPainEvents as number) > 0);
83
+ if (!hasPainSignal) {
84
+ reasons.push('fallback snapshot must contain at least one pain signal');
85
+ }
86
+ }
87
+
88
+ if (!isFallback && isObjectRecord(stats)) {
89
+ if (stats.totalAssistantTurns === null) {
90
+ reasons.push('non-fallback snapshot.stats.totalAssistantTurns must be a number');
91
+ }
92
+ if (stats.totalToolCalls === null) {
93
+ reasons.push('non-fallback snapshot.stats.totalToolCalls must be a number');
94
+ }
95
+ if (stats.totalGateBlocks === null) {
96
+ reasons.push('non-fallback snapshot.stats.totalGateBlocks must be a number');
97
+ }
98
+ if (stats.failureCount === null) {
99
+ reasons.push('non-fallback snapshot.stats.failureCount must be a number');
100
+ }
101
+ }
102
+
103
+ if (reasons.length > 0) {
104
+ return { status: 'invalid', reasons };
105
+ }
106
+
107
+ return {
108
+ status: 'valid',
109
+ reasons: [],
110
+ snapshot: value as unknown as NocturnalSessionSnapshot,
111
+ };
112
+ }
@@ -116,13 +116,15 @@ export interface NocturnalSessionSnapshot {
116
116
  gateBlocks: NocturnalGateBlock[];
117
117
  /**
118
118
  * Summary statistics for quick triage.
119
+ * When _dataSource is 'pain_context_fallback', these fields are null
120
+ * to distinguish "no data" from "data is zero".
119
121
  */
120
122
  stats: {
121
- totalAssistantTurns: number;
122
- totalToolCalls: number;
123
+ totalAssistantTurns: number | null;
124
+ totalToolCalls: number | null;
123
125
  totalPainEvents: number;
124
- totalGateBlocks: number;
125
- failureCount: number;
126
+ totalGateBlocks: number | null;
127
+ failureCount: number | null;
126
128
  };
127
129
  /**
128
130
  * #219: Marker for data source to identify fallback/partial stats.
@@ -364,7 +366,7 @@ export class NocturnalTrajectoryExtractor {
364
366
  * const sessions = extractor.listRecentNocturnalCandidateSessions({ limit: 10 });
365
367
  * const snapshot = extractor.getNocturnalSessionSnapshot(sessionId);
366
368
  */
367
- /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: _stateDir parameter kept for API compatibility but unused in implementation */
369
+
368
370
  export function createNocturnalTrajectoryExtractor(
369
371
  workspaceDir: string,
370
372
  _stateDir?: string