@principles/pd-cli 1.129.0 → 1.130.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.
@@ -62,9 +62,11 @@ vi.mock('@principles/core/principle-tree-ledger', () => ({
62
62
  loadLedger: vi.fn().mockReturnValue({ tree: { principles: {} } }),
63
63
  }));
64
64
 
65
- const mockLoadEffectiveFeatureFlags = vi.fn();
66
- vi.mock('../feature-flag-loader.js', () => ({
67
- loadEffectiveFeatureFlags: mockLoadEffectiveFeatureFlags,
65
+ const mockLoadPdConfig = vi.fn();
66
+ const mockComputeFlagsFromLoadResult = vi.fn();
67
+ vi.mock('../pd-config-loader.js', () => ({
68
+ loadPdConfig: mockLoadPdConfig,
69
+ computeFlagsFromLoadResult: mockComputeFlagsFromLoadResult,
68
70
  }));
69
71
 
70
72
  const mockResolveRuntimeFromPdConfig = vi.fn();
@@ -136,8 +138,10 @@ describe('resolveRuntimeAdapterFromConfig (PRI-431)', () => {
136
138
  // no-op: valid config
137
139
  });
138
140
  // Default: feature flags have l2_dreamer disabled
139
- mockLoadEffectiveFeatureFlags.mockReturnValue({
141
+ mockLoadPdConfig.mockReturnValue({ ok: true, effective: {}, source: 'defaults' });
142
+ mockComputeFlagsFromLoadResult.mockReturnValue({
140
143
  flags: {},
144
+ enabledChannels: [],
141
145
  warnings: [],
142
146
  });
143
147
  });
@@ -288,8 +292,9 @@ describe('resolveRuntimeAdapterFromConfig (PRI-431)', () => {
288
292
  it('returns L2AgentLoopAdapter when runnerKind=dreamer + l2ArtifactReader + l2StateDir + flag enabled', () => {
289
293
  const config = makeValidPiAiConfig();
290
294
  mockResolveRuntimeFromPdConfig.mockReturnValue(makeResolvedConfig(config));
291
- mockLoadEffectiveFeatureFlags.mockReturnValue({
292
- flags: { l2_dreamer: { enabled: true } },
295
+ mockComputeFlagsFromLoadResult.mockReturnValue({
296
+ flags: { l2_dreamer: { id: 'l2_dreamer', enabled: true, category: 'quiet' } },
297
+ enabledChannels: [],
293
298
  warnings: [],
294
299
  });
295
300
  const fakeArtifactReader = { readArtifact: vi.fn() };
@@ -310,8 +315,9 @@ describe('resolveRuntimeAdapterFromConfig (PRI-431)', () => {
310
315
  it('falls back to PiAiRuntimeAdapter when l2_dreamer flag is disabled', () => {
311
316
  const config = makeValidPiAiConfig();
312
317
  mockResolveRuntimeFromPdConfig.mockReturnValue(makeResolvedConfig(config));
313
- mockLoadEffectiveFeatureFlags.mockReturnValue({
314
- flags: { l2_dreamer: { enabled: false } },
318
+ mockComputeFlagsFromLoadResult.mockReturnValue({
319
+ flags: { l2_dreamer: { id: 'l2_dreamer', enabled: false, category: 'quiet' } },
320
+ enabledChannels: [],
315
321
  warnings: [],
316
322
  });
317
323
 
@@ -330,8 +336,9 @@ describe('resolveRuntimeAdapterFromConfig (PRI-431)', () => {
330
336
  it('falls back to PiAiRuntimeAdapter when l2ArtifactReader is missing', () => {
331
337
  const config = makeValidPiAiConfig();
332
338
  mockResolveRuntimeFromPdConfig.mockReturnValue(makeResolvedConfig(config));
333
- mockLoadEffectiveFeatureFlags.mockReturnValue({
334
- flags: { l2_dreamer: { enabled: true } },
339
+ mockComputeFlagsFromLoadResult.mockReturnValue({
340
+ flags: { l2_dreamer: { id: 'l2_dreamer', enabled: true, category: 'quiet' } },
341
+ enabledChannels: [],
335
342
  warnings: [],
336
343
  });
337
344
 
@@ -349,8 +356,9 @@ describe('resolveRuntimeAdapterFromConfig (PRI-431)', () => {
349
356
  it('falls back to PiAiRuntimeAdapter when runnerKind is not dreamer', () => {
350
357
  const config = makeValidPiAiConfig();
351
358
  mockResolveRuntimeFromPdConfig.mockReturnValue(makeResolvedConfig(config));
352
- mockLoadEffectiveFeatureFlags.mockReturnValue({
353
- flags: { l2_dreamer: { enabled: true } },
359
+ mockComputeFlagsFromLoadResult.mockReturnValue({
360
+ flags: { l2_dreamer: { id: 'l2_dreamer', enabled: true, category: 'quiet' } },
361
+ enabledChannels: [],
354
362
  warnings: [],
355
363
  });
356
364
 
@@ -13,7 +13,7 @@
13
13
  * - CLI-gate concerns (process.exit, telemetry, help text) stay at call sites.
14
14
  * This resolver throws structured `ConfigResolutionError` that handlers translate.
15
15
  * - Lives in pd-cli/services (NOT core) because it constructs adapters and reads
16
- * CLI feature flags via `loadEffectiveFeatureFlags`, which is a pd-cli service.
16
+ * PD config via `loadPdConfig` + `computeFlagsFromLoadResult`, which are pd-cli services.
17
17
  *
18
18
  * ERR refs:
19
19
  * - ERR-001 (no any): all types explicit
@@ -34,7 +34,7 @@ import {
34
34
  } from '@principles/core/runtime-v2';
35
35
  import type { PDRuntimeAdapter, PdL2ArtifactReader, RuntimeConfig, RuntimeConfigResult } from '@principles/core/runtime-v2';
36
36
  import { loadLedger } from '@principles/core/principle-tree-ledger';
37
- import { loadEffectiveFeatureFlags } from './feature-flag-loader.js';
37
+ import { loadPdConfig, computeFlagsFromLoadResult } from './pd-config-loader.js';
38
38
  import { resolveRuntimeFromPdConfig } from './resolve-runtime-from-pd-config.js';
39
39
  import type { ResolvedRuntimeFromPdConfig } from './resolve-runtime-from-pd-config.js';
40
40
 
@@ -255,7 +255,7 @@ export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PD
255
255
  // dreamer runner, route through the L2 multi-turn agent loop adapter.
256
256
  // Other runners (philosopher/scribe/...) stay on L1.
257
257
  if (opts.runnerKind === 'dreamer' && opts.l2ArtifactReader && opts.l2StateDir) {
258
- const effectiveFlags = loadEffectiveFeatureFlags(opts.workspaceDir);
258
+ const effectiveFlags = computeFlagsFromLoadResult(loadPdConfig(opts.workspaceDir));
259
259
  const l2FlagDef = Object.hasOwn(effectiveFlags.flags, 'l2_dreamer')
260
260
  ? effectiveFlags.flags.l2_dreamer
261
261
  : undefined;
@@ -46,22 +46,17 @@ vi.mock('@principles/core/runtime-v2', () => ({
46
46
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
47
47
  }));
48
48
 
49
- vi.mock('../../src/services/feature-flag-loader.js', () => ({
50
- loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
51
- source: 'defaults',
52
- configPath: '/fake/workspace/.pd/feature-flags.yaml',
53
- flags: {
54
- prompt: { id: 'prompt', category: 'core', enabled: true, since: '2026-05-24' },
55
- code_tool_hook: { id: 'code_tool_hook', category: 'core', enabled: true, since: '2026-05-24' },
56
- defer_archive: { id: 'defer_archive', category: 'core', enabled: true, since: '2026-05-24' },
57
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
58
- },
59
- warnings: [],
60
- }),
49
+ const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
50
+ mockLoadPdConfig: vi.fn(),
51
+ mockComputeFlagsFromLoadResult: vi.fn(),
52
+ }));
53
+
54
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
55
+ loadPdConfig: mockLoadPdConfig,
56
+ computeFlagsFromLoadResult: mockComputeFlagsFromLoadResult,
61
57
  }));
62
58
 
63
59
  import { runCanaryChecks } from '../../src/commands/runtime-canary.js';
64
- import { loadEffectiveFeatureFlags } from '../../src/services/feature-flag-loader.js';
65
60
 
66
61
  const WS = '/fake/workspace';
67
62
 
@@ -126,6 +121,26 @@ describe('runCanaryChecks', () => {
126
121
  mockAuditConsistency.mockResolvedValue({ status: 'ok', consumedCount: 0, orphanCandidateCount: 0, missingLedgerCount: 0 });
127
122
  mockBuildGfiSnapshot.mockReturnValue(healthyGfiSnapshot());
128
123
  mockClassifyGfiHealth.mockReturnValue({ status: 'healthy', reason: '0 active, 0 stale sessions', staleGfiDegradedThreshold: 40 });
124
+ // Default: GFI enabled, no warnings (canonical config loader)
125
+ mockLoadPdConfig.mockReturnValue({
126
+ ok: true,
127
+ effective: {},
128
+ source: 'defaults',
129
+ configPath: `${WS}/.pd/config.yaml`,
130
+ warnings: [],
131
+ legacyFilesDetected: [],
132
+ legacyFileNextActions: [],
133
+ });
134
+ mockComputeFlagsFromLoadResult.mockReturnValue({
135
+ flags: {
136
+ prompt: { id: 'prompt', category: 'core', enabled: true },
137
+ code_tool_hook: { id: 'code_tool_hook', category: 'core', enabled: true },
138
+ defer_archive: { id: 'defer_archive', category: 'core', enabled: true },
139
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
140
+ },
141
+ enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
142
+ warnings: [],
143
+ });
129
144
  });
130
145
 
131
146
  it('returns healthy when all checks are healthy', async () => {
@@ -275,12 +290,11 @@ describe('runCanaryChecks', () => {
275
290
 
276
291
  describe('GFI config warning degraded status', () => {
277
292
  it('returns healthy when GFI disabled with no warnings', async () => {
278
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
279
- source: 'defaults',
280
- configPath: `${WS}/.pd/feature-flags.yaml`,
293
+ mockComputeFlagsFromLoadResult.mockReturnValue({
281
294
  flags: {
282
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
295
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
283
296
  },
297
+ enabledChannels: [],
284
298
  warnings: [],
285
299
  });
286
300
 
@@ -292,13 +306,12 @@ describe('runCanaryChecks', () => {
292
306
  });
293
307
 
294
308
  it('returns degraded when GFI disabled but config has warnings (malformed YAML)', async () => {
295
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
296
- source: 'defaults',
297
- configPath: `${WS}/.pd/feature-flags.yaml`,
309
+ mockComputeFlagsFromLoadResult.mockReturnValue({
298
310
  flags: {
299
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
311
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
300
312
  },
301
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
313
+ enabledChannels: [],
314
+ warnings: ['.pd/config.yaml: YAML parse error, using defaults'],
302
315
  });
303
316
 
304
317
  const result = await runCanaryChecks(WS);
@@ -307,16 +320,15 @@ describe('runCanaryChecks', () => {
307
320
  expect(gfiCheck?.status).toBe('degraded');
308
321
  expect(gfiCheck?.summary).toContain('warnings');
309
322
  expect(gfiCheck?.details).toBeDefined();
310
- expect(result.recommendedNextActions.some(a => a.includes('feature-flags.yaml'))).toBe(true);
323
+ expect(result.recommendedNextActions.some(a => a.includes('config.yaml'))).toBe(true);
311
324
  });
312
325
 
313
326
  it('returns degraded when GFI disabled but config has malformed override warning', async () => {
314
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
315
- source: 'workspace_file',
316
- configPath: `${WS}/.pd/feature-flags.yaml`,
327
+ mockComputeFlagsFromLoadResult.mockReturnValue({
317
328
  flags: {
318
- gfi: { id: 'gfi', category: 'quiet', enabled: false, since: '2026-05-24' },
329
+ gfi: { id: 'gfi', category: 'quiet', enabled: false },
319
330
  },
331
+ enabledChannels: [],
320
332
  warnings: ["flag 'gfi': malformed override kept default (enabled must be boolean)"],
321
333
  });
322
334
 
@@ -324,16 +336,15 @@ describe('runCanaryChecks', () => {
324
336
  const gfiCheck = result.checks.find(c => c.name === 'gfi_snapshot');
325
337
 
326
338
  expect(gfiCheck?.status).toBe('degraded');
327
- expect(result.recommendedNextActions.some(a => a.includes('feature-flags.yaml') || a.includes('pd runtime features'))).toBe(true);
339
+ expect(result.recommendedNextActions.some(a => a.includes('config.yaml') || a.includes('pd runtime features'))).toBe(true);
328
340
  });
329
341
 
330
342
  it('runs GFI snapshot when flag enabled with no warnings', async () => {
331
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
332
- source: 'workspace_file',
333
- configPath: `${WS}/.pd/feature-flags.yaml`,
343
+ mockComputeFlagsFromLoadResult.mockReturnValue({
334
344
  flags: {
335
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
345
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
336
346
  },
347
+ enabledChannels: [],
337
348
  warnings: [],
338
349
  });
339
350
 
@@ -346,12 +357,11 @@ describe('runCanaryChecks', () => {
346
357
  });
347
358
 
348
359
  it('recommends session lifecycle review for GFI session issues (not config)', async () => {
349
- vi.mocked(loadEffectiveFeatureFlags).mockReturnValue({
350
- source: 'defaults',
351
- configPath: `${WS}/.pd/feature-flags.yaml`,
360
+ mockComputeFlagsFromLoadResult.mockReturnValue({
352
361
  flags: {
353
- gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
362
+ gfi: { id: 'gfi', category: 'quiet', enabled: true },
354
363
  },
364
+ enabledChannels: [],
355
365
  warnings: [],
356
366
  });
357
367
  mockClassifyGfiHealth.mockReturnValue({
@@ -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
- }