@principles/pd-cli 1.152.7 → 1.152.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/runtime-probe-config.test.js +2 -1
- package/dist/commands/__tests__/runtime-probe-config.test.js.map +1 -1
- package/dist/commands/__tests__/version.test.js +104 -0
- package/dist/commands/__tests__/version.test.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +3 -6
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +3 -6
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +9 -3
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +5 -11
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +14 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +67 -15
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/rulehost-readiness.test.js +0 -3
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +0 -4
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +90 -21
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +328 -24
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +0 -4
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/services/version-report.d.ts +16 -0
- package/dist/services/version-report.d.ts.map +1 -1
- package/dist/services/version-report.js +37 -2
- package/dist/services/version-report.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/runtime-probe-config.test.ts +2 -1
- package/src/commands/__tests__/version.test.ts +110 -0
- package/src/commands/diagnose.ts +3 -6
- package/src/commands/pain-retry.ts +3 -6
- package/src/commands/runtime-activation.ts +9 -3
- package/src/commands/runtime-internalization-run-once.ts +5 -11
- package/src/commands/runtime-internalization-run-rulehost.ts +69 -15
- package/src/index.ts +0 -13
- package/src/services/__tests__/rulehost-readiness.test.ts +0 -3
- package/src/services/pain-flood-simulation-runner.ts +0 -4
- package/src/services/rulehost-pipeline-runner.ts +368 -35
- package/src/services/synthetic-baseline-runner.ts +0 -4
- package/src/services/version-report.ts +52 -2
- package/tests/commands/config-doctor.test.ts +0 -2
- package/tests/commands/pain-evidence.test.ts +1 -1
- package/tests/commands/run-rulehost-handler.test.ts +43 -0
- package/tests/commands/runtime-features.test.ts +0 -1
- package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -1
- package/tests/e2e/cross-package-acceptance.test.ts +13 -3
- package/tests/services/pd-config-loader.test.ts +0 -1
- package/tests/services/rulehost-pipeline-runner.test.ts +378 -12
- package/dist/commands/runtime-internalization-context-trace.d.ts +0 -25
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +0 -1
- package/dist/commands/runtime-internalization-context-trace.js +0 -374
- package/dist/commands/runtime-internalization-context-trace.js.map +0 -1
- package/src/commands/runtime-internalization-context-trace.ts +0 -445
- package/tests/bdd/context-trace.steps.ts +0 -229
- package/tests/commands/runtime-internalization-context-trace.test.ts +0 -110
|
@@ -1,445 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3 — read-only context-trace CLI command (design §6.7, PR 5 task 11.1).
|
|
3
|
-
*
|
|
4
|
-
* `pd runtime internalization context-trace --task <taskId> [--artifact <artifactId>] [--json]`
|
|
5
|
-
*
|
|
6
|
-
* Produces three-segment diagnosis (pain→dreamer / dreamer→scribe / scribe→artificer),
|
|
7
|
-
* per-hop summary presence/freshness, budget truncation records, and structured
|
|
8
|
-
* degradation reasons — the single Owner-visible exit point for all internal
|
|
9
|
-
* signals from Layers 0/1/2.
|
|
10
|
-
*
|
|
11
|
-
* CLI gates (cli-1..cli-7):
|
|
12
|
-
* - --json emits exactly one parseable JSON object on stdout (cli-1)
|
|
13
|
-
* - uses process.exitCode (not process.exit) so stdout isn't truncated (cli-2)
|
|
14
|
-
* - no --dry-run / --confirm (command is inherently read-only, cli-4 N/A)
|
|
15
|
-
* - zero state writes: no DB/ledger/artifact/enqueue/successor mutations (cli-5)
|
|
16
|
-
* - every degraded/refused output includes structured reason + nextAction (cli-6)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import * as path from 'node:path';
|
|
20
|
-
import {
|
|
21
|
-
RuntimeStateManager,
|
|
22
|
-
isFeatureEnabled,
|
|
23
|
-
CandidateLineage,
|
|
24
|
-
type LineageNode,
|
|
25
|
-
type LineageNote,
|
|
26
|
-
type LineageResult,
|
|
27
|
-
type LineageTaskReader,
|
|
28
|
-
} from '@principles/core/runtime-v2';
|
|
29
|
-
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
30
|
-
import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
|
|
31
|
-
|
|
32
|
-
export interface ContextTraceOptions {
|
|
33
|
-
workspace?: string;
|
|
34
|
-
task?: string;
|
|
35
|
-
artifact?: string;
|
|
36
|
-
json?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── Output types (design §6.7) ───────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
interface ChainNodeOutput {
|
|
42
|
-
readonly stage: string;
|
|
43
|
-
readonly taskKind: string;
|
|
44
|
-
readonly artifactId: string;
|
|
45
|
-
readonly summaryPresent: boolean;
|
|
46
|
-
readonly predecessorSummaryPresent: boolean;
|
|
47
|
-
readonly predecessorHashMatches: boolean | null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface SegmentOutput {
|
|
51
|
-
readonly segment: 'pain_to_dreamer' | 'dreamer_to_scribe' | 'scribe_to_artificer';
|
|
52
|
-
readonly verdict: 'pass' | 'degraded' | 'fail';
|
|
53
|
-
readonly missingDimensions?: readonly string[];
|
|
54
|
-
readonly detail: string | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface DegradationOutput {
|
|
58
|
-
readonly code: string;
|
|
59
|
-
readonly detail: string;
|
|
60
|
-
readonly artifactId?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface ContextTraceResult {
|
|
64
|
-
readonly ok: boolean;
|
|
65
|
-
readonly command: string;
|
|
66
|
-
readonly taskId?: string;
|
|
67
|
-
readonly flags?: {
|
|
68
|
-
readonly artifact_summary_redundancy: boolean;
|
|
69
|
-
readonly context_manifest_budget: boolean;
|
|
70
|
-
readonly progressive_evaluator: boolean;
|
|
71
|
-
};
|
|
72
|
-
readonly chain?: readonly ChainNodeOutput[];
|
|
73
|
-
readonly segments?: readonly SegmentOutput[];
|
|
74
|
-
readonly truncations?: readonly unknown[];
|
|
75
|
-
readonly degradations: readonly DegradationOutput[];
|
|
76
|
-
readonly error?: { readonly code: string; readonly message: string };
|
|
77
|
-
readonly nextAction?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
83
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function safePreview(value: unknown, maxLen = 200): string {
|
|
87
|
-
try {
|
|
88
|
-
const s = typeof value === 'string' ? value : JSON.stringify(value);
|
|
89
|
-
return s.length <= maxLen ? s : `${s.slice(0, maxLen - 1)}…`;
|
|
90
|
-
} catch {
|
|
91
|
-
return '<unserializable>';
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readSummaryPresent(contentJson: unknown): boolean {
|
|
96
|
-
if (!isRecord(contentJson)) return false;
|
|
97
|
-
return Object.hasOwn(contentJson, 'summary');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function readPredecessorSummaryPresent(contentJson: unknown): boolean {
|
|
101
|
-
if (!isRecord(contentJson)) return false;
|
|
102
|
-
return Object.hasOwn(contentJson, 'predecessorSummary');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── Three-segment verdict computation (design §6.7, Req 10.6) ───────────────
|
|
106
|
-
|
|
107
|
-
/** Required dreamer dimensions that should survive into the scribe's principle text. */
|
|
108
|
-
const REQUIRED_DIMENSIONS = ['betterDecision', 'rationale', 'riskLevel'] as const;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Read a dimension value from a node's predecessorSummary (if the node is
|
|
112
|
-
* downstream of dreamer, the predecessorSummary carries the dreamer dims).
|
|
113
|
-
*/
|
|
114
|
-
function readPredecessorDim(contentJson: unknown, dim: string): string | null {
|
|
115
|
-
if (!isRecord(contentJson)) return null;
|
|
116
|
-
const pred = Object.hasOwn(contentJson, 'predecessorSummary') ? contentJson.predecessorSummary : null;
|
|
117
|
-
if (!isRecord(pred)) return null;
|
|
118
|
-
const summary = Object.hasOwn(pred, 'summary') ? pred.summary : null;
|
|
119
|
-
if (!isRecord(summary)) return null;
|
|
120
|
-
const fields = Object.hasOwn(summary, 'fields') ? summary.fields : null;
|
|
121
|
-
if (!isRecord(fields)) return null;
|
|
122
|
-
const val = Object.hasOwn(fields, dim) ? fields[dim] : undefined;
|
|
123
|
-
return typeof val === 'string' && val.trim() !== '' ? val : null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check whether a node's own summary carries a given dimension.
|
|
128
|
-
*/
|
|
129
|
-
function readOwnDim(contentJson: unknown, dim: string): string | null {
|
|
130
|
-
if (!isRecord(contentJson)) return null;
|
|
131
|
-
const summary = Object.hasOwn(contentJson, 'summary') ? contentJson.summary : null;
|
|
132
|
-
if (!isRecord(summary)) return null;
|
|
133
|
-
const fields = Object.hasOwn(summary, 'fields') ? summary.fields : null;
|
|
134
|
-
if (!isRecord(fields)) return null;
|
|
135
|
-
const val = Object.hasOwn(fields, dim) ? fields[dim] : undefined;
|
|
136
|
-
return typeof val === 'string' && val.trim() !== '' ? val : null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Compute three-segment diagnosis from the resolved chain nodes.
|
|
141
|
-
*
|
|
142
|
-
* Each segment checks whether required dimensions survived the compression hop:
|
|
143
|
-
* - pain_to_dreamer: did pain/diagnosis summaries reach the dreamer node?
|
|
144
|
-
* - dreamer_to_scribe: did dreamer's required dims survive into scribe's text?
|
|
145
|
-
* - scribe_to_artificer: did the principle text reach artificer?
|
|
146
|
-
*
|
|
147
|
-
* When a node is absent or has no summary, the segment is 'fail' (data missing).
|
|
148
|
-
* When a required dimension is absent in the downstream summary, it's 'degraded'.
|
|
149
|
-
* Otherwise 'pass'.
|
|
150
|
-
*/
|
|
151
|
-
function computeSegments(nodes: readonly LineageNode[]): SegmentOutput[] {
|
|
152
|
-
// Index nodes by taskKind for quick lookup.
|
|
153
|
-
const byKind = new Map<string, LineageNode>();
|
|
154
|
-
for (const n of nodes) {
|
|
155
|
-
if (!byKind.has(n.taskKind)) byKind.set(n.taskKind, n);
|
|
156
|
-
}
|
|
157
|
-
const dreamerNode = byKind.get('dreamer');
|
|
158
|
-
const scribeNode = byKind.get('scribe');
|
|
159
|
-
const artificerNode = byKind.get('artificer');
|
|
160
|
-
|
|
161
|
-
// Segment 1: pain_to_dreamer — pain/diagnosis summaries forwarded to dreamer?
|
|
162
|
-
let seg1Verdict: 'pass' | 'degraded' | 'fail' = 'pass';
|
|
163
|
-
let seg1Detail: string | null = null;
|
|
164
|
-
if (!dreamerNode) {
|
|
165
|
-
seg1Verdict = 'fail';
|
|
166
|
-
seg1Detail = 'Dreamer node not found in chain.';
|
|
167
|
-
} else if (!readSummaryPresent(dreamerNode.contentJson)) {
|
|
168
|
-
seg1Verdict = 'fail';
|
|
169
|
-
seg1Detail = `Dreamer artifact ${dreamerNode.artifactId} has no summary envelope.`;
|
|
170
|
-
} else if (!readPredecessorSummaryPresent(dreamerNode.contentJson)) {
|
|
171
|
-
seg1Verdict = 'degraded';
|
|
172
|
-
seg1Detail = 'Dreamer has no predecessorSummary (pain/diagnosis summary may not have been forwarded).';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Segment 2: dreamer_to_scribe — did dreamer's required dims survive into scribe?
|
|
176
|
-
let seg2Verdict: 'pass' | 'degraded' | 'fail' = 'pass';
|
|
177
|
-
let seg2Detail: string | null = null;
|
|
178
|
-
const seg2Missing: string[] = [];
|
|
179
|
-
if (!scribeNode) {
|
|
180
|
-
seg2Verdict = 'fail';
|
|
181
|
-
seg2Detail = 'Scribe node not found in chain.';
|
|
182
|
-
} else if (!readSummaryPresent(scribeNode.contentJson)) {
|
|
183
|
-
seg2Verdict = 'fail';
|
|
184
|
-
seg2Detail = `Scribe artifact ${scribeNode.artifactId} has no summary envelope.`;
|
|
185
|
-
} else {
|
|
186
|
-
// Check if dreamer dims are reachable via scribe's predecessorSummary (via philosopher).
|
|
187
|
-
for (const dim of REQUIRED_DIMENSIONS) {
|
|
188
|
-
const ownVal = readOwnDim(scribeNode.contentJson, dim);
|
|
189
|
-
const predVal = readPredecessorDim(scribeNode.contentJson, dim);
|
|
190
|
-
if (ownVal === null && predVal === null) {
|
|
191
|
-
seg2Missing.push(dim);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (seg2Missing.length > 0) {
|
|
195
|
-
seg2Verdict = 'degraded';
|
|
196
|
-
seg2Detail = `Required dimension(s) missing in scribe summary: ${seg2Missing.join(', ')}`;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Segment 3: scribe_to_artificer — did the principle text reach artificer?
|
|
201
|
-
let seg3Verdict: 'pass' | 'degraded' | 'fail' = 'pass';
|
|
202
|
-
let seg3Detail: string | null = null;
|
|
203
|
-
if (!artificerNode) {
|
|
204
|
-
seg3Verdict = 'fail';
|
|
205
|
-
seg3Detail = 'Artificer node not found in chain.';
|
|
206
|
-
} else if (!readSummaryPresent(artificerNode.contentJson)) {
|
|
207
|
-
seg3Verdict = 'degraded';
|
|
208
|
-
seg3Detail = `Artificer artifact ${artificerNode.artifactId} has no summary envelope.`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return [
|
|
212
|
-
{ segment: 'pain_to_dreamer', verdict: seg1Verdict, missingDimensions: seg1Verdict === 'degraded' ? undefined : undefined, detail: seg1Detail },
|
|
213
|
-
{ segment: 'dreamer_to_scribe', verdict: seg2Verdict, missingDimensions: seg2Missing.length > 0 ? seg2Missing : undefined, detail: seg2Detail },
|
|
214
|
-
{ segment: 'scribe_to_artificer', verdict: seg3Verdict, detail: seg3Detail },
|
|
215
|
-
];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
219
|
-
|
|
220
|
-
export async function handleRuntimeInternalizationContextTrace(opts: ContextTraceOptions): Promise<void> {
|
|
221
|
-
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
222
|
-
|
|
223
|
-
// Validate required --task option.
|
|
224
|
-
if (!opts.task) {
|
|
225
|
-
const result: ContextTraceResult = {
|
|
226
|
-
ok: false,
|
|
227
|
-
command: 'runtime.internalization.context-trace',
|
|
228
|
-
degradations: [],
|
|
229
|
-
error: { code: 'missing_task', message: '--task <taskId> is required' },
|
|
230
|
-
nextAction: 'Provide --task <taskId> to trace the internalization context chain.',
|
|
231
|
-
};
|
|
232
|
-
console.log(JSON.stringify(result, null, 2));
|
|
233
|
-
process.exitCode = 1;
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const taskId = opts.task;
|
|
238
|
-
|
|
239
|
-
// Read feature flags from config (independent of stateManager).
|
|
240
|
-
// CodeRabbit PR #1278 #2: distinguish config-load failure from genuine
|
|
241
|
-
// flags-off — the former needs a different nextAction so the user doesn't
|
|
242
|
-
// waste time editing config that's actually unparseable.
|
|
243
|
-
let summaryFlag = false;
|
|
244
|
-
let manifestFlag = false;
|
|
245
|
-
let progressiveFlag = false;
|
|
246
|
-
let configLoadError: string | null = null;
|
|
247
|
-
try {
|
|
248
|
-
const configLoadResult = loadPdConfig(workspaceDir);
|
|
249
|
-
const flags = computeFlagsFromLoadResult(configLoadResult);
|
|
250
|
-
summaryFlag = isFeatureEnabled(flags, 'artifact_summary_redundancy');
|
|
251
|
-
manifestFlag = isFeatureEnabled(flags, 'context_manifest_budget');
|
|
252
|
-
progressiveFlag = isFeatureEnabled(flags, 'progressive_evaluator');
|
|
253
|
-
} catch (configErr) {
|
|
254
|
-
configLoadError = configErr instanceof Error ? configErr.message : String(configErr);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Config load failed → structured error (not "flags off" masquerade).
|
|
258
|
-
if (configLoadError !== null) {
|
|
259
|
-
const result: ContextTraceResult = {
|
|
260
|
-
ok: false,
|
|
261
|
-
command: 'runtime.internalization.context-trace',
|
|
262
|
-
taskId,
|
|
263
|
-
degradations: [],
|
|
264
|
-
error: { code: 'config_load_failed', message: safePreview(configLoadError) },
|
|
265
|
-
nextAction: 'Fix the .pd/config.yaml parse error, then re-run. The flags could not be read.',
|
|
266
|
-
};
|
|
267
|
-
console.log(JSON.stringify(result, null, 2));
|
|
268
|
-
process.exitCode = 1;
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// All flags off → structured "feature_disabled" output (still ok: true).
|
|
273
|
-
if (!summaryFlag && !manifestFlag && !progressiveFlag) {
|
|
274
|
-
const result: ContextTraceResult = {
|
|
275
|
-
ok: true,
|
|
276
|
-
command: 'runtime.internalization.context-trace',
|
|
277
|
-
taskId,
|
|
278
|
-
flags: {
|
|
279
|
-
artifact_summary_redundancy: false,
|
|
280
|
-
context_manifest_budget: false,
|
|
281
|
-
progressive_evaluator: false,
|
|
282
|
-
},
|
|
283
|
-
chain: [],
|
|
284
|
-
segments: [],
|
|
285
|
-
truncations: [],
|
|
286
|
-
degradations: [
|
|
287
|
-
{
|
|
288
|
-
code: 'feature_disabled',
|
|
289
|
-
detail: 'All three progressive-disclosure flags are off. Three-segment diagnosis unavailable.',
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
nextAction: 'Enable artifact_summary_redundancy and progressive_evaluator in .pd/config.yaml, then re-run the internalization task.',
|
|
293
|
-
};
|
|
294
|
-
console.log(JSON.stringify(result, null, 2));
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// CodeRabbit PR #1278 #1: construct stateManager INSIDE the try block so
|
|
299
|
-
// a constructor throw is caught and produces structured JSON output (cli-6).
|
|
300
|
-
let stateManager: RuntimeStateManager | null = null;
|
|
301
|
-
try {
|
|
302
|
-
// Open state manager in read-only mode (cli-5: zero state writes).
|
|
303
|
-
stateManager = new RuntimeStateManager({ workspaceDir, readonly: true });
|
|
304
|
-
await stateManager.initialize();
|
|
305
|
-
const sm = stateManager;
|
|
306
|
-
|
|
307
|
-
// Resolve the start artifact: either --artifact or the latest artifact for the task.
|
|
308
|
-
const artifactStore = sm.piArtifactStore;
|
|
309
|
-
let startArtifactId: string | undefined = opts.artifact;
|
|
310
|
-
|
|
311
|
-
if (!startArtifactId) {
|
|
312
|
-
const artifacts = await artifactStore.listBySourceTaskId(taskId);
|
|
313
|
-
if (artifacts.length > 0 && artifacts[0]) {
|
|
314
|
-
startArtifactId = artifacts[0].artifactId;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!startArtifactId) {
|
|
319
|
-
const result: ContextTraceResult = {
|
|
320
|
-
ok: false,
|
|
321
|
-
command: 'runtime.internalization.context-trace',
|
|
322
|
-
taskId,
|
|
323
|
-
degradations: [],
|
|
324
|
-
error: { code: 'artifact_not_found', message: `No artifact found for task ${taskId}${opts.artifact ? ` / artifact ${opts.artifact}` : ''}` },
|
|
325
|
-
nextAction: 'Verify the task ID and artifact ID. Use `pd runtime internalization integrity` to check chain health.',
|
|
326
|
-
};
|
|
327
|
-
console.log(JSON.stringify(result, null, 2));
|
|
328
|
-
process.exitCode = 1;
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Build LineageTaskReader adapter (first production implementation).
|
|
333
|
-
const taskReader: LineageTaskReader = {
|
|
334
|
-
async getTaskById(id: string) {
|
|
335
|
-
const t = await sm.getTask(id);
|
|
336
|
-
return t === null ? null : { taskId: t.taskId, taskKind: t.taskKind };
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// Construct CandidateLineage and resolve.
|
|
341
|
-
const lineage = new CandidateLineage({
|
|
342
|
-
artifacts: artifactStore,
|
|
343
|
-
tasks: taskReader,
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const lineageResult: LineageResult = await lineage.resolve(startArtifactId);
|
|
347
|
-
|
|
348
|
-
// Handle lineage errors (data corruption).
|
|
349
|
-
if (!lineageResult.ok) {
|
|
350
|
-
const result: ContextTraceResult = {
|
|
351
|
-
ok: false,
|
|
352
|
-
command: 'runtime.internalization.context-trace',
|
|
353
|
-
taskId,
|
|
354
|
-
degradations: [],
|
|
355
|
-
error: { code: 'lineage_error', message: `Lineage resolution failed: ${lineageResult.error.kind}` },
|
|
356
|
-
nextAction: `Check data integrity: ${lineageResult.error.detail}. Use \`pd runtime internalization integrity --task ${taskId}\` for a full check.`,
|
|
357
|
-
};
|
|
358
|
-
console.log(JSON.stringify(result, null, 2));
|
|
359
|
-
process.exitCode = 1;
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Build the output chain from resolved nodes.
|
|
364
|
-
const chain = lineageResult.value.nodes.map((node: LineageNode): ChainNodeOutput => {
|
|
365
|
-
const { contentJson } = node;
|
|
366
|
-
const summaryPresent = readSummaryPresent(contentJson);
|
|
367
|
-
const predecessorSummaryPresent = readPredecessorSummaryPresent(contentJson);
|
|
368
|
-
return {
|
|
369
|
-
stage: node.taskKind,
|
|
370
|
-
taskKind: node.taskKind,
|
|
371
|
-
artifactId: node.artifactId,
|
|
372
|
-
summaryPresent,
|
|
373
|
-
predecessorSummaryPresent,
|
|
374
|
-
predecessorHashMatches: predecessorSummaryPresent ? true : null,
|
|
375
|
-
};
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Build degradations from lineage notes.
|
|
379
|
-
const degradations: DegradationOutput[] = lineageResult.value.notes.map((note: LineageNote) => ({
|
|
380
|
-
code: note.code === 'ancestor_pruned' ? 'lineage_partial' : note.code,
|
|
381
|
-
detail: safePreview(note.detail),
|
|
382
|
-
artifactId: note.artifactId,
|
|
383
|
-
}));
|
|
384
|
-
|
|
385
|
-
// Add summary_absent for nodes without summary.
|
|
386
|
-
for (const node of lineageResult.value.nodes) {
|
|
387
|
-
if (!readSummaryPresent(node.contentJson)) {
|
|
388
|
-
degradations.push({
|
|
389
|
-
code: 'summary_absent',
|
|
390
|
-
detail: `Artifact ${node.artifactId} (${node.taskKind}) has no summary envelope.`,
|
|
391
|
-
artifactId: node.artifactId,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Build three-segment diagnosis from chain data (design §6.7, Req 10.6).
|
|
397
|
-
// Each segment checks whether the downstream node's summary carries the
|
|
398
|
-
// expected dimensions from its edge predecessor.
|
|
399
|
-
const segments = computeSegments(lineageResult.value.nodes);
|
|
400
|
-
|
|
401
|
-
const result: ContextTraceResult = {
|
|
402
|
-
ok: true,
|
|
403
|
-
command: 'runtime.internalization.context-trace',
|
|
404
|
-
taskId,
|
|
405
|
-
flags: {
|
|
406
|
-
artifact_summary_redundancy: summaryFlag,
|
|
407
|
-
context_manifest_budget: manifestFlag,
|
|
408
|
-
progressive_evaluator: progressiveFlag,
|
|
409
|
-
},
|
|
410
|
-
chain,
|
|
411
|
-
segments,
|
|
412
|
-
truncations: [],
|
|
413
|
-
degradations,
|
|
414
|
-
nextAction: degradations.length > 0
|
|
415
|
-
? `${degradations.length} degradation(s) detected. Review the chain above for missing summaries or partial lineage.`
|
|
416
|
-
: 'Chain is complete with no degradations.',
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
console.log(JSON.stringify(result, null, 2));
|
|
420
|
-
} catch (error: unknown) {
|
|
421
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
422
|
-
const result: ContextTraceResult = {
|
|
423
|
-
ok: false,
|
|
424
|
-
command: 'runtime.internalization.context-trace',
|
|
425
|
-
taskId,
|
|
426
|
-
degradations: [],
|
|
427
|
-
error: { code: 'unexpected_error', message: safePreview(message) },
|
|
428
|
-
nextAction: 'Check workspace configuration and database accessibility.',
|
|
429
|
-
};
|
|
430
|
-
console.log(JSON.stringify(result, null, 2));
|
|
431
|
-
process.exitCode = 1;
|
|
432
|
-
return;
|
|
433
|
-
} finally {
|
|
434
|
-
// CodeRabbit PR #1278 #1: guard close() so it can't overwrite the result
|
|
435
|
-
// or throw unhandled if stateManager was never constructed.
|
|
436
|
-
if (stateManager !== null) {
|
|
437
|
-
try {
|
|
438
|
-
await stateManager.close();
|
|
439
|
-
} catch {
|
|
440
|
-
// Best-effort close on a read-only instance — swallow to preserve the
|
|
441
|
-
// structured JSON output already written to stdout.
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BDD step definitions for context-trace CLI feature (design §6.7, cli-1..cli-7).
|
|
3
|
-
*
|
|
4
|
-
* Approach: in-process handler call with mocked dependencies (same pattern as
|
|
5
|
-
* cli-contract.steps.ts). Captures stdout via vi.spyOn(console, 'log').
|
|
6
|
-
*
|
|
7
|
-
* @see docs/specs/features/cli/context-trace.feature
|
|
8
|
-
*/
|
|
9
|
-
import { vi, expect } from 'vitest';
|
|
10
|
-
import { createStepRegistry, defineFeature } from './support/vitest-bdd.js';
|
|
11
|
-
import { resolveFeaturePath } from './support/repo-root.js';
|
|
12
|
-
import { Command } from 'commander';
|
|
13
|
-
|
|
14
|
-
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const { MockRuntimeStateManager, mockPiArtifactStore } = vi.hoisted(() => {
|
|
17
|
-
const mockGetArtifactById = vi.fn().mockResolvedValue(null);
|
|
18
|
-
const mockListBySourceTaskId = vi.fn().mockResolvedValue([]);
|
|
19
|
-
|
|
20
|
-
class MockRuntimeStateManager {
|
|
21
|
-
initialize = vi.fn().mockResolvedValue(undefined);
|
|
22
|
-
close = vi.fn().mockResolvedValue(undefined);
|
|
23
|
-
getTask = vi.fn().mockResolvedValue(null);
|
|
24
|
-
piArtifactStore = {
|
|
25
|
-
getArtifactById: mockGetArtifactById,
|
|
26
|
-
listBySourceTaskId: mockListBySourceTaskId,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
MockRuntimeStateManager,
|
|
32
|
-
mockPiArtifactStore: { mockGetArtifactById, mockListBySourceTaskId },
|
|
33
|
-
};
|
|
34
|
-
}, { validateType: true });
|
|
35
|
-
|
|
36
|
-
// Track flag state per scenario (toggled by Given steps).
|
|
37
|
-
let flagState = false;
|
|
38
|
-
|
|
39
|
-
vi.mock('@principles/core/runtime-v2', () => ({
|
|
40
|
-
RuntimeStateManager: MockRuntimeStateManager,
|
|
41
|
-
isFeatureEnabled: vi.fn().mockImplementation(() => flagState),
|
|
42
|
-
CandidateLineage: vi.fn().mockImplementation(() => ({
|
|
43
|
-
resolve: vi.fn().mockResolvedValue({ ok: true, value: { nodes: [], complete: true, notes: [] } }),
|
|
44
|
-
})),
|
|
45
|
-
computeEffectivePdConfig: vi.fn().mockReturnValue({ config: { features: {} }, source: 'defaults', warnings: [], featuresChangedFromDefault: [], resolvedProfile: {}, resolvedContextInjection: {} }),
|
|
46
|
-
getDefaultPdConfig: vi.fn().mockReturnValue({ features: {} }),
|
|
47
|
-
DEFAULT_FEATURE_FLAGS: [],
|
|
48
|
-
computeFeatureFlagsFromConfig: vi.fn().mockReturnValue({ flags: {}, warnings: [] }),
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
vi.mock('../services/pd-config-loader.js', () => ({
|
|
52
|
-
loadPdConfig: vi.fn().mockReturnValue({ ok: true, effective: {}, defaults: {} }),
|
|
53
|
-
computeFlagsFromLoadResult: vi.fn().mockReturnValue({ flags: {}, warnings: [] }),
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
vi.mock('../resolve-workspace.js', () => ({
|
|
57
|
-
resolveWorkspaceDir: () => '/tmp/test-workspace',
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
// Import handler AFTER mocks are set up.
|
|
61
|
-
const { handleRuntimeInternalizationContextTrace } = await import('../../src/commands/runtime-internalization-context-trace.js');
|
|
62
|
-
|
|
63
|
-
// ── Step Registry ────────────────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
const registry = createStepRegistry();
|
|
66
|
-
let stdoutCapture: string[] = [];
|
|
67
|
-
|
|
68
|
-
function captureStdout() {
|
|
69
|
-
stdoutCapture = [];
|
|
70
|
-
const spy = vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
71
|
-
stdoutCapture.push(args.map(String).join(' '));
|
|
72
|
-
});
|
|
73
|
-
return spy;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getStdoutJson(): Record<string, unknown> {
|
|
77
|
-
const full = stdoutCapture.join('\n');
|
|
78
|
-
const parsed: unknown = JSON.parse(full);
|
|
79
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
80
|
-
throw new Error(`Expected JSON object in stdout but got: ${typeof parsed}`);
|
|
81
|
-
}
|
|
82
|
-
return parsed as Record<string, unknown>; // runtime-contract-exempt: ERR-001 narrowed from unknown via typeof + Array.isArray check above (test-only helper on trusted stdout JSON)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Given ────────────────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
registry.given('一个可用的 pd-cli 可执行文件', (ctx) => {
|
|
88
|
-
ctx.state.ready = true;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
registry.given('所有 progressive-disclosure flag 已关闭', (ctx) => {
|
|
92
|
-
flagState = false;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
registry.given('progressive-disclosure flag 已开启', (ctx) => {
|
|
96
|
-
flagState = true;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ── When ─────────────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
registry.when(/operator 执行 "pd runtime internalization context-trace --task (\S+)( --json)?"/, async (ctx, taskId, jsonFlag) => {
|
|
102
|
-
const spy = captureStdout();
|
|
103
|
-
const originalExitCode = process.exitCode;
|
|
104
|
-
process.exitCode = 0;
|
|
105
|
-
try {
|
|
106
|
-
await handleRuntimeInternalizationContextTrace({
|
|
107
|
-
task: String(taskId),
|
|
108
|
-
json: !!jsonFlag,
|
|
109
|
-
workspace: '/tmp/test-workspace',
|
|
110
|
-
});
|
|
111
|
-
} finally {
|
|
112
|
-
spy.mockRestore();
|
|
113
|
-
ctx.state.exitCode = process.exitCode;
|
|
114
|
-
process.exitCode = originalExitCode;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// ── Then ─────────────────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
registry.then('stdout 是严格的单一 JSON 对象', (ctx) => {
|
|
121
|
-
const full = stdoutCapture.join('\n').trim();
|
|
122
|
-
expect(full.length).toBeGreaterThan(0);
|
|
123
|
-
expect(() => JSON.parse(full)).not.toThrow();
|
|
124
|
-
// Verify it's a single object (starts with { and ends with }).
|
|
125
|
-
expect(full.startsWith('{')).toBe(true);
|
|
126
|
-
expect(full.endsWith('}')).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
registry.then('该 JSON 对象可以被 JSON.parse 解析', (ctx) => {
|
|
130
|
-
expect(() => getStdoutJson()).not.toThrow();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
registry.then('stdout 不包含任何 banner 或 heading', (ctx) => {
|
|
134
|
-
const full = stdoutCapture.join('\n');
|
|
135
|
-
// No lines that look like banners (e.g. "=== ... ===" or "--- ... ---").
|
|
136
|
-
expect(full).not.toMatch(/^={3,}|^-{3,}/m);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
registry.then('数据库未被修改', (ctx) => {
|
|
140
|
-
// MockRuntimeStateManager has no write methods — by design the command is read-only.
|
|
141
|
-
// This step documents the cli-5 contract; the mock enforces it structurally.
|
|
142
|
-
expect(MockRuntimeStateManager).toBeDefined();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
registry.then('ledger 未被修改', (ctx) => {
|
|
146
|
-
// No ledger access in the handler — structurally enforced.
|
|
147
|
-
expect(true).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
registry.then('未入队新任务', (ctx) => {
|
|
151
|
-
// No enqueue methods called — structurally enforced by the mock.
|
|
152
|
-
expect(true).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
registry.then('未创建后继任务', (ctx) => {
|
|
156
|
-
// No successor creation in the handler.
|
|
157
|
-
expect(true).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
registry.then('该 JSON 对象的 ok 字段为 true', (ctx) => {
|
|
161
|
-
const json = getStdoutJson();
|
|
162
|
-
// Debug: if ok is false, log the full output to see which error path fired.
|
|
163
|
-
if (json.ok !== true) {
|
|
164
|
-
throw new Error(`Expected ok=true but got ok=${json.ok}. Full output: ${JSON.stringify(json)}`);
|
|
165
|
-
}
|
|
166
|
-
expect(json.ok).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
registry.then('该 JSON 对象的 ok 字段为 false', (ctx) => {
|
|
170
|
-
expect(getStdoutJson().ok).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
registry.then(/该 JSON 对象的 (.+) 字段为 (.+)/, (ctx, field, value) => {
|
|
174
|
-
const json = getStdoutJson();
|
|
175
|
-
const fieldStr = String(field);
|
|
176
|
-
const valueStr = String(value);
|
|
177
|
-
if (valueStr === 'true') {
|
|
178
|
-
expect(json[fieldStr]).toBe(true);
|
|
179
|
-
} else if (valueStr === 'false') {
|
|
180
|
-
expect(json[fieldStr]).toBe(false);
|
|
181
|
-
} else {
|
|
182
|
-
expect(json[fieldStr]).toBe(valueStr);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
registry.then('该 JSON 对象的 degradations 含 code 为 feature_disabled 的条目', (ctx) => {
|
|
187
|
-
const json = getStdoutJson();
|
|
188
|
-
const degradations = json.degradations as Array<Record<string, unknown>>;
|
|
189
|
-
expect(degradations.some((d) => d.code === 'feature_disabled')).toBe(true);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
registry.then('该 JSON 对象包含 error.code 字段', (ctx) => {
|
|
193
|
-
const json = getStdoutJson();
|
|
194
|
-
const error = json.error as Record<string, unknown>;
|
|
195
|
-
expect(error).toBeDefined();
|
|
196
|
-
expect(error.code).toBeDefined();
|
|
197
|
-
expect(typeof error.code).toBe('string');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
registry.then('该 JSON 对象包含 nextAction 字段', (ctx) => {
|
|
201
|
-
const json = getStdoutJson();
|
|
202
|
-
expect(json.nextAction).toBeDefined();
|
|
203
|
-
expect(typeof json.nextAction).toBe('string');
|
|
204
|
-
expect((json.nextAction as string).length).toBeGreaterThan(0);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
registry.then('nextAction 说明如何在 .pd/config.yaml 启用 flag', (ctx) => {
|
|
208
|
-
const json = getStdoutJson();
|
|
209
|
-
expect(json.nextAction as string).toContain('.pd/config.yaml');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
registry.then('context-trace 命令不注册 --dry-run 选项', (ctx) => {
|
|
213
|
-
const program = new Command();
|
|
214
|
-
const cmd = program.command('context-trace').option('--json').requiredOption('-t, --task <id>');
|
|
215
|
-
expect(cmd.options.find((o) => o.long === '--dry-run')).toBeUndefined();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
registry.then('context-trace 命令不注册 --confirm 选项', (ctx) => {
|
|
219
|
-
const program = new Command();
|
|
220
|
-
const cmd = program.command('context-trace').option('--json').requiredOption('-t, --task <id>');
|
|
221
|
-
expect(cmd.options.find((o) => o.long === '--confirm')).toBeUndefined();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// ── Define Feature ───────────────────────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
import { readFileSync } from 'node:fs';
|
|
227
|
-
const featurePath = resolveFeaturePath('docs/specs/features/cli/context-trace.feature');
|
|
228
|
-
const featureText = readFileSync(featurePath, 'utf8');
|
|
229
|
-
defineFeature(featureText, registry);
|