@principles/pd-cli 1.129.0 → 1.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +9 -0
  2. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/intent-flag-wiring.test.js +166 -0
  4. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -0
  5. package/dist/commands/intent.d.ts +59 -0
  6. package/dist/commands/intent.d.ts.map +1 -0
  7. package/dist/commands/intent.js +350 -0
  8. package/dist/commands/intent.js.map +1 -0
  9. package/dist/commands/runtime-canary.d.ts.map +1 -1
  10. package/dist/commands/runtime-canary.js +10 -11
  11. package/dist/commands/runtime-canary.js.map +1 -1
  12. package/dist/commands/runtime-internalization-queue.d.ts.map +1 -1
  13. package/dist/commands/runtime-internalization-queue.js +13 -16
  14. package/dist/commands/runtime-internalization-queue.js.map +1 -1
  15. package/dist/index.js +4 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/services/__tests__/runtime-adapter-resolver.test.js +20 -12
  18. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  19. package/dist/services/runtime-adapter-resolver.d.ts +1 -1
  20. package/dist/services/runtime-adapter-resolver.js +3 -3
  21. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/commands/__tests__/intent-flag-wiring.test.ts +197 -0
  24. package/src/commands/intent.ts +388 -0
  25. package/src/commands/runtime-canary.ts +11 -13
  26. package/src/commands/runtime-internalization-queue.ts +14 -19
  27. package/src/index.ts +6 -0
  28. package/src/services/__tests__/runtime-adapter-resolver.test.ts +20 -12
  29. package/src/services/runtime-adapter-resolver.ts +3 -3
  30. package/tests/commands/intent.test.ts +321 -0
  31. package/tests/commands/runtime-canary.test.ts +46 -36
  32. package/tests/commands/runtime-internalization-queue.test.ts +11 -24
  33. package/tests/commands/runtime-internalization-run-once.test.ts +14 -6
  34. package/dist/services/feature-flag-loader.d.ts +0 -6
  35. package/dist/services/feature-flag-loader.d.ts.map +0 -1
  36. package/dist/services/feature-flag-loader.js +0 -53
  37. package/dist/services/feature-flag-loader.js.map +0 -1
  38. package/src/services/feature-flag-loader.ts +0 -73
  39. package/tests/services/feature-flag-loader.test.ts +0 -207
@@ -24,27 +24,14 @@ vi.mock('@principles/core/runtime-v2', () => ({
24
24
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
25
25
  }));
26
26
 
27
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
28
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
29
- flags: {
30
- prompt: { id: 'prompt', enabled: true, category: 'core' },
31
- code_tool_hook: { id: 'code_tool_hook', enabled: true, category: 'core' },
32
- defer_archive: { id: 'defer_archive', enabled: true, category: 'core' },
33
- },
34
- source: 'defaults',
35
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
36
- warnings: [],
37
- }),
38
- }));
39
-
40
27
  const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
41
- mockLoadPdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults' }),
28
+ mockLoadPdConfig: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults' }),
42
29
  mockComputeFlagsFromLoadResult: vi.fn().mockReturnValue({
43
30
  flags: {
44
31
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
45
32
  },
46
- source: 'defaults',
47
- errors: [],
33
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
34
+ warnings: [],
48
35
  }),
49
36
  }));
50
37
 
@@ -89,8 +76,8 @@ describe('handleRuntimeInternalizationQueue', () => {
89
76
  flags: {
90
77
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
91
78
  },
92
- source: 'defaults',
93
- errors: [],
79
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
80
+ warnings: [],
94
81
  });
95
82
  });
96
83
 
@@ -308,8 +295,8 @@ describe('handleRuntimeInternalizationQueue', () => {
308
295
  flags: {
309
296
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
310
297
  },
311
- source: 'config',
312
- errors: [],
298
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
299
+ warnings: [],
313
300
  });
314
301
 
315
302
  await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
@@ -332,8 +319,8 @@ describe('handleRuntimeInternalizationQueue', () => {
332
319
  flags: {
333
320
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
334
321
  },
335
- source: 'config',
336
- errors: [],
322
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
323
+ warnings: [],
337
324
  });
338
325
 
339
326
  await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
@@ -379,8 +366,8 @@ describe('handleRuntimeInternalizationQueue', () => {
379
366
  flags: {
380
367
  internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
381
368
  },
382
- source: 'config',
383
- errors: [],
369
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
370
+ warnings: [],
384
371
  });
385
372
 
386
373
  await handleRuntimeInternalizationQueue({ workspace: WS, json: false });
@@ -41,14 +41,22 @@ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
41
41
  resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
42
42
  }));
43
43
 
44
- // PRI-431: Mock feature-flag-loader so the shared resolver's L2 dreamer sub-branch
45
- // doesn't call the real loadEffectiveFeatureFlags (which needs computeEffectiveFlags
46
- // from @principles/core/runtime-v2 not included in the mock above).
47
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
48
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
44
+ // PRI-460: Mock pd-config-loader so the shared resolver's L2 dreamer sub-branch
45
+ // doesn't call the real loadPdConfig (which needs fs access).
46
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
47
+ loadPdConfig: vi.fn().mockReturnValue({
48
+ ok: true,
49
+ effective: {},
50
+ source: 'defaults',
51
+ configPath: '/fake/workspace/.pd/config.yaml',
52
+ warnings: [],
53
+ legacyFilesDetected: [],
54
+ legacyFileNextActions: [],
55
+ }),
56
+ computeFlagsFromLoadResult: vi.fn().mockReturnValue({
49
57
  flags: {},
58
+ enabledChannels: [],
50
59
  warnings: [],
51
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
52
60
  }),
53
61
  }));
54
62
 
@@ -1,6 +0,0 @@
1
- import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
2
- export declare const FEATURE_FLAGS_CONFIG_FILENAME = "feature-flags.yaml";
3
- export declare const FEATURE_FLAGS_CONFIG_DIR = ".pd";
4
- export declare function getFeatureFlagsConfigPath(workspaceDir: string): string;
5
- export declare function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags;
6
- //# sourceMappingURL=feature-flag-loader.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"feature-flag-loader.d.ts","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEzE,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAClE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAQ9C,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,CAkDrF"}
@@ -1,53 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, } from '@principles/core/runtime-v2';
5
- export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
6
- export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
7
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
8
- function isRecord(value) {
9
- return value !== null && typeof value === 'object' && !Array.isArray(value);
10
- }
11
- export function getFeatureFlagsConfigPath(workspaceDir) {
12
- return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
13
- }
14
- export function loadEffectiveFeatureFlags(workspaceDir) {
15
- const configPath = getFeatureFlagsConfigPath(workspaceDir);
16
- if (!fs.existsSync(configPath)) {
17
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
18
- }
19
- const raw = fs.readFileSync(configPath, 'utf8');
20
- let parsed;
21
- try {
22
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
23
- }
24
- catch {
25
- return {
26
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
27
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
28
- };
29
- }
30
- if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
31
- return {
32
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
33
- warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
34
- };
35
- }
36
- const parsedRecord = Object.create(null);
37
- const warnings = [];
38
- for (const key of Object.keys(parsed)) {
39
- if (DANGEROUS_KEYS.has(key)) {
40
- warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
41
- continue;
42
- }
43
- if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
44
- parsedRecord[key] = parsed[key];
45
- }
46
- }
47
- const result = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
48
- if (warnings.length > 0) {
49
- result.warnings = [...warnings, ...result.warnings];
50
- }
51
- return result;
52
- }
53
- //# sourceMappingURL=feature-flag-loader.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"feature-flag-loader.js","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"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"}
@@ -1,73 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import {
5
- computeEffectiveFlags,
6
- DEFAULT_FEATURE_FLAGS,
7
- } from '@principles/core/runtime-v2';
8
- import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
9
-
10
- export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
11
- export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
12
-
13
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
14
-
15
- function isRecord(value: unknown): value is Record<string, unknown> {
16
- return value !== null && typeof value === 'object' && !Array.isArray(value);
17
- }
18
-
19
- export function getFeatureFlagsConfigPath(workspaceDir: string): string {
20
- return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
21
- }
22
-
23
- export function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags {
24
- const configPath = getFeatureFlagsConfigPath(workspaceDir);
25
-
26
- if (!fs.existsSync(configPath)) {
27
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
28
- }
29
-
30
- const raw = fs.readFileSync(configPath, 'utf8');
31
-
32
-
33
- let parsed: unknown;
34
- try {
35
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
36
- } catch {
37
- return {
38
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
39
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
40
- };
41
- }
42
-
43
- if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
44
- return {
45
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
46
- warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
47
- };
48
- }
49
-
50
- const parsedRecord: Record<string, unknown> = Object.create(null);
51
- const warnings: string[] = [];
52
- for (const key of Object.keys(parsed)) {
53
- if (DANGEROUS_KEYS.has(key)) {
54
- warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
55
- continue;
56
- }
57
- if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
58
- parsedRecord[key] = parsed[key];
59
- }
60
- }
61
-
62
- const result = computeEffectiveFlags(
63
- parsedRecord,
64
- DEFAULT_FEATURE_FLAGS,
65
- configPath,
66
- );
67
-
68
- if (warnings.length > 0) {
69
- result.warnings = [...warnings, ...result.warnings];
70
- }
71
-
72
- return result;
73
- }
@@ -1,207 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
- import * as os from 'node:os';
5
- import {
6
- loadEffectiveFeatureFlags,
7
- getFeatureFlagsConfigPath,
8
- FEATURE_FLAGS_CONFIG_FILENAME,
9
- FEATURE_FLAGS_CONFIG_DIR,
10
- } from '../../src/services/feature-flag-loader.js';
11
-
12
- function createTempWorkspace(): string {
13
- return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-ffl-test-'));
14
- }
15
-
16
- function writeConfig(tmpDir: string, content: string): void {
17
- const configDir = path.join(tmpDir, FEATURE_FLAGS_CONFIG_DIR);
18
- fs.mkdirSync(configDir, { recursive: true });
19
- fs.writeFileSync(path.join(configDir, FEATURE_FLAGS_CONFIG_FILENAME), content, 'utf8');
20
- }
21
-
22
- function cleanup(tmpDir: string): void {
23
- fs.rmSync(tmpDir, { recursive: true, force: true });
24
- }
25
-
26
- describe('feature-flag-loader', () => {
27
- describe('getFeatureFlagsConfigPath', () => {
28
- it('should return path under .pd/feature-flags.yaml', () => {
29
- const result = getFeatureFlagsConfigPath('/workspace/project');
30
- expect(result).toBe(path.join('/workspace/project', '.pd', 'feature-flags.yaml'));
31
- });
32
-
33
- it('should use FEATURE_FLAGS_CONFIG_DIR and FEATURE_FLAGS_CONFIG_FILENAME constants', () => {
34
- const result = getFeatureFlagsConfigPath('/root');
35
- expect(result).toContain(FEATURE_FLAGS_CONFIG_DIR);
36
- expect(result).toContain(FEATURE_FLAGS_CONFIG_FILENAME);
37
- });
38
- });
39
-
40
- describe('loadEffectiveFeatureFlags', () => {
41
- it('should return defaults when config file does not exist', () => {
42
- const tmpDir = createTempWorkspace();
43
- try {
44
- const result = loadEffectiveFeatureFlags(tmpDir);
45
- expect(result.source).toBe('defaults');
46
- expect(result.warnings).toEqual([]);
47
- } finally {
48
- cleanup(tmpDir);
49
- }
50
- });
51
-
52
- it('should return defaults with YAML parse error warning for malformed YAML', () => {
53
- const tmpDir = createTempWorkspace();
54
- writeConfig(tmpDir, 'gfi: [unterminated');
55
- try {
56
- const result = loadEffectiveFeatureFlags(tmpDir);
57
- expect(result.warnings.some(w => w.includes('YAML parse error'))).toBe(true);
58
- } finally {
59
- cleanup(tmpDir);
60
- }
61
- });
62
-
63
- it('should return defaults with mapping warning when YAML contains an array', () => {
64
- const tmpDir = createTempWorkspace();
65
- writeConfig(tmpDir, '- item1\n- item2\n');
66
- try {
67
- const result = loadEffectiveFeatureFlags(tmpDir);
68
- expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
69
- } finally {
70
- cleanup(tmpDir);
71
- }
72
- });
73
-
74
- it('should return defaults with mapping warning when YAML contains null', () => {
75
- const tmpDir = createTempWorkspace();
76
- writeConfig(tmpDir, '---\nnull\n');
77
- try {
78
- const result = loadEffectiveFeatureFlags(tmpDir);
79
- expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
80
- } finally {
81
- cleanup(tmpDir);
82
- }
83
- });
84
-
85
- it('should return defaults with mapping warning when YAML is undefined after parse', () => {
86
- const tmpDir = createTempWorkspace();
87
- writeConfig(tmpDir, '---\n');
88
- try {
89
- const result = loadEffectiveFeatureFlags(tmpDir);
90
- expect(result.source).toBe('defaults');
91
- expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
92
- } finally {
93
- cleanup(tmpDir);
94
- }
95
- });
96
-
97
- it('should return defaults with mapping warning when YAML contains a scalar string', () => {
98
- const tmpDir = createTempWorkspace();
99
- writeConfig(tmpDir, 'just a string\n');
100
- try {
101
- const result = loadEffectiveFeatureFlags(tmpDir);
102
- expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
103
- } finally {
104
- cleanup(tmpDir);
105
- }
106
- });
107
-
108
- it('should return defaults with mapping warning when YAML contains a number', () => {
109
- const tmpDir = createTempWorkspace();
110
- writeConfig(tmpDir, '42\n');
111
- try {
112
- const result = loadEffectiveFeatureFlags(tmpDir);
113
- expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
114
- } finally {
115
- cleanup(tmpDir);
116
- }
117
- });
118
-
119
- it('should reject __proto__ dangerous key', () => {
120
- const tmpDir = createTempWorkspace();
121
- writeConfig(tmpDir, '"__proto__":\n enabled: true\n');
122
- try {
123
- const result = loadEffectiveFeatureFlags(tmpDir);
124
- expect(result.warnings.some(w => w.includes("dangerous key '__proto__' rejected"))).toBe(true);
125
- expect(Object.hasOwn(result.flags, '__proto__')).toBe(false);
126
- } finally {
127
- cleanup(tmpDir);
128
- }
129
- });
130
-
131
- it('should reject constructor dangerous key', () => {
132
- const tmpDir = createTempWorkspace();
133
- writeConfig(tmpDir, 'constructor:\n enabled: true\n');
134
- try {
135
- const result = loadEffectiveFeatureFlags(tmpDir);
136
- expect(result.warnings.some(w => w.includes("dangerous key 'constructor' rejected"))).toBe(true);
137
- expect(Object.hasOwn(result.flags, 'constructor')).toBe(false);
138
- } finally {
139
- cleanup(tmpDir);
140
- }
141
- });
142
-
143
- it('should reject prototype dangerous key', () => {
144
- const tmpDir = createTempWorkspace();
145
- writeConfig(tmpDir, 'prototype:\n enabled: true\n');
146
- try {
147
- const result = loadEffectiveFeatureFlags(tmpDir);
148
- expect(result.warnings.some(w => w.includes("dangerous key 'prototype' rejected"))).toBe(true);
149
- expect(Object.hasOwn(result.flags, 'prototype')).toBe(false);
150
- } finally {
151
- cleanup(tmpDir);
152
- }
153
- });
154
-
155
- it('should allow valid keys while rejecting dangerous ones', () => {
156
- const tmpDir = createTempWorkspace();
157
- writeConfig(tmpDir, 'gfi:\n enabled: true\nprototype:\n enabled: true\n');
158
- try {
159
- const result = loadEffectiveFeatureFlags(tmpDir);
160
- expect(result.warnings.some(w => w.includes("dangerous key 'prototype' rejected"))).toBe(true);
161
- expect(result.flags.gfi).toBeDefined();
162
- if (result.flags.gfi) {
163
- expect(result.flags.gfi.enabled).toBe(true);
164
- }
165
- } finally {
166
- cleanup(tmpDir);
167
- }
168
- });
169
-
170
- it('should parse valid YAML config and set source to workspace_file', () => {
171
- const tmpDir = createTempWorkspace();
172
- writeConfig(tmpDir, 'gfi:\n enabled: true\n');
173
- try {
174
- const result = loadEffectiveFeatureFlags(tmpDir);
175
- expect(result.source).toBe('workspace_file');
176
- expect(result.flags.gfi).toBeDefined();
177
- if (result.flags.gfi) {
178
- expect(result.flags.gfi.enabled).toBe(true);
179
- }
180
- } finally {
181
- cleanup(tmpDir);
182
- }
183
- });
184
-
185
- it('should handle empty YAML mapping with defaults source', () => {
186
- const tmpDir = createTempWorkspace();
187
- writeConfig(tmpDir, '{}\n');
188
- try {
189
- const result = loadEffectiveFeatureFlags(tmpDir);
190
- expect(result.source).toBe('defaults');
191
- expect(result.warnings).toEqual([]);
192
- } finally {
193
- cleanup(tmpDir);
194
- }
195
- });
196
-
197
- it('should include configPath in result', () => {
198
- const tmpDir = createTempWorkspace();
199
- try {
200
- const result = loadEffectiveFeatureFlags(tmpDir);
201
- expect(result.configPath).toContain(FEATURE_FLAGS_CONFIG_FILENAME);
202
- } finally {
203
- cleanup(tmpDir);
204
- }
205
- });
206
- });
207
- });