@untangle-code/config 0.0.0-stage → 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 (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/src/types.ts ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Configuration types (spec.md §4.2–§4.8, §5.7; product.md §11; portfolio.md
3
+ * §14.4; architecture.md §6.3, §23.2).
4
+ *
5
+ * These are the *raw*, *partial*-friendly shapes a `.untangle.json`,
6
+ * environment layer or CLI argument object may contribute. The fully merged,
7
+ * defaulted and validated shape produced by {@link "./load.js"} carries the
8
+ * same interfaces but with all optional fields the product defaults fill in
9
+ * actually present — see `EffectiveUntangleConfigV1` at the bottom of this
10
+ * file.
11
+ */
12
+ import type { ContextArtifactConfig, RepositoryConfig } from "@untangle-code/domain";
13
+
14
+ // Re-exported so consumers of @untangle-code/config never need to reach into
15
+ // @untangle-code/domain themselves for these two config-shaped types (domain
16
+ // README, decision 5: RepositoryConfig lives in domain by WP-01 order;
17
+ // ContextArtifactConfig likewise — this package re-exports rather than
18
+ // duplicating either).
19
+ export type { ContextArtifactConfig, ContextArtifactType } from "@untangle-code/domain";
20
+ export type { RepositoryConfig } from "@untangle-code/domain";
21
+
22
+ /** spec.md §4.4. */
23
+ export interface QdrantConfig {
24
+ mode: "managed" | "external";
25
+ url?: string;
26
+ /** Name of the environment variable holding the API key — never the key itself (architecture.md §23.2). */
27
+ apiKeyEnv?: string;
28
+ collectionPrefix?: string;
29
+ requestTimeoutMs?: number;
30
+ maxRetries?: number;
31
+ upsertMaxPoints?: number;
32
+ upsertBudgetBytes?: number;
33
+ }
34
+
35
+ /** Search backend selection. Relative local paths resolve against the repository root. */
36
+ export interface StorageConfig {
37
+ backend: "lancedb" | "qdrant";
38
+ /** Embedded LanceDB directory; defaults to `.untangle/index/lancedb`. */
39
+ path?: string;
40
+ }
41
+
42
+ /** spec.md §4.5. */
43
+ export interface EmbeddingConfig {
44
+ provider: "ollama";
45
+ mode: "auto" | "managed" | "external";
46
+ url?: string;
47
+ /** Name of the environment variable holding the API key — never the key itself (architecture.md §23.2). */
48
+ apiKeyEnv?: string;
49
+ model: string;
50
+ dimensions: number;
51
+ contextLength: number;
52
+ queryPrefix: string;
53
+ documentPrefix: string;
54
+ includePath: boolean;
55
+ batchSize: number;
56
+ maxConnections: number;
57
+ requestTimeoutMs: number;
58
+ }
59
+
60
+ /** spec.md §4.6. */
61
+ export interface IndexConfig {
62
+ watch: boolean;
63
+ reconcileIntervalMinutes: number;
64
+ maxFileSizeMb: number;
65
+ maxGraphFileSizeMb: number;
66
+ scanConcurrency: number;
67
+ embeddingConcurrency: number;
68
+ filesPerCheckpoint: number;
69
+ targetChunkTokens: number;
70
+ maxChunkTokens: number;
71
+ fallbackOverlapTokens: number;
72
+ includeDotFiles: boolean;
73
+ indexExtensionless: boolean;
74
+ extraExtensions: string[];
75
+ extensionLanguageMap: Record<string, string>;
76
+ }
77
+
78
+ /**
79
+ * Search defaults (Untangle decision — spec.md §13 defines the request/result
80
+ * shapes `SearchRequestV1`/`SearchResultV1` in domain, but names no
81
+ * `SearchConfig` fields; this shape documents which of those request knobs
82
+ * get a project-wide default and adds the boost weights spec.md §13.3 lists
83
+ * without assigning default magnitudes).
84
+ */
85
+ export interface SearchConfig {
86
+ /** Default `limit` when a search request omits it. */
87
+ defaultLimit: number;
88
+ /** Default `candidateLimit` (pre-RRF/pre-filter pool size) when omitted. */
89
+ defaultCandidateLimit: number;
90
+ /** Hard ceiling a caller-supplied `candidateLimit` is clamped to. */
91
+ maxCandidateLimit: number;
92
+ /** Default `diversify` (MMR, architecture.md §18.5) when a request omits it. */
93
+ diversifyByDefault: boolean;
94
+ /** Default `minScore` when a request omits it. */
95
+ minScoreDefault?: number;
96
+ /**
97
+ * Calibrated absence decisions, keyed by the exact embedding profile id.
98
+ *
99
+ * These values are deliberately optional and have no product default: raw
100
+ * cosine scores are model-specific, and their useful separation also
101
+ * depends on the indexed corpus. A policy therefore only applies when its
102
+ * key, workspace id, and physical collection all match the active target.
103
+ */
104
+ relevancePolicies?: Record<string, SearchRelevancePolicyBindingConfigV1>;
105
+ /** Relative weights of the additive score components (spec.md §13.3). */
106
+ boostWeights: {
107
+ exactSymbol: number;
108
+ exactPath: number;
109
+ repoPriority: number;
110
+ graphProximity: number;
111
+ recency: number;
112
+ duplicatePenalty: number;
113
+ };
114
+ }
115
+
116
+ /** Corpus binding for a calibrated search relevance policy (WP-231). */
117
+ export interface SearchRelevancePolicyBindingConfigV1 {
118
+ /** Exact logical workspace used while calibrating the thresholds. */
119
+ workspaceId: string;
120
+ /** Exact physical vector collection used while calibrating the thresholds. */
121
+ collection: string;
122
+ policy: SearchRelevancePolicyConfigV1;
123
+ }
124
+
125
+ /** Config representation of the search engine's calibrated relevance policy (WP-231). */
126
+ export interface SearchRelevancePolicyConfigV1 {
127
+ /** Human- and machine-readable provenance of the measurements behind the thresholds. */
128
+ calibrationId: string;
129
+ /** Absolute minimum cosine score of the strongest dense candidate, in [-1, 1]. */
130
+ minimumDenseScore: number;
131
+ /** Minimum difference between rank 1 and `denseReferenceRank`, in [0, 2]. */
132
+ minimumDenseSpread: number;
133
+ /** One-based dense rank used for the relative comparison; at least 2. */
134
+ denseReferenceRank: number;
135
+ /** Minimum IDF-weighted fraction of query evidence found together in one candidate. */
136
+ minimumCandidateIdfCoverage: number;
137
+ }
138
+
139
+ /**
140
+ * Graph traversal limits (Untangle decision — architecture.md §17.4 names
141
+ * `maxDepth`/`maxNodes`/`maxEdges`/`maxWallTimeMs` as per-query traversal
142
+ * limits without a config shape; this makes them project-configurable
143
+ * defaults). `includeDynamicImports` reflects the `dynamic_import` edge kind
144
+ * from spec.md §14.2 being switchable.
145
+ */
146
+ export interface GraphConfig {
147
+ enabled: boolean;
148
+ maxTraversalDepth: number;
149
+ maxTraversalNodes: number;
150
+ maxTraversalEdges: number;
151
+ maxTraversalWallTimeMs: number;
152
+ includeDynamicImports: boolean;
153
+ }
154
+
155
+ /**
156
+ * Complexity engine switches (Untangle decision — spec.md §16 defines the
157
+ * metrics and algorithm IDs exhaustively but no `ComplexityConfig` shape;
158
+ * product.md §11's example config uses `enabled` and `cognitiveAlgorithm`,
159
+ * which anchor this shape). `cyclomaticVariant` selects which of classic
160
+ * (§16.2) and extended (§16.3) get computed; both are cheap to compute
161
+ * together so "both" is the default.
162
+ */
163
+ export interface ComplexityConfig {
164
+ enabled: boolean;
165
+ /** Overridable for future algorithm versions; defaults to `COGNITIVE_ALGORITHM_ID` from domain. */
166
+ cognitiveAlgorithm: string;
167
+ cyclomaticVariant: "classic" | "extended" | "both";
168
+ maintainabilityIndexEnabled: boolean;
169
+ }
170
+
171
+ /**
172
+ * Change Intelligence configuration (Untangle decision — spec.md §17.6 gives
173
+ * the default risk score weights and requires every component to be
174
+ * disclosed with its weight and contribution, but does not name a config
175
+ * type). Weights MUST sum to `1.0` within a `±0.001` tolerance, mirroring the
176
+ * validation rule portfolio.md §14.4 states for `blueprint.weights`.
177
+ */
178
+ export interface ChangeConfig {
179
+ enabled: boolean;
180
+ riskWeights: {
181
+ churn: number;
182
+ positiveCognitiveDelta: number;
183
+ positiveCyclomaticDelta: number;
184
+ blastRadius: number;
185
+ graphCentrality: number;
186
+ semanticChange: number;
187
+ testDistance: number;
188
+ };
189
+ symbolMatching: {
190
+ /** Minimum token/MinHash similarity (spec.md §17.3 step 4) to accept a rename/move candidate. */
191
+ tokenSimilarityMin: number;
192
+ /** Whether step 6 (embedding cosine at equal profile) participates at all. */
193
+ enableEmbeddingSimilarity: boolean;
194
+ };
195
+ }
196
+
197
+ /** spec.md §4.7. */
198
+ export interface QualityGateConfig {
199
+ mode: "off" | "warn" | "fail";
200
+ newCyclesAllowed: number;
201
+ cognitivePerSymbolMax?: number;
202
+ cognitiveDeltaPerSymbolMax?: number;
203
+ cyclomaticPerSymbolMax?: number;
204
+ cyclomaticDeltaPerSymbolMax?: number;
205
+ maxNestingPerSymbolMax?: number;
206
+ maintainabilityIndexMin?: number;
207
+ riskScoreMax?: number;
208
+ minimumParseQuality: "failed" | "partial" | "complete";
209
+ ignoreExistingViolations: boolean;
210
+ include?: string[];
211
+ exclude?: string[];
212
+ }
213
+
214
+ /**
215
+ * Telemetry (spec.md §24: "Telemetrie MUSS standardmäßig aus sein" — telemetry
216
+ * MUST default to off; §23.2/§23.3 name the log/metrics facilities this
217
+ * switches). Shape beyond `enabled` is an Untangle decision.
218
+ */
219
+ export interface TelemetryConfig {
220
+ /** Default `false` — local-first, opt-in only (spec.md §24). */
221
+ enabled: boolean;
222
+ logLevel: "error" | "warn" | "info" | "debug" | "trace";
223
+ /** Whether internal counters (spec.md §23.3) are exported to an external system. */
224
+ metricsExport: "none" | "prometheus";
225
+ }
226
+
227
+ /** portfolio.md §13. */
228
+ export interface PortfolioEnrichmentConfig {
229
+ /** Default `false` — no registry/network access without opt-in (portfolio.md §13). */
230
+ enabled: boolean;
231
+ registries: ("npm" | "pypi" | "crates" | "maven" | "nuget" | "go" | "composer")[];
232
+ registryUrls?: Record<string, string>;
233
+ /** Default `168` (one week). */
234
+ cacheTtlHours: number;
235
+ timeoutMs: number;
236
+ }
237
+
238
+ /** portfolio.md §14.4 — normative shape. */
239
+ export interface PortfolioConfig {
240
+ /** Default `true`. */
241
+ enabled?: boolean;
242
+ /** Default `false`. */
243
+ includeTransitive?: boolean;
244
+ /** Default `2`. */
245
+ conventionMinRepos?: number;
246
+ conventions?: {
247
+ pinned?: string[];
248
+ suppressed?: string[];
249
+ };
250
+ radar?: {
251
+ /** Default `0.5`. */
252
+ adoptShareMin?: number;
253
+ /** Default `0.2`. */
254
+ maintainShareMin?: number;
255
+ /** Default `540`. */
256
+ retireAfterDays?: number;
257
+ };
258
+ blueprint?: {
259
+ weights?: {
260
+ /** Default `0.40`. */
261
+ adoption?: number;
262
+ /** Default `0.20`. */
263
+ recency?: number;
264
+ /** Default `0.25`. */
265
+ health?: number;
266
+ /** Default `0.15`. */
267
+ versionConsistency?: number;
268
+ };
269
+ };
270
+ enrichment?: PortfolioEnrichmentConfig;
271
+ }
272
+
273
+ /**
274
+ * Main configuration schema (spec.md §4.2), extended with `portfolio`
275
+ * (portfolio.md §14.4, folded into `UntangleConfigV1` per portfolio.md
276
+ * §18.1's planned spec.md §4.2 amendment).
277
+ */
278
+ export interface UntangleConfigV1 {
279
+ schemaVersion: 1;
280
+ workspaceId?: string;
281
+ projectId?: string;
282
+ repositories?: RepositoryConfig[];
283
+ storage?: StorageConfig;
284
+ qdrant?: QdrantConfig;
285
+ embeddings?: EmbeddingConfig;
286
+ index?: IndexConfig;
287
+ search?: SearchConfig;
288
+ graph?: GraphConfig;
289
+ complexity?: ComplexityConfig;
290
+ changes?: ChangeConfig;
291
+ qualityGate?: QualityGateConfig;
292
+ contextArtifacts?: ContextArtifactConfig[];
293
+ telemetry?: TelemetryConfig;
294
+ portfolio?: PortfolioConfig;
295
+ }
296
+
297
+ /** The set of `UntangleConfigV1` top-level keys, used to detect unknown fields (spec.md §4.8). */
298
+ export const UNTANGLE_CONFIG_V1_KEYS = [
299
+ "schemaVersion",
300
+ "workspaceId",
301
+ "projectId",
302
+ "repositories",
303
+ "storage",
304
+ "qdrant",
305
+ "embeddings",
306
+ "index",
307
+ "search",
308
+ "graph",
309
+ "complexity",
310
+ "changes",
311
+ "qualityGate",
312
+ "contextArtifacts",
313
+ "telemetry",
314
+ "portfolio",
315
+ ] as const satisfies readonly (keyof UntangleConfigV1)[];
316
+
317
+ /**
318
+ * Recursively makes every field of `T` optional, including inside array
319
+ * elements. Used for {@link RawConfigLayer}: a single JSON file, an env-var
320
+ * layer, or an `explicit` argument object commonly sets only a handful of
321
+ * leaves inside a nested config section (e.g. just `qdrant.collectionPrefix`)
322
+ * — a shallow `Partial<T>` would still require every OTHER field of
323
+ * `QdrantConfig` once `qdrant` is present at all, which is not how any of
324
+ * the six layers in spec.md §4.1 are actually written. Runtime validation
325
+ * (`validate.ts`) is what enforces which fields are truly required once all
326
+ * layers are merged with the product defaults.
327
+ */
328
+ export type DeepPartial<T> = T extends (infer U)[]
329
+ ? DeepPartial<U>[]
330
+ : T extends object
331
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
332
+ : T;
333
+
334
+ /**
335
+ * A raw configuration layer as read from JSON, environment mapping or passed
336
+ * explicitly: every field optional (deeply — see {@link DeepPartial}),
337
+ * `schemaVersion` not required (defaults are applied and validated later).
338
+ * `$schema` is accepted and ignored (spec.md §11's example config carries it).
339
+ */
340
+ export type RawConfigLayer = {
341
+ $schema?: string;
342
+ schemaVersion?: number;
343
+ } & DeepPartial<Omit<UntangleConfigV1, "schemaVersion">>;
344
+
345
+ /**
346
+ * The fully merged, defaulted and validated configuration
347
+ * ({@link "./load.js"}'s `loadConfig` result). All fields the product
348
+ * defaults (spec.md §4.5, this package's `defaults.ts`) supply are guaranteed
349
+ * present; `repositories` and `contextArtifacts` stay optional because they
350
+ * have no universal default (spec.md §4.3: the current project path MAY
351
+ * serve as an implicit single repository instead).
352
+ */
353
+ export interface EffectiveUntangleConfigV1 extends UntangleConfigV1 {
354
+ schemaVersion: 1;
355
+ storage: StorageConfig;
356
+ qdrant: QdrantConfig;
357
+ embeddings: EmbeddingConfig;
358
+ index: IndexConfig;
359
+ search: SearchConfig;
360
+ graph: GraphConfig;
361
+ complexity: ComplexityConfig;
362
+ changes: ChangeConfig;
363
+ qualityGate: QualityGateConfig;
364
+ telemetry: TelemetryConfig;
365
+ portfolio: PortfolioConfig;
366
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * User configuration location (spec.md §4.1 layer 5: "Nutzerkonfiguration im
3
+ * OS-Konfigurationsverzeichnis"). The exact directory and file name are an
4
+ * Untangle decision — spec.md does not name them.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ export const USER_CONFIG_FILE_NAME = "config.json";
11
+
12
+ /**
13
+ * Resolves the per-OS Untangle config directory:
14
+ * - Linux: `$XDG_CONFIG_HOME/untangle` or `~/.config/untangle`;
15
+ * - macOS: `~/Library/Application Support/untangle`;
16
+ * - Windows: `%APPDATA%\untangle` or `~/AppData/Roaming/untangle`.
17
+ *
18
+ * `env` and `platform` are injectable for deterministic tests.
19
+ */
20
+ export function resolveUserConfigDir(
21
+ env: NodeJS.ProcessEnv = process.env,
22
+ platform: NodeJS.Platform = process.platform,
23
+ ): string {
24
+ if (platform === "win32") {
25
+ const appData = env["APPDATA"];
26
+ return join(
27
+ appData && appData.length > 0 ? appData : join(homedir(), "AppData", "Roaming"),
28
+ "untangle",
29
+ );
30
+ }
31
+ if (platform === "darwin") {
32
+ return join(homedir(), "Library", "Application Support", "untangle");
33
+ }
34
+ const xdgConfigHome = env["XDG_CONFIG_HOME"];
35
+ return join(
36
+ xdgConfigHome && xdgConfigHome.length > 0 ? xdgConfigHome : join(homedir(), ".config"),
37
+ "untangle",
38
+ );
39
+ }
40
+
41
+ /** Full path to the user config file, or `undefined` when it does not exist. */
42
+ export function resolveUserConfigPath(
43
+ env: NodeJS.ProcessEnv = process.env,
44
+ platform: NodeJS.Platform = process.platform,
45
+ ): string | undefined {
46
+ const candidate = join(resolveUserConfigDir(env, platform), USER_CONFIG_FILE_NAME);
47
+ return existsSync(candidate) ? candidate : undefined;
48
+ }