principles-disciple 1.11.0 → 1.13.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.
- package/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -200,26 +200,17 @@ const READ_TOOLS: Set<string> = new Set([
|
|
|
200
200
|
* Normalizes a file path to POSIX forward-slash format for consistent matching.
|
|
201
201
|
* Handles Windows backslash paths on any platform.
|
|
202
202
|
*/
|
|
203
|
-
function
|
|
203
|
+
function normalizePathPosix(filePath: string): string {
|
|
204
204
|
return filePath.replace(/\\/g, '/');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
/**
|
|
208
|
-
* Returns true if the file path matches any of the given patterns when normalized.
|
|
209
|
-
*/
|
|
210
|
-
function pathMatches(filePath: string | undefined, patterns: RegExp[]): boolean {
|
|
211
|
-
if (!filePath) return false;
|
|
212
|
-
const normalized = normalizePath(filePath);
|
|
213
|
-
return patterns.some((p) => p.test(normalized));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
207
|
// ---------------------------------------------------------------------------
|
|
217
208
|
// Opportunity Detection
|
|
218
209
|
// ---------------------------------------------------------------------------
|
|
219
210
|
|
|
220
211
|
/**
|
|
221
212
|
* Detects whether a given session presents an APPLICABLE OPPORTUNITY
|
|
222
|
-
* for a specific
|
|
213
|
+
* for a specific principle.
|
|
223
214
|
*
|
|
224
215
|
* An opportunity exists when the session context falls within the
|
|
225
216
|
* principle's applicability scope — regardless of whether the agent
|
|
@@ -227,8 +218,28 @@ function pathMatches(filePath: string | undefined, patterns: RegExp[]): boolean
|
|
|
227
218
|
*
|
|
228
219
|
* IMPORTANT: This does NOT assess compliance. It only answers:
|
|
229
220
|
* "Could the principle have applied here?"
|
|
221
|
+
*
|
|
222
|
+
* #216: For P_* principles (not T-xx), uses generic detection based on
|
|
223
|
+
* pain events and tool calls — any session with a pain signal is considered
|
|
224
|
+
* an opportunity for a pain-derived principle.
|
|
230
225
|
*/
|
|
231
226
|
export function detectOpportunity(principleId: string, session: SessionEvents): OpportunityMatch {
|
|
227
|
+
// #216: P_* principles (pain-derived) — generic opportunity detection
|
|
228
|
+
if (principleId.startsWith('P_')) {
|
|
229
|
+
// Any session with pain signals, tool failures, or gate blocks is an opportunity
|
|
230
|
+
// for a pain-derived principle. This is conservative: better to over-count
|
|
231
|
+
// opportunities than to miss real violations.
|
|
232
|
+
const hasPainSignal = session.painSignals.length > 0;
|
|
233
|
+
const hasToolFailure = session.toolCalls.some((tc) => tc.outcome === 'failure');
|
|
234
|
+
const hasGateBlock = session.gateBlocks.length > 0;
|
|
235
|
+
if (hasPainSignal || hasToolFailure || hasGateBlock) {
|
|
236
|
+
return { applicable: true, reason: `P_* principle — session has ${hasPainSignal ? 'pain signal' : hasToolFailure ? 'tool failure' : 'gate block'}` };
|
|
237
|
+
}
|
|
238
|
+
return { applicable: false, reason: `P_* principle — no pain/tool-failure/gate-block in session` };
|
|
239
|
+
}
|
|
240
|
+
|
|
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 */
|
|
232
243
|
switch (principleId) {
|
|
233
244
|
case 'T-01':
|
|
234
245
|
return detectT01Opportunity(session);
|
|
@@ -276,7 +287,7 @@ function detectT01Opportunity(session: SessionEvents): OpportunityMatch {
|
|
|
276
287
|
function detectT02Opportunity(session: SessionEvents): OpportunityMatch {
|
|
277
288
|
const hasConstraintInteraction = session.toolCalls.some((call) => {
|
|
278
289
|
if (!call.filePath) return false;
|
|
279
|
-
const normalized =
|
|
290
|
+
const normalized = normalizePathPosix(call.filePath);
|
|
280
291
|
return (
|
|
281
292
|
/\.(ts|tsx|js|jsx)$/.test(normalized) || // type-aware files
|
|
282
293
|
/\b(test|spec|contract|schema|interface|type)\b/i.test(normalized)
|
|
@@ -364,6 +375,7 @@ function detectT05Opportunity(session: SessionEvents): OpportunityMatch {
|
|
|
364
375
|
if (RISKY_TOOLS.has(call.toolName)) return true;
|
|
365
376
|
// Check bash for dangerous patterns
|
|
366
377
|
if (call.toolName === 'bash' && call.errorMessage) {
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: call.errorMessage guard above ensures truthiness
|
|
367
379
|
return DANGEROUS_BASH_PATTERNS.some((p) => p.test(call.errorMessage!));
|
|
368
380
|
}
|
|
369
381
|
return false;
|
|
@@ -411,7 +423,8 @@ function detectT06Opportunity(session: SessionEvents): OpportunityMatch {
|
|
|
411
423
|
function detectT07Opportunity(session: SessionEvents): OpportunityMatch {
|
|
412
424
|
const filePaths = session.toolCalls
|
|
413
425
|
.filter((call) => call.filePath !== undefined)
|
|
414
|
-
|
|
426
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
|
|
427
|
+
.map((call) => normalizePathPosix(call.filePath!));
|
|
415
428
|
const uniqueFiles = new Set(filePaths);
|
|
416
429
|
if (uniqueFiles.size >= 3) {
|
|
417
430
|
return {
|
|
@@ -456,7 +469,8 @@ function detectT09Opportunity(session: SessionEvents): OpportunityMatch {
|
|
|
456
469
|
const uniqueFiles = new Set(
|
|
457
470
|
session.toolCalls
|
|
458
471
|
.filter((call) => call.filePath !== undefined)
|
|
459
|
-
|
|
472
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
|
|
473
|
+
.map((call) => normalizePathPosix(call.filePath!))
|
|
460
474
|
);
|
|
461
475
|
const hasComplexity = toolCallCount >= 5 || uniqueFiles.size >= 3;
|
|
462
476
|
|
|
@@ -493,8 +507,37 @@ function detectT09Opportunity(session: SessionEvents): OpportunityMatch {
|
|
|
493
507
|
* opportunity was applicable.
|
|
494
508
|
*
|
|
495
509
|
* Returns a ViolationMatch with violated=true if violation signals are present.
|
|
510
|
+
*
|
|
511
|
+
* #216: For P_* principles (pain-derived), violation is detected when the session
|
|
512
|
+
* has pain signals, tool failures, or gate blocks that match the principle's
|
|
513
|
+
* trigger pattern. Since P_* principles don't have T-xx specific detectors,
|
|
514
|
+
* we use the presence of negative signals as violation evidence.
|
|
496
515
|
*/
|
|
497
516
|
export function detectViolation(principleId: string, session: SessionEvents): ViolationMatch {
|
|
517
|
+
// #216: P_* principles (pain-derived) — generic violation detection
|
|
518
|
+
if (principleId.startsWith('P_')) {
|
|
519
|
+
// For pain-derived principles, a violation is indicated when the session
|
|
520
|
+
// contains pain signals, tool failures, or gate blocks — these are the
|
|
521
|
+
// same signals that triggered principle creation in the first place.
|
|
522
|
+
// A principle was violated if the bad outcome recurred after it was created.
|
|
523
|
+
const painSignals = session.painSignals.filter((p) => p.score >= 50);
|
|
524
|
+
const toolFailures = session.toolCalls.filter((tc) => tc.outcome === 'failure');
|
|
525
|
+
const {gateBlocks} = session;
|
|
526
|
+
|
|
527
|
+
if (painSignals.length > 0) {
|
|
528
|
+
return { violated: true, reason: `P_* principle — ${painSignals.length} pain signal(s) detected (max score: ${Math.max(...painSignals.map(p => p.score))})` };
|
|
529
|
+
}
|
|
530
|
+
if (toolFailures.length > 0) {
|
|
531
|
+
return { violated: true, reason: `P_* principle — ${toolFailures.length} tool failure(s) detected` };
|
|
532
|
+
}
|
|
533
|
+
if (gateBlocks.length > 0) {
|
|
534
|
+
return { violated: true, reason: `P_* principle — ${gateBlocks.length} gate block(s) detected` };
|
|
535
|
+
}
|
|
536
|
+
return { violated: false, reason: `P_* principle — no violation signals in session` };
|
|
537
|
+
}
|
|
538
|
+
|
|
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 */
|
|
498
541
|
switch (principleId) {
|
|
499
542
|
case 'T-01':
|
|
500
543
|
return detectT01Violation(session);
|
|
@@ -529,7 +572,8 @@ function detectT01Violation(session: SessionEvents): ViolationMatch {
|
|
|
529
572
|
const readFiles = new Set(
|
|
530
573
|
session.toolCalls
|
|
531
574
|
.filter((call) => READ_TOOLS.has(call.toolName) && call.filePath !== undefined)
|
|
532
|
-
|
|
575
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
|
|
576
|
+
.map((call) => normalizePathPosix(call.filePath!))
|
|
533
577
|
);
|
|
534
578
|
|
|
535
579
|
// Find edits to files that were NOT read first
|
|
@@ -537,14 +581,14 @@ function detectT01Violation(session: SessionEvents): ViolationMatch {
|
|
|
537
581
|
(call) =>
|
|
538
582
|
EDIT_TOOLS.has(call.toolName) &&
|
|
539
583
|
call.filePath !== undefined &&
|
|
540
|
-
!readFiles.has(
|
|
584
|
+
!readFiles.has(normalizePathPosix(call.filePath))
|
|
541
585
|
);
|
|
542
586
|
|
|
543
587
|
// If there were edits to unread files AND pain/failure followed → T-01 likely violated
|
|
544
588
|
if (unreadEdits.length > 0) {
|
|
545
589
|
const painOnUnreadEdit = session.painSignals.some(
|
|
546
590
|
(p) =>
|
|
547
|
-
unreadEdits.some((e) => e.filePath !== undefined && p.source.includes(e.filePath
|
|
591
|
+
unreadEdits.some((e) => e.filePath !== undefined && p.source.includes(e.filePath)) ||
|
|
548
592
|
/structure|architecture|dependency|context|before.*edit|survey/i.test(p.reason ?? '')
|
|
549
593
|
);
|
|
550
594
|
|
|
@@ -722,7 +766,8 @@ function detectT07Violation(session: SessionEvents): ViolationMatch {
|
|
|
722
766
|
const modifiedFiles = new Set(
|
|
723
767
|
session.toolCalls
|
|
724
768
|
.filter((call) => EDIT_TOOLS.has(call.toolName) && call.filePath !== undefined)
|
|
725
|
-
|
|
769
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
|
|
770
|
+
.map((call) => normalizePathPosix(call.filePath!))
|
|
726
771
|
);
|
|
727
772
|
|
|
728
773
|
const failures = session.toolCalls.filter((call) => call.outcome === 'failure');
|
|
@@ -778,7 +823,8 @@ function detectT09Violation(session: SessionEvents): ViolationMatch {
|
|
|
778
823
|
const uniqueFiles = new Set(
|
|
779
824
|
session.toolCalls
|
|
780
825
|
.filter((call) => call.filePath !== undefined)
|
|
781
|
-
|
|
826
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: filter ensures filePath is defined
|
|
827
|
+
.map((call) => normalizePathPosix(call.filePath!))
|
|
782
828
|
);
|
|
783
829
|
|
|
784
830
|
// Only applies if the session was complex
|
|
@@ -940,6 +986,7 @@ function computeViolationTrend(
|
|
|
940
986
|
/**
|
|
941
987
|
* Builds a human-readable explanation for the compliance result.
|
|
942
988
|
*/
|
|
989
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: explanation builder requires all context parameters - refactoring would break API
|
|
943
990
|
function buildExplanation(
|
|
944
991
|
principleId: string,
|
|
945
992
|
applicableOpportunityCount: number,
|
|
@@ -1016,6 +1063,7 @@ export function groupEventsIntoSessions(events: RawEventEntry[]): Map<string, Se
|
|
|
1016
1063
|
});
|
|
1017
1064
|
}
|
|
1018
1065
|
|
|
1066
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: set() above guarantees get() returns non-null
|
|
1019
1067
|
const session = sessionMap.get(sessionId)!;
|
|
1020
1068
|
|
|
1021
1069
|
switch (event.type) {
|
|
@@ -46,6 +46,18 @@ export type NocturnalReviewStatus =
|
|
|
46
46
|
| 'rejected'
|
|
47
47
|
| 'superseded';
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Sample classification for replay evaluation.
|
|
51
|
+
* Used by the ReplayEngine to select appropriate test samples.
|
|
52
|
+
* - 'pain-negative': samples that triggered pain signals or were blocked by gates
|
|
53
|
+
* - 'success-positive': samples that were successful interactions
|
|
54
|
+
* - 'principle-anchor': samples that embody core principle behavior
|
|
55
|
+
*
|
|
56
|
+
* Artifact kind is intentionally tracked outside this type so replay
|
|
57
|
+
* classification remains behavioral-only.
|
|
58
|
+
*/
|
|
59
|
+
export type SampleClassification = 'pain-negative' | 'success-positive' | 'principle-anchor';
|
|
60
|
+
|
|
49
61
|
/**
|
|
50
62
|
* A nocturnal dataset record — the immutable lineage entry for one sample.
|
|
51
63
|
*
|
|
@@ -105,6 +117,13 @@ export interface NocturnalDatasetRecord {
|
|
|
105
117
|
* Absolute path to the artifact file.
|
|
106
118
|
*/
|
|
107
119
|
artifactPath: string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sample classification for replay evaluation.
|
|
123
|
+
* Used by ReplayEngine to select samples by category.
|
|
124
|
+
* Null means not yet classified for replay.
|
|
125
|
+
*/
|
|
126
|
+
classification: SampleClassification | null;
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
/**
|
|
@@ -240,7 +259,8 @@ function writeRegistry(workspaceDir: string, records: NocturnalDatasetRecord[]):
|
|
|
240
259
|
* Execute a read-modify-write on the registry under an exclusive lock.
|
|
241
260
|
* This prevents concurrent writers from racing on the same file.
|
|
242
261
|
*/
|
|
243
|
-
|
|
262
|
+
/* eslint-disable no-unused-vars -- Reason: _records is a type signature parameter, unused by design in function type definition */
|
|
263
|
+
function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatasetRecord[]) => T): T {
|
|
244
264
|
const registryPath = getRegistryPath(workspaceDir);
|
|
245
265
|
return withLock(registryPath, () => {
|
|
246
266
|
const records = readRegistry(workspaceDir);
|
|
@@ -260,13 +280,16 @@ function withRegistryLock<T>(workspaceDir: string, fn: (records: NocturnalDatase
|
|
|
260
280
|
* @param artifact - The approved NocturnalArtifact
|
|
261
281
|
* @param artifactPath - Absolute path where the artifact file is stored
|
|
262
282
|
* @param targetModelFamily - Model family binding (required for export-ready)
|
|
283
|
+
* @param classification - Optional replay classification
|
|
263
284
|
* @returns RegisterSampleResult
|
|
264
285
|
*/
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: sample registration requires workspace + artifact + path + family - refactoring would break API
|
|
265
287
|
export function registerSample(
|
|
266
288
|
workspaceDir: string,
|
|
267
289
|
artifact: NocturnalArtifact,
|
|
268
290
|
artifactPath: string,
|
|
269
|
-
targetModelFamily: string | null = null
|
|
291
|
+
targetModelFamily: string | null = null,
|
|
292
|
+
classification: SampleClassification | null = null
|
|
270
293
|
): RegisterSampleResult {
|
|
271
294
|
const fingerprint = generateFingerprintFromArtifact(artifact);
|
|
272
295
|
const now = new Date().toISOString();
|
|
@@ -293,6 +316,7 @@ export function registerSample(
|
|
|
293
316
|
createdAt: now,
|
|
294
317
|
updatedAt: now,
|
|
295
318
|
artifactPath: path.normalize(artifactPath),
|
|
319
|
+
classification,
|
|
296
320
|
};
|
|
297
321
|
|
|
298
322
|
records.push(record);
|
|
@@ -401,6 +425,7 @@ const VALID_TRANSITIONS: Record<NocturnalReviewStatus, NocturnalReviewStatus[]>
|
|
|
401
425
|
* @returns Updated record, or null if not found
|
|
402
426
|
* @throws Error if transition is invalid
|
|
403
427
|
*/
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: status update requires workspace + fingerprint + status - refactoring would break API
|
|
404
429
|
export function updateReviewStatus(
|
|
405
430
|
workspaceDir: string,
|
|
406
431
|
sampleFingerprint: string,
|
|
@@ -666,3 +691,71 @@ export function migrateSampleArtifacts(
|
|
|
666
691
|
|
|
667
692
|
return newCount;
|
|
668
693
|
}
|
|
694
|
+
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
// Replay Classification Support
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* List samples filtered by replay classification.
|
|
701
|
+
*
|
|
702
|
+
* @param workspaceDir - Workspace directory
|
|
703
|
+
* @param classification - Classification to filter by
|
|
704
|
+
* @returns Records matching the classification, sorted by createdAt descending
|
|
705
|
+
*/
|
|
706
|
+
export function listSamplesByClassification(
|
|
707
|
+
workspaceDir: string,
|
|
708
|
+
classification: SampleClassification
|
|
709
|
+
): NocturnalDatasetRecord[] {
|
|
710
|
+
const records = readRegistry(workspaceDir);
|
|
711
|
+
return records
|
|
712
|
+
.filter((r) => r.classification === classification)
|
|
713
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Load and parse the artifact content for a given dataset record.
|
|
718
|
+
*
|
|
719
|
+
* @param workspaceDir - Workspace directory
|
|
720
|
+
* @param record - The dataset record whose artifact to load
|
|
721
|
+
* @returns The parsed artifact JSON content
|
|
722
|
+
* @throws Error if artifact file is missing or unreadable
|
|
723
|
+
*/
|
|
724
|
+
export function loadSampleContent(
|
|
725
|
+
workspaceDir: string,
|
|
726
|
+
record: NocturnalDatasetRecord
|
|
727
|
+
): unknown {
|
|
728
|
+
if (!fs.existsSync(record.artifactPath)) {
|
|
729
|
+
throw new Error(`Artifact file missing for sample ${record.sampleFingerprint}: ${record.artifactPath}`);
|
|
730
|
+
}
|
|
731
|
+
const content = fs.readFileSync(record.artifactPath, 'utf-8');
|
|
732
|
+
return JSON.parse(content);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Update the classification of a dataset record.
|
|
737
|
+
*
|
|
738
|
+
* @param workspaceDir - Workspace directory
|
|
739
|
+
* @param sampleFingerprint - The fingerprint of the record to update
|
|
740
|
+
* @param classification - New classification value
|
|
741
|
+
* @returns Updated record
|
|
742
|
+
*/
|
|
743
|
+
export function updateSampleClassification(
|
|
744
|
+
workspaceDir: string,
|
|
745
|
+
sampleFingerprint: string,
|
|
746
|
+
classification: SampleClassification | null
|
|
747
|
+
): NocturnalDatasetRecord {
|
|
748
|
+
return withRegistryLock(workspaceDir, (records) => {
|
|
749
|
+
const idx = records.findIndex((r) => r.sampleFingerprint === sampleFingerprint);
|
|
750
|
+
if (idx === -1) {
|
|
751
|
+
throw new Error(`Dataset record not found: ${sampleFingerprint}`);
|
|
752
|
+
}
|
|
753
|
+
records[idx] = {
|
|
754
|
+
...records[idx],
|
|
755
|
+
classification,
|
|
756
|
+
updatedAt: new Date().toISOString(),
|
|
757
|
+
};
|
|
758
|
+
writeRegistry(workspaceDir, records);
|
|
759
|
+
return records[idx];
|
|
760
|
+
});
|
|
761
|
+
}
|
|
@@ -203,7 +203,7 @@ function parseBoundedAction(text: string): BoundedAction | null {
|
|
|
203
203
|
|
|
204
204
|
// Pattern: "Read/Check/Verify X" or "X. Read/Check/Verify Y"
|
|
205
205
|
const boundedPattern = /^([A-Za-z]+)\s+(?:the\s+)?([^\s.,]+(?:\s+[^\s.,]+){0,5})/i;
|
|
206
|
-
const match =
|
|
206
|
+
const match = boundedPattern.exec(trimmed);
|
|
207
207
|
|
|
208
208
|
if (match) {
|
|
209
209
|
const verb = match[1].toLowerCase();
|
|
@@ -216,7 +216,7 @@ function parseBoundedAction(text: string): BoundedAction | null {
|
|
|
216
216
|
|
|
217
217
|
// Pattern: "[Verb] the [target]" — e.g., "read the file"
|
|
218
218
|
const thePattern = /^(read|check|verify|edit|write|delete|search|grep|look|examine|inspect|review)\s+(?:the\s+)?(.+)/i;
|
|
219
|
-
const theMatch =
|
|
219
|
+
const theMatch = thePattern.exec(lower);
|
|
220
220
|
if (theMatch) {
|
|
221
221
|
return { verb: theMatch[1], target: theMatch[2].trim(), fullText: trimmed };
|
|
222
222
|
}
|
|
@@ -294,7 +294,6 @@ export function validateExecutability(artifact: {
|
|
|
294
294
|
|
|
295
295
|
// Check betterDecision is executable
|
|
296
296
|
const boundedAction = parseBoundedAction(artifact.betterDecision);
|
|
297
|
-
const lowerBetter = artifact.betterDecision.toLowerCase();
|
|
298
297
|
|
|
299
298
|
// Check 1: Not a hollow pattern
|
|
300
299
|
if (containsHollowPattern(artifact.betterDecision)) {
|
|
@@ -96,12 +96,12 @@ export interface ORPOExportManifest {
|
|
|
96
96
|
datasetFingerprint: string;
|
|
97
97
|
exportPath: string;
|
|
98
98
|
manifestPath: string;
|
|
99
|
-
samples:
|
|
99
|
+
samples: {
|
|
100
100
|
sampleFingerprint: string;
|
|
101
101
|
artifactId: string;
|
|
102
102
|
sessionId: string;
|
|
103
103
|
principleId: string;
|
|
104
|
-
}
|
|
104
|
+
}[];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
@@ -136,6 +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
|
function serializeORPOSample(
|
|
140
141
|
record: NocturnalDatasetRecord,
|
|
141
142
|
artifact: ReturnType<typeof readDatasetArtifact>,
|
|
@@ -182,7 +183,7 @@ function serializeORPOSample(
|
|
|
182
183
|
export function exportORPOSamples(
|
|
183
184
|
workspaceDir: string,
|
|
184
185
|
targetModelFamily?: string | null,
|
|
185
|
-
_options: Record<string, never> = {}
|
|
186
|
+
_options: Record<string, never> = {} // eslint-disable-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: options parameter intentionally unused
|
|
186
187
|
): ExportResult {
|
|
187
188
|
const exportId = crypto.randomUUID();
|
|
188
189
|
const now = new Date().toISOString();
|
|
@@ -194,6 +195,7 @@ export function exportORPOSamples(
|
|
|
194
195
|
reviewStatus: 'approved_for_training',
|
|
195
196
|
});
|
|
196
197
|
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in both if/else branches
|
|
197
199
|
let eligibleRecords: typeof allApprovedRecords;
|
|
198
200
|
|
|
199
201
|
if (targetModelFamily !== undefined && targetModelFamily !== null) {
|
|
@@ -242,6 +244,7 @@ export function exportORPOSamples(
|
|
|
242
244
|
}
|
|
243
245
|
|
|
244
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
|
|
245
248
|
let artifact;
|
|
246
249
|
try {
|
|
247
250
|
artifact = readDatasetArtifact(workspaceDir, record.sampleFingerprint);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createRuleHostHelpers, type RuleHostHelpers } from './rule-host-helpers.js';
|
|
2
|
+
import { loadRuleImplementationModule } from './rule-implementation-runtime.js';
|
|
3
|
+
import type {
|
|
4
|
+
RuleHostDecision,
|
|
5
|
+
RuleHostInput,
|
|
6
|
+
RuleHostMeta,
|
|
7
|
+
RuleHostResult,
|
|
8
|
+
} from './rule-host-types.js';
|
|
9
|
+
|
|
10
|
+
export interface RuleImplementationValidationFailure {
|
|
11
|
+
code:
|
|
12
|
+
| 'forbidden-api'
|
|
13
|
+
| 'compile-error'
|
|
14
|
+
| 'missing-meta'
|
|
15
|
+
| 'invalid-meta'
|
|
16
|
+
| 'missing-evaluate'
|
|
17
|
+
| 'invalid-result';
|
|
18
|
+
message: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RuleImplementationValidationResult {
|
|
23
|
+
passed: boolean;
|
|
24
|
+
failures: RuleImplementationValidationFailure[];
|
|
25
|
+
helperUsage: string[];
|
|
26
|
+
normalizedSource?: string;
|
|
27
|
+
meta?: RuleHostMeta;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FORBIDDEN_PATTERNS: { pattern: RegExp; label: string }[] = [
|
|
31
|
+
{ pattern: /\beval\s*\(/, label: 'eval' },
|
|
32
|
+
{ pattern: /\bFunction\s*\(/, label: 'Function' },
|
|
33
|
+
{ pattern: /\bimport\s*\(/, label: 'dynamic import' },
|
|
34
|
+
{ pattern: /\brequire\s*\(/, label: 'require' },
|
|
35
|
+
{ pattern: /\bfetch\s*\(/, label: 'fetch' },
|
|
36
|
+
{ pattern: /\bXMLHttpRequest\b/, label: 'XMLHttpRequest' },
|
|
37
|
+
{ pattern: /\bchild_process\b/, label: 'child_process' },
|
|
38
|
+
{ pattern: /\bprocess\b/, label: 'process' },
|
|
39
|
+
{ pattern: /\bfs\b/, label: 'fs' },
|
|
40
|
+
{ pattern: /\bhttp\b/, label: 'http' },
|
|
41
|
+
{ pattern: /\bhttps\b/, label: 'https' },
|
|
42
|
+
{ pattern: /\bnet\b/, label: 'net' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const HELPER_NAMES: (keyof RuleHostHelpers)[] = [
|
|
46
|
+
'isRiskPath',
|
|
47
|
+
'getToolName',
|
|
48
|
+
'getEstimatedLineChanges',
|
|
49
|
+
'getBashRisk',
|
|
50
|
+
'hasPlanFile',
|
|
51
|
+
'getPlanStatus',
|
|
52
|
+
'getCurrentEpiTier',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function createValidationInput(): RuleHostInput {
|
|
56
|
+
return {
|
|
57
|
+
action: {
|
|
58
|
+
toolName: 'write',
|
|
59
|
+
normalizedPath: 'src/risk.ts',
|
|
60
|
+
paramsSummary: {},
|
|
61
|
+
},
|
|
62
|
+
workspace: {
|
|
63
|
+
isRiskPath: true,
|
|
64
|
+
planStatus: 'DRAFT',
|
|
65
|
+
hasPlanFile: true,
|
|
66
|
+
},
|
|
67
|
+
session: {
|
|
68
|
+
sessionId: 'validator-session',
|
|
69
|
+
currentGfi: 3,
|
|
70
|
+
recentThinking: false,
|
|
71
|
+
},
|
|
72
|
+
evolution: {
|
|
73
|
+
epTier: 2,
|
|
74
|
+
},
|
|
75
|
+
derived: {
|
|
76
|
+
estimatedLineChanges: 42,
|
|
77
|
+
bashRisk: 'normal',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractHelperUsage(sourceCode: string): string[] {
|
|
83
|
+
return HELPER_NAMES.filter((helperName) =>
|
|
84
|
+
new RegExp(`\\bhelpers\\.${helperName}\\b|\\b${helperName}\\s*\\(`).test(sourceCode)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateMeta(meta: unknown): RuleImplementationValidationFailure[] {
|
|
89
|
+
if (!meta || typeof meta !== 'object') {
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
code: 'missing-meta',
|
|
93
|
+
message: 'Candidate must export a meta object.',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const candidate = meta as Partial<RuleHostMeta>;
|
|
99
|
+
const failures: RuleImplementationValidationFailure[] = [];
|
|
100
|
+
|
|
101
|
+
if (typeof candidate.name !== 'string' || candidate.name.trim().length === 0) {
|
|
102
|
+
failures.push({
|
|
103
|
+
code: 'invalid-meta',
|
|
104
|
+
message: 'meta.name must be a non-empty string.',
|
|
105
|
+
detail: 'name',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (typeof candidate.version !== 'string' || candidate.version.trim().length === 0) {
|
|
109
|
+
failures.push({
|
|
110
|
+
code: 'invalid-meta',
|
|
111
|
+
message: 'meta.version must be a non-empty string.',
|
|
112
|
+
detail: 'version',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (typeof candidate.ruleId !== 'string' || candidate.ruleId.trim().length === 0) {
|
|
116
|
+
failures.push({
|
|
117
|
+
code: 'invalid-meta',
|
|
118
|
+
message: 'meta.ruleId must be a non-empty string.',
|
|
119
|
+
detail: 'ruleId',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
typeof candidate.coversCondition !== 'string' ||
|
|
124
|
+
candidate.coversCondition.trim().length === 0
|
|
125
|
+
) {
|
|
126
|
+
failures.push({
|
|
127
|
+
code: 'invalid-meta',
|
|
128
|
+
message: 'meta.coversCondition must be a non-empty string.',
|
|
129
|
+
detail: 'coversCondition',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return failures;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateResult(result: unknown): RuleImplementationValidationFailure[] {
|
|
137
|
+
if (!result || typeof result !== 'object') {
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
code: 'invalid-result',
|
|
141
|
+
message: 'evaluate must return a RuleHostResult object.',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const candidate = result as Partial<RuleHostResult>;
|
|
147
|
+
const failures: RuleImplementationValidationFailure[] = [];
|
|
148
|
+
const allowedDecisions: RuleHostDecision[] = ['allow', 'block', 'requireApproval'];
|
|
149
|
+
|
|
150
|
+
if (!allowedDecisions.includes(candidate.decision as RuleHostDecision)) {
|
|
151
|
+
failures.push({
|
|
152
|
+
code: 'invalid-result',
|
|
153
|
+
message: 'evaluate.decision must be allow, block, or requireApproval.',
|
|
154
|
+
detail: 'decision',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (typeof candidate.matched !== 'boolean') {
|
|
158
|
+
failures.push({
|
|
159
|
+
code: 'invalid-result',
|
|
160
|
+
message: 'evaluate.matched must be a boolean.',
|
|
161
|
+
detail: 'matched',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (typeof candidate.reason !== 'string') {
|
|
165
|
+
failures.push({
|
|
166
|
+
code: 'invalid-result',
|
|
167
|
+
message: 'evaluate.reason must be a string.',
|
|
168
|
+
detail: 'reason',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return failures;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateRuleImplementationCandidate(
|
|
176
|
+
sourceCode: string
|
|
177
|
+
): RuleImplementationValidationResult {
|
|
178
|
+
const helperUsage = extractHelperUsage(sourceCode);
|
|
179
|
+
const failures: RuleImplementationValidationFailure[] = [];
|
|
180
|
+
|
|
181
|
+
for (const forbidden of FORBIDDEN_PATTERNS) {
|
|
182
|
+
if (forbidden.pattern.test(sourceCode)) {
|
|
183
|
+
failures.push({
|
|
184
|
+
code: 'forbidden-api',
|
|
185
|
+
message: `Candidate uses forbidden API: ${forbidden.label}.`,
|
|
186
|
+
detail: forbidden.label,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (failures.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
passed: false,
|
|
194
|
+
failures,
|
|
195
|
+
helperUsage,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const normalizedSource = sourceCode;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const moduleExports = loadRuleImplementationModule(normalizedSource, 'nocturnal-candidate.js') as {
|
|
203
|
+
meta?: unknown;
|
|
204
|
+
/* eslint-disable no-unused-vars -- Reason: type signature parameters, unused by design in function type definition */
|
|
205
|
+
evaluate?: (_input: RuleHostInput, _helpers: RuleHostHelpers) => unknown;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const metaFailures = validateMeta(moduleExports.meta);
|
|
209
|
+
failures.push(...metaFailures);
|
|
210
|
+
|
|
211
|
+
if (typeof moduleExports.evaluate !== 'function') {
|
|
212
|
+
failures.push({
|
|
213
|
+
code: 'missing-evaluate',
|
|
214
|
+
message: 'Candidate must export an evaluate function.',
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
const result = moduleExports.evaluate(
|
|
218
|
+
createValidationInput(),
|
|
219
|
+
createRuleHostHelpers(createValidationInput())
|
|
220
|
+
);
|
|
221
|
+
failures.push(...validateResult(result));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
passed: failures.length === 0,
|
|
226
|
+
failures,
|
|
227
|
+
helperUsage,
|
|
228
|
+
normalizedSource,
|
|
229
|
+
meta: metaFailures.length === 0 ? (moduleExports.meta as RuleHostMeta) : undefined,
|
|
230
|
+
};
|
|
231
|
+
} catch (error: unknown) {
|
|
232
|
+
return {
|
|
233
|
+
passed: false,
|
|
234
|
+
failures: [
|
|
235
|
+
{
|
|
236
|
+
code: 'compile-error',
|
|
237
|
+
message: 'Candidate could not be compiled with RuleHost-compatible semantics.',
|
|
238
|
+
detail: String(error),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
helperUsage,
|
|
242
|
+
normalizedSource,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|