@principles/pd-cli 1.152.11 → 1.152.13
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/dist/commands/candidate.d.ts +3 -3
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +9 -21
- package/dist/commands/candidate.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +4 -17
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +13 -7
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
- package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
- package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
- package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
- package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
- package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
- package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/version.test.d.ts +0 -2
- package/dist/commands/__tests__/version.test.d.ts.map +0 -1
- package/dist/commands/__tests__/version.test.js +0 -238
- package/dist/commands/__tests__/version.test.js.map +0 -1
- package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
- package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
- package/dist/commands/runtime-uat.guard.test.js +0 -153
- package/dist/commands/runtime-uat.guard.test.js.map +0 -1
- package/dist/services/__tests__/cli-output.test.d.ts +0 -18
- package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
- package/dist/services/__tests__/cli-output.test.js +0 -103
- package/dist/services/__tests__/cli-output.test.js.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
- package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
- package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
- package/dist/utils/production-workspace-guard.test.d.ts +0 -2
- package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
- package/dist/utils/production-workspace-guard.test.js +0 -137
- package/dist/utils/production-workspace-guard.test.js.map +0 -1
- package/scripts/llm-dogfood.ts +0 -456
- package/scripts/migrate-illegal-expected-decision.ts +0 -252
- package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
- package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
- package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
- package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
- package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
- package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
- package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
- package/src/commands/__tests__/version.test.ts +0 -251
- package/src/commands/admission-gate.ts +0 -30
- package/src/commands/artifact.ts +0 -82
- package/src/commands/build-trajectory-evidence.ts +0 -298
- package/src/commands/candidate.ts +0 -1260
- package/src/commands/codex-ingest-catchup.ts +0 -114
- package/src/commands/codex-ingest-quarantine.ts +0 -127
- package/src/commands/codex-reconcile.ts +0 -84
- package/src/commands/codex-setup.ts +0 -476
- package/src/commands/codex-worker.ts +0 -185
- package/src/commands/command-helpers.ts +0 -24
- package/src/commands/config-doctor.ts +0 -193
- package/src/commands/console.ts +0 -797
- package/src/commands/context.ts +0 -72
- package/src/commands/demo-story-a.ts +0 -241
- package/src/commands/diagnose.ts +0 -756
- package/src/commands/errors-list.ts +0 -441
- package/src/commands/evolution-tasks-list.ts +0 -48
- package/src/commands/evolution-tasks-show.ts +0 -62
- package/src/commands/flow.ts +0 -60
- package/src/commands/health-codex.ts +0 -576
- package/src/commands/health.ts +0 -315
- package/src/commands/history.ts +0 -63
- package/src/commands/intent.ts +0 -457
- package/src/commands/legacy-cleanup.ts +0 -531
- package/src/commands/legacy-import.ts +0 -104
- package/src/commands/mvp-smoke.ts +0 -160
- package/src/commands/pain-evidence.ts +0 -226
- package/src/commands/pain-list.ts +0 -239
- package/src/commands/pain-record.ts +0 -612
- package/src/commands/pain-retry.ts +0 -916
- package/src/commands/principles-stats.ts +0 -700
- package/src/commands/proven-channel-baseline.ts +0 -115
- package/src/commands/quality-scorecard.ts +0 -272
- package/src/commands/remediation-output.ts +0 -66
- package/src/commands/rulecode.ts +0 -465
- package/src/commands/run.ts +0 -89
- package/src/commands/runtime-activation.ts +0 -1629
- package/src/commands/runtime-artifact-repair.ts +0 -524
- package/src/commands/runtime-canary.ts +0 -395
- package/src/commands/runtime-compatibility-scan.ts +0 -105
- package/src/commands/runtime-diagnostics-export.ts +0 -229
- package/src/commands/runtime-features.ts +0 -153
- package/src/commands/runtime-gfi-snapshot.ts +0 -135
- package/src/commands/runtime-health-snapshot.ts +0 -108
- package/src/commands/runtime-init.ts +0 -429
- package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
- package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
- package/src/commands/runtime-internalization-integrity.ts +0 -107
- package/src/commands/runtime-internalization-queue.ts +0 -135
- package/src/commands/runtime-internalization-retry.ts +0 -191
- package/src/commands/runtime-internalization-run-once.ts +0 -791
- package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
- package/src/commands/runtime-internalization-wake-once.ts +0 -87
- package/src/commands/runtime-pain-flood-simulation.ts +0 -121
- package/src/commands/runtime-pruning.ts +0 -438
- package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
- package/src/commands/runtime-recovery.ts +0 -109
- package/src/commands/runtime-synthetic-baseline.ts +0 -70
- package/src/commands/runtime-uat.guard.test.ts +0 -175
- package/src/commands/runtime-uat.ts +0 -380
- package/src/commands/runtime.ts +0 -473
- package/src/commands/samples-list.ts +0 -39
- package/src/commands/samples-review.ts +0 -36
- package/src/commands/task.ts +0 -271
- package/src/commands/telemetry.ts +0 -349
- package/src/commands/trace.ts +0 -174
- package/src/commands/trajectory-db-unavailable.ts +0 -19
- package/src/commands/trajectory.ts +0 -64
- package/src/commands/version.ts +0 -49
- package/src/config-reader.ts +0 -122
- package/src/index.ts +0 -1276
- package/src/legacy/legacy-import.ts +0 -179
- package/src/legacy/session-history-import.ts +0 -203
- package/src/resolve-workspace.ts +0 -106
- package/src/services/__tests__/cli-output.test.ts +0 -130
- package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
- package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
- package/src/services/cli-output.ts +0 -95
- package/src/services/config-doctor.ts +0 -697
- package/src/services/console-launcher.ts +0 -537
- package/src/services/demo-rule-compiler.ts +0 -40
- package/src/services/demo-story-a-runner.ts +0 -471
- package/src/services/mainline-snapshot-assembler.ts +0 -579
- package/src/services/pain-flood-simulation-runner.ts +0 -462
- package/src/services/pd-config-loader.ts +0 -433
- package/src/services/proven-channel-baseline-runner.ts +0 -157
- package/src/services/quality-scorecard/data-extractor.ts +0 -150
- package/src/services/quality-scorecard/local-evaluator.ts +0 -142
- package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
- package/src/services/resolve-runtime-from-pd-config.ts +0 -230
- package/src/services/rulehost-pipeline-runner.ts +0 -1216
- package/src/services/rulehost-readiness.ts +0 -326
- package/src/services/runtime-adapter-resolver.ts +0 -364
- package/src/services/synthetic-baseline-runner.ts +0 -400
- package/src/services/version-report.ts +0 -253
- package/src/services/workspace-tool-semantics.ts +0 -26
- package/src/utils/path-security.ts +0 -96
- package/src/utils/production-workspace-guard.test.ts +0 -157
- package/src/utils/production-workspace-guard.ts +0 -210
- package/tests/bdd/cli-contract.steps.ts +0 -346
- package/tests/bdd/principles-stats.steps.ts +0 -211
- package/tests/bdd/support/gherkin-loader.ts +0 -147
- package/tests/bdd/support/repo-root.ts +0 -62
- package/tests/bdd/support/vitest-bdd.ts +0 -179
- package/tests/commands/admission-gate-coverage.test.ts +0 -79
- package/tests/commands/build-trajectory-evidence.test.ts +0 -508
- package/tests/commands/candidate-audit-repair.test.ts +0 -421
- package/tests/commands/candidate-intake.test.ts +0 -732
- package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
- package/tests/commands/candidate-internalize-lineage.test.ts +0 -565
- package/tests/commands/candidate-internalize.test.ts +0 -299
- package/tests/commands/candidate-route.test.ts +0 -329
- package/tests/commands/candidate-show.test.ts +0 -95
- package/tests/commands/cli-command-tree.test.ts +0 -144
- package/tests/commands/cli-help-snapshot.test.ts +0 -155
- package/tests/commands/cli-skill-contract.test.ts +0 -121
- package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
- package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
- package/tests/commands/codex-setup.test.ts +0 -297
- package/tests/commands/codex-worker-daemon.test.ts +0 -95
- package/tests/commands/codex-worker-registration.test.ts +0 -207
- package/tests/commands/config-doctor.test.ts +0 -336
- package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
- package/tests/commands/console-open.test.ts +0 -1381
- package/tests/commands/context.test.ts +0 -114
- package/tests/commands/demo-story-a.test.ts +0 -318
- package/tests/commands/diagnose.test.ts +0 -1803
- package/tests/commands/errors-list.test.ts +0 -679
- package/tests/commands/evolution-tasks-list.test.ts +0 -41
- package/tests/commands/evolution-tasks-show.test.ts +0 -46
- package/tests/commands/health-codex.test.ts +0 -349
- package/tests/commands/health-host-wiring.test.ts +0 -76
- package/tests/commands/health-reliability.test.ts +0 -99
- package/tests/commands/health.test.ts +0 -413
- package/tests/commands/intent.test.ts +0 -321
- package/tests/commands/legacy-cleanup.test.ts +0 -148
- package/tests/commands/pain-evidence.test.ts +0 -518
- package/tests/commands/pain-list.test.ts +0 -219
- package/tests/commands/pain-record-async.test.ts +0 -335
- package/tests/commands/pain-record-session-parser.test.ts +0 -228
- package/tests/commands/pain-record.test.ts +0 -851
- package/tests/commands/pain-retry.test.ts +0 -1121
- package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
- package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
- package/tests/commands/principles-stats-wiring.test.ts +0 -135
- package/tests/commands/principles-stats.test.ts +0 -274
- package/tests/commands/product-path-regression.test.ts +0 -86
- package/tests/commands/proven-channel-baseline.test.ts +0 -441
- package/tests/commands/rulecode.test.ts +0 -177
- package/tests/commands/run-rulehost-handler.test.ts +0 -908
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
- package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
- package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
- package/tests/commands/runtime-activation.test.ts +0 -2052
- package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
- package/tests/commands/runtime-artifact-repair.test.ts +0 -404
- package/tests/commands/runtime-canary.test.ts +0 -430
- package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
- package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
- package/tests/commands/runtime-features.test.ts +0 -249
- package/tests/commands/runtime-health-snapshot.test.ts +0 -358
- package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
- package/tests/commands/runtime-init.test.ts +0 -420
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
- package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
- package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
- package/tests/commands/runtime-internalization-queue.test.ts +0 -380
- package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
- package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
- package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
- package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
- package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
- package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
- package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
- package/tests/commands/runtime-internalization.test.ts +0 -53
- package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
- package/tests/commands/runtime-pruning.test.ts +0 -694
- package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
- package/tests/commands/runtime-recovery.test.ts +0 -120
- package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
- package/tests/commands/runtime-uat.test.ts +0 -397
- package/tests/commands/runtime.test.ts +0 -392
- package/tests/commands/samples-list.test.ts +0 -43
- package/tests/commands/samples-review.test.ts +0 -41
- package/tests/commands/task.test.ts +0 -153
- package/tests/commands/telemetry.test.ts +0 -190
- package/tests/commands/trace.test.ts +0 -316
- package/tests/config-reader.test.ts +0 -143
- package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
- package/tests/e2e/cli-full-flow.test.ts +0 -233
- package/tests/e2e/cross-package-acceptance.test.ts +0 -635
- package/tests/helpers/pd-cli-path.ts +0 -5
- package/tests/pd-cli-smoke.test.ts +0 -13
- package/tests/resolve-workspace.test.ts +0 -196
- package/tests/services/config-doctor-signal-health.test.ts +0 -109
- package/tests/services/console-launcher.test.ts +0 -114
- package/tests/services/demo-rule-compiler.test.ts +0 -242
- package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
- package/tests/services/pd-config-loader.test.ts +0 -505
- package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
- package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
- package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
- package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
- package/tests/services/rulehost-pipeline-runner.test.ts +0 -1175
- package/tests/services/workspace-tool-semantics.test.ts +0 -76
- package/tests/utils/path-security.test.ts +0 -180
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -9
|
@@ -1,1175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* runRuleHostPipeline unit tests (PRI-429).
|
|
3
|
-
*
|
|
4
|
-
* Covers the atomic capability contract (ArtificerL2 + Evaluator both run or
|
|
5
|
-
* neither runs) and exact sourcePainId matching (D fix: pain-1 must NOT match
|
|
6
|
-
* pain-10).
|
|
7
|
-
*
|
|
8
|
-
* ERR refs considered:
|
|
9
|
-
* - ERR-001: parsed JSON treated as unknown
|
|
10
|
-
* - ERR-005/007: no `as` bypass; type narrowing via typeof + Object.hasOwn
|
|
11
|
-
* - ERR-009: missing sourcePainId = no match (fail loud)
|
|
12
|
-
* - ERR-013: Object.hasOwn for untrusted key checks
|
|
13
|
-
*/
|
|
14
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/class-methods-use-this, @typescript-eslint/require-await */
|
|
15
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
16
|
-
import * as os from 'node:os';
|
|
17
|
-
import * as path from 'node:path';
|
|
18
|
-
import * as fs from 'node:fs';
|
|
19
|
-
import { runRuleHostPipeline, backfillScribeIdentity } from '../../src/services/rulehost-pipeline-runner.js';
|
|
20
|
-
import type { CodeRuleCapability } from '../../src/services/rulehost-pipeline-runner.js';
|
|
21
|
-
import type { PDRuntimeAdapter, RunHandle, RunStatus, PIArtifactStore, RuntimeCapabilities, RuntimeHealth, RuntimeArtifactRef, ContextItem, StructuredRunOutput, StartRunInput } from '@principles/core/runtime-v2';
|
|
22
|
-
import { RuntimeStateManager, createPITaskDiagnosticJson, SqliteApprovalQueueStore } from '@principles/core/runtime-v2';
|
|
23
|
-
import { addPrincipleToLedger, updatePrinciple } from '@principles/core/principle-tree-ledger';
|
|
24
|
-
import { saveHostToolDeclaration } from '@principles/host-runtime';
|
|
25
|
-
|
|
26
|
-
type StageFactory = (taskId: string, priorArtifactId?: string) => unknown;
|
|
27
|
-
type EvaluatorFactory = (taskId: string, artificerArtifactId: string) => unknown;
|
|
28
|
-
|
|
29
|
-
class ScriptedAdapter implements PDRuntimeAdapter {
|
|
30
|
-
readonly startRunCalls: { taskId: string }[] = [];
|
|
31
|
-
readonly startRunInputs = new Map<string, StartRunInput>();
|
|
32
|
-
artifactStore: PIArtifactStore | null = null;
|
|
33
|
-
constructor(private readonly factories: { dreamer: StageFactory; philosopher: StageFactory; scribe: StageFactory; artificer: StageFactory; evaluator: EvaluatorFactory }) {}
|
|
34
|
-
|
|
35
|
-
private kindFor(taskId: string): 'dreamer' | 'philosopher' | 'scribe' | 'artificer' | 'evaluator' {
|
|
36
|
-
if (taskId.startsWith('dreamer')) return 'dreamer';
|
|
37
|
-
if (taskId.includes('philosopher')) return 'philosopher';
|
|
38
|
-
if (taskId.includes('scribe')) return 'scribe';
|
|
39
|
-
if (taskId.includes('artificer')) return 'artificer';
|
|
40
|
-
return 'evaluator';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private async priorArtifactId(priorKind: string): Promise<string | undefined> {
|
|
44
|
-
const calls = this.startRunCalls.filter((c) => this.kindFor(c.taskId) === priorKind);
|
|
45
|
-
const last = calls[calls.length - 1];
|
|
46
|
-
if (!last || !this.artifactStore) return undefined;
|
|
47
|
-
const arts = await this.artifactStore.listBySourceTaskId(last.taskId);
|
|
48
|
-
return arts[0]?.artifactId;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async startRun(input: StartRunInput): Promise<RunHandle> {
|
|
52
|
-
this.startRunCalls.push({ taskId: input.taskRef.taskId });
|
|
53
|
-
const runId = `run-${input.taskRef.taskId}`;
|
|
54
|
-
this.startRunInputs.set(runId, input);
|
|
55
|
-
return { runId, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
|
|
56
|
-
}
|
|
57
|
-
async pollRun(_runId: string): Promise<RunStatus> { return { status: 'succeeded', runId: 'run-x' }; }
|
|
58
|
-
async fetchOutput(runId: string): Promise<StructuredRunOutput | null> {
|
|
59
|
-
const taskId = runId.replace(/^run-/, '');
|
|
60
|
-
const kind = this.kindFor(taskId);
|
|
61
|
-
let payload: unknown;
|
|
62
|
-
if (kind === 'dreamer') payload = this.factories.dreamer(taskId);
|
|
63
|
-
else if (kind === 'philosopher') payload = this.factories.philosopher(taskId, await this.priorArtifactId('dreamer'));
|
|
64
|
-
else if (kind === 'scribe') payload = this.factories.scribe(taskId, await this.priorArtifactId('philosopher'));
|
|
65
|
-
else if (kind === 'artificer') payload = this.factories.artificer(taskId, await this.priorArtifactId('scribe'));
|
|
66
|
-
else {
|
|
67
|
-
let artificerArtifactId = await this.priorArtifactId('artificer');
|
|
68
|
-
if (!artificerArtifactId) {
|
|
69
|
-
const inputPayload = this.startRunInputs.get(runId)?.inputPayload;
|
|
70
|
-
if (typeof inputPayload === 'string') {
|
|
71
|
-
const parsed: unknown = JSON.parse(inputPayload);
|
|
72
|
-
if (parsed !== null && typeof parsed === 'object' && Object.hasOwn(parsed, 'sourceArtificerArtifactId')) {
|
|
73
|
-
const candidate = Reflect.get(parsed, 'sourceArtificerArtifactId');
|
|
74
|
-
if (typeof candidate === 'string') artificerArtifactId = candidate;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
payload = this.factories.evaluator(taskId, requireLineage(artificerArtifactId, 'sourceArtificerArtifactId'));
|
|
79
|
-
}
|
|
80
|
-
return { runId, payload };
|
|
81
|
-
}
|
|
82
|
-
async cancelRun(_runId: string): Promise<void> { /* noop */ }
|
|
83
|
-
async getCapabilities(): Promise<RuntimeCapabilities> {
|
|
84
|
-
return {
|
|
85
|
-
supportsStructuredJsonOutput: false,
|
|
86
|
-
supportsToolUse: false,
|
|
87
|
-
supportsWorkingDirectory: false,
|
|
88
|
-
supportsModelSelection: false,
|
|
89
|
-
supportsLongRunningSessions: false,
|
|
90
|
-
supportsCancellation: false,
|
|
91
|
-
supportsArtifactWriteBack: false,
|
|
92
|
-
supportsConcurrentRuns: false,
|
|
93
|
-
supportsStreaming: false,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
async healthCheck(): Promise<RuntimeHealth> {
|
|
97
|
-
return { healthy: true, degraded: false, warnings: [], lastCheckedAt: new Date().toISOString() };
|
|
98
|
-
}
|
|
99
|
-
async fetchArtifacts(_runId: string): Promise<RuntimeArtifactRef[]> { return []; }
|
|
100
|
-
async appendContext(_runId: string, _items: ContextItem[]): Promise<void> { /* noop */ }
|
|
101
|
-
async refreshCapabilities(): Promise<RuntimeCapabilities> { return this.getCapabilities(); }
|
|
102
|
-
kind(): 'test-double' { return 'test-double'; }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── Output factories ─────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
function dreamerOut(taskId: string, painId: string): unknown {
|
|
108
|
-
return {
|
|
109
|
-
valid: true, taskId,
|
|
110
|
-
candidates: [{ candidateIndex: 0, badDecision: 'Wrote to /etc/passwd', betterDecision: 'Block system path writes', rationale: 'OS damage risk', confidence: 0.85, riskLevel: 'low' as const, strategicPerspective: 'safety' }],
|
|
111
|
-
sourcePainId: painId, contextRefs: [], generatedAt: new Date().toISOString(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function requireLineage(id: string | undefined, field: string): string {
|
|
116
|
-
if (!id) throw new Error(`missing required lineage field: ${field}`);
|
|
117
|
-
return id;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function philosopherOut(taskId: string, priorId?: string): unknown {
|
|
121
|
-
return {
|
|
122
|
-
taskId, sourceDreamerArtifactId: requireLineage(priorId, 'sourceDreamerArtifactId'),
|
|
123
|
-
thesis: 'System path writes must be blocked',
|
|
124
|
-
principleCandidate: { title: 'Block system path writes', rationale: 'OS corruption risk', scope: 'write ops', confidence: 0.9 },
|
|
125
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function scribeOut(taskId: string, priorId?: string): unknown {
|
|
130
|
-
return {
|
|
131
|
-
taskId, sourcePhilosopherArtifactId: requireLineage(priorId, 'sourcePhilosopherArtifactId'),
|
|
132
|
-
principleDraft: { title: 'Block system path writes', statement: 'Writes to /etc, /boot, /sys must be blocked.', rationale: 'OS corruption risk', applicability: ['write_file'], antiPatterns: ['Hardcoded allow'], confidence: 0.9 },
|
|
133
|
-
sourceTrace: { philosopherArtifactId: requireLineage(priorId, 'sourceTrace.philosopherArtifactId') },
|
|
134
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// PRI-780: v2-only generation — the pipeline requires an Owner-labelled pack
|
|
139
|
-
// and the artificer output must satisfy the v2 contract (case parity incl.
|
|
140
|
-
// ruleContext + verbatim evidenceRefs).
|
|
141
|
-
const PIPE_RULE_CONTEXT = {
|
|
142
|
-
version: 2 as const,
|
|
143
|
-
history: { status: 'available' as const, truncated: false, calls: [] },
|
|
144
|
-
facts: { priorReadOfTarget: 'unknown' as const, readCount: 0, writeCount: 0, uniqueWritePathCount: 0, sameActionBlockCount: null },
|
|
145
|
-
};
|
|
146
|
-
const PIPE_POS_CASE = { caseId: 'pos-1', kind: 'positive' as const, toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' as const, ruleContext: PIPE_RULE_CONTEXT };
|
|
147
|
-
const PIPE_NEG_CASE = { caseId: 'neg-1', kind: 'negative' as const, toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' as const, ruleContext: PIPE_RULE_CONTEXT };
|
|
148
|
-
const PIPE_PACK = {
|
|
149
|
-
sourceNegativeCase: PIPE_NEG_CASE,
|
|
150
|
-
ownerDesiredOutcome: 'block system path writes',
|
|
151
|
-
positiveCounterexamples: [PIPE_POS_CASE],
|
|
152
|
-
evidenceRefs: ['pain://pipe-1'],
|
|
153
|
-
redactionNotes: [],
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
function artificerV2(taskId: string, priorId?: string): unknown {
|
|
157
|
-
return {
|
|
158
|
-
taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
|
|
159
|
-
requiresContextVersion: 2,
|
|
160
|
-
evidenceRefs: ['pain://pipe-1'],
|
|
161
|
-
implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
|
|
162
|
-
implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
|
|
163
|
-
implementationSummary: 'Block system path writes',
|
|
164
|
-
goldenTraceCases: [PIPE_POS_CASE, PIPE_NEG_CASE],
|
|
165
|
-
affectedTools: ['write_file'],
|
|
166
|
-
sourceTrace: { scribeArtifactId: requireLineage(priorId, 'sourceTrace.scribeArtifactId') },
|
|
167
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function evaluatorApproved(taskId: string, artificerArtifactId: string): unknown {
|
|
172
|
-
return {
|
|
173
|
-
taskId, sourceArtificerArtifactId: artificerArtifactId,
|
|
174
|
-
evaluation: { decision: 'approved', summary: 'approved', score: 0.9, strengths: [], concerns: [], requiredChanges: [] },
|
|
175
|
-
sourceTrace: { artificerArtifactId },
|
|
176
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
177
|
-
codeReview: { intentConsistency: { aligned: true, explanation: 'ok' }, scopePrecision: { verdict: 'precise' as const, explanation: 'ok' }, traceCoverage: { sufficient: true, gaps: [], explanation: 'ok' } },
|
|
178
|
-
adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/etc/shadow' }, expectedDecision: 'block' as const, rationale: 'system path' }],
|
|
179
|
-
adversarialResult: { passed: true, failedCases: [] },
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function evaluatorRejected(taskId: string, artificerArtifactId: string): unknown {
|
|
184
|
-
return {
|
|
185
|
-
taskId, sourceArtificerArtifactId: artificerArtifactId,
|
|
186
|
-
evaluation: { decision: 'rejected', summary: 'rejected: code too narrow', score: 0.3, strengths: [], concerns: ['misses /boot path'], requiredChanges: ['add /boot matcher'] },
|
|
187
|
-
sourceTrace: { artificerArtifactId },
|
|
188
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
189
|
-
codeReview: { intentConsistency: { aligned: false, explanation: 'misses /boot' }, scopePrecision: { verdict: 'imprecise' as const, explanation: 'narrow' }, traceCoverage: { sufficient: false, gaps: ['/boot'], explanation: 'missing' } },
|
|
190
|
-
adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/boot/grub' }, expectedDecision: 'block' as const, rationale: 'system path' }],
|
|
191
|
-
adversarialResult: { passed: false, failedCases: [{ caseId: 'adv-1', errorType: 'wrong_decision', message: 'expected block, got allow' }] },
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function evaluatorNeedsRevision(taskId: string, artificerArtifactId: string): unknown {
|
|
196
|
-
return {
|
|
197
|
-
taskId, sourceArtificerArtifactId: artificerArtifactId,
|
|
198
|
-
evaluation: { decision: 'needs_revision', summary: 'needs revision: adversarial replay failed', score: 0.4, strengths: [], concerns: ['adversarial case failed'], requiredChanges: ['fix matcher'] },
|
|
199
|
-
sourceTrace: { artificerArtifactId },
|
|
200
|
-
risks: [], generatedAt: new Date().toISOString(),
|
|
201
|
-
codeReview: { intentConsistency: { aligned: false, explanation: 'misses system paths' }, scopePrecision: { verdict: 'too_narrow' as const, explanation: 'narrow' }, traceCoverage: { sufficient: false, gaps: [], explanation: 'missing' } },
|
|
202
|
-
adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, toolName: 'write_file', params: { path: '/etc/shadow' }, expectedDecision: 'block' as const, rationale: 'system path' }],
|
|
203
|
-
adversarialResult: { passed: false, failedCases: [{ caseId: 'adv-1', attackType: 'boundary' as const, actualDecision: 'allow', expectedDecision: 'block', rationale: 'system path' }] },
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
let tmpDir = '';
|
|
210
|
-
|
|
211
|
-
function makeTmpDir(): string {
|
|
212
|
-
// mkdtempSync's random suffix keeps the path unpredictable (CodeQL
|
|
213
|
-
// js/insecure-temporary-file: the pipeline language tests WRITE
|
|
214
|
-
// .pd/config.yaml under this dir).
|
|
215
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pipe-'));
|
|
216
|
-
// PRI-661: the pipeline's evaluator replay resolves the production gate
|
|
217
|
-
// context from durable workspace provenance — seed a declaration like every
|
|
218
|
-
// real host does on startup.
|
|
219
|
-
saveHostToolDeclaration(dir, {
|
|
220
|
-
version: 1,
|
|
221
|
-
hostKind: 'testhost',
|
|
222
|
-
mappings: [
|
|
223
|
-
{ rawToolName: 'Write', canonicalKind: 'write' },
|
|
224
|
-
{ rawToolName: 'Edit', canonicalKind: 'write' },
|
|
225
|
-
{ rawToolName: 'Bash', canonicalKind: 'execute' },
|
|
226
|
-
],
|
|
227
|
-
declaredAt: new Date().toISOString(),
|
|
228
|
-
});
|
|
229
|
-
return dir;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function makeAdapter(opts?: { evaluator?: EvaluatorFactory }): ScriptedAdapter {
|
|
233
|
-
return new ScriptedAdapter({
|
|
234
|
-
dreamer: (taskId) => dreamerOut(taskId, 'pain-test-001'),
|
|
235
|
-
philosopher: philosopherOut,
|
|
236
|
-
scribe: scribeOut,
|
|
237
|
-
artificer: artificerV2,
|
|
238
|
-
evaluator: opts?.evaluator ?? evaluatorApproved,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Seed a dreamer task with sourcePainId embedded as a TOP-LEVEL key in
|
|
244
|
-
* diagnosticJson (outside the pi_metadata envelope). This mirrors the pattern
|
|
245
|
-
* from source-trace-locator.test.ts and PainSignalBridge.
|
|
246
|
-
*/
|
|
247
|
-
async function seedDreamerWithId(
|
|
248
|
-
sm: RuntimeStateManager,
|
|
249
|
-
taskId: string,
|
|
250
|
-
painId: string,
|
|
251
|
-
channel: 'prompt' | 'code_tool_hook' | 'defer_archive' = 'code_tool_hook',
|
|
252
|
-
candidateId?: string,
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
const baseMetadata = JSON.parse(createPITaskDiagnosticJson({
|
|
255
|
-
dependencyTaskIds: [], channel, timeoutMs: 1000, inputArtifactRefs: [], outputArtifactRefs: [],
|
|
256
|
-
})) as Record<string, unknown>;
|
|
257
|
-
const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId, ...(candidateId ? { candidateId } : {}) });
|
|
258
|
-
await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Seed a dreamer task with raw diagnosticJson (for missing/wrong-type tests). */
|
|
262
|
-
async function seedDreamerRaw(sm: RuntimeStateManager, taskId: string, diagnosticJson: string): Promise<void> {
|
|
263
|
-
await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match', () => {
|
|
269
|
-
afterEach(() => {
|
|
270
|
-
vi.restoreAllMocks();
|
|
271
|
-
if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ── Test 1: Capability ON + approved → candidate_ready_for_owner_review ──
|
|
275
|
-
it('capability ON + evaluator approved → candidate_ready_for_owner_review; scribe identity backfilled on the rule path too (EP002-R4 follow-up #2)', async () => {
|
|
276
|
-
tmpDir = makeTmpDir();
|
|
277
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
278
|
-
await sm.initialize();
|
|
279
|
-
await seedDreamerWithId(sm, 'dreamer-seeded-001', 'pain-test-001', 'code_tool_hook', 'cand-001');
|
|
280
|
-
await sm.close();
|
|
281
|
-
addPrincipleToLedger(path.join(tmpDir, '.state'), {
|
|
282
|
-
id: 'ledger-principle-001', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
|
|
283
|
-
status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
|
|
284
|
-
valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-001'],
|
|
285
|
-
ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const adapter = makeAdapter();
|
|
289
|
-
const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
|
|
290
|
-
const result = await runRuleHostPipeline({
|
|
291
|
-
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
292
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
293
|
-
codeRuleCapability: capability,
|
|
294
|
-
behaviorExamplePack: PIPE_PACK,
|
|
295
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
299
|
-
expect(result.stages.map((s) => s.name)).toEqual(['pain_lookup', 'dreamer', 'philosopher', 'scribe', 'adversarial_loop']);
|
|
300
|
-
expect(result.ruleArtifactId).not.toBeNull();
|
|
301
|
-
// P1 #1 fix: candidate should be auto-enqueued into the ApprovalQueue
|
|
302
|
-
expect(result.approvalId).not.toBeNull();
|
|
303
|
-
|
|
304
|
-
// EP002-R4 follow-up #2: the scribe artifact carries the LEDGER UUID (not
|
|
305
|
-
// a title) so the evaluator propagates it into the rule artifact and the
|
|
306
|
-
// Console groups the approval by an id the detail page can resolve.
|
|
307
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
308
|
-
await verify.initialize();
|
|
309
|
-
try {
|
|
310
|
-
const scribeArt = await verify.piArtifactStore.getArtifactById(
|
|
311
|
-
(await verify.piArtifactStore.listBySourceTaskId(result.stages.find((s) => s.name === 'scribe')!.taskId!))
|
|
312
|
-
.find((a) => a.artifactKind === 'principle')!.artifactId,
|
|
313
|
-
);
|
|
314
|
-
expect(scribeArt?.sourcePrincipleId).toBe('ledger-principle-001');
|
|
315
|
-
const ruleArt = await verify.piArtifactStore.getArtifactById(result.ruleArtifactId!);
|
|
316
|
-
expect(ruleArt?.sourcePrincipleId).toBe('ledger-principle-001');
|
|
317
|
-
} finally {
|
|
318
|
-
await verify.close();
|
|
319
|
-
}
|
|
320
|
-
}, 60_000);
|
|
321
|
-
|
|
322
|
-
it('adversarial feedback loop drives a second artificer round before creating a candidate', async () => {
|
|
323
|
-
tmpDir = makeTmpDir();
|
|
324
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
325
|
-
await sm.initialize();
|
|
326
|
-
await seedDreamerWithId(sm, 'dreamer-feedback-001', 'pain-feedback-001');
|
|
327
|
-
await sm.close();
|
|
328
|
-
|
|
329
|
-
let adapter: ScriptedAdapter;
|
|
330
|
-
let artificerCallCount = 0;
|
|
331
|
-
const artificerPrompts: string[] = [];
|
|
332
|
-
adapter = new ScriptedAdapter({
|
|
333
|
-
dreamer: (taskId) => dreamerOut(taskId, 'pain-feedback-001'),
|
|
334
|
-
philosopher: philosopherOut,
|
|
335
|
-
scribe: scribeOut,
|
|
336
|
-
artificer: (taskId, priorId) => {
|
|
337
|
-
artificerCallCount++;
|
|
338
|
-
const runId = `run-${taskId}`;
|
|
339
|
-
const inputPayload = adapter.startRunInputs.get(runId)?.inputPayload;
|
|
340
|
-
if (typeof inputPayload === 'string') artificerPrompts.push(inputPayload);
|
|
341
|
-
|
|
342
|
-
const base = artificerV2(taskId, priorId);
|
|
343
|
-
if (artificerCallCount === 1) {
|
|
344
|
-
// Round 1: code fails the adversarial replay, forcing needs_revision.
|
|
345
|
-
return {
|
|
346
|
-
...base,
|
|
347
|
-
implementationCode: 'function evaluate() { return { decision: "allow", matched: false, reason: "bug" }; }',
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
// Round 2: fixed code passes the evaluator.
|
|
351
|
-
return base;
|
|
352
|
-
},
|
|
353
|
-
evaluator: (taskId, artificerArtifactId) => {
|
|
354
|
-
if (artificerCallCount === 1) {
|
|
355
|
-
return evaluatorNeedsRevision(taskId, artificerArtifactId);
|
|
356
|
-
}
|
|
357
|
-
return evaluatorApproved(taskId, artificerArtifactId);
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const result = await runRuleHostPipeline({
|
|
362
|
-
workspaceDir: tmpDir,
|
|
363
|
-
painId: 'pain-feedback-001',
|
|
364
|
-
runtimeAdapter: adapter,
|
|
365
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
366
|
-
behaviorExamplePack: PIPE_PACK,
|
|
367
|
-
channel: 'code_tool_hook',
|
|
368
|
-
pollIntervalMs: 5,
|
|
369
|
-
timeoutMs: 1000,
|
|
370
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
expect(artificerCallCount).toBe(2);
|
|
374
|
-
expect(artificerPrompts).toHaveLength(2);
|
|
375
|
-
expect(artificerPrompts[1]).toContain('Prior adversarial replay failures');
|
|
376
|
-
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
377
|
-
expect(result.ruleArtifactId).toMatch(/^pi-rule-/);
|
|
378
|
-
// Owner Decision Experience v1 Phase A: this seed carries no candidateId and
|
|
379
|
-
// no ledger principle, so identity binding is unverified (candidate_unresolved)
|
|
380
|
-
// and the governance publication boundary refuses the approval enqueue — the
|
|
381
|
-
// two-round adversarial loop itself is unaffected.
|
|
382
|
-
expect(result.approvalId).toBeNull();
|
|
383
|
-
expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
|
|
384
|
-
}, 60_000);
|
|
385
|
-
|
|
386
|
-
// ── Test 2: Capability OFF (explicitly disabled) → text_principle_only ──
|
|
387
|
-
it('capability OFF (explicitly disabled) → text_principle_only + prompt approval enqueue + identity backfill (PRI-804)', async () => {
|
|
388
|
-
tmpDir = makeTmpDir();
|
|
389
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
390
|
-
await sm.initialize();
|
|
391
|
-
await seedDreamerWithId(sm, 'dreamer-seeded-002', 'pain-test-001', 'code_tool_hook', 'cand-002');
|
|
392
|
-
await sm.close();
|
|
393
|
-
// PRI-804(a): seed the ledger with the principle this chain's candidate maps
|
|
394
|
-
// to, mirroring what the intake bridge creates in production workspaces.
|
|
395
|
-
addPrincipleToLedger(path.join(tmpDir, '.state'), {
|
|
396
|
-
id: 'ledger-principle-002', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
|
|
397
|
-
status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
|
|
398
|
-
valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-002'],
|
|
399
|
-
ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
const adapter = makeAdapter();
|
|
403
|
-
const capability: CodeRuleCapability = { enabled: false, disabledReason: 'artificer agent disabled in config' };
|
|
404
|
-
const result = await runRuleHostPipeline({
|
|
405
|
-
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
406
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
407
|
-
codeRuleCapability: capability,
|
|
408
|
-
behaviorExamplePack: PIPE_PACK,
|
|
409
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(result.decision).toBe('text_principle_only');
|
|
413
|
-
expect(result.ruleArtifactId).toBeNull();
|
|
414
|
-
// Scribe ran, so a principle artifact should exist.
|
|
415
|
-
expect(result.principleArtifactId).not.toBeNull();
|
|
416
|
-
expect(result.degradationReason).toContain('code_rule_capability_off');
|
|
417
|
-
// Adversarial loop stage should be skipped (not present or skipped status).
|
|
418
|
-
const advStage = result.stages.find((s) => s.name === 'adversarial_loop');
|
|
419
|
-
expect(advStage?.status).toBe('skipped');
|
|
420
|
-
|
|
421
|
-
// PRI-804: the text principle must NOT dead-end — it is enqueued into the
|
|
422
|
-
// EXISTING approval queue on the prompt channel for Owner Console review.
|
|
423
|
-
expect(result.approvalId).not.toBeNull();
|
|
424
|
-
expect(result.approvalId).toBe(`apr_prompt_${result.principleArtifactId}`);
|
|
425
|
-
|
|
426
|
-
// Verify the persisted approval row, the PRI-804(a) identity backfill, and
|
|
427
|
-
// the terminal validation marking (real store, real SQLite, real ledger).
|
|
428
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
429
|
-
await verify.initialize();
|
|
430
|
-
try {
|
|
431
|
-
const approvalStore = new SqliteApprovalQueueStore(verify.connection);
|
|
432
|
-
const record = await approvalStore.getById(result.approvalId!);
|
|
433
|
-
expect(record).not.toBeNull();
|
|
434
|
-
expect(record!.status).toBe('pending');
|
|
435
|
-
expect(record!.channel).toBe('prompt');
|
|
436
|
-
expect(record!.artifactId).toBe(result.principleArtifactId);
|
|
437
|
-
const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
|
|
438
|
-
expect(artifact?.sourcePrincipleId).toBe('ledger-principle-002');
|
|
439
|
-
expect(artifact?.validationStatus).toBe('validated');
|
|
440
|
-
} finally {
|
|
441
|
-
await verify.close();
|
|
442
|
-
}
|
|
443
|
-
}, 60_000);
|
|
444
|
-
|
|
445
|
-
// ── Test 3: Capability ON + evaluator rejected → generation_rejected ──
|
|
446
|
-
it('capability ON + evaluator rejected → generation_rejected', async () => {
|
|
447
|
-
tmpDir = makeTmpDir();
|
|
448
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
449
|
-
await sm.initialize();
|
|
450
|
-
await seedDreamerWithId(sm, 'dreamer-seeded-003', 'pain-test-001');
|
|
451
|
-
await sm.close();
|
|
452
|
-
|
|
453
|
-
const adapter = makeAdapter({ evaluator: evaluatorRejected });
|
|
454
|
-
const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
|
|
455
|
-
const result = await runRuleHostPipeline({
|
|
456
|
-
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
457
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
458
|
-
codeRuleCapability: capability,
|
|
459
|
-
behaviorExamplePack: PIPE_PACK,
|
|
460
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
expect(result.decision).toBe('generation_rejected');
|
|
464
|
-
expect(result.ruleArtifactId).toBeNull();
|
|
465
|
-
}, 60_000);
|
|
466
|
-
|
|
467
|
-
// ── Test 4: Capability OFF by default (not provided) → text_principle_only ──
|
|
468
|
-
it('capability OFF by default (not provided) → text_principle_only', async () => {
|
|
469
|
-
tmpDir = makeTmpDir();
|
|
470
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
471
|
-
await sm.initialize();
|
|
472
|
-
await seedDreamerWithId(sm, 'dreamer-seeded-004', 'pain-test-001');
|
|
473
|
-
await sm.close();
|
|
474
|
-
|
|
475
|
-
const adapter = makeAdapter();
|
|
476
|
-
// No codeRuleCapability provided — defaults to OFF.
|
|
477
|
-
const result = await runRuleHostPipeline({
|
|
478
|
-
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
479
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
480
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
expect(result.decision).toBe('text_principle_only');
|
|
484
|
-
expect(result.ruleArtifactId).toBeNull();
|
|
485
|
-
expect(result.degradationReason).toContain('code_rule_capability');
|
|
486
|
-
}, 60_000);
|
|
487
|
-
|
|
488
|
-
// ── Test 5: Exact sourcePainId match — pain-1 must NOT match pain-10 ──
|
|
489
|
-
it('exact sourcePainId match: pain-1 does NOT match pain-10', async () => {
|
|
490
|
-
tmpDir = makeTmpDir();
|
|
491
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
492
|
-
await sm.initialize();
|
|
493
|
-
// Seed dreamer for pain-10 but query for pain-1 — must NOT match.
|
|
494
|
-
await seedDreamerWithId(sm, 'dreamer-seeded-pain-10', 'pain-10');
|
|
495
|
-
await sm.close();
|
|
496
|
-
|
|
497
|
-
const adapter = makeAdapter();
|
|
498
|
-
const result = await runRuleHostPipeline({
|
|
499
|
-
workspaceDir: tmpDir, painId: 'pain-1', runtimeAdapter: adapter,
|
|
500
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
501
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
expect(result.decision).toBe('generation_rejected');
|
|
505
|
-
expect(result.degradationReason).toContain('no_dreamer_task_seeded');
|
|
506
|
-
}, 60_000);
|
|
507
|
-
|
|
508
|
-
// ── Test 6: No dreamer task at all → generation_rejected ──
|
|
509
|
-
it('no dreamer task seeded for pain → generation_rejected', async () => {
|
|
510
|
-
tmpDir = makeTmpDir();
|
|
511
|
-
const adapter = makeAdapter();
|
|
512
|
-
const result = await runRuleHostPipeline({
|
|
513
|
-
workspaceDir: tmpDir, painId: 'pain-nonexistent', runtimeAdapter: adapter,
|
|
514
|
-
pollIntervalMs: 5, timeoutMs: 1000,
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
expect(result.decision).toBe('generation_rejected');
|
|
518
|
-
expect(result.degradationReason).toContain('no_dreamer_task_seeded');
|
|
519
|
-
expect(result.stages[0]!.name).toBe('pain_lookup');
|
|
520
|
-
expect(result.stages[0]!.status).toBe('failed');
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// ── Test 7: Missing or wrong-type sourcePainId → no match ──
|
|
524
|
-
it('missing or wrong-type sourcePainId in diagnosticJson → no match', async () => {
|
|
525
|
-
tmpDir = makeTmpDir();
|
|
526
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
527
|
-
await sm.initialize();
|
|
528
|
-
// Seed a dreamer task WITHOUT sourcePainId (only pi_metadata).
|
|
529
|
-
await seedDreamerRaw(sm, 'dreamer-no-painid-001', JSON.stringify({ pi_metadata: { channel: 'prompt' } }));
|
|
530
|
-
// Seed a dreamer task with WRONG-TYPE sourcePainId (number, not string).
|
|
531
|
-
await seedDreamerRaw(sm, 'dreamer-wrong-type-001', JSON.stringify({ pi_metadata: { channel: 'prompt' }, sourcePainId: 12345 }));
|
|
532
|
-
await sm.close();
|
|
533
|
-
|
|
534
|
-
const adapter = makeAdapter();
|
|
535
|
-
const result = await runRuleHostPipeline({
|
|
536
|
-
workspaceDir: tmpDir, painId: '12345', runtimeAdapter: adapter,
|
|
537
|
-
pollIntervalMs: 5, timeoutMs: 1000,
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
// Neither task should match — missing sourcePainId and wrong-type sourcePainId
|
|
541
|
-
// are both skipped (not silently matched).
|
|
542
|
-
expect(result.decision).toBe('generation_rejected');
|
|
543
|
-
expect(result.degradationReason).toContain('no_dreamer_task_seeded');
|
|
544
|
-
}, 60_000);
|
|
545
|
-
|
|
546
|
-
it('rejects ambiguous lineage when multiple runnable Dreamer tasks have the same sourcePainId', async () => {
|
|
547
|
-
tmpDir = makeTmpDir();
|
|
548
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
549
|
-
await sm.initialize();
|
|
550
|
-
await seedDreamerWithId(sm, 'dreamer-ambiguous-a', 'pain-ambiguous');
|
|
551
|
-
await seedDreamerWithId(sm, 'dreamer-ambiguous-b', 'pain-ambiguous');
|
|
552
|
-
await sm.close();
|
|
553
|
-
|
|
554
|
-
const result = await runRuleHostPipeline({
|
|
555
|
-
workspaceDir: tmpDir,
|
|
556
|
-
painId: 'pain-ambiguous',
|
|
557
|
-
runtimeAdapter: makeAdapter(),
|
|
558
|
-
pollIntervalMs: 5,
|
|
559
|
-
timeoutMs: 1000,
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
expect(result.decision).toBe('generation_rejected');
|
|
563
|
-
expect(result.degradationReason).toContain('ambiguous_dreamer_tasks_for_pain');
|
|
564
|
-
expect(result.stages[0]?.status).toBe('failed');
|
|
565
|
-
}, 60_000);
|
|
566
|
-
|
|
567
|
-
it('selects the Dreamer task matching the requested activation channel', async () => {
|
|
568
|
-
tmpDir = makeTmpDir();
|
|
569
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
570
|
-
await sm.initialize();
|
|
571
|
-
await seedDreamerWithId(sm, 'dreamer-prompt', 'pain-multi-channel', 'prompt');
|
|
572
|
-
await seedDreamerWithId(sm, 'dreamer-code-hook', 'pain-multi-channel', 'code_tool_hook');
|
|
573
|
-
await sm.close();
|
|
574
|
-
|
|
575
|
-
const adapter = makeAdapter();
|
|
576
|
-
const result = await runRuleHostPipeline({
|
|
577
|
-
workspaceDir: tmpDir,
|
|
578
|
-
painId: 'pain-multi-channel',
|
|
579
|
-
runtimeAdapter: adapter,
|
|
580
|
-
channel: 'code_tool_hook',
|
|
581
|
-
pollIntervalMs: 5,
|
|
582
|
-
timeoutMs: 1000,
|
|
583
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
expect(result.stages[0]).toMatchObject({
|
|
587
|
-
name: 'pain_lookup',
|
|
588
|
-
status: 'succeeded',
|
|
589
|
-
taskId: 'dreamer-code-hook',
|
|
590
|
-
});
|
|
591
|
-
}, 60_000);
|
|
592
|
-
|
|
593
|
-
it('uses the sole pain-linked Dreamer seed when its source channel differs from the target channel', async () => {
|
|
594
|
-
tmpDir = makeTmpDir();
|
|
595
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
596
|
-
await sm.initialize();
|
|
597
|
-
await seedDreamerWithId(sm, 'dreamer-prompt-seed', 'pain-cross-channel', 'prompt');
|
|
598
|
-
await sm.close();
|
|
599
|
-
|
|
600
|
-
const adapter = makeAdapter();
|
|
601
|
-
const result = await runRuleHostPipeline({
|
|
602
|
-
workspaceDir: tmpDir,
|
|
603
|
-
painId: 'pain-cross-channel',
|
|
604
|
-
runtimeAdapter: adapter,
|
|
605
|
-
channel: 'code_tool_hook',
|
|
606
|
-
pollIntervalMs: 5,
|
|
607
|
-
timeoutMs: 1000,
|
|
608
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
expect(result.stages[0]).toMatchObject({
|
|
612
|
-
name: 'pain_lookup',
|
|
613
|
-
status: 'succeeded',
|
|
614
|
-
taskId: 'dreamer-prompt-seed',
|
|
615
|
-
});
|
|
616
|
-
}, 60_000);
|
|
617
|
-
|
|
618
|
-
it('resumes from a succeeded Dreamer seed instead of trying to lease it again', async () => {
|
|
619
|
-
tmpDir = makeTmpDir();
|
|
620
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
621
|
-
await sm.initialize();
|
|
622
|
-
await seedDreamerWithId(sm, 'dreamer-resume', 'pain-resume');
|
|
623
|
-
await sm.close();
|
|
624
|
-
|
|
625
|
-
const firstAdapter = makeAdapter();
|
|
626
|
-
await runRuleHostPipeline({
|
|
627
|
-
workspaceDir: tmpDir, painId: 'pain-resume', runtimeAdapter: firstAdapter,
|
|
628
|
-
pollIntervalMs: 5, timeoutMs: 1000,
|
|
629
|
-
onStoreReady: (store) => { firstAdapter.artifactStore = store; },
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const resumedAdapter = makeAdapter();
|
|
633
|
-
const resumed = await runRuleHostPipeline({
|
|
634
|
-
workspaceDir: tmpDir, painId: 'pain-resume', runtimeAdapter: resumedAdapter,
|
|
635
|
-
pollIntervalMs: 5, timeoutMs: 1000,
|
|
636
|
-
onStoreReady: (store) => { resumedAdapter.artifactStore = store; },
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
expect(resumed.stages.slice(0, 2)).toEqual([
|
|
640
|
-
{ name: 'pain_lookup', status: 'succeeded', taskId: 'dreamer-resume' },
|
|
641
|
-
{ name: 'dreamer', taskId: 'dreamer-resume', status: 'succeeded' },
|
|
642
|
-
]);
|
|
643
|
-
expect(resumed.degradationReason).not.toContain('lease');
|
|
644
|
-
}, 60_000);
|
|
645
|
-
|
|
646
|
-
// ── Test 8 (E fix): retried status is NOT terminal — bounded retry succeeds ──
|
|
647
|
-
it('retried status triggers bounded retry and eventually succeeds (E fix)', async () => {
|
|
648
|
-
tmpDir = makeTmpDir();
|
|
649
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
650
|
-
await sm.initialize();
|
|
651
|
-
await seedDreamerWithId(sm, 'dreamer-retry-001', 'pain-retry-001');
|
|
652
|
-
await sm.close();
|
|
653
|
-
|
|
654
|
-
// Adapter that returns retried on first dreamer call, then succeeds.
|
|
655
|
-
let dreamerCallCount = 0;
|
|
656
|
-
const adapter = new ScriptedAdapter({
|
|
657
|
-
dreamer: (taskId) => {
|
|
658
|
-
dreamerCallCount++;
|
|
659
|
-
if (dreamerCallCount === 1) {
|
|
660
|
-
// First call: return a payload that will cause the runner to mark
|
|
661
|
-
// the task as retried. We simulate this by throwing — the base
|
|
662
|
-
// runner catches errors and may mark as retried.
|
|
663
|
-
throw new Error('transient LLM error');
|
|
664
|
-
}
|
|
665
|
-
return dreamerOut(taskId, 'pain-retry-001');
|
|
666
|
-
},
|
|
667
|
-
philosopher: philosopherOut,
|
|
668
|
-
scribe: scribeOut,
|
|
669
|
-
artificer: artificerV2,
|
|
670
|
-
evaluator: evaluatorApproved,
|
|
671
|
-
});
|
|
672
|
-
const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
|
|
673
|
-
|
|
674
|
-
const result = await runRuleHostPipeline({
|
|
675
|
-
workspaceDir: tmpDir, painId: 'pain-retry-001', runtimeAdapter: adapter,
|
|
676
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
677
|
-
maxStageRetries: 2,
|
|
678
|
-
codeRuleCapability: capability,
|
|
679
|
-
behaviorExamplePack: PIPE_PACK,
|
|
680
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// The retry contract is exact: one transient failure, one fresh successful
|
|
684
|
-
// attempt, then the full pipeline succeeds.
|
|
685
|
-
const dreamerStage = result.stages.find((s) => s.name === 'dreamer');
|
|
686
|
-
expect(dreamerCallCount).toBe(2);
|
|
687
|
-
expect(dreamerStage?.status).toBe('succeeded');
|
|
688
|
-
expect(result.decision).toBe('candidate_ready_for_owner_review');
|
|
689
|
-
// Owner Decision Experience v1 Phase A: this seed carries NO candidateId,
|
|
690
|
-
// so the chain cannot bind a ledger principle (candidate_unresolved). The
|
|
691
|
-
// rule artifact is produced and preserved, but the governance publication
|
|
692
|
-
// boundary REFUSES to create an Owner approval subject for it.
|
|
693
|
-
expect(result.approvalId).toBeNull();
|
|
694
|
-
expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
|
|
695
|
-
expect(result.degradationReason ?? '').toContain('candidate_unresolved');
|
|
696
|
-
expect(result.ruleArtifactId).not.toBeNull();
|
|
697
|
-
}, 60_000);
|
|
698
|
-
|
|
699
|
-
// ── Test 9 (E fix): retried status exhausted → stage marked 'degraded' ──
|
|
700
|
-
it('retried status exhausted marks stage as degraded, not failed (E fix)', async () => {
|
|
701
|
-
tmpDir = makeTmpDir();
|
|
702
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
703
|
-
await sm.initialize();
|
|
704
|
-
await seedDreamerWithId(sm, 'dreamer-exhaust-001', 'pain-exhaust-001');
|
|
705
|
-
await sm.close();
|
|
706
|
-
|
|
707
|
-
// Adapter that always throws on dreamer — simulating persistent transient
|
|
708
|
-
// errors. The runner will mark the task as retried each time until
|
|
709
|
-
// maxStageRetries is exhausted.
|
|
710
|
-
const adapter = new ScriptedAdapter({
|
|
711
|
-
dreamer: () => { throw new Error('persistent transient error'); },
|
|
712
|
-
philosopher: philosopherOut,
|
|
713
|
-
scribe: scribeOut,
|
|
714
|
-
artificer: artificerV2,
|
|
715
|
-
evaluator: evaluatorApproved,
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
const result = await runRuleHostPipeline({
|
|
719
|
-
workspaceDir: tmpDir, painId: 'pain-exhaust-001', runtimeAdapter: adapter,
|
|
720
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
721
|
-
maxStageRetries: 1, // Only 1 retry to keep the test fast
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
// Pipeline must reject — dreamer never succeeded.
|
|
725
|
-
expect(result.decision).toBe('generation_rejected');
|
|
726
|
-
expect(result.degradationReason).toContain('dreamer_failed');
|
|
727
|
-
// The stage must be precisely degraded after the initial attempt + one retry.
|
|
728
|
-
const dreamerStage = result.stages.find((s) => s.name === 'dreamer');
|
|
729
|
-
expect(adapter.startRunCalls.filter((call) => call.taskId === 'dreamer-exhaust-001')).toHaveLength(2);
|
|
730
|
-
expect(dreamerStage?.status).toBe('degraded');
|
|
731
|
-
// The reason must be present (Runtime Contract Rule 9: no silent degradation).
|
|
732
|
-
expect(dreamerStage?.reason).toBeTruthy();
|
|
733
|
-
}, 60_000);
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
// ── PRI-714 (review fix #3): language wiring ─────────────────────────────────
|
|
737
|
-
//
|
|
738
|
-
// The PR's original fault class was "the parameter exists but is not wired".
|
|
739
|
-
// These regressions drive the REAL pipeline and assert the LANGUAGE DIRECTIVE
|
|
740
|
-
// on the actual systemPrompt handed to adapter.startRun (PRI-633: the
|
|
741
|
-
// instruction left the payload for the system channel):
|
|
742
|
-
// - explicit `principles.outputLanguage: 'en'` in the workspace config
|
|
743
|
-
// overrides the zh-CN default;
|
|
744
|
-
// - no config → the zh-CN default applies.
|
|
745
|
-
// Deleting the pipeline's language resolution makes these fail.
|
|
746
|
-
|
|
747
|
-
function writeLanguageConfig(dir: string, outputLanguage?: 'en'): void {
|
|
748
|
-
fs.mkdirSync(path.join(dir, '.pd'), { recursive: true });
|
|
749
|
-
// Full valid-config shape (mirrors pd-config-loader.test.ts) so validation
|
|
750
|
-
// keeps `principles` intact instead of degrading to defaults.
|
|
751
|
-
const config: Record<string, unknown> = {
|
|
752
|
-
version: 1,
|
|
753
|
-
features: {
|
|
754
|
-
prompt: { category: 'core', enabled: true },
|
|
755
|
-
code_tool_hook: { category: 'core', enabled: true },
|
|
756
|
-
defer_archive: { category: 'core', enabled: true },
|
|
757
|
-
correction_observer: { category: 'quiet', enabled: false },
|
|
758
|
-
gfi: { category: 'quiet', enabled: false },
|
|
759
|
-
},
|
|
760
|
-
runtimeProfiles: {
|
|
761
|
-
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
762
|
-
},
|
|
763
|
-
internalAgents: {
|
|
764
|
-
defaultRuntime: 'openclaw.default',
|
|
765
|
-
agents: {
|
|
766
|
-
diagnostician: { enabled: true, runtimeProfile: 'openclaw.default' },
|
|
767
|
-
dreamer: { enabled: true },
|
|
768
|
-
scribe: { enabled: true },
|
|
769
|
-
artificer: { enabled: true },
|
|
770
|
-
philosopher: { enabled: false },
|
|
771
|
-
evaluator: { enabled: false },
|
|
772
|
-
rolloutReviewer: { enabled: false },
|
|
773
|
-
correctionObserver: { enabled: false },
|
|
774
|
-
},
|
|
775
|
-
},
|
|
776
|
-
ui: { diagnostics: { mode: 'simple' } },
|
|
777
|
-
};
|
|
778
|
-
if (outputLanguage !== undefined) config.principles = { outputLanguage };
|
|
779
|
-
// JSON is valid YAML — same trick the shared executor tests use.
|
|
780
|
-
fs.writeFileSync(path.join(dir, '.pd', 'config.yaml'), JSON.stringify(config));
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function collectStartRunInputs(adapter: ScriptedAdapter): Array<{ taskId: string; outputSchemaRef?: string; payload: string; systemPrompt?: string }> {
|
|
784
|
-
const inputs: Array<{ taskId: string; outputSchemaRef?: string; payload: string; systemPrompt?: string }> = [];
|
|
785
|
-
for (const input of adapter.startRunInputs.values()) {
|
|
786
|
-
inputs.push({
|
|
787
|
-
taskId: input.taskRef.taskId,
|
|
788
|
-
outputSchemaRef: input.outputSchemaRef,
|
|
789
|
-
payload: typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload),
|
|
790
|
-
systemPrompt: input.systemPrompt,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
return inputs;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
describe('runRuleHostPipeline — outputLanguage reaches adapter.startRun messages (PRI-714 review fix)', () => {
|
|
797
|
-
afterEach(() => {
|
|
798
|
-
vi.restoreAllMocks();
|
|
799
|
-
if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
it('explicit principles.outputLanguage=en overrides the zh-CN default on stage messages', async () => {
|
|
803
|
-
tmpDir = makeTmpDir();
|
|
804
|
-
writeLanguageConfig(tmpDir, 'en');
|
|
805
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
806
|
-
await sm.initialize();
|
|
807
|
-
await seedDreamerWithId(sm, 'dreamer-lang-en-001', 'pain-lang-en');
|
|
808
|
-
await sm.close();
|
|
809
|
-
|
|
810
|
-
const adapter = makeAdapter();
|
|
811
|
-
const result = await runRuleHostPipeline({
|
|
812
|
-
workspaceDir: tmpDir, painId: 'pain-lang-en', runtimeAdapter: adapter,
|
|
813
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
814
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
815
|
-
behaviorExamplePack: PIPE_PACK,
|
|
816
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
817
|
-
});
|
|
818
|
-
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
819
|
-
|
|
820
|
-
const inputs = collectStartRunInputs(adapter);
|
|
821
|
-
// Dreamer: dreamer-subject field list, English directive on the system channel.
|
|
822
|
-
const dreamer = inputs.find((i) => i.taskId.startsWith('dreamer'));
|
|
823
|
-
expect(dreamer).toBeDefined();
|
|
824
|
-
expect(dreamer?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
|
|
825
|
-
expect(dreamer?.systemPrompt).toContain('English');
|
|
826
|
-
expect(dreamer?.systemPrompt).toContain('(candidates[].badDecision, candidates[].betterDecision, candidates[].rationale, candidates[].strategicPerspective)');
|
|
827
|
-
expect(dreamer?.payload).toContain('dreamer-lang-en-001');
|
|
828
|
-
expect(dreamer?.payload).not.toContain('LANGUAGE DIRECTIVE');
|
|
829
|
-
// Philosopher: philosopher-subject field list, English directive.
|
|
830
|
-
const philosopher = inputs.find((i) => i.taskId.includes('philosopher'));
|
|
831
|
-
expect(philosopher).toBeDefined();
|
|
832
|
-
expect(philosopher?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
|
|
833
|
-
expect(philosopher?.systemPrompt).toContain('English');
|
|
834
|
-
expect(philosopher?.systemPrompt).toContain('(thesis, principleCandidate.title, principleCandidate.rationale, principleCandidate.scope, risks[])');
|
|
835
|
-
// Evaluator: review-subject explicit nested paths + PRI-630 ledger echo rule.
|
|
836
|
-
const evaluator = inputs.find((i) => i.outputSchemaRef === 'evaluator-output-v1');
|
|
837
|
-
expect(evaluator).toBeDefined();
|
|
838
|
-
expect(evaluator?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
|
|
839
|
-
expect(evaluator?.systemPrompt).toContain('English');
|
|
840
|
-
expect(evaluator?.systemPrompt).toContain('codeReview.traceCoverage.gaps');
|
|
841
|
-
expect(evaluator?.systemPrompt).toContain('adversarialCases[].rationale');
|
|
842
|
-
expect(evaluator?.systemPrompt).toContain('requirementLedger[].statement MUST be copied verbatim');
|
|
843
|
-
// The explicit en must have fully replaced the zh-CN default everywhere.
|
|
844
|
-
for (const i of inputs) {
|
|
845
|
-
expect(i.systemPrompt ?? '', 'Simplified Chinese leaked into an en-configured pipeline').not.toContain('Simplified Chinese');
|
|
846
|
-
expect(i.payload, 'Simplified Chinese leaked into an en-configured pipeline').not.toContain('Simplified Chinese');
|
|
847
|
-
}
|
|
848
|
-
}, 60_000);
|
|
849
|
-
|
|
850
|
-
it('no config file resolves the zh-CN default on the dreamer message', async () => {
|
|
851
|
-
tmpDir = makeTmpDir();
|
|
852
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
853
|
-
await sm.initialize();
|
|
854
|
-
await seedDreamerWithId(sm, 'dreamer-lang-zh-001', 'pain-lang-zh');
|
|
855
|
-
await sm.close();
|
|
856
|
-
|
|
857
|
-
const adapter = makeAdapter();
|
|
858
|
-
const result = await runRuleHostPipeline({
|
|
859
|
-
workspaceDir: tmpDir, painId: 'pain-lang-zh', runtimeAdapter: adapter,
|
|
860
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
861
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
862
|
-
behaviorExamplePack: PIPE_PACK,
|
|
863
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
864
|
-
});
|
|
865
|
-
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
866
|
-
|
|
867
|
-
const inputs = collectStartRunInputs(adapter);
|
|
868
|
-
const dreamer = inputs.find((i) => i.taskId.startsWith('dreamer'));
|
|
869
|
-
expect(dreamer).toBeDefined();
|
|
870
|
-
expect(dreamer?.systemPrompt).toContain('LANGUAGE DIRECTIVE');
|
|
871
|
-
expect(dreamer?.systemPrompt).toContain('Simplified Chinese');
|
|
872
|
-
expect(dreamer?.payload).toContain('dreamer-lang-zh-001');
|
|
873
|
-
}, 60_000);
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// ── Owner Decision Experience v1 Phase A — identity fix-forward (SPEC §9) ────
|
|
877
|
-
//
|
|
878
|
-
// §9.4 deterministic acceptance slice, exercised against the REAL production
|
|
879
|
-
// path (real RuntimeStateManager, real SQLite, real ledger adapter):
|
|
880
|
-
// success / ledger missing / ambiguous / conflict / write failure / replay.
|
|
881
|
-
// Failure slices must prove: NO approval enqueue, NO activation, NO duplicate
|
|
882
|
-
// ledger — while the generated artifacts stay preserved for later replay.
|
|
883
|
-
|
|
884
|
-
describe('runRuleHostPipeline — Phase A governance publication boundary (identity fix-forward)', () => {
|
|
885
|
-
afterEach(() => {
|
|
886
|
-
vi.restoreAllMocks();
|
|
887
|
-
if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
function seedLedger(dir: string, id: string, candidateId: string): void {
|
|
891
|
-
addPrincipleToLedger(path.join(dir, '.state'), {
|
|
892
|
-
id, version: 1, text: `principle ${id}`, triggerPattern: '', action: '',
|
|
893
|
-
status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
|
|
894
|
-
valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: [candidateId],
|
|
895
|
-
ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async function countApprovalsForArtifact(dir: string, artifactId: string): Promise<number> {
|
|
900
|
-
const verify = new RuntimeStateManager({ workspaceDir: dir });
|
|
901
|
-
await verify.initialize();
|
|
902
|
-
try {
|
|
903
|
-
const rows = verify.connection.getDb()
|
|
904
|
-
.prepare('SELECT COUNT(*) AS n FROM approvals WHERE artifact_id = ?').all(artifactId);
|
|
905
|
-
const first = rows[0] as { n: number } | undefined;
|
|
906
|
-
return first?.n ?? 0;
|
|
907
|
-
} finally {
|
|
908
|
-
await verify.close();
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
async function countActivations(dir: string): Promise<number> {
|
|
913
|
-
const verify = new RuntimeStateManager({ workspaceDir: dir });
|
|
914
|
-
await verify.initialize();
|
|
915
|
-
try {
|
|
916
|
-
const rows = verify.connection.getDb().prepare('SELECT COUNT(*) AS n FROM activations').all();
|
|
917
|
-
const first = rows[0] as { n: number } | undefined;
|
|
918
|
-
return first?.n ?? 0;
|
|
919
|
-
} finally {
|
|
920
|
-
await verify.close();
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
async function ledgerPrincipleCount(dir: string): Promise<number> {
|
|
925
|
-
const ledgerPath = path.join(dir, '.state', 'principle_training_state.json');
|
|
926
|
-
if (!fs.existsSync(ledgerPath)) return 0; // no ledger file yet = zero principles
|
|
927
|
-
const parsed = JSON.parse(fs.readFileSync(ledgerPath, 'utf8')) as { _tree?: { principles?: Record<string, unknown> } };
|
|
928
|
-
return Object.keys(parsed._tree?.principles ?? {}).length;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
it('ledger_missing: rule artifact preserved, but NO approval subject, NO activation, ledger untouched', async () => {
|
|
932
|
-
tmpDir = makeTmpDir();
|
|
933
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
934
|
-
await sm.initialize();
|
|
935
|
-
await seedDreamerWithId(sm, 'dreamer-id-missing', 'pain-id-missing', 'code_tool_hook', 'cand-no-ledger');
|
|
936
|
-
await sm.close();
|
|
937
|
-
// Deliberately NO ledger principle for cand-no-ledger.
|
|
938
|
-
|
|
939
|
-
const adapter = makeAdapter();
|
|
940
|
-
const result = await runRuleHostPipeline({
|
|
941
|
-
workspaceDir: tmpDir, painId: 'pain-id-missing', runtimeAdapter: adapter,
|
|
942
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
943
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
944
|
-
behaviorExamplePack: PIPE_PACK,
|
|
945
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
949
|
-
// The generation chain produced and PRESERVED its artifacts…
|
|
950
|
-
expect(result.ruleArtifactId).not.toBeNull();
|
|
951
|
-
// …but the publication boundary refused to create an Owner approval subject.
|
|
952
|
-
expect(result.approvalId).toBeNull();
|
|
953
|
-
expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
|
|
954
|
-
expect(result.degradationReason ?? '').toContain('ledger_missing');
|
|
955
|
-
expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
|
|
956
|
-
expect(await countActivations(tmpDir)).toBe(0);
|
|
957
|
-
expect(await ledgerPrincipleCount(tmpDir)).toBe(0);
|
|
958
|
-
}, 60_000);
|
|
959
|
-
|
|
960
|
-
it('ledger_ambiguous: two ledger targets for one candidate refuse publication and refuse a silent first-match binding', async () => {
|
|
961
|
-
tmpDir = makeTmpDir();
|
|
962
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
963
|
-
await sm.initialize();
|
|
964
|
-
await seedDreamerWithId(sm, 'dreamer-id-ambiguous', 'pain-id-ambiguous', 'code_tool_hook', 'cand-ambiguous');
|
|
965
|
-
await sm.close();
|
|
966
|
-
seedLedger(tmpDir, 'ledger-A', 'cand-ambiguous');
|
|
967
|
-
seedLedger(tmpDir, 'ledger-B', 'cand-ambiguous');
|
|
968
|
-
|
|
969
|
-
const adapter = makeAdapter();
|
|
970
|
-
const result = await runRuleHostPipeline({
|
|
971
|
-
workspaceDir: tmpDir, painId: 'pain-id-ambiguous', runtimeAdapter: adapter,
|
|
972
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
973
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
974
|
-
behaviorExamplePack: PIPE_PACK,
|
|
975
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
expect(result.decision).toBe('candidate_ready_for_owner_review');
|
|
979
|
-
expect(result.approvalId).toBeNull();
|
|
980
|
-
expect(result.degradationReason ?? '').toContain('ledger_ambiguous');
|
|
981
|
-
expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
|
|
982
|
-
// The scribe artifact must NOT be silently bound to the first match.
|
|
983
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
984
|
-
await verify.initialize();
|
|
985
|
-
try {
|
|
986
|
-
const scribeTaskId = result.stages.find((s) => s.name === 'scribe')!.taskId!;
|
|
987
|
-
const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
|
|
988
|
-
expect(scribeArt?.sourcePrincipleId).toBeUndefined();
|
|
989
|
-
} finally {
|
|
990
|
-
await verify.close();
|
|
991
|
-
}
|
|
992
|
-
}, 60_000);
|
|
993
|
-
|
|
994
|
-
it('identity_conflict: an existing binding to a different ledger target is never overwritten (§9.1.7)', async () => {
|
|
995
|
-
tmpDir = makeTmpDir();
|
|
996
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
997
|
-
await sm.initialize();
|
|
998
|
-
await seedDreamerWithId(sm, 'dreamer-id-conflict', 'pain-id-conflict', 'code_tool_hook', 'cand-conflict');
|
|
999
|
-
await sm.close();
|
|
1000
|
-
seedLedger(tmpDir, 'ledger-original', 'cand-conflict');
|
|
1001
|
-
|
|
1002
|
-
const adapter = makeAdapter();
|
|
1003
|
-
const first = await runRuleHostPipeline({
|
|
1004
|
-
workspaceDir: tmpDir, painId: 'pain-id-conflict', runtimeAdapter: adapter,
|
|
1005
|
-
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
1006
|
-
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
1007
|
-
behaviorExamplePack: PIPE_PACK,
|
|
1008
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
1009
|
-
});
|
|
1010
|
-
expect(first.approvalId).not.toBeNull();
|
|
1011
|
-
|
|
1012
|
-
// Repoint the candidate: detach it from the original principle so the
|
|
1013
|
-
// chain now resolves to a DIFFERENT (still unique) target. Otherwise the
|
|
1014
|
-
// plural lookup would correctly report ledger_ambiguous, not a conflict.
|
|
1015
|
-
updatePrinciple(path.join(tmpDir, '.state'), 'ledger-original', { derivedFromPainIds: [] });
|
|
1016
|
-
seedLedger(tmpDir, 'ledger-replacement', 'cand-conflict');
|
|
1017
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1018
|
-
await verify.initialize();
|
|
1019
|
-
try {
|
|
1020
|
-
const scribeTaskId = first.stages.find((s) => s.name === 'scribe')!.taskId!;
|
|
1021
|
-
const binding = await backfillScribeIdentity({
|
|
1022
|
-
stateManager: verify,
|
|
1023
|
-
artifactStore: verify.piArtifactStore,
|
|
1024
|
-
scribeTaskId,
|
|
1025
|
-
dreamerTaskId: 'dreamer-id-conflict',
|
|
1026
|
-
workspaceDir: tmpDir,
|
|
1027
|
-
now: new Date().toISOString(),
|
|
1028
|
-
});
|
|
1029
|
-
expect(binding).toEqual({ status: 'identity_conflict', existingPrincipleId: 'ledger-original', resolvedPrincipleId: 'ledger-replacement' });
|
|
1030
|
-
const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
|
|
1031
|
-
expect(scribeArt?.sourcePrincipleId).toBe('ledger-original');
|
|
1032
|
-
} finally {
|
|
1033
|
-
await verify.close();
|
|
1034
|
-
}
|
|
1035
|
-
}, 60_000);
|
|
1036
|
-
|
|
1037
|
-
it('binding_write_failed / binding_readback_failed: store failures are structured, never silently accepted', async () => {
|
|
1038
|
-
tmpDir = makeTmpDir();
|
|
1039
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1040
|
-
await sm.initialize();
|
|
1041
|
-
await seedDreamerWithId(sm, 'dreamer-id-wf', 'pain-id-wf', 'code_tool_hook', 'cand-wf');
|
|
1042
|
-
await sm.close();
|
|
1043
|
-
seedLedger(tmpDir, 'ledger-wf', 'cand-wf');
|
|
1044
|
-
|
|
1045
|
-
// Run once through the real path so a REAL scribe artifact exists.
|
|
1046
|
-
const adapter = makeAdapter();
|
|
1047
|
-
const run = await runRuleHostPipeline({
|
|
1048
|
-
workspaceDir: tmpDir, painId: 'pain-id-wf', runtimeAdapter: adapter,
|
|
1049
|
-
channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
|
|
1050
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
1051
|
-
});
|
|
1052
|
-
expect(run.decision).toBe('text_principle_only');
|
|
1053
|
-
|
|
1054
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1055
|
-
await verify.initialize();
|
|
1056
|
-
const scribeTaskId = run.stages.find((s) => s.name === 'scribe')!.taskId!;
|
|
1057
|
-
try {
|
|
1058
|
-
// The real run already bound + verified — replay must be idempotent.
|
|
1059
|
-
const idempotent = await backfillScribeIdentity({
|
|
1060
|
-
stateManager: verify, artifactStore: verify.piArtifactStore, scribeTaskId,
|
|
1061
|
-
dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
|
|
1062
|
-
});
|
|
1063
|
-
expect(idempotent).toEqual({ status: 'already_bound', principleId: 'ledger-wf' });
|
|
1064
|
-
|
|
1065
|
-
// Write failure: upsert throws.
|
|
1066
|
-
const throwingStore = new Proxy(verify.piArtifactStore, {
|
|
1067
|
-
get(target, prop, receiver) {
|
|
1068
|
-
if (prop === 'upsertArtifact') {
|
|
1069
|
-
return async () => { throw new Error('sqlite locked (simulated)'); };
|
|
1070
|
-
}
|
|
1071
|
-
return Reflect.get(target, prop, receiver);
|
|
1072
|
-
},
|
|
1073
|
-
});
|
|
1074
|
-
// For the write-failure slice we need an UNBOUND artifact: use a second,
|
|
1075
|
-
// unbound scribe-shaped artifact created directly in the store.
|
|
1076
|
-
const taskId2 = 'scribe-unbound-task';
|
|
1077
|
-
await verify.createTask({ taskId: taskId2, taskKind: 'scribe', status: 'succeeded', attemptCount: 0, maxAttempts: 3, diagnosticJson: JSON.stringify({ pi_metadata: { channel: 'prompt', dependencyTaskIds: [], timeoutMs: 1000 } }) });
|
|
1078
|
-
await verify.piArtifactStore.createArtifact({
|
|
1079
|
-
artifactId: 'pi-art-unbound-1', artifactKind: 'principle', sourceTaskId: taskId2,
|
|
1080
|
-
lineageArtifactIds: [], validationStatus: 'pending', contentJson: '{}',
|
|
1081
|
-
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
1082
|
-
});
|
|
1083
|
-
const failed = await backfillScribeIdentity({
|
|
1084
|
-
stateManager: verify, artifactStore: throwingStore, scribeTaskId: taskId2,
|
|
1085
|
-
dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
|
|
1086
|
-
});
|
|
1087
|
-
expect(failed.status).toBe('binding_write_failed');
|
|
1088
|
-
|
|
1089
|
-
// Read-back failure: upsert "succeeds" but the store returns a row without
|
|
1090
|
-
// the binding (write lost / wrong row).
|
|
1091
|
-
const staleStore = new Proxy(verify.piArtifactStore, {
|
|
1092
|
-
get(target, prop, receiver) {
|
|
1093
|
-
if (prop === 'getArtifactById') {
|
|
1094
|
-
return async () => null;
|
|
1095
|
-
}
|
|
1096
|
-
return Reflect.get(target, prop, receiver);
|
|
1097
|
-
},
|
|
1098
|
-
});
|
|
1099
|
-
const stale = await backfillScribeIdentity({
|
|
1100
|
-
stateManager: verify, artifactStore: staleStore, scribeTaskId: taskId2,
|
|
1101
|
-
dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
|
|
1102
|
-
});
|
|
1103
|
-
expect(stale.status).toBe('binding_readback_failed');
|
|
1104
|
-
} finally {
|
|
1105
|
-
await verify.close();
|
|
1106
|
-
}
|
|
1107
|
-
}, 60_000);
|
|
1108
|
-
|
|
1109
|
-
it('text path: unverified binding preserves the principle artifact but creates NO approval (candidate_unresolved)', async () => {
|
|
1110
|
-
tmpDir = makeTmpDir();
|
|
1111
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1112
|
-
await sm.initialize();
|
|
1113
|
-
// No candidateId on the seed — candidate_unresolved.
|
|
1114
|
-
await seedDreamerWithId(sm, 'dreamer-text-noid', 'pain-text-noid', 'prompt');
|
|
1115
|
-
await sm.close();
|
|
1116
|
-
|
|
1117
|
-
const adapter = makeAdapter();
|
|
1118
|
-
const result = await runRuleHostPipeline({
|
|
1119
|
-
workspaceDir: tmpDir, painId: 'pain-text-noid', runtimeAdapter: adapter,
|
|
1120
|
-
channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
|
|
1121
|
-
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
expect(result.decision).toBe('text_principle_only');
|
|
1125
|
-
expect(result.principleArtifactId).not.toBeNull();
|
|
1126
|
-
expect(result.approvalId).toBeNull();
|
|
1127
|
-
expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
|
|
1128
|
-
expect(result.degradationReason ?? '').toContain('candidate_unresolved');
|
|
1129
|
-
expect(await countApprovalsForArtifact(tmpDir, result.principleArtifactId!)).toBe(0);
|
|
1130
|
-
// The artifact is still marked validated — preserved, publishable later.
|
|
1131
|
-
const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1132
|
-
await verify.initialize();
|
|
1133
|
-
try {
|
|
1134
|
-
const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
|
|
1135
|
-
expect(artifact?.validationStatus).toBe('validated');
|
|
1136
|
-
expect(artifact?.sourcePrincipleId).toBeUndefined();
|
|
1137
|
-
} finally {
|
|
1138
|
-
await verify.close();
|
|
1139
|
-
}
|
|
1140
|
-
}, 60_000);
|
|
1141
|
-
|
|
1142
|
-
it('replay after identity repair publishes: seed ledger mid-flight is unnecessary — repair-then-rerun enqueues (§9.1.8)', async () => {
|
|
1143
|
-
tmpDir = makeTmpDir();
|
|
1144
|
-
const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
|
|
1145
|
-
await sm.initialize();
|
|
1146
|
-
await seedDreamerWithId(sm, 'dreamer-repair', 'pain-repair', 'prompt', 'cand-repair');
|
|
1147
|
-
await sm.close();
|
|
1148
|
-
|
|
1149
|
-
const adapter1 = makeAdapter();
|
|
1150
|
-
const refused = await runRuleHostPipeline({
|
|
1151
|
-
workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter1,
|
|
1152
|
-
channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
|
|
1153
|
-
onStoreReady: (store) => { adapter1.artifactStore = store; },
|
|
1154
|
-
});
|
|
1155
|
-
expect(refused.approvalId).toBeNull();
|
|
1156
|
-
|
|
1157
|
-
// Identity repair: the ledger entry for the candidate appears (e.g. intake
|
|
1158
|
-
// completed or the ledger was restored). Replay publishes.
|
|
1159
|
-
seedLedger(tmpDir, 'ledger-repaired', 'cand-repair');
|
|
1160
|
-
const adapter2 = makeAdapter();
|
|
1161
|
-
// Run 2 RESUMES run 1's succeeded dreamer seed (no new dreamer startRun) —
|
|
1162
|
-
// pre-register that call so the scripted philosopher can resolve its
|
|
1163
|
-
// sourceDreamerArtifactId lineage from the shared store.
|
|
1164
|
-
adapter2.startRunCalls.push({ taskId: 'dreamer-repair' });
|
|
1165
|
-
const published = await runRuleHostPipeline({
|
|
1166
|
-
workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter2,
|
|
1167
|
-
channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
|
|
1168
|
-
onStoreReady: (store) => { adapter2.artifactStore = store; },
|
|
1169
|
-
});
|
|
1170
|
-
expect(published.approvalId, JSON.stringify(published, null, 1)).not.toBeNull();
|
|
1171
|
-
expect(published.principleArtifactId).not.toBe(refused.principleArtifactId);
|
|
1172
|
-
// Exactly one ledger principle exists — the pipeline never creates one.
|
|
1173
|
-
expect(await ledgerPrincipleCount(tmpDir)).toBe(1);
|
|
1174
|
-
}, 60_000);
|
|
1175
|
-
});
|