@principles/pd-cli 1.133.0 → 1.135.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/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +42 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/console-launcher.d.ts +6 -0
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +41 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/console-launcher.ts +6 -0
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +229 -32
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -45,6 +45,11 @@ import {
|
|
|
45
45
|
DEFAULT_MAX_ROUNDS,
|
|
46
46
|
SqliteApprovalQueueStore,
|
|
47
47
|
getChannelRiskLevel,
|
|
48
|
+
// PRI-510: feature-flag resolvers imported from the core barrel (EP-02:
|
|
49
|
+
// pd-config-loader.ts only re-exports loadPdConfig + computeFlagsFromLoadResult;
|
|
50
|
+
// computeFeatureFlagsFromConfig / isFeatureEnabled live in core).
|
|
51
|
+
computeFeatureFlagsFromConfig,
|
|
52
|
+
isFeatureEnabled,
|
|
48
53
|
} from '@principles/core/runtime-v2';
|
|
49
54
|
import type {
|
|
50
55
|
AdversarialLoopResult,
|
|
@@ -53,10 +58,30 @@ import type {
|
|
|
53
58
|
RefinerRuleHostGateDeps,
|
|
54
59
|
PIArtifactStore,
|
|
55
60
|
ApprovalRecord,
|
|
61
|
+
BehaviorExamplePack,
|
|
62
|
+
// PRI-510: type-only import for the repair-loop deps contract (EP-02 wiring).
|
|
63
|
+
// RuntimeStateManager / StoreEventEmitter are already imported as values
|
|
64
|
+
// above; EvaluatorValidator is type-only.
|
|
65
|
+
EvaluatorRunnerDeps,
|
|
66
|
+
SeedArtificerRepairParams,
|
|
67
|
+
EvaluatorValidator,
|
|
56
68
|
} from '@principles/core/runtime-v2';
|
|
69
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
70
|
+
import { loadPdConfig } from './pd-config-loader.js';
|
|
57
71
|
/* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after main, matching codebase convention */
|
|
58
72
|
import { compileDemoRule } from './demo-rule-compiler.js';
|
|
59
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Layer 0 content-hash function for the internalization progressive
|
|
76
|
+
* disclosure (design §6.1). Core never imports `node:crypto`, so the plugin/CLI
|
|
77
|
+
* layer injects the algorithm. Consumed only when the
|
|
78
|
+
* `artifact_summary_redundancy` flag is on; harmless otherwise.
|
|
79
|
+
*
|
|
80
|
+
* Exported so other CLI command modules (e.g. `runtime-internalization-run-once`)
|
|
81
|
+
* can reuse the same algorithm without each re-importing `node:crypto`.
|
|
82
|
+
*/
|
|
83
|
+
export const contentHashFn = (input: string): string => createHash('sha256').update(input).digest('hex');
|
|
84
|
+
|
|
60
85
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
61
86
|
|
|
62
87
|
/**
|
|
@@ -113,6 +138,10 @@ export interface RuleHostPipelineOptions {
|
|
|
113
138
|
* capability is treated as OFF with reason 'code_rule_capability not provided'.
|
|
114
139
|
*/
|
|
115
140
|
readonly codeRuleCapability?: CodeRuleCapability;
|
|
141
|
+
/** Explicit Artificer contract selected by the workspace feature flag. */
|
|
142
|
+
readonly contextMode?: 'v1' | 'v2';
|
|
143
|
+
/** Required in v2 mode; assembled from Owner-labelled production evidence. */
|
|
144
|
+
readonly behaviorExamplePack?: BehaviorExamplePack;
|
|
116
145
|
/** Internalization channel for created tasks (default 'code_tool_hook'). */
|
|
117
146
|
readonly channel?: 'prompt' | 'code_tool_hook' | 'defer_archive';
|
|
118
147
|
/** Max adversarial rounds (PRD cap = 2). */
|
|
@@ -286,7 +315,7 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
|
|
|
286
315
|
stages.push({ name: 'dreamer', taskId: dreamerSeedTaskId, status: 'succeeded' });
|
|
287
316
|
} else {
|
|
288
317
|
const dreamerRunner = new DreamerRunner(
|
|
289
|
-
{ stateManager, runtimeAdapter: agentAdapters.dreamer, eventEmitter, validator: new DefaultDreamerValidator(), artifactStore },
|
|
318
|
+
{ stateManager, runtimeAdapter: agentAdapters.dreamer, eventEmitter, validator: new DefaultDreamerValidator(), artifactStore, contentHashFn },
|
|
290
319
|
runnerOptsFor(agentAdapters.dreamer),
|
|
291
320
|
);
|
|
292
321
|
const dreamerResult = await runStage(dreamerRunner, dreamerSeedTaskId, { maxStageRetries, pollIntervalMs });
|
|
@@ -303,7 +332,7 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
|
|
|
303
332
|
const philosopherTaskId = `${correlation}-philosopher-${Date.now().toString(36)}`;
|
|
304
333
|
await createInternalizationTask(stateManager, philosopherTaskId, 'philosopher', [dreamerSeedTaskId], channel, timeoutMs);
|
|
305
334
|
const philosopherRunner = new PhilosopherRunner(
|
|
306
|
-
{ stateManager, runtimeAdapter: agentAdapters.philosopher, eventEmitter, validator: new DefaultPhilosopherValidator(), artifactStore },
|
|
335
|
+
{ stateManager, runtimeAdapter: agentAdapters.philosopher, eventEmitter, validator: new DefaultPhilosopherValidator(), artifactStore, contentHashFn },
|
|
307
336
|
runnerOptsFor(agentAdapters.philosopher),
|
|
308
337
|
);
|
|
309
338
|
const philosopherResult = await runStage(philosopherRunner, philosopherTaskId, { maxStageRetries, pollIntervalMs });
|
|
@@ -319,7 +348,7 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
|
|
|
319
348
|
const scribeTaskId = `${correlation}-scribe-${Date.now().toString(36)}`;
|
|
320
349
|
await createInternalizationTask(stateManager, scribeTaskId, 'scribe', [philosopherTaskId], channel, timeoutMs);
|
|
321
350
|
const scribeRunner = new ScribeRunner(
|
|
322
|
-
{ stateManager, runtimeAdapter: agentAdapters.scribe, eventEmitter, validator: new DefaultScribeValidator(), artifactStore },
|
|
351
|
+
{ stateManager, runtimeAdapter: agentAdapters.scribe, eventEmitter, validator: new DefaultScribeValidator(), artifactStore, contentHashFn },
|
|
323
352
|
runnerOptsFor(agentAdapters.scribe),
|
|
324
353
|
);
|
|
325
354
|
const scribeResult = await runStage(scribeRunner, scribeTaskId, { maxStageRetries, pollIntervalMs });
|
|
@@ -352,11 +381,25 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
|
|
|
352
381
|
}
|
|
353
382
|
onProgress('adversarial_loop', 'start');
|
|
354
383
|
const artificerRunner = new ArtificerRunner(
|
|
355
|
-
{
|
|
384
|
+
{
|
|
385
|
+
stateManager, runtimeAdapter: capability.artificerAdapter, eventEmitter, validator: new DefaultArtificerValidator(), artifactStore,
|
|
386
|
+
contextMode: opts.contextMode ?? 'v1', behaviorExamplePack: opts.behaviorExamplePack, contentHashFn,
|
|
387
|
+
},
|
|
356
388
|
runnerOptsFor(capability.artificerAdapter),
|
|
357
389
|
);
|
|
390
|
+
// PRI-510 (DEFECT-004): construct EvaluatorRunnerDeps via the centralized
|
|
391
|
+
// helper so the repair-loop wiring (isRepairLoopEnabled + seeder) is
|
|
392
|
+
// actually invoked in the production CLI path. EP-02: prior code passed
|
|
393
|
+
// only the 5 base deps, leaving the repair loop as dead code at runtime.
|
|
358
394
|
const evaluatorRunner = new EvaluatorRunner(
|
|
359
|
-
{
|
|
395
|
+
createEvaluatorRunnerDeps({
|
|
396
|
+
stateManager,
|
|
397
|
+
runtimeAdapter: agentAdapters.evaluator,
|
|
398
|
+
eventEmitter,
|
|
399
|
+
validator: new DefaultEvaluatorValidator(),
|
|
400
|
+
artifactStore,
|
|
401
|
+
workspaceDir: opts.workspaceDir,
|
|
402
|
+
}),
|
|
360
403
|
{ ...runnerOptsFor(agentAdapters.evaluator), gateDeps: createSandboxGateDeps() },
|
|
361
404
|
);
|
|
362
405
|
|
|
@@ -405,7 +448,7 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
|
|
|
405
448
|
// Enqueue failed — the candidate artifact exists but is not in the
|
|
406
449
|
// approval queue. Degrade gracefully with a structured reason (ERR-002).
|
|
407
450
|
const enqueueErr = err instanceof Error ? err.message : String(err);
|
|
408
|
-
const degradeReason = `candidate_approved_but_enqueue_failed: ${enqueueErr}. Manual enqueue required: pd
|
|
451
|
+
const degradeReason = `candidate_approved_but_enqueue_failed: ${enqueueErr}. Manual enqueue required: pd activation dispatch --artifact-id ${loopResult.ruleArtifactId} --channel ${channel}`;
|
|
409
452
|
return {
|
|
410
453
|
decision: pipelineDecision,
|
|
411
454
|
painId: opts.painId,
|
|
@@ -528,6 +571,114 @@ async function createInternalizationTask(
|
|
|
528
571
|
});
|
|
529
572
|
}
|
|
530
573
|
|
|
574
|
+
// ── PRI-510 (DEFECT-004): EvaluatorRunner repair-loop CLI wiring ────────────
|
|
575
|
+
//
|
|
576
|
+
// EP-02 (Production Path Wiring): PRI-509 added the `isRepairLoopEnabled` and
|
|
577
|
+
// `seedArtificerRepairTask` deps to `EvaluatorRunnerDeps` (core), but the two
|
|
578
|
+
// CLI production paths (runtime-internalization-run-once.ts and this file)
|
|
579
|
+
// constructed `EvaluatorRunner` with only the base deps — the repair loop was
|
|
580
|
+
// dead code at runtime (evaluator needs_revision seeded nothing).
|
|
581
|
+
//
|
|
582
|
+
// This helper centralizes the construction of the full deps so both CLI sites
|
|
583
|
+
// stay in sync. The flag resolver reads `.pd/config.yaml` (the unified config
|
|
584
|
+
// — ADR-0016); the seeder writes the repair task via `stateManager.createTask`
|
|
585
|
+
// (the only sanctioned task-creation path; core peer runners are forbidden
|
|
586
|
+
// from calling it directly — architecture-regression.test.ts enforces this).
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Inputs for {@link createEvaluatorRunnerDeps}. Carries the base `PeerRunnerDeps`
|
|
590
|
+
* fields plus `workspaceDir` (used to resolve the feature flag from
|
|
591
|
+
* `.pd/config.yaml`).
|
|
592
|
+
*/
|
|
593
|
+
export interface CreateEvaluatorRunnerDepsInputs {
|
|
594
|
+
readonly stateManager: RuntimeStateManager;
|
|
595
|
+
readonly runtimeAdapter: PDRuntimeAdapter;
|
|
596
|
+
readonly eventEmitter: StoreEventEmitter;
|
|
597
|
+
readonly validator: EvaluatorValidator;
|
|
598
|
+
readonly artifactStore: PIArtifactStore;
|
|
599
|
+
/** Workspace directory containing `.pd/config.yaml` (flag source). */
|
|
600
|
+
readonly workspaceDir: string;
|
|
601
|
+
/**
|
|
602
|
+
* Layer 0 content-hash function (design §6.1). Optional — defaults to the
|
|
603
|
+
* module-level sha256-hex `contentHashFn`. Allow override for tests.
|
|
604
|
+
*/
|
|
605
|
+
readonly contentHashFn?: (input: string) => string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Build the full `EvaluatorRunnerDeps` for CLI production paths, including the
|
|
610
|
+
* PRI-509 repair-loop wiring (EP-02: production path must invoke core logic).
|
|
611
|
+
*
|
|
612
|
+
* - `isRepairLoopEnabled`: reads `.pd/config.yaml` and resolves the
|
|
613
|
+
* `evaluator_artificer_repair_loop` flag. Defaults to false when the config
|
|
614
|
+
* is missing or malformed (rc-9: fail safe — legacy path runs, never throws).
|
|
615
|
+
* - `seedArtificerRepairTask`: creates a new `artificer` task carrying the
|
|
616
|
+
* `repairPayload` in `diagnosticJson` (rc-1, rc-6: serialized via the
|
|
617
|
+
* validated `createPITaskDiagnosticJson`, no `as` bypass). Reuses the
|
|
618
|
+
* artificer runner (PRI-509 D1) rather than introducing a new task kind.
|
|
619
|
+
*
|
|
620
|
+
* Returns the newly created repair task's ID (UUIDv4, rc-7: fresh per call).
|
|
621
|
+
*/
|
|
622
|
+
export function createEvaluatorRunnerDeps(inputs: CreateEvaluatorRunnerDepsInputs): EvaluatorRunnerDeps {
|
|
623
|
+
const { stateManager, runtimeAdapter, eventEmitter, validator, artifactStore, workspaceDir } = inputs;
|
|
624
|
+
return {
|
|
625
|
+
stateManager,
|
|
626
|
+
runtimeAdapter,
|
|
627
|
+
eventEmitter,
|
|
628
|
+
validator,
|
|
629
|
+
artifactStore,
|
|
630
|
+
// Layer 0 (design §6.1): inject the content-hash function so the evaluator
|
|
631
|
+
// writer can attach a `predecessorSummary.contentHash` for staleness
|
|
632
|
+
// detection. Defaults to the module-level sha256-hex constant.
|
|
633
|
+
contentHashFn: inputs.contentHashFn ?? contentHashFn,
|
|
634
|
+
isRepairLoopEnabled: (): boolean => {
|
|
635
|
+
// rc-9: never throw on malformed config — fail safe to false so the
|
|
636
|
+
// legacy (non-repair) path runs. The malformed config is already
|
|
637
|
+
// surfaced by `pd config doctor` and CLI start-up warnings.
|
|
638
|
+
try {
|
|
639
|
+
const result = loadPdConfig(workspaceDir);
|
|
640
|
+
const effective = result.ok ? result.effective : result.defaults;
|
|
641
|
+
const flags = computeFeatureFlagsFromConfig(effective);
|
|
642
|
+
return isFeatureEnabled(flags, 'evaluator_artificer_repair_loop');
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
seedArtificerRepairTask: async (params: SeedArtificerRepairParams): Promise<string> => {
|
|
648
|
+
// rc-7: each call gets a fresh task ID — never reuse a cached ID.
|
|
649
|
+
const repairTaskId = `artificer-repair-${randomUUID()}`;
|
|
650
|
+
await stateManager.createTask({
|
|
651
|
+
taskId: repairTaskId,
|
|
652
|
+
// D1 (PRI-509): task kind is 'artificer' — reuses the artificer
|
|
653
|
+
// runner, which detects repairPayload in diagnosticJson and
|
|
654
|
+
// forwards it to the prompt builder as repairFeedback.
|
|
655
|
+
taskKind: 'artificer',
|
|
656
|
+
status: 'pending',
|
|
657
|
+
attemptCount: 0,
|
|
658
|
+
maxAttempts: 3,
|
|
659
|
+
// rc-1, rc-6: serialize via the validated helper — repairPayload
|
|
660
|
+
// is untrusted LLM output but `createPITaskDiagnosticJson` writes
|
|
661
|
+
// it through `serializePITaskMetadata`, which `parsePITaskMetadata`
|
|
662
|
+
// re-validates on read (defense in depth).
|
|
663
|
+
diagnosticJson: createPITaskDiagnosticJson({
|
|
664
|
+
// PITaskMetadata fields are mutable arrays (the metadata envelope
|
|
665
|
+
// serializes them as JSON arrays). The SeedArtificerRepairParams
|
|
666
|
+
// contract carries readonly arrays (rc-1: untrusted input from the
|
|
667
|
+
// evaluator LLM). Spread to a fresh mutable array — no mutation of
|
|
668
|
+
// the caller's data.
|
|
669
|
+
dependencyTaskIds: [...params.inheritedDependencyTaskIds],
|
|
670
|
+
channel: params.inheritedChannel,
|
|
671
|
+
timeoutMs: params.inheritedTimeoutMs,
|
|
672
|
+
inputArtifactRefs: [...params.inheritedInputArtifactRefs],
|
|
673
|
+
outputArtifactRefs: [],
|
|
674
|
+
repairPayload: params.repairPayload,
|
|
675
|
+
}),
|
|
676
|
+
});
|
|
677
|
+
return repairTaskId;
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
531
682
|
/**
|
|
532
683
|
* Run a single pipeline stage with bounded retry for transient `retried` status.
|
|
533
684
|
*
|
|
@@ -103,6 +103,7 @@ export interface ResolveAdapterOptions {
|
|
|
103
103
|
apiKeyEnv?: string;
|
|
104
104
|
baseUrl?: string;
|
|
105
105
|
maxRetries?: number;
|
|
106
|
+
maxTokens?: number;
|
|
106
107
|
};
|
|
107
108
|
/**
|
|
108
109
|
* CLI override for openclaw mode (from --openclaw-local / --openclaw-gateway flags).
|
|
@@ -215,6 +216,7 @@ export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PD
|
|
|
215
216
|
const apiKeyEnv = opts.piAiOverrides?.apiKeyEnv ?? configFields.apiKeyEnv;
|
|
216
217
|
const baseUrl = opts.piAiOverrides?.baseUrl ?? configFields.baseUrl;
|
|
217
218
|
const maxRetries = opts.piAiOverrides?.maxRetries ?? configFields.maxRetries;
|
|
219
|
+
const maxTokens = opts.piAiOverrides?.maxTokens ?? configFields.maxTokens;
|
|
218
220
|
const adapterTimeoutMs = opts.timeoutMs ?? configFields.timeoutMs;
|
|
219
221
|
|
|
220
222
|
// PRI-431 Step 1d + PR review fix: manual missing-field check on MERGED values
|
|
@@ -283,9 +285,11 @@ export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PD
|
|
|
283
285
|
model: String(model),
|
|
284
286
|
apiKeyEnv: String(apiKeyEnv),
|
|
285
287
|
maxRetries,
|
|
288
|
+
maxTokens,
|
|
286
289
|
timeoutMs: adapterTimeoutMs,
|
|
287
290
|
baseUrl,
|
|
288
291
|
workspace: opts.workspaceDir,
|
|
292
|
+
...(configFields.systemPrompt ? { systemPrompt: configFields.systemPrompt } : {}),
|
|
289
293
|
});
|
|
290
294
|
}
|
|
291
295
|
|
|
@@ -21,6 +21,7 @@ import { auditCandidateLedgerConsistency } from '@principles/core/runtime-v2';
|
|
|
21
21
|
import { OperatorHealthReadModel } from '@principles/core/runtime-v2';
|
|
22
22
|
import { createInternalizationQueueReadModel } from '@principles/core/runtime-v2';
|
|
23
23
|
import { createPITaskDiagnosticJson } from '@principles/core/runtime-v2';
|
|
24
|
+
import { contentHashFn } from './rulehost-pipeline-runner.js';
|
|
24
25
|
import {
|
|
25
26
|
computeOverallStatus,
|
|
26
27
|
boundedEvidence,
|
|
@@ -148,6 +149,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
148
149
|
artifactStore: stateManager.piArtifactStore,
|
|
149
150
|
validator: new DefaultDiagRootCauseValidator(),
|
|
150
151
|
contextAssembler,
|
|
152
|
+
contentHashFn,
|
|
151
153
|
},
|
|
152
154
|
{
|
|
153
155
|
owner: 'synthetic-baseline',
|
|
@@ -164,6 +166,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
164
166
|
eventEmitter,
|
|
165
167
|
artifactStore: stateManager.piArtifactStore,
|
|
166
168
|
validator: new DefaultDiagDistillerValidator(),
|
|
169
|
+
contentHashFn,
|
|
167
170
|
},
|
|
168
171
|
{
|
|
169
172
|
owner: 'synthetic-baseline',
|
|
@@ -180,6 +183,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
180
183
|
eventEmitter,
|
|
181
184
|
artifactStore: stateManager.piArtifactStore,
|
|
182
185
|
committer,
|
|
186
|
+
contentHashFn,
|
|
183
187
|
},
|
|
184
188
|
{
|
|
185
189
|
owner: 'synthetic-baseline',
|
|
@@ -214,6 +218,13 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
214
218
|
painType: 'tool_failure',
|
|
215
219
|
source: 'synthetic-baseline',
|
|
216
220
|
reason: 'Synthetic baseline deterministic pain signal',
|
|
221
|
+
// Synthetic baseline is a deterministic smoke tool — it has no real host
|
|
222
|
+
// session to harvest evidence from. Provide a synthetic evidence entry so
|
|
223
|
+
// the PEAT-B1 admission gate (painEvidenceAdmission, default-on) does not
|
|
224
|
+
// short-circuit the pain before diagnosis. Without this, the baseline
|
|
225
|
+
// reports `short_circuited: input evidence empty` and every downstream
|
|
226
|
+
// stage fails, even though the pipeline itself is healthy.
|
|
227
|
+
evidence: [{ sourceRef: `synthetic:${painId}`, note: 'Synthetic baseline deterministic evidence anchor' }],
|
|
217
228
|
});
|
|
218
229
|
|
|
219
230
|
const painPassed = bridgeResult.status === 'succeeded';
|
|
@@ -5,23 +5,27 @@ import {
|
|
|
5
5
|
isProductionWorkspace,
|
|
6
6
|
guardUatWorkspace,
|
|
7
7
|
getSafeUatWorkspacePath,
|
|
8
|
+
resolveWorkspacePath,
|
|
8
9
|
formatGuardRefusal,
|
|
9
10
|
type GuardRefusal,
|
|
10
11
|
} from './production-workspace-guard.js';
|
|
11
12
|
|
|
13
|
+
// Use os.homedir()-derived paths so the tests work cross-platform
|
|
14
|
+
// (previously hardcoded Windows paths like D:\.openclaw\workspace which on
|
|
15
|
+
// Linux resolve to <cwd>/D:\.openclaw\workspace — meaningless).
|
|
16
|
+
const HOMEDIR_WORKSPACE = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
17
|
+
|
|
12
18
|
const PRODUCTION_PATHS = [
|
|
13
|
-
|
|
14
|
-
'C:\\.openclaw\\workspace',
|
|
15
|
-
'C:\\Users\\Administrator\\.openclaw\\workspace',
|
|
16
|
-
path.join(os.homedir(), '.openclaw', 'workspace'),
|
|
19
|
+
HOMEDIR_WORKSPACE,
|
|
17
20
|
];
|
|
18
21
|
|
|
19
22
|
const SAFE_PATHS = [
|
|
20
|
-
'
|
|
21
|
-
'
|
|
23
|
+
path.join(os.homedir(), '.openclaw', 'workspace-test'),
|
|
24
|
+
path.join(os.homedir(), '.openclaw', 'workspace-backup'),
|
|
22
25
|
path.join(os.tmpdir(), 'pd-uat-workspace'),
|
|
23
26
|
path.join(os.tmpdir(), 'pd-test-any'),
|
|
24
|
-
|
|
27
|
+
// A path that is clearly unrelated on every platform
|
|
28
|
+
path.join(os.tmpdir(), 'completely-unrelated-work'),
|
|
25
29
|
];
|
|
26
30
|
|
|
27
31
|
describe('isProductionWorkspace', () => {
|
|
@@ -32,18 +36,15 @@ describe('isProductionWorkspace', () => {
|
|
|
32
36
|
expect(isProductionWorkspace(path.resolve(safePath))).toBe(false);
|
|
33
37
|
});
|
|
34
38
|
it('detects descendant', () => {
|
|
35
|
-
|
|
36
|
-
// Windows (where D:\... resolves as-is) and Linux CI (where D:\... is
|
|
37
|
-
// treated as a relative path and resolves under cwd).
|
|
38
|
-
const prodPath = path.resolve(path.join(os.homedir(), '.openclaw', 'workspace'));
|
|
39
|
+
const prodPath = path.resolve(HOMEDIR_WORKSPACE);
|
|
39
40
|
const descendantPath = path.join(prodPath, 'sub', 'child');
|
|
40
41
|
expect(isProductionWorkspace(descendantPath)).toBe(true);
|
|
41
42
|
});
|
|
42
43
|
it('rejects sibling workspace-test (ERR-030)', () => {
|
|
43
|
-
expect(isProductionWorkspace(path.resolve('
|
|
44
|
+
expect(isProductionWorkspace(path.resolve(path.join(os.homedir(), '.openclaw', 'workspace-test')))).toBe(false);
|
|
44
45
|
});
|
|
45
46
|
it('rejects sibling workspace-backup (ERR-030)', () => {
|
|
46
|
-
expect(isProductionWorkspace(path.resolve('
|
|
47
|
+
expect(isProductionWorkspace(path.resolve(path.join(os.homedir(), '.openclaw', 'workspace-backup')))).toBe(false);
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
|
|
@@ -58,7 +59,7 @@ describe('guardUatWorkspace', () => {
|
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
it('refuses descendant', () => {
|
|
61
|
-
const prodPath = path.resolve(
|
|
62
|
+
const prodPath = path.resolve(HOMEDIR_WORKSPACE);
|
|
62
63
|
const descendantPath = path.join(prodPath, 'subdir');
|
|
63
64
|
expect(guardUatWorkspace(descendantPath, 'test').refused).toBe(true);
|
|
64
65
|
});
|
|
@@ -68,8 +69,10 @@ describe('guardUatWorkspace', () => {
|
|
|
68
69
|
const r = guardUatWorkspace(safePath, 'test');
|
|
69
70
|
expect(r.refused).toBe(false);
|
|
70
71
|
});
|
|
71
|
-
it.each([
|
|
72
|
-
|
|
72
|
+
it.each([
|
|
73
|
+
path.join(os.homedir(), '.openclaw', 'workspace-test'),
|
|
74
|
+
path.join(os.homedir(), '.openclaw', 'workspace-backup'),
|
|
75
|
+
])('allows sibling: %s (ERR-030)', (p) => {
|
|
73
76
|
expect(guardUatWorkspace(p, 'test').refused).toBe(false);
|
|
74
77
|
});
|
|
75
78
|
});
|
|
@@ -77,7 +80,7 @@ describe('guardUatWorkspace', () => {
|
|
|
77
80
|
|
|
78
81
|
describe('JSON output (EP-04)', () => {
|
|
79
82
|
it('outputs single object with reason and nextAction', () => {
|
|
80
|
-
const r = guardUatWorkspace(
|
|
83
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
81
84
|
const json = formatGuardRefusal(r as GuardRefusal, 'test', true);
|
|
82
85
|
const parsed = JSON.parse(json);
|
|
83
86
|
expect(parsed).toMatchObject({
|
|
@@ -87,7 +90,7 @@ describe('JSON output (EP-04)', () => {
|
|
|
87
90
|
expect(Array.isArray(parsed)).toBe(false);
|
|
88
91
|
});
|
|
89
92
|
it('no console prefixes in JSON', () => {
|
|
90
|
-
const r = guardUatWorkspace(
|
|
93
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
91
94
|
const json = formatGuardRefusal(r as GuardRefusal, 'test', true);
|
|
92
95
|
expect(json).not.toContain('[pd-cli]');
|
|
93
96
|
expect(json).not.toContain('ERROR:');
|
|
@@ -97,7 +100,7 @@ describe('JSON output (EP-04)', () => {
|
|
|
97
100
|
|
|
98
101
|
describe('text output (EP-03)', () => {
|
|
99
102
|
it('includes reason and nextAction', () => {
|
|
100
|
-
const r = guardUatWorkspace(
|
|
103
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
101
104
|
const text = formatGuardRefusal(r as GuardRefusal, 'test', false);
|
|
102
105
|
expect(text).toContain('Reason:');
|
|
103
106
|
expect(text).toContain('Next Action:');
|
|
@@ -112,4 +115,43 @@ describe('getSafeUatWorkspacePath', () => {
|
|
|
112
115
|
expect(p1).toContain(os.tmpdir());
|
|
113
116
|
expect(p1).toContain('pd-uat-workspace');
|
|
114
117
|
});
|
|
115
|
-
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('resolveWorkspacePath', () => {
|
|
121
|
+
it('resolves an absolute path as-is', () => {
|
|
122
|
+
const abs = path.resolve('/tmp/my-workspace');
|
|
123
|
+
expect(resolveWorkspacePath(abs)).toBe(abs);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('resolves a relative path against cwd', () => {
|
|
127
|
+
const result = resolveWorkspacePath('relative/path');
|
|
128
|
+
const expected = path.resolve(process.cwd(), 'relative/path');
|
|
129
|
+
expect(result).toBe(expected);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('defaults to cwd when no input is provided', () => {
|
|
133
|
+
const result = resolveWorkspacePath(undefined);
|
|
134
|
+
expect(result).toBe(path.resolve(process.cwd()));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('PD_PRODUCTION_WORKSPACE env var (module-load-time)', () => {
|
|
139
|
+
// NOTE: PRODUCTION_WORKSPACE_PATHS is built once at module load time from
|
|
140
|
+
// process.env.PD_PRODUCTION_WORKSPACE. Setting the env var after the module
|
|
141
|
+
// is loaded does NOT change behavior. These tests verify that the default
|
|
142
|
+
// production path (HOMEDIR_WORKSPACE) is correctly protected, which is the
|
|
143
|
+
// primary regression risk.
|
|
144
|
+
|
|
145
|
+
it('guardUatWorkspace refuses the default production workspace', () => {
|
|
146
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
147
|
+
expect(r.refused).toBe(true);
|
|
148
|
+
if (r.refused) {
|
|
149
|
+
expect(r.isProduction).toBe(true);
|
|
150
|
+
expect(r.workspace).toBe(path.resolve(HOMEDIR_WORKSPACE));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('isProductionWorkspace returns true for the default production path', () => {
|
|
155
|
+
expect(isProductionWorkspace(path.resolve(HOMEDIR_WORKSPACE))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prevents UAT/runtime test commands from writing to production workspaces.
|
|
5
5
|
*
|
|
6
|
-
* Production workspaces
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
6
|
+
* Production workspaces:
|
|
7
|
+
* - Default: ~/.openclaw/workspace (cross-platform)
|
|
8
|
+
* - Additional paths declared via PD_PRODUCTION_WORKSPACE env var
|
|
9
|
+
* (use path.delimiter — `;` on Windows, `:` on Unix — to separate multiple paths)
|
|
10
10
|
*
|
|
11
11
|
* This module follows ERR-030 (path prefix matching must use segment boundaries)
|
|
12
12
|
* and EP-03/EP-04 (fail loud with structured reason and nextAction).
|
|
@@ -20,19 +20,22 @@ import { existsSync } from 'fs';
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* List of production workspace paths that should be protected from UAT/test writes.
|
|
23
|
-
*
|
|
23
|
+
* Built from: the universal default (~/.openclaw/workspace) plus any paths
|
|
24
|
+
* declared via PD_PRODUCTION_WORKSPACE.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
function buildProductionWorkspacePaths(): string[] {
|
|
27
|
+
const paths = [path.resolve(path.join(os.homedir(), '.openclaw', 'workspace'))];
|
|
28
|
+
const extra = process.env.PD_PRODUCTION_WORKSPACE;
|
|
29
|
+
if (extra) {
|
|
30
|
+
for (const p of extra.split(path.delimiter)) {
|
|
31
|
+
const trimmed = p.trim();
|
|
32
|
+
if (trimmed) paths.push(path.resolve(trimmed));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return paths;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PRODUCTION_WORKSPACE_PATHS = buildProductionWorkspacePaths();
|
|
36
39
|
|
|
37
40
|
// ── Resolution helpers ───────────────────────────────────────────────────────
|
|
38
41
|
|