@ontrails/config 1.0.0-beta.14 → 1.0.0-beta.15

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 (136) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +20 -28
  4. package/dist/app-config.d.ts +4 -4
  5. package/dist/app-config.d.ts.map +1 -1
  6. package/dist/app-config.js +12 -7
  7. package/dist/app-config.js.map +1 -1
  8. package/dist/compose.d.ts +11 -15
  9. package/dist/compose.d.ts.map +1 -1
  10. package/dist/compose.js +7 -9
  11. package/dist/compose.js.map +1 -1
  12. package/dist/config-layer.d.ts +3 -3
  13. package/dist/config-resource.d.ts +3 -0
  14. package/dist/config-resource.d.ts.map +1 -0
  15. package/dist/{config-provision.js → config-resource.js} +6 -6
  16. package/dist/config-resource.js.map +1 -0
  17. package/dist/define-config.d.ts +9 -9
  18. package/dist/define-config.d.ts.map +1 -1
  19. package/dist/define-config.js +9 -12
  20. package/dist/define-config.js.map +1 -1
  21. package/dist/{generate → derive}/env.d.ts +1 -1
  22. package/dist/derive/env.d.ts.map +1 -0
  23. package/dist/{generate → derive}/env.js +3 -3
  24. package/dist/derive/env.js.map +1 -0
  25. package/dist/{generate → derive}/example.d.ts +1 -1
  26. package/dist/derive/example.d.ts.map +1 -0
  27. package/dist/{generate → derive}/example.js +1 -1
  28. package/dist/derive/example.js.map +1 -0
  29. package/dist/{generate → derive}/helpers.d.ts +1 -1
  30. package/dist/derive/helpers.d.ts.map +1 -0
  31. package/dist/{generate → derive}/helpers.js +1 -1
  32. package/dist/derive/helpers.js.map +1 -0
  33. package/dist/derive/index.d.ts +4 -0
  34. package/dist/derive/index.d.ts.map +1 -0
  35. package/dist/derive/index.js +4 -0
  36. package/dist/derive/index.js.map +1 -0
  37. package/dist/{generate → derive}/json-schema.d.ts +1 -1
  38. package/dist/derive/json-schema.d.ts.map +1 -0
  39. package/dist/{generate → derive}/json-schema.js +1 -1
  40. package/dist/derive/json-schema.js.map +1 -0
  41. package/dist/{describe.d.ts → derive-fields.d.ts} +2 -2
  42. package/dist/derive-fields.d.ts.map +1 -0
  43. package/dist/{describe.js → derive-fields.js} +2 -2
  44. package/dist/derive-fields.js.map +1 -0
  45. package/dist/{explain.d.ts → derive-provenance.d.ts} +5 -5
  46. package/dist/derive-provenance.d.ts.map +1 -0
  47. package/dist/{explain.js → derive-provenance.js} +3 -3
  48. package/dist/derive-provenance.js.map +1 -0
  49. package/dist/index.d.ts +7 -8
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +6 -7
  52. package/dist/index.js.map +1 -1
  53. package/dist/registry.d.ts +2 -2
  54. package/dist/resolve.d.ts +6 -6
  55. package/dist/resolve.d.ts.map +1 -1
  56. package/dist/resolve.js +7 -7
  57. package/dist/resolve.js.map +1 -1
  58. package/dist/trails/config-check.d.ts +1 -1
  59. package/dist/trails/config-check.d.ts.map +1 -1
  60. package/dist/trails/config-check.js +3 -3
  61. package/dist/trails/config-check.js.map +1 -1
  62. package/dist/trails/config-describe.d.ts +1 -1
  63. package/dist/trails/config-describe.d.ts.map +1 -1
  64. package/dist/trails/config-describe.js +5 -5
  65. package/dist/trails/config-describe.js.map +1 -1
  66. package/dist/trails/config-explain.d.ts +1 -1
  67. package/dist/trails/config-explain.d.ts.map +1 -1
  68. package/dist/trails/config-explain.js +8 -8
  69. package/dist/trails/config-explain.js.map +1 -1
  70. package/dist/trails/config-init.d.ts +1 -1
  71. package/dist/trails/config-init.d.ts.map +1 -1
  72. package/dist/trails/config-init.js +7 -7
  73. package/dist/trails/config-init.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/__tests__/app-config.test.ts +19 -6
  76. package/src/__tests__/compose.test.ts +15 -15
  77. package/src/__tests__/config-check.test.ts +7 -7
  78. package/src/__tests__/config-describe.test.ts +7 -7
  79. package/src/__tests__/config-explain.test.ts +7 -7
  80. package/src/__tests__/config-init.test.ts +7 -7
  81. package/src/__tests__/{config-provision.test.ts → config-resource.test.ts} +12 -12
  82. package/src/__tests__/define-config.test.ts +24 -18
  83. package/src/__tests__/{describe.test.ts → derive-fields.test.ts} +11 -11
  84. package/src/__tests__/{explain.test.ts → derive-provenance.test.ts} +18 -18
  85. package/src/__tests__/{generate.test.ts → derive.test.ts} +30 -30
  86. package/src/__tests__/resolve.test.ts +31 -31
  87. package/src/app-config.ts +23 -12
  88. package/src/compose.ts +16 -22
  89. package/src/{config-provision.ts → config-resource.ts} +5 -5
  90. package/src/define-config.ts +12 -15
  91. package/src/{generate → derive}/env.ts +3 -3
  92. package/src/{generate → derive}/example.ts +1 -1
  93. package/src/{generate → derive}/helpers.ts +1 -1
  94. package/src/derive/index.ts +3 -0
  95. package/src/{generate → derive}/json-schema.ts +1 -1
  96. package/src/{describe.ts → derive-fields.ts} +1 -1
  97. package/src/{explain.ts → derive-provenance.ts} +6 -6
  98. package/src/index.ts +12 -18
  99. package/src/registry.ts +2 -2
  100. package/src/resolve.ts +13 -13
  101. package/src/trails/config-check.ts +3 -3
  102. package/src/trails/config-describe.ts +5 -5
  103. package/src/trails/config-explain.ts +13 -13
  104. package/src/trails/config-init.ts +10 -10
  105. package/tsconfig.tests.json +10 -0
  106. package/tsconfig.tsbuildinfo +1 -1
  107. package/dist/config-gate.d.ts +0 -11
  108. package/dist/config-gate.d.ts.map +0 -1
  109. package/dist/config-gate.js +0 -6
  110. package/dist/config-gate.js.map +0 -1
  111. package/dist/config-provision.d.ts +0 -3
  112. package/dist/config-provision.d.ts.map +0 -1
  113. package/dist/config-provision.js.map +0 -1
  114. package/dist/config-service.d.ts +0 -3
  115. package/dist/config-service.d.ts.map +0 -1
  116. package/dist/config-service.js +0 -26
  117. package/dist/config-service.js.map +0 -1
  118. package/dist/describe.d.ts.map +0 -1
  119. package/dist/describe.js.map +0 -1
  120. package/dist/explain.d.ts.map +0 -1
  121. package/dist/explain.js.map +0 -1
  122. package/dist/generate/env.d.ts.map +0 -1
  123. package/dist/generate/env.js.map +0 -1
  124. package/dist/generate/example.d.ts.map +0 -1
  125. package/dist/generate/example.js.map +0 -1
  126. package/dist/generate/helpers.d.ts.map +0 -1
  127. package/dist/generate/helpers.js.map +0 -1
  128. package/dist/generate/index.d.ts +0 -4
  129. package/dist/generate/index.d.ts.map +0 -1
  130. package/dist/generate/index.js +0 -4
  131. package/dist/generate/index.js.map +0 -1
  132. package/dist/generate/json-schema.d.ts.map +0 -1
  133. package/dist/generate/json-schema.js.map +0 -1
  134. package/src/__tests__/config-gate.test.ts +0 -53
  135. package/src/config-gate.ts +0 -15
  136. package/src/generate/index.ts +0 -3
@@ -2,9 +2,9 @@ import { describe, expect, test } from 'bun:test';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { deprecated, env, secret } from '../extensions.js';
5
- import { describeConfig } from '../describe.js';
5
+ import { deriveConfigFields } from '../derive-fields.js';
6
6
 
7
- describe('describeConfig', () => {
7
+ describe('deriveConfigFields', () => {
8
8
  describe('basic field descriptions', () => {
9
9
  test('returns path, type, and required for each field', () => {
10
10
  const schema = z.object({
@@ -13,7 +13,7 @@ describe('describeConfig', () => {
13
13
  port: z.number(),
14
14
  });
15
15
 
16
- const fields = describeConfig(schema);
16
+ const fields = deriveConfigFields(schema);
17
17
 
18
18
  expect(fields).toHaveLength(3);
19
19
  const host = fields.find((f) => f.path === 'host');
@@ -32,7 +32,7 @@ describe('describeConfig', () => {
32
32
  host: z.string().describe('The server hostname'),
33
33
  });
34
34
 
35
- const fields = describeConfig(schema);
35
+ const fields = deriveConfigFields(schema);
36
36
  const host = fields.find((f) => f.path === 'host');
37
37
  expect(host?.description).toBe('The server hostname');
38
38
  });
@@ -47,7 +47,7 @@ describe('describeConfig', () => {
47
47
  ),
48
48
  });
49
49
 
50
- const fields = describeConfig(schema);
50
+ const fields = deriveConfigFields(schema);
51
51
 
52
52
  const apiKey = fields.find((f) => f.path === 'apiKey');
53
53
  expect(apiKey?.env).toBe('API_KEY');
@@ -65,7 +65,7 @@ describe('describeConfig', () => {
65
65
  port: z.number().default(3000),
66
66
  });
67
67
 
68
- const fields = describeConfig(schema);
68
+ const fields = deriveConfigFields(schema);
69
69
 
70
70
  const port = fields.find((f) => f.path === 'port');
71
71
  expect(port?.default).toBe(3000);
@@ -85,7 +85,7 @@ describe('describeConfig', () => {
85
85
  }),
86
86
  });
87
87
 
88
- const fields = describeConfig(schema);
88
+ const fields = deriveConfigFields(schema);
89
89
 
90
90
  const dbHost = fields.find((f) => f.path === 'db.host');
91
91
  expect(dbHost?.type).toBe('string');
@@ -106,7 +106,7 @@ describe('describeConfig', () => {
106
106
  .optional(),
107
107
  });
108
108
 
109
- const fields = describeConfig(schema);
109
+ const fields = deriveConfigFields(schema);
110
110
 
111
111
  expect(fields).toEqual([
112
112
  expect.objectContaining({ path: 'db.host', required: true }),
@@ -121,7 +121,7 @@ describe('describeConfig', () => {
121
121
  env: z.enum(['development', 'production', 'test']),
122
122
  });
123
123
 
124
- const fields = describeConfig(schema);
124
+ const fields = deriveConfigFields(schema);
125
125
  const envField = fields.find((f) => f.path === 'env');
126
126
  expect(envField?.type).toBe('enum');
127
127
  expect(envField?.constraints?.values).toEqual([
@@ -136,7 +136,7 @@ describe('describeConfig', () => {
136
136
  port: z.number().min(1).max(65_535),
137
137
  });
138
138
 
139
- const fields = describeConfig(schema);
139
+ const fields = deriveConfigFields(schema);
140
140
  const port = fields.find((f) => f.path === 'port');
141
141
  expect(port?.constraints?.min).toBe(1);
142
142
  expect(port?.constraints?.max).toBe(65_535);
@@ -150,7 +150,7 @@ describe('describeConfig', () => {
150
150
  nickname: z.string().optional(),
151
151
  });
152
152
 
153
- const fields = describeConfig(schema);
153
+ const fields = deriveConfigFields(schema);
154
154
  const nickname = fields.find((f) => f.path === 'nickname');
155
155
  expect(nickname?.required).toBe(false);
156
156
  });
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { env, secret } from '../extensions.js';
5
- import { explainConfig } from '../explain.js';
5
+ import { deriveConfigProvenance } from '../derive-provenance.js';
6
6
 
7
7
  const schema = z.object({
8
8
  debug: z.boolean().default(false),
@@ -10,10 +10,10 @@ const schema = z.object({
10
10
  port: z.number().default(3000),
11
11
  });
12
12
 
13
- describe('explainConfig', () => {
13
+ describe('deriveConfigProvenance', () => {
14
14
  describe('default source', () => {
15
15
  test('reports "default" when no other source provides value', () => {
16
- const entries = explainConfig({
16
+ const entries = deriveConfigProvenance({
17
17
  resolved: { debug: false, host: 'localhost', port: 3000 },
18
18
  schema,
19
19
  });
@@ -26,7 +26,7 @@ describe('explainConfig', () => {
26
26
 
27
27
  describe('base overrides default', () => {
28
28
  test('reports "base" when base provides value', () => {
29
- const entries = explainConfig({
29
+ const entries = deriveConfigProvenance({
30
30
  base: { host: 'base.example.com' },
31
31
  resolved: { debug: false, host: 'base.example.com', port: 3000 },
32
32
  schema,
@@ -38,27 +38,27 @@ describe('explainConfig', () => {
38
38
  });
39
39
  });
40
40
 
41
- describe('loadout overrides base', () => {
42
- test('reports "loadout" when loadout provides winning value', () => {
43
- const entries = explainConfig({
41
+ describe('profile overrides base', () => {
42
+ test('reports "profile" when profile provides winning value', () => {
43
+ const entries = deriveConfigProvenance({
44
44
  base: { host: 'base.example.com' },
45
- loadout: { host: 'loadout.example.com' },
46
- resolved: { debug: false, host: 'loadout.example.com', port: 3000 },
45
+ profile: { host: 'profile.example.com' },
46
+ resolved: { debug: false, host: 'profile.example.com', port: 3000 },
47
47
  schema,
48
48
  });
49
49
 
50
50
  const host = entries.find((e) => e.path === 'host');
51
- expect(host?.source).toBe('loadout');
52
- expect(host?.value).toBe('loadout.example.com');
51
+ expect(host?.source).toBe('profile');
52
+ expect(host?.value).toBe('profile.example.com');
53
53
  });
54
54
  });
55
55
 
56
- describe('local overrides loadout', () => {
56
+ describe('local overrides profile', () => {
57
57
  test('reports "local" when local provides winning value', () => {
58
- const entries = explainConfig({
58
+ const entries = deriveConfigProvenance({
59
59
  base: { host: 'base.example.com' },
60
- loadout: { host: 'loadout.example.com' },
61
60
  local: { host: 'local.example.com' },
61
+ profile: { host: 'profile.example.com' },
62
62
  resolved: { debug: false, host: 'local.example.com', port: 3000 },
63
63
  schema,
64
64
  });
@@ -75,7 +75,7 @@ describe('explainConfig', () => {
75
75
  port: z.number().default(3000),
76
76
  });
77
77
 
78
- const entries = explainConfig({
78
+ const entries = deriveConfigProvenance({
79
79
  base: { host: 'base.example.com' },
80
80
  env: { APP_HOST: 'env.example.com' },
81
81
  resolved: { host: 'env.example.com', port: 3000 },
@@ -96,7 +96,7 @@ describe('explainConfig', () => {
96
96
  .optional(),
97
97
  });
98
98
 
99
- const entries = explainConfig({
99
+ const entries = deriveConfigProvenance({
100
100
  env: { DB_HOST: 'env.example.com' },
101
101
  resolved: { db: { host: 'env.example.com' } },
102
102
  schema: envSchema,
@@ -115,7 +115,7 @@ describe('explainConfig', () => {
115
115
  host: z.string().default('localhost'),
116
116
  });
117
117
 
118
- const entries = explainConfig({
118
+ const entries = deriveConfigProvenance({
119
119
  env: { API_KEY: 'super-secret-key' },
120
120
  resolved: { apiKey: 'super-secret-key', host: 'localhost' },
121
121
  schema: secretSchema,
@@ -127,7 +127,7 @@ describe('explainConfig', () => {
127
127
  });
128
128
 
129
129
  test('does not redact non-secret fields', () => {
130
- const entries = explainConfig({
130
+ const entries = deriveConfigProvenance({
131
131
  resolved: { debug: false, host: 'localhost', port: 3000 },
132
132
  schema,
133
133
  });
@@ -3,10 +3,10 @@ import { z } from 'zod';
3
3
 
4
4
  import { deprecated, env, secret } from '../extensions.js';
5
5
  import {
6
- generateEnvExample,
7
- generateExample,
8
- generateJsonSchema,
9
- } from '../generate/index.js';
6
+ deriveConfigEnvExample,
7
+ deriveConfigExample,
8
+ deriveConfigJsonSchema,
9
+ } from '../derive/index.js';
10
10
 
11
11
  /** Shared test schema used across generator tests. */
12
12
  const testSchema = z.object({
@@ -39,10 +39,10 @@ const nestedSchema = z.object({
39
39
  }),
40
40
  });
41
41
 
42
- describe('generateExample()', () => {
42
+ describe('deriveConfigExample()', () => {
43
43
  describe('TOML format', () => {
44
44
  test('produces valid TOML with comments for descriptions', () => {
45
- const result = generateExample(testSchema, 'toml');
45
+ const result = deriveConfigExample(testSchema, 'toml');
46
46
 
47
47
  expect(result).toContain('# The server hostname');
48
48
  expect(result).toContain('host = "localhost"');
@@ -53,13 +53,13 @@ describe('generateExample()', () => {
53
53
  });
54
54
 
55
55
  test('annotates deprecated fields in TOML', () => {
56
- const result = generateExample(annotatedSchema, 'toml');
56
+ const result = deriveConfigExample(annotatedSchema, 'toml');
57
57
 
58
58
  expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
59
59
  });
60
60
 
61
61
  test('handles nested objects as TOML sections', () => {
62
- const result = generateExample(nestedSchema, 'toml');
62
+ const result = deriveConfigExample(nestedSchema, 'toml');
63
63
 
64
64
  expect(result).toContain('[db]');
65
65
  expect(result).toContain('[server]');
@@ -70,7 +70,7 @@ describe('generateExample()', () => {
70
70
 
71
71
  describe('JSON format', () => {
72
72
  test('produces valid JSON without comments', () => {
73
- const result = generateExample(testSchema, 'json');
73
+ const result = deriveConfigExample(testSchema, 'json');
74
74
  const parsed = JSON.parse(result);
75
75
 
76
76
  expect(parsed).toEqual({
@@ -81,7 +81,7 @@ describe('generateExample()', () => {
81
81
  });
82
82
 
83
83
  test('handles nested objects', () => {
84
- const result = generateExample(nestedSchema, 'json');
84
+ const result = deriveConfigExample(nestedSchema, 'json');
85
85
  const parsed = JSON.parse(result);
86
86
 
87
87
  expect(parsed).toHaveProperty('db');
@@ -92,7 +92,7 @@ describe('generateExample()', () => {
92
92
 
93
93
  describe('JSONC format', () => {
94
94
  test('produces JSON with // comments for descriptions', () => {
95
- const result = generateExample(testSchema, 'jsonc');
95
+ const result = deriveConfigExample(testSchema, 'jsonc');
96
96
 
97
97
  expect(result).toContain('// The server hostname');
98
98
  expect(result).toContain('"host"');
@@ -100,13 +100,13 @@ describe('generateExample()', () => {
100
100
  });
101
101
 
102
102
  test('annotates deprecated fields in JSONC', () => {
103
- const result = generateExample(annotatedSchema, 'jsonc');
103
+ const result = deriveConfigExample(annotatedSchema, 'jsonc');
104
104
 
105
105
  expect(result).toContain('// DEPRECATED: Use newEndpoint instead');
106
106
  });
107
107
 
108
108
  test('handles nested objects', () => {
109
- const result = generateExample(nestedSchema, 'jsonc');
109
+ const result = deriveConfigExample(nestedSchema, 'jsonc');
110
110
 
111
111
  expect(result).toContain('"db"');
112
112
  expect(result).toContain('"host"');
@@ -117,7 +117,7 @@ describe('generateExample()', () => {
117
117
 
118
118
  describe('YAML format', () => {
119
119
  test('produces valid YAML with comments for descriptions', () => {
120
- const result = generateExample(testSchema, 'yaml');
120
+ const result = deriveConfigExample(testSchema, 'yaml');
121
121
 
122
122
  expect(result).toContain('# The server hostname');
123
123
  expect(result).toContain('host: "localhost"');
@@ -128,13 +128,13 @@ describe('generateExample()', () => {
128
128
  });
129
129
 
130
130
  test('annotates deprecated fields in YAML', () => {
131
- const result = generateExample(annotatedSchema, 'yaml');
131
+ const result = deriveConfigExample(annotatedSchema, 'yaml');
132
132
 
133
133
  expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
134
134
  });
135
135
 
136
136
  test('handles nested objects', () => {
137
- const result = generateExample(nestedSchema, 'yaml');
137
+ const result = deriveConfigExample(nestedSchema, 'yaml');
138
138
 
139
139
  expect(result).toContain('db:');
140
140
  expect(result).toContain(' host: "localhost"');
@@ -143,9 +143,9 @@ describe('generateExample()', () => {
143
143
  });
144
144
  });
145
145
 
146
- describe('generateJsonSchema()', () => {
146
+ describe('deriveConfigJsonSchema()', () => {
147
147
  test('produces valid JSON Schema with $schema, type, and properties', () => {
148
- const result = generateJsonSchema(testSchema);
148
+ const result = deriveConfigJsonSchema(testSchema);
149
149
 
150
150
  expect(result.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
151
151
  expect(result.type).toBe('object');
@@ -153,7 +153,7 @@ describe('generateJsonSchema()', () => {
153
153
  });
154
154
 
155
155
  test('includes title and description from options', () => {
156
- const result = generateJsonSchema(testSchema, {
156
+ const result = deriveConfigJsonSchema(testSchema, {
157
157
  description: 'Server configuration',
158
158
  title: 'ServerConfig',
159
159
  });
@@ -163,7 +163,7 @@ describe('generateJsonSchema()', () => {
163
163
  });
164
164
 
165
165
  test('includes descriptions from .describe()', () => {
166
- const result = generateJsonSchema(testSchema);
166
+ const result = deriveConfigJsonSchema(testSchema);
167
167
  const props = result.properties as Record<string, Record<string, unknown>>;
168
168
 
169
169
  expect(props['host']?.description).toBe('The server hostname');
@@ -171,7 +171,7 @@ describe('generateJsonSchema()', () => {
171
171
  });
172
172
 
173
173
  test('includes defaults', () => {
174
- const result = generateJsonSchema(testSchema);
174
+ const result = deriveConfigJsonSchema(testSchema);
175
175
  const props = result.properties as Record<string, Record<string, unknown>>;
176
176
 
177
177
  expect(props['host']?.default).toBe('localhost');
@@ -187,7 +187,7 @@ describe('generateJsonSchema()', () => {
187
187
  name: z.string().describe('A name'),
188
188
  });
189
189
 
190
- const result = generateJsonSchema(enumSchema);
190
+ const result = deriveConfigJsonSchema(enumSchema);
191
191
  const props = result.properties as Record<string, Record<string, unknown>>;
192
192
 
193
193
  expect(props['name']?.type).toBe('string');
@@ -197,21 +197,21 @@ describe('generateJsonSchema()', () => {
197
197
  });
198
198
 
199
199
  test('marks deprecated fields', () => {
200
- const result = generateJsonSchema(annotatedSchema);
200
+ const result = deriveConfigJsonSchema(annotatedSchema);
201
201
  const props = result.properties as Record<string, Record<string, unknown>>;
202
202
 
203
203
  expect(props['oldEndpoint']?.deprecated).toBe(true);
204
204
  });
205
205
 
206
206
  test('lists required fields (those without defaults or optional)', () => {
207
- const result = generateJsonSchema(annotatedSchema);
207
+ const result = deriveConfigJsonSchema(annotatedSchema);
208
208
 
209
209
  expect(result.required).toContain('apiKey');
210
210
  expect(result.required).toContain('oldEndpoint');
211
211
  });
212
212
 
213
213
  test('recurses into nested object fields', () => {
214
- const result = generateJsonSchema(nestedSchema);
214
+ const result = deriveConfigJsonSchema(nestedSchema);
215
215
  const props = result.properties as Record<string, Record<string, unknown>>;
216
216
 
217
217
  expect(props['db']?.type).toBe('object');
@@ -232,9 +232,9 @@ describe('generateJsonSchema()', () => {
232
232
  });
233
233
  });
234
234
 
235
- describe('generateEnvExample()', () => {
235
+ describe('deriveConfigEnvExample()', () => {
236
236
  test('lists env vars with type info', () => {
237
- const result = generateEnvExample(testSchema);
237
+ const result = deriveConfigEnvExample(testSchema);
238
238
 
239
239
  expect(result).toContain('HOST=');
240
240
  expect(result).toContain('PORT=');
@@ -243,14 +243,14 @@ describe('generateEnvExample()', () => {
243
243
  });
244
244
 
245
245
  test('annotates secrets', () => {
246
- const result = generateEnvExample(annotatedSchema);
246
+ const result = deriveConfigEnvExample(annotatedSchema);
247
247
 
248
248
  expect(result).toContain('API_KEY=');
249
249
  expect(result).toContain('secret');
250
250
  });
251
251
 
252
252
  test('shows defaults as comments', () => {
253
- const result = generateEnvExample(testSchema);
253
+ const result = deriveConfigEnvExample(testSchema);
254
254
 
255
255
  expect(result).toContain('default: "localhost"');
256
256
  expect(result).toContain('default: 3000');
@@ -262,7 +262,7 @@ describe('generateEnvExample()', () => {
262
262
  verbose: z.boolean().default(false),
263
263
  });
264
264
 
265
- const result = generateEnvExample(noEnvSchema);
265
+ const result = deriveConfigEnvExample(noEnvSchema);
266
266
 
267
267
  expect(result).toBe('');
268
268
  });
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { env } from '../extensions.js';
5
- import { resolveConfig } from '../resolve.js';
5
+ import { deriveConfig } from '../resolve.js';
6
6
 
7
7
  const baseSchema = z.object({
8
8
  debug: z.boolean().default(false),
@@ -10,10 +10,10 @@ const baseSchema = z.object({
10
10
  port: z.number().default(3000),
11
11
  });
12
12
 
13
- describe('resolveConfig', () => {
13
+ describe('deriveConfig', () => {
14
14
  describe('schema defaults', () => {
15
15
  test('applies schema defaults when no other source provides values', () => {
16
- const result = resolveConfig({ schema: baseSchema });
16
+ const result = deriveConfig({ schema: baseSchema });
17
17
 
18
18
  expect(result.isOk()).toBe(true);
19
19
  expect(result.unwrap()).toEqual({
@@ -26,7 +26,7 @@ describe('resolveConfig', () => {
26
26
 
27
27
  describe('base config', () => {
28
28
  test('overrides schema defaults', () => {
29
- const result = resolveConfig({
29
+ const result = deriveConfig({
30
30
  base: { host: 'example.com', port: 8080 },
31
31
  schema: baseSchema,
32
32
  });
@@ -40,12 +40,12 @@ describe('resolveConfig', () => {
40
40
  });
41
41
  });
42
42
 
43
- describe('loadouts', () => {
44
- test('overrides base config for matching loadout', () => {
45
- const result = resolveConfig({
43
+ describe('profiles', () => {
44
+ test('overrides base config for matching profile', () => {
45
+ const result = deriveConfig({
46
46
  base: { host: 'example.com', port: 8080 },
47
- loadout: 'production',
48
- loadouts: {
47
+ profile: 'production',
48
+ profiles: {
49
49
  production: { host: 'prod.example.com', port: 443 },
50
50
  },
51
51
  schema: baseSchema,
@@ -58,11 +58,11 @@ describe('resolveConfig', () => {
58
58
  expect(value.debug).toBe(false);
59
59
  });
60
60
 
61
- test('silently ignores unrecognized loadout (base only)', () => {
62
- const result = resolveConfig({
61
+ test('silently ignores unrecognized profile (base only)', () => {
62
+ const result = deriveConfig({
63
63
  base: { host: 'example.com' },
64
- loadout: 'staging',
65
- loadouts: {
64
+ profile: 'staging',
65
+ profiles: {
66
66
  production: { host: 'prod.example.com' },
67
67
  },
68
68
  schema: baseSchema,
@@ -74,7 +74,7 @@ describe('resolveConfig', () => {
74
74
  });
75
75
 
76
76
  describe('local overrides', () => {
77
- test('deep-merge on top of loadout', () => {
77
+ test('deep-merge on top of profile', () => {
78
78
  const nestedSchema = z.object({
79
79
  db: z
80
80
  .object({
@@ -84,13 +84,13 @@ describe('resolveConfig', () => {
84
84
  .default({}),
85
85
  });
86
86
 
87
- const result = resolveConfig({
87
+ const result = deriveConfig({
88
88
  base: { db: { host: 'db.example.com', port: 5432 } },
89
- loadout: 'production',
90
- loadouts: {
89
+ localOverrides: { db: { port: 9999 } },
90
+ profile: 'production',
91
+ profiles: {
91
92
  production: { db: { host: 'prod-db.example.com' } },
92
93
  },
93
- localOverrides: { db: { port: 9999 } },
94
94
  schema: nestedSchema,
95
95
  });
96
96
 
@@ -108,7 +108,7 @@ describe('resolveConfig', () => {
108
108
  port: env(z.number(), 'APP_PORT').default(3000),
109
109
  });
110
110
 
111
- const result = resolveConfig({
111
+ const result = deriveConfig({
112
112
  base: { host: 'example.com', port: 8080 },
113
113
  env: { APP_HOST: 'env-host.example.com', APP_PORT: '9090' },
114
114
  schema,
@@ -125,7 +125,7 @@ describe('resolveConfig', () => {
125
125
  port: env(z.number(), 'PORT').default(3000),
126
126
  });
127
127
 
128
- const result = resolveConfig({
128
+ const result = deriveConfig({
129
129
  env: { PORT: '8080' },
130
130
  schema,
131
131
  });
@@ -139,7 +139,7 @@ describe('resolveConfig', () => {
139
139
  port: env(z.number(), 'PORT').default(3000),
140
140
  });
141
141
 
142
- const result = resolveConfig({
142
+ const result = deriveConfig({
143
143
  env: { PORT: 'abc' },
144
144
  schema,
145
145
  });
@@ -153,7 +153,7 @@ describe('resolveConfig', () => {
153
153
  verbose: env(z.boolean(), 'VERBOSE').default(false),
154
154
  });
155
155
 
156
- const trueValues = resolveConfig({
156
+ const trueValues = deriveConfig({
157
157
  env: { DEBUG: 'true', VERBOSE: '1' },
158
158
  schema,
159
159
  });
@@ -161,7 +161,7 @@ describe('resolveConfig', () => {
161
161
  expect(trueValues.unwrap().debug).toBe(true);
162
162
  expect(trueValues.unwrap().verbose).toBe(true);
163
163
 
164
- const falseValues = resolveConfig({
164
+ const falseValues = deriveConfig({
165
165
  env: { DEBUG: 'false', VERBOSE: '0' },
166
166
  schema,
167
167
  });
@@ -177,7 +177,7 @@ describe('resolveConfig', () => {
177
177
  required: z.string(),
178
178
  });
179
179
 
180
- const result = resolveConfig({ schema });
180
+ const result = deriveConfig({ schema });
181
181
 
182
182
  expect(result.isErr()).toBe(true);
183
183
  });
@@ -190,7 +190,7 @@ describe('resolveConfig', () => {
190
190
  });
191
191
  const base = { host: 'dev-host' };
192
192
 
193
- const first = resolveConfig({
193
+ const first = deriveConfig({
194
194
  base,
195
195
  env: { APP_HOST: 'env-host' },
196
196
  schema,
@@ -198,7 +198,7 @@ describe('resolveConfig', () => {
198
198
  expect(first.isOk()).toBe(true);
199
199
  expect(first.unwrap().host).toBe('env-host');
200
200
 
201
- const second = resolveConfig({ base, schema });
201
+ const second = deriveConfig({ base, schema });
202
202
  expect(second.isOk()).toBe(true);
203
203
  expect(second.unwrap().host).toBe('dev-host');
204
204
  expect(base.host).toBe('dev-host');
@@ -215,16 +215,16 @@ describe('resolveConfig', () => {
215
215
  port: z.number().default(3000),
216
216
  });
217
217
 
218
- const result = resolveConfig({
218
+ const result = deriveConfig({
219
219
  // base overrides name
220
220
  base: { name: 'my-app', port: 8080 },
221
221
  // env overrides debug and apiUrl
222
222
  env: { API_URL: 'https://api.prod.com', DEBUG: 'true' },
223
- // loadout overrides port
224
- loadout: 'production',
225
- loadouts: { production: { port: 443 } },
226
223
  // local overrides local
227
224
  localOverrides: { local: 'my-local-value' },
225
+ // profile overrides port
226
+ profile: 'production',
227
+ profiles: { production: { port: 443 } },
228
228
  schema,
229
229
  });
230
230
 
@@ -234,7 +234,7 @@ describe('resolveConfig', () => {
234
234
  // (nothing left at just default — name was overridden by base)
235
235
  // Base
236
236
  expect(value.name).toBe('my-app');
237
- // Loadout overrides base port
237
+ // Profile overrides base port
238
238
  expect(value.port).toBe(443);
239
239
  // Local overrides
240
240
  expect(value.local).toBe('my-local-value');
package/src/app-config.ts CHANGED
@@ -9,10 +9,13 @@ import type { z } from 'zod';
9
9
 
10
10
  import type { CheckResult } from './doctor.js';
11
11
  import { checkConfig } from './doctor.js';
12
- import type { FieldDescription } from './describe.js';
13
- import { describeConfig } from './describe.js';
14
- import type { ExplainConfigOptions, ProvenanceEntry } from './explain.js';
15
- import { explainConfig } from './explain.js';
12
+ import type { FieldDescription } from './derive-fields.js';
13
+ import { deriveConfigFields } from './derive-fields.js';
14
+ import type {
15
+ DeriveConfigProvenanceOptions,
16
+ ProvenanceEntry,
17
+ } from './derive-provenance.js';
18
+ import { deriveConfigProvenance } from './derive-provenance.js';
16
19
  import type { ConfigRef } from './ref.js';
17
20
  import { configRef } from './ref.js';
18
21
 
@@ -39,8 +42,8 @@ export interface ResolveOptions {
39
42
  }
40
43
 
41
44
  /** Options for the `explain()` method on AppConfig, excluding schema. */
42
- export type AppConfigExplainOptions = Omit<
43
- ExplainConfigOptions<z.ZodType>,
45
+ export type AppConfigDeriveProvenanceOptions = Omit<
46
+ DeriveConfigProvenanceOptions<z.ZodType>,
44
47
  'schema'
45
48
  >;
46
49
 
@@ -62,7 +65,9 @@ export interface AppConfig<T extends z.ZodType> {
62
65
  ): CheckResult;
63
66
 
64
67
  /** Show which source won for each config field. */
65
- explain(options: AppConfigExplainOptions): readonly ProvenanceEntry[];
68
+ explain(
69
+ options: AppConfigDeriveProvenanceOptions
70
+ ): readonly ProvenanceEntry[];
66
71
 
67
72
  /** Create a lazy reference to a config field for use as a trail input default. */
68
73
  ref(fieldPath: string): ConfigRef;
@@ -72,7 +77,12 @@ export interface AppConfig<T extends z.ZodType> {
72
77
  // Default values
73
78
  // ---------------------------------------------------------------------------
74
79
 
75
- const DEFAULT_FORMATS: readonly ConfigFormat[] = ['toml', 'json', 'yaml'];
80
+ const DEFAULT_FORMATS: readonly ConfigFormat[] = [
81
+ 'toml',
82
+ 'json',
83
+ 'jsonc',
84
+ 'yaml',
85
+ ];
76
86
 
77
87
  // ---------------------------------------------------------------------------
78
88
  // Internal helpers (defined before consumers — no-use-before-define)
@@ -225,7 +235,7 @@ const discoverConfigFile = async (
225
235
  };
226
236
 
227
237
  /** Resolve a config file — either from an explicit path or via discovery. */
228
- const resolveConfig = async <T extends z.ZodType>(
238
+ const resolveAppConfigFile = async <T extends z.ZodType>(
229
239
  name: string,
230
240
  schema: T,
231
241
  formats: readonly ConfigFormat[],
@@ -292,16 +302,17 @@ export const appConfig = <T extends z.ZodType>(
292
302
  return {
293
303
  check: (values, checkOpts) => checkConfig(schema, values, checkOpts),
294
304
  describe: () =>
295
- describeConfig(
305
+ deriveConfigFields(
296
306
  schema as unknown as z.ZodObject<Record<string, z.ZodType>>
297
307
  ),
298
308
  dotfile,
299
- explain: (explainOpts) => explainConfig({ ...explainOpts, schema }),
309
+ explain: (explainOpts) =>
310
+ deriveConfigProvenance({ ...explainOpts, schema }),
300
311
  formats,
301
312
  name,
302
313
  ref: (fieldPath) => configRef(fieldPath),
303
314
  resolve: (resolveOptions?: ResolveOptions) =>
304
- resolveConfig(name, schema, formats, dotfile, resolveOptions),
315
+ resolveAppConfigFile(name, schema, formats, dotfile, resolveOptions),
305
316
  schema,
306
317
  };
307
318
  };