@untangle-code/config 0.0.0-stage → 0.1.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 (86) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +262 -2
  4. package/THIRD_PARTY_LICENSES.md +29 -0
  5. package/dist/chunker-budget.d.ts +58 -0
  6. package/dist/chunker-budget.d.ts.map +1 -0
  7. package/dist/chunker-budget.js +11 -0
  8. package/dist/chunker-budget.js.map +1 -0
  9. package/dist/defaults.d.ts +50 -0
  10. package/dist/defaults.d.ts.map +1 -0
  11. package/dist/defaults.js +179 -0
  12. package/dist/defaults.js.map +1 -0
  13. package/dist/discovery.d.ts +40 -0
  14. package/dist/discovery.d.ts.map +1 -0
  15. package/dist/discovery.js +76 -0
  16. package/dist/discovery.js.map +1 -0
  17. package/dist/env.d.ts +37 -0
  18. package/dist/env.d.ts.map +1 -0
  19. package/dist/env.js +100 -0
  20. package/dist/env.js.map +1 -0
  21. package/dist/hash.d.ts +37 -0
  22. package/dist/hash.d.ts.map +1 -0
  23. package/dist/hash.js +52 -0
  24. package/dist/hash.js.map +1 -0
  25. package/dist/index.d.ts +22 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +37 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/load.d.ts +61 -0
  30. package/dist/load.d.ts.map +1 -0
  31. package/dist/load.js +264 -0
  32. package/dist/load.js.map +1 -0
  33. package/dist/merge.d.ts +32 -0
  34. package/dist/merge.d.ts.map +1 -0
  35. package/dist/merge.js +65 -0
  36. package/dist/merge.js.map +1 -0
  37. package/dist/migrations.d.ts +26 -0
  38. package/dist/migrations.d.ts.map +1 -0
  39. package/dist/migrations.js +40 -0
  40. package/dist/migrations.js.map +1 -0
  41. package/dist/parse.d.ts +7 -0
  42. package/dist/parse.d.ts.map +1 -0
  43. package/dist/parse.js +14 -0
  44. package/dist/parse.js.map +1 -0
  45. package/dist/paths.d.ts +14 -0
  46. package/dist/paths.d.ts.map +1 -0
  47. package/dist/paths.js +37 -0
  48. package/dist/paths.js.map +1 -0
  49. package/dist/schema.d.ts +5 -0
  50. package/dist/schema.d.ts.map +1 -0
  51. package/dist/schema.js +19 -0
  52. package/dist/schema.js.map +1 -0
  53. package/dist/secrets.d.ts +33 -0
  54. package/dist/secrets.d.ts.map +1 -0
  55. package/dist/secrets.js +69 -0
  56. package/dist/secrets.js.map +1 -0
  57. package/dist/types.d.ts +323 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +20 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/user-config.d.ts +13 -0
  62. package/dist/user-config.d.ts.map +1 -0
  63. package/dist/user-config.js +34 -0
  64. package/dist/user-config.js.map +1 -0
  65. package/dist/validate.d.ts +36 -0
  66. package/dist/validate.d.ts.map +1 -0
  67. package/dist/validate.js +337 -0
  68. package/dist/validate.js.map +1 -0
  69. package/package.json +65 -3
  70. package/schemas/config-v1.json +375 -0
  71. package/src/chunker-budget.ts +63 -0
  72. package/src/defaults.ts +207 -0
  73. package/src/discovery.ts +95 -0
  74. package/src/env.ts +141 -0
  75. package/src/hash.ts +71 -0
  76. package/src/index.ts +51 -0
  77. package/src/load.ts +346 -0
  78. package/src/merge.ts +69 -0
  79. package/src/migrations.ts +64 -0
  80. package/src/parse.ts +18 -0
  81. package/src/paths.ts +42 -0
  82. package/src/schema.ts +23 -0
  83. package/src/secrets.ts +78 -0
  84. package/src/types.ts +366 -0
  85. package/src/user-config.ts +48 -0
  86. package/src/validate.ts +430 -0
package/dist/load.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { RawConfigLayer, EffectiveUntangleConfigV1 } from "./types.js";
2
+ import type { ConfigIssue } from "./validate.js";
3
+ export interface LoadConfigOptions {
4
+ /** Directory to start upward config-file discovery from. Defaults to `process.cwd()`. */
5
+ cwd?: string;
6
+ /** Highest-priority layer (spec.md §4.1 layer 1): explicit API/CLI arguments. */
7
+ explicit?: RawConfigLayer;
8
+ env?: NodeJS.ProcessEnv;
9
+ platform?: NodeJS.Platform;
10
+ }
11
+ export interface ConfigSources {
12
+ userConfigPath: string | undefined;
13
+ projectConfigPath: string | undefined;
14
+ /**
15
+ * `untangle.toml` (layer 4b) — the file `untangle init` writes. Reported
16
+ * separately so a diagnostic command can name the file the user actually
17
+ * has instead of the one it expected (WP-222).
18
+ */
19
+ tomlConfigPath: string | undefined;
20
+ localConfigPath: string | undefined;
21
+ explicitProvided: boolean;
22
+ /** `UNTANGLE_*` variables from `ENV_MAPPING` that were actually set. */
23
+ envVarsApplied: string[];
24
+ /**
25
+ * Fallback directory shown for diagnostics only: `.untangle.json`'s
26
+ * directory, else `untangle.toml`'s, else `.untangle.local.json`'s, else
27
+ * `cwd`. It is NOT
28
+ * necessarily what `repositories`/`contextArtifacts` paths were resolved
29
+ * against — each of those resolves against the directory of whichever
30
+ * layer actually defined it (spec.md §4.3; see `loadConfig`'s
31
+ * `originBaseDirFor`), which may be the OS user config directory or `cwd`
32
+ * (env/explicit layers) instead.
33
+ */
34
+ configBaseDir: string;
35
+ /** Logical project root for `.untangle/config.json`; legacy layouts keep their existing base. */
36
+ projectRoot?: string;
37
+ }
38
+ export interface LoadConfigResult {
39
+ effectiveConfig: EffectiveUntangleConfigV1;
40
+ warnings: ConfigIssue[];
41
+ errors: ConfigIssue[];
42
+ sources: ConfigSources;
43
+ /** Canonical effective-config hash; Qdrant excludes additive storage fields for legacy identity. */
44
+ configHash: string;
45
+ /** `buildWorkspaceId({ explicitId: effectiveConfig.workspaceId, configHash })` (spec.md §5.1). */
46
+ workspaceId: string;
47
+ /** `buildEmbeddingProfileId` over the derived profile (spec.md §5.7). */
48
+ embeddingProfileId: string;
49
+ }
50
+ /**
51
+ * Loads and resolves the effective Untangle configuration through all six
52
+ * layers of spec.md §4.1: explicit args > env (`UNTANGLE_*`) >
53
+ * `.untangle.local.json` > `.untangle.json` > OS user config > product
54
+ * defaults. Never throws for validation problems — see {@link ConfigIssue}
55
+ * and `validate.ts`'s `assertConfigValid` for the "MUST abort" behavior
56
+ * spec.md §4.8 requires from a caller.
57
+ */
58
+ export declare function loadConfig(options?: LoadConfigOptions): LoadConfigResult;
59
+ /** Re-exported for callers that only need to check whether a discovered path exists (rare; mostly test convenience). */
60
+ export declare function configFileExists(path: string | undefined): boolean;
61
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC;;;;OAIG;IACH,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;;;;;;;;;OASG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,yBAAyB,CAAC;IAC3C,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,CAAC;IACvB,oGAAoG;IACpG,UAAU,EAAE,MAAM,CAAC;IACnB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AA2FD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA0K5E;AAED,wHAAwH;AACxH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElE"}
package/dist/load.js ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Top-level configuration loading: discovery + six-level layering (spec.md
3
+ * §4.1) + path resolution (spec.md §4.3) + validation (spec.md §4.8) +
4
+ * canonical hashing and embedding profile derivation (architecture.md §6.3).
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import { buildWorkspaceId } from "@untangle-code/domain";
9
+ import { parse as parseToml } from "smol-toml";
10
+ import { PRODUCT_DEFAULT_CONFIG } from "./defaults.js";
11
+ import { discoverConfigFiles } from "./discovery.js";
12
+ import { buildEnvConfigLayer } from "./env.js";
13
+ import { computeConfigHash, deriveEmbeddingProfileId } from "./hash.js";
14
+ import { mergeConfigLayers } from "./merge.js";
15
+ import { migrateConfig } from "./migrations.js";
16
+ import { resolveConfigPath } from "./paths.js";
17
+ import { resolveUserConfigPath } from "./user-config.js";
18
+ import { validateEffectiveConfig } from "./validate.js";
19
+ function readJsonConfigFile(path) {
20
+ let text;
21
+ try {
22
+ text = readFileSync(path, "utf8");
23
+ }
24
+ catch (error) {
25
+ return {
26
+ layer: {},
27
+ error: {
28
+ code: "CONFIG_INVALID",
29
+ message: `failed to read config file "${path}": ${String(error)}`,
30
+ path,
31
+ },
32
+ };
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(text);
37
+ }
38
+ catch (error) {
39
+ return {
40
+ layer: {},
41
+ error: {
42
+ code: "CONFIG_INVALID",
43
+ message: `"${path}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
44
+ path,
45
+ },
46
+ };
47
+ }
48
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
49
+ return {
50
+ layer: {},
51
+ error: { code: "CONFIG_INVALID", message: `"${path}" must contain a JSON object`, path },
52
+ };
53
+ }
54
+ const migrated = migrateConfig(parsed);
55
+ return { layer: migrated.config, error: undefined };
56
+ }
57
+ /**
58
+ * Reads `untangle.toml` into the same raw layer shape a JSON config produces.
59
+ *
60
+ * The TOML file `untangle init` writes carries the SAME keys as
61
+ * `.untangle.json` — `schemaVersion`, `repositories`, `qdrant`, `embeddings`
62
+ * and so on — so it needs no separate schema, only a separate parser. It
63
+ * runs through `migrateConfig` and the ordinary validation like every other
64
+ * layer; a typo in it is reported the same way, in the same list, against
65
+ * the same paths.
66
+ */
67
+ function readTomlConfigFile(path) {
68
+ let text;
69
+ try {
70
+ text = readFileSync(path, "utf8");
71
+ }
72
+ catch (error) {
73
+ return {
74
+ layer: {},
75
+ error: {
76
+ code: "CONFIG_INVALID",
77
+ message: `failed to read config file "${path}": ${String(error)}`,
78
+ path,
79
+ },
80
+ };
81
+ }
82
+ let parsed;
83
+ try {
84
+ parsed = parseToml(text);
85
+ }
86
+ catch (error) {
87
+ return {
88
+ layer: {},
89
+ error: {
90
+ code: "CONFIG_INVALID",
91
+ message: `"${path}" is not valid TOML: ${error instanceof Error ? error.message : String(error)}`,
92
+ path,
93
+ },
94
+ };
95
+ }
96
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
97
+ return {
98
+ layer: {},
99
+ error: { code: "CONFIG_INVALID", message: `"${path}" must contain a TOML table`, path },
100
+ };
101
+ }
102
+ const migrated = migrateConfig(parsed);
103
+ return { layer: migrated.config, error: undefined };
104
+ }
105
+ /**
106
+ * Loads and resolves the effective Untangle configuration through all six
107
+ * layers of spec.md §4.1: explicit args > env (`UNTANGLE_*`) >
108
+ * `.untangle.local.json` > `.untangle.json` > OS user config > product
109
+ * defaults. Never throws for validation problems — see {@link ConfigIssue}
110
+ * and `validate.ts`'s `assertConfigValid` for the "MUST abort" behavior
111
+ * spec.md §4.8 requires from a caller.
112
+ */
113
+ export function loadConfig(options = {}) {
114
+ const cwd = options.cwd ?? process.cwd();
115
+ const env = options.env ?? process.env;
116
+ const platform = options.platform ?? process.platform;
117
+ const { projectConfigPath, tomlConfigPath, localConfigPath, projectRoot } = discoverConfigFiles(cwd);
118
+ const userConfigPath = resolveUserConfigPath(env, platform);
119
+ const errors = [];
120
+ const userLayerOutcome = userConfigPath
121
+ ? readJsonConfigFile(userConfigPath)
122
+ : { layer: {}, error: undefined };
123
+ const projectLayerOutcome = projectConfigPath
124
+ ? readJsonConfigFile(projectConfigPath)
125
+ : { layer: {}, error: undefined };
126
+ const tomlLayerOutcome = tomlConfigPath
127
+ ? readTomlConfigFile(tomlConfigPath)
128
+ : { layer: {}, error: undefined };
129
+ const localLayerOutcome = localConfigPath
130
+ ? readJsonConfigFile(localConfigPath)
131
+ : { layer: {}, error: undefined };
132
+ for (const outcome of [
133
+ userLayerOutcome,
134
+ tomlLayerOutcome,
135
+ projectLayerOutcome,
136
+ localLayerOutcome,
137
+ ]) {
138
+ if (outcome.error) {
139
+ errors.push(outcome.error);
140
+ }
141
+ }
142
+ const envLayerResult = buildEnvConfigLayer(env);
143
+ for (const parseError of envLayerResult.parseErrors) {
144
+ errors.push({
145
+ code: "CONFIG_INVALID",
146
+ message: `environment variable ${parseError.env}: ${parseError.message}`,
147
+ path: parseError.env,
148
+ });
149
+ }
150
+ const explicitLayer = options.explicit ?? {};
151
+ // Each layer's OWN directory (spec.md §4.3: a path resolves relative to
152
+ // the config file that declared it). Layers with no backing file (product
153
+ // defaults, the env layer, explicit args) have no directory of their own,
154
+ // so they fall back to `cwd` — the directory the caller is running from.
155
+ const namedLayers = [
156
+ { layer: PRODUCT_DEFAULT_CONFIG, baseDir: cwd },
157
+ { layer: userLayerOutcome.layer, baseDir: userConfigPath ? dirname(userConfigPath) : cwd },
158
+ // Layer 4b, directly beneath `.untangle.json`: `untangle.toml`. Where
159
+ // both exist the JSON file wins — and `warnings` says so out loud, so a
160
+ // user never wonders why an edit had no effect (WP-222).
161
+ { layer: tomlLayerOutcome.layer, baseDir: tomlConfigPath ? dirname(tomlConfigPath) : cwd },
162
+ {
163
+ layer: projectLayerOutcome.layer,
164
+ baseDir: projectConfigPath ? dirname(projectConfigPath) : cwd,
165
+ },
166
+ { layer: localLayerOutcome.layer, baseDir: localConfigPath ? dirname(localConfigPath) : cwd },
167
+ { layer: envLayerResult.layer, baseDir: cwd },
168
+ { layer: explicitLayer, baseDir: cwd },
169
+ ];
170
+ // Preserve existing Qdrant projects without letting the product-default
171
+ // qdrant section choose a backend for new projects. This compatibility
172
+ // default sits below every user layer: an explicit storage.backend at any
173
+ // priority overrides it, while storage.path alone never migrates an index.
174
+ const configuredLayers = namedLayers.slice(1).map((named) => named.layer);
175
+ const legacyStorageDefault = configuredLayers.some((layer) => layer.qdrant !== undefined)
176
+ ? { storage: { backend: "qdrant" } }
177
+ : {};
178
+ const merged = mergeConfigLayers([
179
+ PRODUCT_DEFAULT_CONFIG,
180
+ legacyStorageDefault,
181
+ ...configuredLayers,
182
+ ]);
183
+ const configBaseDir = projectConfigPath
184
+ ? dirname(projectConfigPath)
185
+ : tomlConfigPath
186
+ ? dirname(tomlConfigPath)
187
+ : localConfigPath
188
+ ? dirname(localConfigPath)
189
+ : cwd;
190
+ /**
191
+ * `mergeConfigLayers` REPLACES arrays wholesale rather than concatenating
192
+ * them (merge.ts) — so the final `repositories`/`contextArtifacts` array
193
+ * in `merged` originates entirely from ONE layer: the highest-priority
194
+ * layer that defined it. Its entries' `path` fields MUST resolve against
195
+ * THAT layer's own directory (spec.md §4.3), not a single global
196
+ * `configBaseDir` computed after the merge — otherwise a repository
197
+ * defined in the OS user config (or via an env/explicit layer) silently
198
+ * loses its origin and resolves against the wrong directory. Walking
199
+ * `namedLayers` highest-priority-first and taking the first one that set
200
+ * `key` recovers exactly the layer the merge itself used.
201
+ */
202
+ function originBaseDirFor(key) {
203
+ for (let i = namedLayers.length - 1; i >= 0; i -= 1) {
204
+ const candidate = namedLayers[i];
205
+ if (candidate && Array.isArray(candidate.layer[key])) {
206
+ return candidate.baseDir;
207
+ }
208
+ }
209
+ return cwd;
210
+ }
211
+ if (Array.isArray(merged["repositories"])) {
212
+ const baseDir = originBaseDirFor("repositories");
213
+ merged["repositories"] = merged["repositories"].map((repo) => ({
214
+ ...repo,
215
+ path: resolveConfigPath(repo.path, baseDir),
216
+ }));
217
+ }
218
+ if (Array.isArray(merged["contextArtifacts"])) {
219
+ const baseDir = originBaseDirFor("contextArtifacts");
220
+ merged["contextArtifacts"] = merged["contextArtifacts"].map((artifact) => ({
221
+ ...artifact,
222
+ path: resolveConfigPath(artifact.path, baseDir),
223
+ }));
224
+ }
225
+ const { errors: validationErrors, warnings } = validateEffectiveConfig(merged);
226
+ errors.push(...validationErrors);
227
+ if (projectConfigPath !== undefined && tomlConfigPath !== undefined) {
228
+ warnings.push({
229
+ code: "CONFIG_DUPLICATE_PROJECT_FILE",
230
+ message: `both "${projectConfigPath}" and "${tomlConfigPath}" exist; ` +
231
+ `"${projectConfigPath}" wins for every key it sets`,
232
+ path: "(project config)",
233
+ });
234
+ }
235
+ const effectiveConfig = merged;
236
+ const configHash = computeConfigHash(effectiveConfig);
237
+ const workspaceId = buildWorkspaceId(effectiveConfig.workspaceId === undefined
238
+ ? { configHash }
239
+ : { explicitId: effectiveConfig.workspaceId, configHash });
240
+ const embeddingProfileId = deriveEmbeddingProfileId(effectiveConfig);
241
+ return {
242
+ effectiveConfig,
243
+ warnings,
244
+ errors,
245
+ sources: {
246
+ userConfigPath,
247
+ projectConfigPath,
248
+ tomlConfigPath,
249
+ localConfigPath,
250
+ explicitProvided: options.explicit !== undefined,
251
+ envVarsApplied: Object.keys(env).filter((key) => key.startsWith("UNTANGLE_") && env[key] !== undefined),
252
+ configBaseDir,
253
+ ...(projectRoot === undefined ? {} : { projectRoot }),
254
+ },
255
+ configHash,
256
+ workspaceId,
257
+ embeddingProfileId,
258
+ };
259
+ }
260
+ /** Re-exported for callers that only need to check whether a discovered path exists (rare; mostly test convenience). */
261
+ export function configFileExists(path) {
262
+ return path !== undefined && existsSync(path);
263
+ }
264
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deep merge for the layering pipeline (spec.md §4.1).
3
+ *
4
+ * Array semantics: arrays REPLACE, they never concatenate. A layer with
5
+ * `index.extraExtensions: [".proto"]` fully replaces a lower layer's array —
6
+ * two layers are never spliced together, because there is no reasonable way
7
+ * to know whether a higher layer meant to add to or reset a list (e.g.
8
+ * `exclude` globs), and silent concatenation would make higher-priority
9
+ * layers unable to shrink a list at all.
10
+ *
11
+ * Relevance-policy bindings are another deliberate replacement boundary:
12
+ * entries below `search.relevancePolicies.<profileId>` are calibrated,
13
+ * corpus-bound units. Merging a new workspace/collection with thresholds
14
+ * inherited from a lower layer would silently apply one corpus's policy to
15
+ * another corpus.
16
+ */
17
+ /**
18
+ * Merges `override` onto `base`. Plain objects merge key by key (recursively);
19
+ * arrays, primitives, `null` and any non-plain object (e.g. would-be class
20
+ * instances) from `override` replace the corresponding value in `base`
21
+ * wholesale. `undefined` values in `override` are treated as "not set" and do
22
+ * not overwrite a present `base` value — this lets a layer omit a field
23
+ * without needing to know the value from lower layers.
24
+ */
25
+ export declare function deepMergeConfig<T>(base: T, override: unknown): T;
26
+ /**
27
+ * Merges layers left to right — later layers win — mirroring spec.md §4.1's
28
+ * priority order when called with `[productDefaults, userConfig, projectConfig,
29
+ * localConfig, envLayer, explicitArgs]` (lowest priority first).
30
+ */
31
+ export declare function mergeConfigLayers<T>(layers: readonly unknown[]): T;
32
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,CAAC,CAEhE;AA8BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,CAAC,CAElE"}
package/dist/merge.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Deep merge for the layering pipeline (spec.md §4.1).
3
+ *
4
+ * Array semantics: arrays REPLACE, they never concatenate. A layer with
5
+ * `index.extraExtensions: [".proto"]` fully replaces a lower layer's array —
6
+ * two layers are never spliced together, because there is no reasonable way
7
+ * to know whether a higher layer meant to add to or reset a list (e.g.
8
+ * `exclude` globs), and silent concatenation would make higher-priority
9
+ * layers unable to shrink a list at all.
10
+ *
11
+ * Relevance-policy bindings are another deliberate replacement boundary:
12
+ * entries below `search.relevancePolicies.<profileId>` are calibrated,
13
+ * corpus-bound units. Merging a new workspace/collection with thresholds
14
+ * inherited from a lower layer would silently apply one corpus's policy to
15
+ * another corpus.
16
+ */
17
+ function isPlainObject(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ /**
21
+ * Merges `override` onto `base`. Plain objects merge key by key (recursively);
22
+ * arrays, primitives, `null` and any non-plain object (e.g. would-be class
23
+ * instances) from `override` replace the corresponding value in `base`
24
+ * wholesale. `undefined` values in `override` are treated as "not set" and do
25
+ * not overwrite a present `base` value — this lets a layer omit a field
26
+ * without needing to know the value from lower layers.
27
+ */
28
+ export function deepMergeConfig(base, override) {
29
+ return deepMergeConfigAtPath(base, override, []);
30
+ }
31
+ function isAtomicReplacementPath(path) {
32
+ return path.length === 3 && path[0] === "search" && path[1] === "relevancePolicies";
33
+ }
34
+ function deepMergeConfigAtPath(base, override, path) {
35
+ if (override === undefined) {
36
+ return base;
37
+ }
38
+ if (isAtomicReplacementPath(path)) {
39
+ return override;
40
+ }
41
+ if (!isPlainObject(base) || !isPlainObject(override)) {
42
+ return override;
43
+ }
44
+ const result = { ...base };
45
+ for (const [key, value] of Object.entries(override)) {
46
+ if (value === undefined) {
47
+ continue;
48
+ }
49
+ const baseValue = base[key];
50
+ result[key] =
51
+ isPlainObject(baseValue) && isPlainObject(value)
52
+ ? deepMergeConfigAtPath(baseValue, value, [...path, key])
53
+ : value;
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Merges layers left to right — later layers win — mirroring spec.md §4.1's
59
+ * priority order when called with `[productDefaults, userConfig, projectConfig,
60
+ * localConfig, envLayer, explicitArgs]` (lowest priority first).
61
+ */
62
+ export function mergeConfigLayers(layers) {
63
+ return layers.reduce((acc, layer) => deepMergeConfig(acc, layer), {});
64
+ }
65
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAI,IAAO,EAAE,QAAiB;IAC3D,OAAO,qBAAqB,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,uBAAuB,CAAC,IAAuB;IACtD,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,mBAAmB,CAAC;AACtF,CAAC;AAED,SAAS,qBAAqB,CAAI,IAAO,EAAE,QAAiB,EAAE,IAAuB;IACnF,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,QAAa,CAAC;IACvB,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,OAAO,QAAa,CAAC;IACvB,CAAC;IACD,MAAM,MAAM,GAA4B,EAAE,GAAG,IAAI,EAAE,CAAC;IACpD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,SAAS;QACX,CAAC;QACD,MAAM,SAAS,GAAI,IAAgC,CAAC,GAAG,CAAC,CAAC;QACzD,MAAM,CAAC,GAAG,CAAC;YACT,aAAa,CAAC,SAAS,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC;gBAC9C,CAAC,CAAC,qBAAqB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;gBACzD,CAAC,CAAC,KAAK,CAAC;IACd,CAAC;IACD,OAAO,MAAW,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAI,MAA0B;IAC7D,OAAO,MAAM,CAAC,MAAM,CAAU,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAM,CAAC;AACtF,CAAC"}
@@ -0,0 +1,26 @@
1
+ export interface MigrationStep {
2
+ fromVersion: number;
3
+ toVersion: number;
4
+ /** Pure transform: old raw config shape in, new raw config shape out. */
5
+ migrate: (raw: Record<string, unknown>) => Record<string, unknown>;
6
+ }
7
+ /** Registers a migration step keyed by its `fromVersion`. Overwrites a step already registered for that version. */
8
+ export declare function registerMigration(step: MigrationStep): void;
9
+ /** Removes every registered migration step (test isolation). */
10
+ export declare function clearMigrations(): void;
11
+ export interface MigrationResult {
12
+ config: Record<string, unknown>;
13
+ finalVersion: number;
14
+ /** Versions the config was migrated through, e.g. `[1, 2]` for a single `1 -> 2` hop. */
15
+ appliedFrom: number[];
16
+ }
17
+ /**
18
+ * Applies registered migration steps in sequence starting from
19
+ * `raw.schemaVersion` (defaulting to `1` when absent) until
20
+ * `targetVersion` (default: {@link CURRENT_SCHEMA_VERSION}) is reached.
21
+ *
22
+ * A config already at `targetVersion` is returned unchanged — the identity
23
+ * migration — without requiring any step to be registered for it.
24
+ */
25
+ export declare function migrateConfig(raw: Record<string, unknown>, targetVersion?: number): MigrationResult;
26
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpE;AAID,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAE3D;AAED,gEAAgE;AAChE,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,aAAa,GAAE,MAA+B,GAC7C,eAAe,CAkBjB"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Migration scaffold for future `schemaVersion` jumps (architecture.md §6.3:
3
+ * "Migration älterer Configversionen"). spec.md is at `schemaVersion: 1`
4
+ * today; this registry lets a future WP register a `1 -> 2` step (etc.)
5
+ * without `loadConfig` needing to change.
6
+ */
7
+ import { CURRENT_SCHEMA_VERSION } from "./defaults.js";
8
+ const registry = new Map();
9
+ /** Registers a migration step keyed by its `fromVersion`. Overwrites a step already registered for that version. */
10
+ export function registerMigration(step) {
11
+ registry.set(step.fromVersion, step);
12
+ }
13
+ /** Removes every registered migration step (test isolation). */
14
+ export function clearMigrations() {
15
+ registry.clear();
16
+ }
17
+ /**
18
+ * Applies registered migration steps in sequence starting from
19
+ * `raw.schemaVersion` (defaulting to `1` when absent) until
20
+ * `targetVersion` (default: {@link CURRENT_SCHEMA_VERSION}) is reached.
21
+ *
22
+ * A config already at `targetVersion` is returned unchanged — the identity
23
+ * migration — without requiring any step to be registered for it.
24
+ */
25
+ export function migrateConfig(raw, targetVersion = CURRENT_SCHEMA_VERSION) {
26
+ let current = raw;
27
+ let version = typeof raw["schemaVersion"] === "number" ? raw["schemaVersion"] : 1;
28
+ const appliedFrom = [version];
29
+ while (version < targetVersion) {
30
+ const step = registry.get(version);
31
+ if (step === undefined) {
32
+ throw new Error(`no migration registered from schemaVersion ${version} towards ${targetVersion}`);
33
+ }
34
+ current = { ...step.migrate(current), schemaVersion: step.toVersion };
35
+ version = step.toVersion;
36
+ appliedFrom.push(version);
37
+ }
38
+ return { config: current, finalVersion: version, appliedFrom };
39
+ }
40
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AASvD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;AAElD,oHAAoH;AACpH,MAAM,UAAU,iBAAiB,CAAC,IAAmB;IACnD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,eAAe;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AASD;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,GAA4B,EAC5B,gBAAwB,sBAAsB;IAE9C,IAAI,OAAO,GAA4B,GAAG,CAAC;IAC3C,IAAI,OAAO,GAAG,OAAO,GAAG,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,WAAW,GAAa,CAAC,OAAO,CAAC,CAAC;IAExC,OAAO,OAAO,GAAG,aAAa,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,8CAA8C,OAAO,YAAY,aAAa,EAAE,CACjF,CAAC;QACJ,CAAC;QACD,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QACtE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QACzB,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { RawConfigLayer } from "./types.js";
2
+ /**
3
+ * Parses a config document without filesystem access, defaults or environment
4
+ * overrides. Syntax and non-object input errors propagate to the caller.
5
+ */
6
+ export declare function parseConfigText(text: string, format: "json" | "toml"): RawConfigLayer;
7
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAQrF"}
package/dist/parse.js ADDED
@@ -0,0 +1,14 @@
1
+ import { parse as parseToml } from "smol-toml";
2
+ import { migrateConfig } from "./migrations.js";
3
+ /**
4
+ * Parses a config document without filesystem access, defaults or environment
5
+ * overrides. Syntax and non-object input errors propagate to the caller.
6
+ */
7
+ export function parseConfigText(text, format) {
8
+ const parsed = format === "json" ? JSON.parse(text) : parseToml(text);
9
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10
+ throw new TypeError(`Configuration must contain a ${format === "json" ? "JSON object" : "TOML table"}.`);
11
+ }
12
+ return migrateConfig(parsed).config;
13
+ }
14
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,MAAuB;IACnE,MAAM,MAAM,GAAY,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/E,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,SAAS,CACjB,gCAAgC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,GAAG,CACpF,CAAC;IACJ,CAAC;IACD,OAAO,aAAa,CAAC,MAAiC,CAAC,CAAC,MAAwB,CAAC;AACnF,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolves `candidatePath` against `configFileDir` (the directory containing
3
+ * the config file that declared it). An already-absolute `candidatePath` is
4
+ * returned canonicalized as-is (spec.md §4.3 allows absolute paths).
5
+ */
6
+ export declare function resolveConfigPath(candidatePath: string, configFileDir: string): string;
7
+ /**
8
+ * Resolves every `RepositoryConfig.path` in `repositories` relative to
9
+ * `configFileDir`, returning new objects (inputs are not mutated).
10
+ */
11
+ export declare function resolveRepositoryPaths<T extends {
12
+ path: string;
13
+ }>(repositories: readonly T[], configFileDir: string): T[];
14
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC/D,YAAY,EAAE,SAAS,CAAC,EAAE,EAC1B,aAAa,EAAE,MAAM,GACpB,CAAC,EAAE,CAKL"}
package/dist/paths.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Path resolution relative to the configuration file (spec.md §4.3:
3
+ * "Pfade MÜSSEN relativ zur Konfigurationsdatei oder absolut auflösbar
4
+ * sein."). Results are canonical: absolute, `/`-separated, no trailing slash,
5
+ * no `.`/`..` segments.
6
+ */
7
+ import { isAbsolute, resolve, sep } from "node:path";
8
+ /** Canonicalizes a resolved absolute filesystem path to forward slashes, no trailing slash. */
9
+ function toCanonicalAbsolute(absolutePath) {
10
+ const normalized = absolutePath.split(sep).join("/");
11
+ if (normalized.length > 1 && normalized.endsWith("/")) {
12
+ return normalized.slice(0, -1);
13
+ }
14
+ return normalized;
15
+ }
16
+ /**
17
+ * Resolves `candidatePath` against `configFileDir` (the directory containing
18
+ * the config file that declared it). An already-absolute `candidatePath` is
19
+ * returned canonicalized as-is (spec.md §4.3 allows absolute paths).
20
+ */
21
+ export function resolveConfigPath(candidatePath, configFileDir) {
22
+ const absolute = isAbsolute(candidatePath)
23
+ ? candidatePath
24
+ : resolve(configFileDir, candidatePath);
25
+ return toCanonicalAbsolute(absolute);
26
+ }
27
+ /**
28
+ * Resolves every `RepositoryConfig.path` in `repositories` relative to
29
+ * `configFileDir`, returning new objects (inputs are not mutated).
30
+ */
31
+ export function resolveRepositoryPaths(repositories, configFileDir) {
32
+ return repositories.map((repo) => ({
33
+ ...repo,
34
+ path: resolveConfigPath(repo.path, configFileDir),
35
+ }));
36
+ }
37
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErD,+FAA+F;AAC/F,SAAS,mBAAmB,CAAC,YAAoB;IAC/C,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACtD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,aAAqB,EAAE,aAAqB;IAC5E,MAAM,QAAQ,GAAG,UAAU,CAAC,aAAa,CAAC;QACxC,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAC1C,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,YAA0B,EAC1B,aAAqB;IAErB,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjC,GAAG,IAAI;QACP,IAAI,EAAE,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC;KAClD,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** Absolute filesystem path to the hand-authored JSON Schema (Draft 2020-12) for `UntangleConfigV1`. */
2
+ export declare const CONFIG_SCHEMA_PATH: string;
3
+ /** The parsed JSON Schema object itself, read once at module load. */
4
+ export declare const CONFIG_SCHEMA: Record<string, unknown>;
5
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAgBA,wGAAwG;AACxG,eAAO,MAAM,kBAAkB,EAAE,MAAiC,CAAC;AAEnE,sEAAsE;AACtE,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAEtB,CAAC"}