dialekt 0.1.0 → 0.1.1

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 (71) hide show
  1. package/README.md +8 -10
  2. package/TESTING.md +29 -29
  3. package/dist/cli/main.d.mts +1 -1
  4. package/dist/cli/main.mjs +549 -362
  5. package/dist/formatters-De4Q-X1d.mjs +516 -435
  6. package/dist/index.d.mts +162 -25
  7. package/dist/index.mjs +119 -34
  8. package/package.json +3 -3
  9. package/pnpm-workspace.yaml +3 -3
  10. package/src/adapter/types.test.ts +57 -57
  11. package/src/adapter/types.ts +7 -4
  12. package/src/benchmark/metrics.test.ts +141 -69
  13. package/src/benchmark/metrics.ts +6 -6
  14. package/src/benchmark/report.test.ts +38 -38
  15. package/src/benchmark/report.ts +6 -6
  16. package/src/benchmark/runner.test.ts +70 -72
  17. package/src/benchmark/runner.ts +4 -4
  18. package/src/cli/commands/add.test.ts +90 -109
  19. package/src/cli/commands/add.ts +40 -28
  20. package/src/cli/commands/benchmark.test.ts +77 -64
  21. package/src/cli/commands/benchmark.ts +64 -41
  22. package/src/cli/commands/languages.test.ts +45 -42
  23. package/src/cli/commands/languages.ts +16 -12
  24. package/src/cli/commands/missing.test.ts +143 -92
  25. package/src/cli/commands/missing.ts +24 -17
  26. package/src/cli/commands/translate.test.ts +79 -79
  27. package/src/cli/commands/translate.ts +41 -31
  28. package/src/cli/commands/unused.test.ts +62 -51
  29. package/src/cli/commands/unused.ts +18 -14
  30. package/src/cli/commands/validate.test.ts +130 -72
  31. package/src/cli/commands/validate.ts +25 -20
  32. package/src/cli/config-resolution.test.ts +169 -49
  33. package/src/cli/config-resolution.ts +5 -7
  34. package/src/cli/format.test.ts +50 -50
  35. package/src/cli/format.ts +57 -60
  36. package/src/cli/formatters.test.ts +128 -106
  37. package/src/cli/formatters.ts +72 -95
  38. package/src/cli/main.ts +13 -13
  39. package/src/config/define-config.test.ts +44 -29
  40. package/src/config/define-config.ts +1 -1
  41. package/src/config/load-config.test.ts +21 -18
  42. package/src/config/load-config.ts +5 -5
  43. package/src/config/types.test.ts +50 -44
  44. package/src/config/types.ts +2 -2
  45. package/src/index.ts +22 -26
  46. package/src/keys/flatten.test.ts +52 -52
  47. package/src/keys/flatten.ts +7 -9
  48. package/src/sdk/file-io.test.ts +47 -59
  49. package/src/sdk/file-io.ts +2 -2
  50. package/src/sdk/node-layer.test.ts +18 -18
  51. package/src/sdk/node-layer.ts +2 -2
  52. package/src/sdk/php-array-reader.test.ts +49 -40
  53. package/src/sdk/php-array-reader.ts +5 -5
  54. package/src/translation/chunking.test.ts +52 -44
  55. package/src/translation/chunking.ts +1 -1
  56. package/src/translation/missing-keys.test.ts +86 -93
  57. package/src/translation/missing-keys.ts +4 -6
  58. package/src/translation/model-registry.test.ts +41 -32
  59. package/src/translation/model-registry.ts +9 -9
  60. package/src/translation/one-shot-strategy.test.ts +105 -86
  61. package/src/translation/one-shot-strategy.ts +10 -12
  62. package/src/translation/orchestrator.test.ts +90 -101
  63. package/src/translation/orchestrator.ts +26 -26
  64. package/src/translation/prompt.test.ts +76 -76
  65. package/src/translation/prompt.ts +2 -2
  66. package/src/translation/tool-loop-strategy.test.ts +134 -107
  67. package/src/translation/tool-loop-strategy.ts +14 -18
  68. package/src/translation/types.test.ts +22 -22
  69. package/src/translation/types.ts +3 -3
  70. package/tsdown.config.ts +3 -3
  71. package/vitest.config.ts +3 -3
@@ -1,69 +1,75 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { DialektConfig, ModelConfig, ChunkingConfig, RetryConfig } from './types.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import type { DialektConfig, ModelConfig, ChunkingConfig, RetryConfig } from "./types.js";
3
3
 
4
- describe('config type conformance', () => {
5
- it('accepts all known model providers', () => {
6
- const providers: ModelConfig['provider'][] = ['openai', 'anthropic', 'google', 'mistral', 'cohere'];
4
+ describe("config type conformance", () => {
5
+ it("accepts all known model providers", () => {
6
+ const providers: ModelConfig["provider"][] = [
7
+ "openai",
8
+ "anthropic",
9
+ "google",
10
+ "mistral",
11
+ "cohere",
12
+ ];
7
13
  for (const provider of providers) {
8
- const m: ModelConfig = { provider, modelId: 'test-model' };
14
+ const m: ModelConfig = { provider, modelId: "test-model" };
9
15
  expect(m.provider).toBe(provider);
10
16
  }
11
17
  });
12
18
 
13
- it('accepts any string provider at compile time', () => {
19
+ it("accepts any string provider at compile time", () => {
14
20
  // provider is typed as string, not a literal union
15
- const valid: ModelConfig = { provider: 'unknown-provider', modelId: 'x' };
16
- expect(valid.provider).toBe('unknown-provider');
21
+ const valid: ModelConfig = { provider: "unknown-provider", modelId: "x" };
22
+ expect(valid.provider).toBe("unknown-provider");
17
23
  });
18
24
 
19
- it('ChunkingConfig enforces positive maxTokens', () => {
25
+ it("ChunkingConfig enforces positive maxTokens", () => {
20
26
  const c: ChunkingConfig = { maxTokens: 1, charsPerToken: 1.0, concurrency: 1 };
21
27
  expect(c.maxTokens).toBe(1);
22
28
  expect(c.concurrency).toBe(1);
23
29
  });
24
30
 
25
- it('RetryConfig enforces positive maxAttempts', () => {
31
+ it("RetryConfig enforces positive maxAttempts", () => {
26
32
  const r: RetryConfig = { maxAttempts: 1, baseDelayMs: 0 };
27
33
  expect(r.maxAttempts).toBe(1);
28
34
  expect(r.baseDelayMs).toBe(0);
29
35
  });
30
36
 
31
- it('DialektConfig requires sourceLocale and targetLocales', () => {
37
+ it("DialektConfig requires sourceLocale and targetLocales", () => {
32
38
  const minimal: DialektConfig = {
33
- sourceLocale: 'en',
34
- targetLocales: ['de'],
35
- strategy: 'one-shot',
36
- model: { provider: 'openai', modelId: 'gpt-4o' },
37
- fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
39
+ sourceLocale: "en",
40
+ targetLocales: ["de"],
41
+ strategy: "one-shot",
42
+ model: { provider: "openai", modelId: "gpt-4o" },
43
+ fastModel: { provider: "openai", modelId: "gpt-4o-mini" },
38
44
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
39
45
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
40
46
  adapters: [],
41
47
  };
42
- expect(minimal.sourceLocale).toBe('en');
43
- expect(minimal.targetLocales).toEqual(['de']);
48
+ expect(minimal.sourceLocale).toBe("en");
49
+ expect(minimal.targetLocales).toEqual(["de"]);
44
50
  });
45
51
 
46
- it('DialektConfig accepts tool-loop-agent strategy', () => {
52
+ it("DialektConfig accepts tool-loop-agent strategy", () => {
47
53
  const config: DialektConfig = {
48
- sourceLocale: 'en',
49
- targetLocales: ['de'],
50
- strategy: 'tool-loop-agent',
51
- model: { provider: 'openai', modelId: 'gpt-4o' },
52
- fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
54
+ sourceLocale: "en",
55
+ targetLocales: ["de"],
56
+ strategy: "tool-loop-agent",
57
+ model: { provider: "openai", modelId: "gpt-4o" },
58
+ fastModel: { provider: "openai", modelId: "gpt-4o-mini" },
53
59
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
54
60
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
55
61
  adapters: [],
56
62
  };
57
- expect(config.strategy).toBe('tool-loop-agent');
63
+ expect(config.strategy).toBe("tool-loop-agent");
58
64
  });
59
65
 
60
- it('targetLocales can be empty', () => {
66
+ it("targetLocales can be empty", () => {
61
67
  const config: DialektConfig = {
62
- sourceLocale: 'en',
68
+ sourceLocale: "en",
63
69
  targetLocales: [],
64
- strategy: 'one-shot',
65
- model: { provider: 'openai', modelId: 'gpt-4o' },
66
- fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
70
+ strategy: "one-shot",
71
+ model: { provider: "openai", modelId: "gpt-4o" },
72
+ fastModel: { provider: "openai", modelId: "gpt-4o-mini" },
67
73
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
68
74
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
69
75
  adapters: [],
@@ -71,13 +77,13 @@ describe('config type conformance', () => {
71
77
  expect(config.targetLocales).toEqual([]);
72
78
  });
73
79
 
74
- it('accepts multiple target locales', () => {
80
+ it("accepts multiple target locales", () => {
75
81
  const config: DialektConfig = {
76
- sourceLocale: 'en',
77
- targetLocales: ['de', 'fr', 'es', 'ja'],
78
- strategy: 'one-shot',
79
- model: { provider: 'openai', modelId: 'gpt-4o' },
80
- fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
82
+ sourceLocale: "en",
83
+ targetLocales: ["de", "fr", "es", "ja"],
84
+ strategy: "one-shot",
85
+ model: { provider: "openai", modelId: "gpt-4o" },
86
+ fastModel: { provider: "openai", modelId: "gpt-4o-mini" },
81
87
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
82
88
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
83
89
  adapters: [],
@@ -85,17 +91,17 @@ describe('config type conformance', () => {
85
91
  expect(config.targetLocales).toHaveLength(4);
86
92
  });
87
93
 
88
- it('fastModel can be different provider from model', () => {
94
+ it("fastModel can be different provider from model", () => {
89
95
  const config: DialektConfig = {
90
- sourceLocale: 'en',
91
- targetLocales: ['de'],
92
- strategy: 'one-shot',
93
- model: { provider: 'openai', modelId: 'gpt-4o' },
94
- fastModel: { provider: 'anthropic', modelId: 'claude-3-haiku' },
96
+ sourceLocale: "en",
97
+ targetLocales: ["de"],
98
+ strategy: "one-shot",
99
+ model: { provider: "openai", modelId: "gpt-4o" },
100
+ fastModel: { provider: "anthropic", modelId: "claude-3-haiku" },
95
101
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
96
102
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
97
103
  adapters: [],
98
104
  };
99
- expect(config.fastModel.provider).toBe('anthropic');
105
+ expect(config.fastModel.provider).toBe("anthropic");
100
106
  });
101
107
  });
@@ -1,4 +1,4 @@
1
- import type { TranslationAdapter } from '../adapter/types.js';
1
+ import type { TranslationAdapter } from "../adapter/types.js";
2
2
 
3
3
  export interface ModelConfig {
4
4
  readonly provider: string;
@@ -19,7 +19,7 @@ export interface RetryConfig {
19
19
  export interface DialektConfig {
20
20
  readonly sourceLocale: string;
21
21
  readonly targetLocales: readonly string[] | null;
22
- readonly strategy: 'one-shot' | 'tool-loop-agent';
22
+ readonly strategy: "one-shot" | "tool-loop-agent";
23
23
  readonly model: ModelConfig;
24
24
  readonly fastModel: ModelConfig;
25
25
  readonly chunking: ChunkingConfig;
package/src/index.ts CHANGED
@@ -1,25 +1,21 @@
1
- export { defineConfig } from './config/define-config.js';
2
- export type { DialektConfig, ModelConfig, ChunkingConfig, RetryConfig } from './config/types.js';
3
- export type {
4
- ResourceRef,
5
- TranslationAdapter,
6
- AdapterCapabilities,
7
- } from './adapter/types.js';
8
- export { AdapterReadError, AdapterWriteError } from './adapter/types.js';
9
- export { flattenObject, unflattenObject, diffKeys } from './keys/flatten.js';
10
- export { chunkKeys } from './translation/chunking.js';
11
- export { NodePlatformLayer } from './sdk/node-layer.js';
12
- export { readFileIfExists, writeFileEnsuringDir } from './sdk/file-io.js';
13
- export { readPhpArrayAsJson, PhpExecutionError } from './sdk/php-array-reader.js';
14
- export { resolveModel, UnknownProviderError } from './translation/model-registry.js';
15
- export type { TranslationContext, TranslationStrategy } from './translation/types.js';
16
- export { TranslationFailedError } from './translation/types.js';
17
- export { createOneShotStrategy } from './translation/one-shot-strategy.js';
18
- export { createToolLoopStrategy } from './translation/tool-loop-strategy.js';
19
- export { runTranslation } from './translation/orchestrator.js';
20
- export { buildSystemPrompt, buildUserPrompt } from './translation/prompt.js';
21
- export { computeMissingKeys } from './translation/missing-keys.js';
22
- export { loadConfig, ConfigLoadError } from './config/load-config.js';
1
+ export { defineConfig } from "./config/define-config.js";
2
+ export type { DialektConfig, ModelConfig, ChunkingConfig, RetryConfig } from "./config/types.js";
3
+ export type { ResourceRef, TranslationAdapter, AdapterCapabilities } from "./adapter/types.js";
4
+ export { AdapterReadError, AdapterWriteError } from "./adapter/types.js";
5
+ export { flattenObject, unflattenObject, diffKeys } from "./keys/flatten.js";
6
+ export { chunkKeys } from "./translation/chunking.js";
7
+ export { NodePlatformLayer } from "./sdk/node-layer.js";
8
+ export { readFileIfExists, writeFileEnsuringDir } from "./sdk/file-io.js";
9
+ export { readPhpArrayAsJson, PhpExecutionError } from "./sdk/php-array-reader.js";
10
+ export { resolveModel, UnknownProviderError } from "./translation/model-registry.js";
11
+ export type { TranslationContext, TranslationStrategy } from "./translation/types.js";
12
+ export { TranslationFailedError } from "./translation/types.js";
13
+ export { createOneShotStrategy } from "./translation/one-shot-strategy.js";
14
+ export { createToolLoopStrategy } from "./translation/tool-loop-strategy.js";
15
+ export { runTranslation } from "./translation/orchestrator.js";
16
+ export { buildSystemPrompt, buildUserPrompt } from "./translation/prompt.js";
17
+ export { computeMissingKeys } from "./translation/missing-keys.js";
18
+ export { loadConfig, ConfigLoadError } from "./config/load-config.js";
23
19
  export {
24
20
  detectFormat,
25
21
  color,
@@ -32,8 +28,8 @@ export {
32
28
  warning,
33
29
  info,
34
30
  keyValue,
35
- } from './cli/format.js';
36
- export type { OutputFormat } from './cli/format.js';
31
+ } from "./cli/format.js";
32
+ export type { OutputFormat } from "./cli/format.js";
37
33
  export {
38
34
  formatMissingKeys,
39
35
  formatUnusedKeys,
@@ -43,7 +39,7 @@ export {
43
39
  formatAdd,
44
40
  formatBenchmark,
45
41
  formatError,
46
- } from './cli/formatters.js';
42
+ } from "./cli/formatters.js";
47
43
  export type {
48
44
  MissingKeyEntry,
49
45
  UnusedKeyEntry,
@@ -53,4 +49,4 @@ export type {
53
49
  TranslateResult,
54
50
  AddResult,
55
51
  BenchmarkEntry,
56
- } from './cli/formatters.js';
52
+ } from "./cli/formatters.js";
@@ -1,111 +1,111 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { flattenObject, unflattenObject, diffKeys } from './flatten.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import { flattenObject, unflattenObject, diffKeys } from "./flatten.js";
3
3
 
4
- describe('flattenObject', () => {
5
- it('flattens nested objects to dot-notation keys', () => {
4
+ describe("flattenObject", () => {
5
+ it("flattens nested objects to dot-notation keys", () => {
6
6
  expect(
7
7
  flattenObject({
8
- validation: { email: 'Email', nested: { deep: 'x' } },
8
+ validation: { email: "Email", nested: { deep: "x" } },
9
9
  }),
10
- ).toEqual({ 'validation.email': 'Email', 'validation.nested.deep': 'x' });
10
+ ).toEqual({ "validation.email": "Email", "validation.nested.deep": "x" });
11
11
  });
12
12
 
13
- it('returns {} for an empty object', () => {
13
+ it("returns {} for an empty object", () => {
14
14
  expect(flattenObject({})).toEqual({});
15
15
  });
16
16
 
17
- it('preserves empty string values', () => {
18
- expect(flattenObject({ a: '' })).toEqual({ a: '' });
17
+ it("preserves empty string values", () => {
18
+ expect(flattenObject({ a: "" })).toEqual({ a: "" });
19
19
  });
20
20
 
21
- it('handles deeply nested structures', () => {
22
- expect(
23
- flattenObject({ a: { b: { c: { d: { e: 'deep' } } } } }),
24
- ).toEqual({ 'a.b.c.d.e': 'deep' });
21
+ it("handles deeply nested structures", () => {
22
+ expect(flattenObject({ a: { b: { c: { d: { e: "deep" } } } } })).toEqual({
23
+ "a.b.c.d.e": "deep",
24
+ });
25
25
  });
26
26
 
27
- it('ignores arrays (not flattened)', () => {
28
- expect(flattenObject({ items: ['first', 'second'] })).toEqual({});
27
+ it("ignores arrays (not flattened)", () => {
28
+ expect(flattenObject({ items: ["first", "second"] })).toEqual({});
29
29
  });
30
30
 
31
- it('ignores mixed arrays and objects', () => {
32
- expect(flattenObject({ items: [{ name: 'a' }, { name: 'b' }] })).toEqual({});
31
+ it("ignores mixed arrays and objects", () => {
32
+ expect(flattenObject({ items: [{ name: "a" }, { name: "b" }] })).toEqual({});
33
33
  });
34
34
 
35
- it('ignores null values', () => {
35
+ it("ignores null values", () => {
36
36
  expect(flattenObject({ a: null })).toEqual({});
37
37
  });
38
38
 
39
- it('ignores numeric values', () => {
39
+ it("ignores numeric values", () => {
40
40
  expect(flattenObject({ count: 42 })).toEqual({});
41
41
  });
42
42
 
43
- it('ignores boolean values', () => {
43
+ it("ignores boolean values", () => {
44
44
  expect(flattenObject({ enabled: true })).toEqual({});
45
45
  });
46
46
 
47
- it('preserves unicode values', () => {
48
- expect(flattenObject({ greeting: 'Héllo 🌍' })).toEqual({
49
- greeting: 'Héllo 🌍',
47
+ it("preserves unicode values", () => {
48
+ expect(flattenObject({ greeting: "Héllo 🌍" })).toEqual({
49
+ greeting: "Héllo 🌍",
50
50
  });
51
51
  });
52
52
  });
53
53
 
54
- describe('unflattenObject', () => {
55
- it('rebuilds nested structure from dot-notation keys', () => {
56
- expect(unflattenObject({ 'validation.email': 'Email' })).toEqual({
57
- validation: { email: 'Email' },
54
+ describe("unflattenObject", () => {
55
+ it("rebuilds nested structure from dot-notation keys", () => {
56
+ expect(unflattenObject({ "validation.email": "Email" })).toEqual({
57
+ validation: { email: "Email" },
58
58
  });
59
59
  });
60
60
 
61
- it('round-trips with flattenObject', () => {
62
- const original = { a: { b: { c: '1' }, d: '2' } };
61
+ it("round-trips with flattenObject", () => {
62
+ const original = { a: { b: { c: "1" }, d: "2" } };
63
63
  expect(unflattenObject(flattenObject(original))).toEqual(original);
64
64
  });
65
65
 
66
- it('handles deeply nested keys', () => {
67
- expect(
68
- unflattenObject({ 'a.b.c.d.e': 'deep' }),
69
- ).toEqual({ a: { b: { c: { d: { e: 'deep' } } } } });
66
+ it("handles deeply nested keys", () => {
67
+ expect(unflattenObject({ "a.b.c.d.e": "deep" })).toEqual({
68
+ a: { b: { c: { d: { e: "deep" } } } },
69
+ });
70
70
  });
71
71
 
72
- it('treats numeric keys as object properties, not array indices', () => {
73
- expect(unflattenObject({ 'items.0': 'first', 'items.1': 'second' })).toEqual({
74
- items: { '0': 'first', '1': 'second' },
72
+ it("treats numeric keys as object properties, not array indices", () => {
73
+ expect(unflattenObject({ "items.0": "first", "items.1": "second" })).toEqual({
74
+ items: { "0": "first", "1": "second" },
75
75
  });
76
76
  });
77
77
 
78
- it('handles empty flat object', () => {
78
+ it("handles empty flat object", () => {
79
79
  expect(unflattenObject({})).toEqual({});
80
80
  });
81
81
 
82
- it('handles single-level keys', () => {
83
- expect(unflattenObject({ a: '1', b: '2' })).toEqual({ a: '1', b: '2' });
82
+ it("handles single-level keys", () => {
83
+ expect(unflattenObject({ a: "1", b: "2" })).toEqual({ a: "1", b: "2" });
84
84
  });
85
85
  });
86
86
 
87
- describe('diffKeys', () => {
88
- it('returns keys present in source but missing in target', () => {
89
- expect(diffKeys({ a: '1', b: '2' }, { a: '1' })).toEqual(['b']);
87
+ describe("diffKeys", () => {
88
+ it("returns keys present in source but missing in target", () => {
89
+ expect(diffKeys({ a: "1", b: "2" }, { a: "1" })).toEqual(["b"]);
90
90
  });
91
91
 
92
- it('returns [] when target has all source keys', () => {
93
- expect(diffKeys({ a: '1' }, { a: '1', b: '2' })).toEqual([]);
92
+ it("returns [] when target has all source keys", () => {
93
+ expect(diffKeys({ a: "1" }, { a: "1", b: "2" })).toEqual([]);
94
94
  });
95
95
 
96
- it('returns all keys when target is empty', () => {
97
- expect(diffKeys({ a: '1', b: '2' }, {})).toEqual(['a', 'b']);
96
+ it("returns all keys when target is empty", () => {
97
+ expect(diffKeys({ a: "1", b: "2" }, {})).toEqual(["a", "b"]);
98
98
  });
99
99
 
100
- it('returns [] when source is empty', () => {
101
- expect(diffKeys({}, { a: '1' })).toEqual([]);
100
+ it("returns [] when source is empty", () => {
101
+ expect(diffKeys({}, { a: "1" })).toEqual([]);
102
102
  });
103
103
 
104
- it('handles keys with different values', () => {
105
- expect(diffKeys({ a: '1' }, { a: '2' })).toEqual([]);
104
+ it("handles keys with different values", () => {
105
+ expect(diffKeys({ a: "1" }, { a: "2" })).toEqual([]);
106
106
  });
107
107
 
108
- it('handles nested flattened keys', () => {
109
- expect(diffKeys({ 'a.b': '1', 'a.c': '2' }, { 'a.b': '1' })).toEqual(['a.c']);
108
+ it("handles nested flattened keys", () => {
109
+ expect(diffKeys({ "a.b": "1", "a.c": "2" }, { "a.b": "1" })).toEqual(["a.c"]);
110
110
  });
111
111
  });
@@ -1,29 +1,27 @@
1
1
  export function flattenObject(
2
2
  input: Readonly<Record<string, unknown>>,
3
- prefix = '',
3
+ prefix = "",
4
4
  ): Record<string, string> {
5
5
  const output: Record<string, string> = {};
6
6
  for (const [key, value] of Object.entries(input)) {
7
- const fullKey = prefix === '' ? key : `${prefix}.${key}`;
8
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
7
+ const fullKey = prefix === "" ? key : `${prefix}.${key}`;
8
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
9
9
  Object.assign(output, flattenObject(value as Record<string, unknown>, fullKey));
10
- } else if (typeof value === 'string') {
10
+ } else if (typeof value === "string") {
11
11
  output[fullKey] = value;
12
12
  }
13
13
  }
14
14
  return output;
15
15
  }
16
16
 
17
- export function unflattenObject(
18
- input: Readonly<Record<string, string>>,
19
- ): Record<string, unknown> {
17
+ export function unflattenObject(input: Readonly<Record<string, string>>): Record<string, unknown> {
20
18
  const output: Record<string, unknown> = {};
21
19
  for (const [dottedKey, value] of Object.entries(input)) {
22
- const segments = dottedKey.split('.');
20
+ const segments = dottedKey.split(".");
23
21
  let cursor = output;
24
22
  for (let i = 0; i < segments.length - 1; i++) {
25
23
  const segment = segments[i]!;
26
- if (typeof cursor[segment] !== 'object' || cursor[segment] === null) {
24
+ if (typeof cursor[segment] !== "object" || cursor[segment] === null) {
27
25
  cursor[segment] = {};
28
26
  }
29
27
  cursor = cursor[segment] as Record<string, unknown>;
@@ -1,7 +1,7 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Effect, Layer } from 'effect';
3
- import { FileSystem, Path } from '@effect/platform';
4
- import { readFileIfExists, writeFileEnsuringDir } from './file-io.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import { Effect, Layer } from "effect";
3
+ import { FileSystem, Path } from "@effect/platform";
4
+ import { readFileIfExists, writeFileEnsuringDir } from "./file-io.js";
5
5
 
6
6
  function makeFsLayer(files: Record<string, string>) {
7
7
  const stub = FileSystem.makeNoop({
@@ -9,9 +9,7 @@ function makeFsLayer(files: Record<string, string>) {
9
9
  readFileString: (path) =>
10
10
  path in files
11
11
  ? Effect.succeed(files[path]!)
12
- : Effect.fail(
13
- new Error(`ENOENT: ${path}`) as never,
14
- ),
12
+ : Effect.fail(new Error(`ENOENT: ${path}`) as never),
15
13
  writeFileString: (path, content) => {
16
14
  files[path] = content;
17
15
  return Effect.void;
@@ -21,119 +19,109 @@ function makeFsLayer(files: Record<string, string>) {
21
19
  return Layer.succeed(FileSystem.FileSystem, stub);
22
20
  }
23
21
 
24
- describe('readFileIfExists', () => {
25
- it('returns content when file exists', async () => {
26
- const files = { '/a/b.txt': 'hello' };
27
- const program = readFileIfExists('/a/b.txt').pipe(
28
- Effect.provide(makeFsLayer(files)),
29
- );
22
+ describe("readFileIfExists", () => {
23
+ it("returns content when file exists", async () => {
24
+ const files = { "/a/b.txt": "hello" };
25
+ const program = readFileIfExists("/a/b.txt").pipe(Effect.provide(makeFsLayer(files)));
30
26
  const result = await Effect.runPromise(program);
31
- expect(result).toBe('hello');
27
+ expect(result).toBe("hello");
32
28
  });
33
29
 
34
- it('returns null when file does not exist', async () => {
35
- const program = readFileIfExists('/a/missing.txt').pipe(
36
- Effect.provide(makeFsLayer({})),
37
- );
30
+ it("returns null when file does not exist", async () => {
31
+ const program = readFileIfExists("/a/missing.txt").pipe(Effect.provide(makeFsLayer({})));
38
32
  const result = await Effect.runPromise(program);
39
33
  expect(result).toBeNull();
40
34
  });
41
35
 
42
- it('returns null for empty file system', async () => {
43
- const program = readFileIfExists('/any/path.txt').pipe(
44
- Effect.provide(makeFsLayer({})),
45
- );
36
+ it("returns null for empty file system", async () => {
37
+ const program = readFileIfExists("/any/path.txt").pipe(Effect.provide(makeFsLayer({})));
46
38
  const result = await Effect.runPromise(program);
47
39
  expect(result).toBeNull();
48
40
  });
49
41
 
50
- it('returns content for deeply nested path', async () => {
51
- const files = { '/very/deep/nested/file.txt': 'deep content' };
52
- const program = readFileIfExists('/very/deep/nested/file.txt').pipe(
42
+ it("returns content for deeply nested path", async () => {
43
+ const files = { "/very/deep/nested/file.txt": "deep content" };
44
+ const program = readFileIfExists("/very/deep/nested/file.txt").pipe(
53
45
  Effect.provide(makeFsLayer(files)),
54
46
  );
55
47
  const result = await Effect.runPromise(program);
56
- expect(result).toBe('deep content');
48
+ expect(result).toBe("deep content");
57
49
  });
58
50
 
59
- it('handles unicode content', async () => {
60
- const files = { '/unicode.txt': 'Héllo 🌍 — 日本語' };
61
- const program = readFileIfExists('/unicode.txt').pipe(
62
- Effect.provide(makeFsLayer(files)),
63
- );
51
+ it("handles unicode content", async () => {
52
+ const files = { "/unicode.txt": "Héllo 🌍 — 日本語" };
53
+ const program = readFileIfExists("/unicode.txt").pipe(Effect.provide(makeFsLayer(files)));
64
54
  const result = await Effect.runPromise(program);
65
- expect(result).toBe('Héllo 🌍 — 日本語');
55
+ expect(result).toBe("Héllo 🌍 — 日本語");
66
56
  });
67
57
 
68
- it('handles empty string content', async () => {
69
- const files = { '/empty.txt': '' };
70
- const program = readFileIfExists('/empty.txt').pipe(
71
- Effect.provide(makeFsLayer(files)),
72
- );
58
+ it("handles empty string content", async () => {
59
+ const files = { "/empty.txt": "" };
60
+ const program = readFileIfExists("/empty.txt").pipe(Effect.provide(makeFsLayer(files)));
73
61
  const result = await Effect.runPromise(program);
74
- expect(result).toBe('');
62
+ expect(result).toBe("");
75
63
  });
76
64
  });
77
65
 
78
- describe('writeFileEnsuringDir', () => {
79
- it('writes content and creates parent directories', async () => {
66
+ describe("writeFileEnsuringDir", () => {
67
+ it("writes content and creates parent directories", async () => {
80
68
  const files: Record<string, string> = {};
81
- const program = writeFileEnsuringDir('/a/b/c.txt', 'content').pipe(
69
+ const program = writeFileEnsuringDir("/a/b/c.txt", "content").pipe(
82
70
  Effect.provide(makeFsLayer(files)),
83
71
  Effect.provide(Path.layer),
84
72
  );
85
73
  await Effect.runPromise(program);
86
- expect(files['/a/b/c.txt']).toBe('content');
74
+ expect(files["/a/b/c.txt"]).toBe("content");
87
75
  });
88
76
 
89
- it('overwrites existing file', async () => {
90
- const files: Record<string, string> = { '/existing.txt': 'old' };
91
- const program = writeFileEnsuringDir('/existing.txt', 'new').pipe(
77
+ it("overwrites existing file", async () => {
78
+ const files: Record<string, string> = { "/existing.txt": "old" };
79
+ const program = writeFileEnsuringDir("/existing.txt", "new").pipe(
92
80
  Effect.provide(makeFsLayer(files)),
93
81
  Effect.provide(Path.layer),
94
82
  );
95
83
  await Effect.runPromise(program);
96
- expect(files['/existing.txt']).toBe('new');
84
+ expect(files["/existing.txt"]).toBe("new");
97
85
  });
98
86
 
99
- it('handles deeply nested paths', async () => {
87
+ it("handles deeply nested paths", async () => {
100
88
  const files: Record<string, string> = {};
101
- const program = writeFileEnsuringDir('/a/b/c/d/e.txt', 'nested').pipe(
89
+ const program = writeFileEnsuringDir("/a/b/c/d/e.txt", "nested").pipe(
102
90
  Effect.provide(makeFsLayer(files)),
103
91
  Effect.provide(Path.layer),
104
92
  );
105
93
  await Effect.runPromise(program);
106
- expect(files['/a/b/c/d/e.txt']).toBe('nested');
94
+ expect(files["/a/b/c/d/e.txt"]).toBe("nested");
107
95
  });
108
96
 
109
- it('handles empty content', async () => {
97
+ it("handles empty content", async () => {
110
98
  const files: Record<string, string> = {};
111
- const program = writeFileEnsuringDir('/empty.txt', '').pipe(
99
+ const program = writeFileEnsuringDir("/empty.txt", "").pipe(
112
100
  Effect.provide(makeFsLayer(files)),
113
101
  Effect.provide(Path.layer),
114
102
  );
115
103
  await Effect.runPromise(program);
116
- expect(files['/empty.txt']).toBe('');
104
+ expect(files["/empty.txt"]).toBe("");
117
105
  });
118
106
 
119
- it('handles unicode content', async () => {
107
+ it("handles unicode content", async () => {
120
108
  const files: Record<string, string> = {};
121
- const content = 'Héllo 🌍 — 日本語';
122
- const program = writeFileEnsuringDir('/unicode.txt', content).pipe(
109
+ const content = "Héllo 🌍 — 日本語";
110
+ const program = writeFileEnsuringDir("/unicode.txt", content).pipe(
123
111
  Effect.provide(makeFsLayer(files)),
124
112
  Effect.provide(Path.layer),
125
113
  );
126
114
  await Effect.runPromise(program);
127
- expect(files['/unicode.txt']).toBe(content);
115
+ expect(files["/unicode.txt"]).toBe(content);
128
116
  });
129
117
 
130
- it('handles paths with spaces', async () => {
118
+ it("handles paths with spaces", async () => {
131
119
  const files: Record<string, string> = {};
132
- const program = writeFileEnsuringDir('/path with spaces/file.txt', 'content').pipe(
120
+ const program = writeFileEnsuringDir("/path with spaces/file.txt", "content").pipe(
133
121
  Effect.provide(makeFsLayer(files)),
134
122
  Effect.provide(Path.layer),
135
123
  );
136
124
  await Effect.runPromise(program);
137
- expect(files['/path with spaces/file.txt']).toBe('content');
125
+ expect(files["/path with spaces/file.txt"]).toBe("content");
138
126
  });
139
127
  });