@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/NOTICE ADDED
@@ -0,0 +1,90 @@
1
+ # NOTICE
2
+
3
+ Copyright (C) 2026 Luke Bölling and contributors.
4
+
5
+ This attribution covers original Untangle work. Contributors retain copyright in
6
+ their contributions. Third-party packages, grammars and other materials retain
7
+ their own copyright holders, licenses and notices; this line does not replace them.
8
+
9
+ ## License — binding decision
10
+
11
+ Untangle is licensed under the **GNU Affero General Public License, Version 3
12
+ (AGPL-3.0-only)**. See [`LICENSE`](./LICENSE).
13
+
14
+ This decision is **final, not provisional**, and was explicitly instructed by
15
+ the project owner. No contributor, agent or tool may change it without a new
16
+ explicit instruction.
17
+
18
+ The repository-wide license covers Untangle's first-party source code, scripts,
19
+ tests, fixtures, configuration, documentation, website and brand artwork, including
20
+ the logo and 3D yarn illustrations. Every workspace `package.json` declares
21
+ `"license": "AGPL-3.0-only"`. There is no separately licensed enterprise component
22
+ or separate permissive license for documentation or brand assets.
23
+
24
+ Third-party code, embedded grammars, dependency assets and their notices retain
25
+ their original licenses. Their inclusion does not relicense those works. The
26
+ repository license does not grant trademark rights or change the license of a
27
+ repository analyzed with Untangle. See the repository's `docs/LICENSING.md` for
28
+ scope and verification details.
29
+
30
+ This resolves the license question left open in `vision.md` §14 and completes
31
+ `product.md` §16, item 2.
32
+
33
+ ## Rationale
34
+
35
+ - **Network copyleft fits the intended product model.** AGPL §13 sets source
36
+ availability requirements for modified versions used through a computer network.
37
+ The current local CLI/stdio-MCP entry and future shared-server modes share the
38
+ same license.
39
+ - **Local-first analysis has a clear boundary.** Running Untangle against a codebase
40
+ does not replace that codebase's own license with Untangle's license.
41
+ - **First-party components share one license.** Implemented components and future
42
+ official plugins use this repository's licensing policy. Planned integrations do
43
+ not establish implemented or published products.
44
+
45
+ ## Deliberately accepted consequence
46
+
47
+ The project deliberately chooses AGPL rather than a permissive embedding license.
48
+ Redistribution, modifications and combined works must be assessed against the
49
+ actual terms in `LICENSE`, including applicable Corresponding Source requirements.
50
+ No permissive exception, commercial dual license or special contract-only package
51
+ license is granted or promised by this notice.
52
+
53
+ ## Dependencies
54
+
55
+ Third-party runtime dependencies are checked against the allowlist in
56
+ `scripts/check-licenses.mjs`. Their licenses and copyright notices remain unchanged
57
+ and traceable through package `THIRD_PARTY_LICENSES.md` files, grammar provenance,
58
+ the lockfile and release SBOM. The allowlist includes MIT / Apache-2.0 / BSD / ISC
59
+ and other explicitly listed licenses; it is not a license grant for dependencies.
60
+
61
+ The allowlist in `scripts/check-licenses.mjs` applies to **dependencies**, not
62
+ workspace packages. Untangle's own packages carry AGPL-3.0-only and are excluded
63
+ from that check.
64
+
65
+ ## Clean room
66
+
67
+ Untangle is an independent implementation with its own data models, APIs, tool
68
+ names, tests and sources. Feature ideas may be derived from publicly observable
69
+ behavior and generally known techniques. Third-party source code is not copied,
70
+ including from AGPL projects even where the license would permit it.
71
+
72
+ ## Contribution terms
73
+
74
+ Pull requests are welcome under the repository's existing AGPL-3.0-only license.
75
+ No separate CLA, copyright assignment or DCO sign-off is required. Contributors
76
+ must have the right to submit their work under those terms. See CONTRIBUTING.md.
77
+
78
+ ## Release decisions
79
+
80
+ - [x] Copyright attribution: Luke Bölling, with contributors retaining their rights.
81
+ - [x] Contributions use AGPL-3.0-only without an additional CLA or DCO gate.
82
+
83
+ ## Open legal items before public beta
84
+
85
+ - [ ] Trademark search for the product name `Untangle`.
86
+ - [ ] Registration and legal review of the npm scope `@untangle-code`.
87
+ - [ ] Verify source availability and notices for each released network daemon/server
88
+ mode (WP-59). The current stdio MCP `untangle_about` already reports the license,
89
+ source repository and build commit when supplied; metadata alone does not prove
90
+ that a concrete deployed version's source is available.
package/README.md CHANGED
@@ -1,3 +1,263 @@
1
- # Temporary Holding Version
1
+ # @untangle-code/config
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Untangle configuration (WP-02): types, discovery, six-layer resolution, handwritten JSON Schema,
4
+ secret redaction, canonical effective configuration with hashes and `embedding_profile_id`, and
5
+ migration infrastructure for future `schemaVersion` changes. A public package
6
+ (`@untangle-code/config`, product.md §6.2).
7
+
8
+ Normative sources: [`spec.md`](../../spec.md) §4.1–§4.8, §5.7 (takes precedence),
9
+ [`product.md`](../../product.md) §11, [`architecture.md`](../../architecture.md) §6.3, §23.2,
10
+ [`portfolio.md`](../../portfolio.md) §13, §14.4.
11
+
12
+ `src/index.ts` is the only public interface. Its sole runtime dependency is
13
+ `@untangle-code/domain` (`workspace:*`); no other Untangle or external runtime dependencies.
14
+
15
+ <a id="modulübersicht"></a>
16
+
17
+ ## Modules
18
+
19
+ | Module | Contents | Normative source |
20
+ |---|---|---|
21
+ | `types.ts` | `UntangleConfigV1`, `QdrantConfig`, `EmbeddingConfig`, `IndexConfig`, `SearchConfig`, `GraphConfig`, `ComplexityConfig`, `ChangeConfig`, `QualityGateConfig`, `TelemetryConfig`, `PortfolioConfig`/`PortfolioEnrichmentConfig`, `EffectiveUntangleConfigV1`, `RawConfigLayer`/`DeepPartial`; re-exports domain's `RepositoryConfig`/`ContextArtifactConfig` | spec §4.2–§4.7; portfolio §13, §14.4 |
22
+ | `defaults.ts` | Defaults per section, `PRODUCT_DEFAULT_CONFIG` (layer 6), `CURRENT_SCHEMA_VERSION` | spec §4.5; product §11 |
23
+ | `schema.ts` | Parsed `CONFIG_SCHEMA` and `CONFIG_SCHEMA_PATH` | arch §6.3, §7.3 |
24
+ | `discovery.ts` | `discoverConfigFile`, `discoverConfigFiles`: upward discovery to repository/filesystem root | spec §4.1 |
25
+ | `user-config.ts` | `resolveUserConfigDir`/`resolveUserConfigPath`: OS configuration directory | spec §4.1 layer 5 |
26
+ | `env.ts` | `ENV_MAPPING`, `buildEnvConfigLayer`: `UNTANGLE_*` to config paths | spec §4.1 layer 2 |
27
+ | `merge.ts` | `deepMergeConfig`, `mergeConfigLayers`: replace arrays, never concatenate | spec §4.1 |
28
+ | `paths.ts` | Canonical `resolveConfigPath`, `resolveRepositoryPaths`, relative to configuration file | spec §4.3 |
29
+ | `secrets.ts` | `redactSecrets`, `toRedactedJson`, `resolveApiKey` | spec §4.4/§4.5, §24; arch §23.2 |
30
+ | `hash.ts` | `computeConfigHash`, `deriveEmbeddingProfile`/`deriveEmbeddingProfileId` | arch §6.3; spec §5.1, §5.7 |
31
+ | `validate.ts` | `validateEffectiveConfig`, `assertConfigValid`, `assertEmbeddingDimensionsMatch` | spec §4.8 |
32
+ | `migrations.ts` | `registerMigration`, `migrateConfig`, `clearMigrations` | arch §6.3 |
33
+ | `load.ts` | `loadConfig`: orchestrates these modules | spec §4.1–§4.8 |
34
+
35
+ ## Layering (spec.md §4.1)
36
+
37
+ Highest priority first; `loadConfig` merges in the following order, lowest first and last wins:
38
+
39
+ ```text
40
+ 1. Explicit API/CLI arguments (loadConfig({ explicit }))
41
+ 2. Environment variables (UNTANGLE_*, see env.ts ENV_MAPPING)
42
+ 3. .untangle.local.json (upward discovery from cwd)
43
+ 4. .untangle.json (upward discovery from cwd)
44
+ 5. User configuration (OS directory) (user-config.ts)
45
+ 6. Product defaults (defaults.ts PRODUCT_DEFAULT_CONFIG)
46
+ ```
47
+
48
+ Merge semantics: objects merge recursively; **arrays replace completely**, never concatenate.
49
+ Replacing a full list such as `exclude` is the only way a higher layer can shrink it; silently
50
+ combining array sources would be unpredictable.
51
+
52
+ `.untangle.local.json` and `.untangle.json` are discovered **independently**, each searching upward
53
+ from `cwd` to the repository root (a directory containing `.git`) or filesystem root, whichever
54
+ comes first.
55
+
56
+ <a id="untangle_-umgebungsvariablen-auszug-additiv-erweiterbar"></a>
57
+
58
+ ## `UNTANGLE_*` environment variables (excerpt, additively extensible)
59
+
60
+ | Variable | Configuration path |
61
+ |---|---|
62
+ | `UNTANGLE_WORKSPACE_ID` | `workspaceId` |
63
+ | `UNTANGLE_PROJECT_ID` | `projectId` |
64
+ | `UNTANGLE_STORAGE_BACKEND` | `storage.backend` (`lancedb` or `qdrant`) |
65
+ | `UNTANGLE_LANCEDB_PATH` | `storage.path` (local directory) |
66
+ | `UNTANGLE_QDRANT_MODE` / `_URL` / `_API_KEY_ENV` / `_COLLECTION_PREFIX` / `_REQUEST_TIMEOUT_MS` / `_MAX_RETRIES` | `qdrant.*` |
67
+ | `UNTANGLE_EMBEDDINGS_MODE` / `_URL` / `_API_KEY_ENV` / `_MODEL` / `_DIMENSIONS` / `_REQUEST_TIMEOUT_MS` | `embeddings.*` |
68
+ | `UNTANGLE_INDEX_WATCH` / `_MAX_FILE_SIZE_MB` / `_EXTRA_EXTENSIONS` (CSV) | `index.*` |
69
+ | `UNTANGLE_QUALITY_GATE_MODE` / `_MINIMUM_PARSE_QUALITY` | `qualityGate.*` |
70
+ | `UNTANGLE_TELEMETRY_ENABLED` / `_LOG_LEVEL` | `telemetry.*` |
71
+ | `UNTANGLE_PORTFOLIO_ENABLED` | `portfolio.enabled` |
72
+
73
+ The complete machine-readable table is `ENV_MAPPING` in `src/env.ts`. `*_API_KEY_ENV` sets another
74
+ environment variable's **name**, never its secret value (see Secrets).
75
+
76
+ ## Search storage
77
+
78
+ New projects use embedded LanceDB at `.untangle/index/lancedb`. A relative `storage.path`
79
+ resolves against the repository root, including when configuration lives in
80
+ `.untangle/config.json`. Absolute local directories are accepted; URI schemes and network
81
+ paths are rejected.
82
+
83
+ Existing projects that explicitly configure a `qdrant` section or a mapped
84
+ `UNTANGLE_QDRANT_*` variable retain Qdrant. An explicit `storage.backend` overrides that
85
+ compatibility default, with the ordinary layer precedence. Setting only `storage.path`
86
+ does not move an existing Qdrant project. Qdrant connection options remain under `qdrant`:
87
+
88
+ ```toml
89
+ [storage]
90
+ backend = "qdrant"
91
+
92
+ [qdrant]
93
+ mode = "external"
94
+ url = "http://127.0.0.1:16333"
95
+ ```
96
+
97
+ `parseConfigText(text, "json" | "toml")` parses and migrates an existing document without
98
+ filesystem access, product defaults or environment overrides. Callers such as `init` can
99
+ preserve declared storage choices while inspecting the original configuration.
100
+
101
+ ## Secrets (spec.md §4.4/§4.5, §24; architecture.md §23.2)
102
+
103
+ `QdrantConfig.apiKeyEnv` / `EmbeddingConfig.apiKeyEnv` store only environment variable **names**, so
104
+ effective configuration structurally never contains secret values. `resolveApiKey(envVarName, env)`
105
+ resolves values at runtime on demand; results must never be written back to configuration objects.
106
+ `redactSecrets`/`toRedactedJson` provide additional protection: every field resembling a resolved
107
+ secret (`apiKey`, `token`, `secret`, `password`, `credential`, but NOT `...Env`) is replaced with
108
+ `[REDACTED]` before serialization, even if accidentally attached to configuration. Verified in
109
+ `secrets.test.ts`.
110
+
111
+ <a id="validierung-specmd-48"></a>
112
+
113
+ ## Validation (spec.md §4.8)
114
+
115
+ `loadConfig` **never** throws for warnings or errors; it returns
116
+ `{ effectiveConfig, warnings, errors }`. Unknown top-level fields warn with the field name as path.
117
+ Invalid enums, unresolved core values (such as missing `qdrant.url` with `mode: "external"`),
118
+ duplicate repository/artifact IDs and `portfolio.blueprint.weights` sums outside `1.0 ± 0.001`
119
+ produce `CONFIG_INVALID`. Aborting is deliberately the caller's choice:
120
+ `assertConfigValid(result.errors)` throws a domain `UntangleError`.
121
+ `assertEmbeddingDimensionsMatch(configured, existing)` checks conflicts before the first upsert
122
+ (`EMBEDDING_DIMENSION_MISMATCH`).
123
+
124
+ <a id="korpusgebundene-relevanz-policies-wp-231"></a>
125
+
126
+ ### Corpus-bound relevance policies (WP-231)
127
+
128
+ `search.relevancePolicies` deliberately has **no product default**. Raw cosine values and their
129
+ separation depend jointly on embedding profile and indexed corpus. Each entry is therefore keyed
130
+ by complete `embeddingProfileId` and bound to exactly one `workspaceId` and physical `collection`:
131
+
132
+ ```toml
133
+ workspaceId = "workspace-stable-id"
134
+
135
+ [search.relevancePolicies."complete-embedding-profile-id"]
136
+ workspaceId = "workspace-stable-id"
137
+ collection = "untangle_project_code_profile_v1"
138
+
139
+ [search.relevancePolicies."complete-embedding-profile-id".policy]
140
+ calibrationId = "model/corpus/revision"
141
+ minimumDenseScore = 0.45
142
+ minimumDenseSpread = 0.061
143
+ denseReferenceRank = 50
144
+ minimumCandidateIdfCoverage = 0.291
145
+ ```
146
+
147
+ If any profile/workspace/collection scope fails to match, search discards no results and reports
148
+ `RELEVANCE_POLICY_NOT_EVALUATED`. The same applies when retrieval did not actually reach the
149
+ calibrated dense reference rank or the sparse branch is missing. IDF evidence uses fixed
150
+ dense/sparse prefixes through the reference rank, so raising `candidateLimit` cannot move the
151
+ calibrated yes/no decision. Significant corpus changes require remeasurement. An already-indexed
152
+ workspace should declare a top-level `workspaceId`, otherwise adding the policy changes the
153
+ configuration hash and thus the derived workspace ID itself.
154
+
155
+ <a id="kanonische-effective-config-hash-und-embedding_profile_id"></a>
156
+
157
+ ## Canonical effective configuration, hash and `embedding_profile_id`
158
+
159
+ `computeConfigHash(effectiveConfig)` equals `SHA256(canonical_json(effectiveConfig))`, using
160
+ domain's `canonicalJson`/`sha256Hex`, for LanceDB. For Qdrant, the additive `storage` section is
161
+ excluded so existing configuration hashes and derived workspace identities remain unchanged;
162
+ the endpoint and other connection options remain represented by `qdrant`. Without explicit `workspaceId`, `loadConfig` uses that hash as
163
+ `configHash` input to `buildWorkspaceId` (spec §5.1). `deriveEmbeddingProfile`/
164
+ `deriveEmbeddingProfileId` construct domain's `EmbeddingProfile` from `EmbeddingConfig` plus fixed
165
+ `DEFAULT_EMBEDDING_NORMALIZATION` (`"none"`) and `PREPARED_TEXT_VERSION`
166
+ (`"untangle-prepared-text-v1"`), then call `buildEmbeddingProfileId` (spec §5.7).
167
+
168
+ ## Migration (architecture.md §6.3)
169
+
170
+ `registerMigration({ fromVersion, toVersion, migrate })` registers one step;
171
+ `migrateConfig(raw, targetVersion)` chains registered steps from `raw.schemaVersion` (default `1`)
172
+ to `targetVersion` (default `CURRENT_SCHEMA_VERSION`). Configuration already at the target version
173
+ undergoes **identity migration** and returns unchanged without requiring registered steps (tested).
174
+ `loadConfig` migrates each raw file configuration separately before merging it.
175
+
176
+ <a id="entscheidungen"></a>
177
+
178
+ ## Decisions
179
+
180
+ 1. **`RepositoryConfig`/`ContextArtifactConfig` re-export domain types instead of duplicating
181
+ them.** Domain README decision 5 explicitly requires this for `RepositoryConfig`; the same
182
+ applies to `ContextArtifactConfig` (BUILD_PLAN §3.3 lists it among domain core contracts).
183
+
184
+ 2. **`SearchConfig`, `GraphConfig`, `ComplexityConfig`, `ChangeConfig` are designed here.**
185
+ Spec.md §4.2 names them without shapes, unlike §4.4–§4.7's four concrete infrastructure/index/
186
+ quality schemas. `TelemetryConfig` is likewise unspecified beyond mandatory default-off
187
+ behavior (spec §24). Each shape references its closest normative source in `types.ts`:
188
+ `SearchConfig` uses `SearchRequestV1`/§13.3 boosts, `GraphConfig` architecture.md §17.4 traversal
189
+ limits, `ComplexityConfig` product.md §11's example, and `ChangeConfig` spec §17.6 risk defaults.
190
+ Downstream work may extend them additively, not redefine them locally.
191
+
192
+ 3. **`PortfolioConfig` follows portfolio.md §14.4 verbatim**, including every default; its required
193
+ ±0.001 sum validation for `blueprint.weights` is implemented in `validate.ts`.
194
+
195
+ 4. **JSON Schema lives in `packages/config/schemas/config-v1.json`, not root `schemas/`.**
196
+ BUILD_PLAN §4 also assigns `schemas/**` to WP-02, but this package's task explicitly limited
197
+ writes to `packages/config/**`. As noted in `openIssues`, an authorized owner must mirror it
198
+ to `schemas/config-v1.json`. `schema.ts` resolves `../schemas/config-v1.json` from
199
+ `import.meta.url`, working from both `src/` tests and `dist/` builds because both are siblings
200
+ of `schemas/`.
201
+
202
+ 5. **`additionalProperties: false` in JSON Schema despite spec §4.8's warn-not-fail rule.** The
203
+ handwritten schema is tooling for editor completion/external validators; this package does not
204
+ run it through a runtime schema validator such as ajv. `validate.ts` implements exact warning/
205
+ abort semantics from spec §4.8 directly in TypeScript without another runtime dependency. The schema documents
206
+ this decision in `description`.
207
+
208
+ 6. **`normalization` and `preparedTextVersion` are not `EmbeddingConfig` fields but are required
209
+ for `embedding_profile_id`.** Spec §4.5 lists exactly ten fields excluding them, while §5.7
210
+ requires them for profile identity. Fixed defaults in `defaults.ts` supply
211
+ `DEFAULT_EMBEDDING_NORMALIZATION = "none"` and
212
+ `PREPARED_TEXT_VERSION = "untangle-prepared-text-v1"`; `deriveEmbeddingProfile`'s second
213
+ argument can override them for future configuration extensions.
214
+
215
+ 7. **Deep-partial raw layers (`RawConfigLayer`/`DeepPartial<T>`).** An individual file/environment/
216
+ argument layer rarely supplies complete `QdrantConfig`/`EmbeddingConfig`; a shallow
217
+ `Partial<UntangleConfigV1>` would still require every field within any supplied child object.
218
+ `DeepPartial` makes every field recursively optional; after merging product defaults,
219
+ `validate.ts` enforces actual requirements. Its condition deliberately uses `T extends object`
220
+ rather than `T extends Record<string, unknown>`: the latter is structurally false for
221
+ interfaces lacking index signatures in TypeScript conditional types, a verified compiler trap.
222
+
223
+ 8. **`loadConfig` never throws itself.** File read errors, invalid JSON and validation failures all
224
+ enter `errors`/`warnings`; only explicitly invoked `assertConfigValid` throws
225
+ `UntangleError("CONFIG_INVALID", …)`. This follows the requested result-object rather than
226
+ exception behavior for warnings, consistently extended to errors so a CLI can collect and
227
+ present every issue before aborting.
228
+
229
+ 9. **Discovery stops at a `.git` directory entry OR filesystem root.** Spec §4.1 names repository/
230
+ filesystem roots without defining repository detection. A directory containing `.git` as a file
231
+ or directory, supporting Git worktrees, is a repository root: it is searched, its parent is not.
232
+ No `GitPort` is needed; filesystem existence checks suffice, and discovery already needs filesystem access.
233
+
234
+ 10. **JSON Schema uses `schemaVersion` `const: 1`, while `RawConfigLayer` uses `number`.** Raw
235
+ unmigrated files may carry any historical version, read by `migrateConfig` before merge.
236
+ JSON Schema describes the *current* v1 tooling contract.
237
+
238
+ <a id="verifikation"></a>
239
+
240
+ ## Verification
241
+
242
+ ```bash
243
+ pnpm --filter @untangle-code/config run typecheck # tsc, 0 errors
244
+ pnpm --filter @untangle-code/config run test # vitest, 73 tests passed, 1 skipped (Windows path test on POSIX)
245
+ pnpm --filter @untangle-code/config run build # emit to dist/
246
+ ```
247
+
248
+ <a id="lizenz"></a>
249
+
250
+ ## License
251
+
252
+
253
+ **AGPL-3.0-only.** This package includes the complete license in [`LICENSE`](./LICENSE), the
254
+ license-choice rationale in [`NOTICE`](./NOTICE), and licenses for all third-party production
255
+ dependencies in [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md). All three ship in the npm
256
+ tarball: AGPL-3.0-only §4 and §5 require recipients to receive the license; a README alone is not
257
+ a copy of the license.
258
+
259
+ Local use alone does not trigger an obligation. Copyleft obligations apply to redistribution and
260
+ providing a network service (AGPL §13).
261
+
262
+ Corresponding TypeScript source ships under `src/` in the same tarball; the complete repository
263
+ is available at <https://github.com/lboel/untangle>.
@@ -0,0 +1,29 @@
1
+ # Third-party licenses — @untangle-code/config
2
+
3
+ Generated by `pnpm third-party-licenses` from this package's installed
4
+ production dependency tree: transitive `dependencies`, across
5
+ workspace edges, with no `devDependencies` included. The generator reads
6
+ each resolved package's `package.json` in `node_modules`; nothing is maintained by hand
7
+ or estimated.
8
+
9
+ Do not edit manually: `pnpm third-party-licenses --check` fails on drift,
10
+ and `pnpm check-publishable` rejects packages without a current file.
11
+
12
+ This file exists because `docs/BUILD_PLAN.md`, risk R6, makes
13
+ per-package `THIRD_PARTY_LICENSES.md` a release gate for npm publication.
14
+
15
+ Untangle itself is **AGPL-3.0-only** (see `LICENSE` and `NOTICE` in
16
+ this package). The packages below are third-party works; their own
17
+ licenses and copyright notices continue to apply unchanged. Workspace
18
+ packages (`@untangle-code/*`) are not listed here; they belong to the same
19
+ work and use the same AGPL-3.0-only license.
20
+
21
+ ## Production dependencies (transitive, 1)
22
+
23
+ | Package | Version | License | Project |
24
+ | --- | --- | --- | --- |
25
+ | `smol-toml` | 1.8.0 | BSD-3-Clause | `github:squirrelchat/smol-toml` |
26
+
27
+ The allowlist (MIT / Apache-2.0 / BSD / ISC / …) is checked by
28
+ `scripts/check-licenses.mjs` against this exact tree on every run;
29
+ the GPL family is excluded (architecture.md §2.4, BUILD_PLAN R6).
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Chunk-token budget wiring: `IndexConfig` (spec.md §4.6) →
3
+ * `@untangle-code/chunker`'s `ChunkerConfigInput.maxTokensPerChunk`.
4
+ *
5
+ * spec.md §4.6 defines TWO index-level chunk-size knobs —
6
+ * `targetChunkTokens` (a soft aim) and `maxChunkTokens` (a hard ceiling) —
7
+ * with their product defaults (900 / 1500) fixed exactly once, in this
8
+ * package's `defaults.ts` (`DEFAULT_INDEX_CONFIG`). That is the single
9
+ * source of truth this file wires onward.
10
+ *
11
+ * `@untangle-code/chunker` currently exposes exactly ONE budget knob,
12
+ * `ChunkerConfigInput.maxTokensPerChunk`, and treats it everywhere as a hard
13
+ * "must not exceed" ceiling (every comparison in chunker's `split.ts` and
14
+ * `line-fallback-strategy.ts` is `<=`). The correct mapping is therefore
15
+ * `maxChunkTokens → maxTokensPerChunk`. `targetChunkTokens` has no
16
+ * chunker-side counterpart today — the chunker does not currently support
17
+ * aiming below its hard ceiling — and is deliberately NOT passed here;
18
+ * smuggling it in as `maxTokensPerChunk` instead would silently turn a soft
19
+ * aim into the hard ceiling the chunker actually enforces, which is a
20
+ * different (smaller) budget than spec.md §4.6 configures.
21
+ *
22
+ * `@untangle-code/config` deliberately does NOT take a runtime dependency on
23
+ * `@untangle-code/chunker` for this: config sits below chunker in the
24
+ * dependency graph (chunker already depends on domain; depending on chunker
25
+ * from here would invert that layering for the sake of one field mapping),
26
+ * and chunker is maintained independently of this package. `deriveChunkerBudget`
27
+ * therefore returns a plain object structurally compatible with
28
+ * `ChunkerConfigInput` (verified in `chunker-budget.test.ts` against the
29
+ * fields spec.md and the chunker README actually document) rather than
30
+ * importing its type.
31
+ *
32
+ * Whatever code builds the indexer (constructing `ChunkerConfigInput` to
33
+ * pass to `@untangle-code/chunker`'s `resolveChunkerConfig`/`chunkFile`) MUST
34
+ * spread this function's result into that input — e.g.
35
+ * `resolveChunkerConfig({ ...deriveChunkerBudget(effectiveConfig.index), preparedText })`
36
+ * — rather than reading `index.maxChunkTokens` directly or (worse) relying
37
+ * on the chunker's own independent default. That is the one piece of wiring
38
+ * this fix cannot complete by itself: no indexer package exists in this
39
+ * repository yet to receive it (see this package's `package.json`
40
+ * `dependencies`, and `packages/`'s directory listing) — but every future
41
+ * caller now has exactly one function to call instead of a field name to
42
+ * remember, which is what keeps a changed config value from silently
43
+ * failing to reach the chunker.
44
+ */
45
+ import type { IndexConfig } from "./types.js";
46
+ /** Structurally assignable to `@untangle-code/chunker`'s `ChunkerConfigInput` — see file doc comment. */
47
+ export interface ChunkerBudgetInput {
48
+ maxTokensPerChunk: number;
49
+ }
50
+ /**
51
+ * Derives the chunker's token-budget input from the effective `IndexConfig`
52
+ * (spec.md §4.6). This is the ONLY function in the codebase permitted to
53
+ * decide which `IndexConfig` field feeds
54
+ * `ChunkerConfigInput.maxTokensPerChunk` — see file doc comment for why it
55
+ * is `maxChunkTokens`, not `targetChunkTokens`.
56
+ */
57
+ export declare function deriveChunkerBudget(index: Pick<IndexConfig, "maxChunkTokens">): ChunkerBudgetInput;
58
+ //# sourceMappingURL=chunker-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker-budget.d.ts","sourceRoot":"","sources":["../src/chunker-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,yGAAyG;AACzG,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAC,GACzC,kBAAkB,CAEpB"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Derives the chunker's token-budget input from the effective `IndexConfig`
3
+ * (spec.md §4.6). This is the ONLY function in the codebase permitted to
4
+ * decide which `IndexConfig` field feeds
5
+ * `ChunkerConfigInput.maxTokensPerChunk` — see file doc comment for why it
6
+ * is `maxChunkTokens`, not `targetChunkTokens`.
7
+ */
8
+ export function deriveChunkerBudget(index) {
9
+ return { maxTokensPerChunk: index.maxChunkTokens };
10
+ }
11
+ //# sourceMappingURL=chunker-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker-budget.js","sourceRoot":"","sources":["../src/chunker-budget.ts"],"names":[],"mappings":"AAmDA;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAA0C;IAE1C,OAAO,EAAE,iBAAiB,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC;AACrD,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { ChangeConfig, ComplexityConfig, EmbeddingConfig, GraphConfig, IndexConfig, PortfolioConfig, QdrantConfig, QualityGateConfig, RawConfigLayer, SearchConfig, StorageConfig, TelemetryConfig } from "./types.js";
2
+ /**
3
+ * Embedding profile fields that are NOT part of `EmbeddingConfig` (spec.md
4
+ * §4.5 lists exactly ten fields, none of them `normalization` or
5
+ * `preparedTextVersion`) but ARE required inputs to
6
+ * `buildEmbeddingProfileId` (spec.md §5.7). Untangle decision: default
7
+ * normalization to "none" (no vector post-processing) and mint an explicit
8
+ * prepared-text format version so a future change to how document text is
9
+ * assembled can roll `embedding_profile_id` forward deliberately.
10
+ */
11
+ export declare const DEFAULT_EMBEDDING_NORMALIZATION: "none";
12
+ export declare const PREPARED_TEXT_VERSION: "untangle-prepared-text-v1";
13
+ /** spec.md §4.5 "Defaultprofil für MVP" — Ollama, nomic-embed-text, 768 dims. */
14
+ export declare const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig;
15
+ /**
16
+ * Qdrant defaults. spec.md §4.4 defines the shape but not default values;
17
+ * `managed` mode with no explicit `url` matches product.md §4.1's
18
+ * zero-config narrative (the CLI starts a managed container and fills `url`
19
+ * in from that at runtime — this package only supplies the mode default).
20
+ */
21
+ export declare const DEFAULT_QDRANT_CONFIG: QdrantConfig;
22
+ /** Embedded search for new projects; explicit legacy Qdrant layers retain their backend. */
23
+ export declare const DEFAULT_STORAGE_CONFIG: StorageConfig;
24
+ /** spec.md §4.6; numeric defaults are an Untangle decision, chosen conservatively. */
25
+ export declare const DEFAULT_INDEX_CONFIG: IndexConfig;
26
+ /** Untangle decision (see `types.ts` `SearchConfig` doc comment). */
27
+ export declare const DEFAULT_SEARCH_CONFIG: SearchConfig;
28
+ /** Untangle decision, values taken from architecture.md §17.4's per-query limits. */
29
+ export declare const DEFAULT_GRAPH_CONFIG: GraphConfig;
30
+ /** Untangle decision (see `types.ts` `ComplexityConfig` doc comment). */
31
+ export declare const DEFAULT_COMPLEXITY_CONFIG: ComplexityConfig;
32
+ /** spec.md §17.6 default risk score weights. */
33
+ export declare const DEFAULT_CHANGE_CONFIG: ChangeConfig;
34
+ /** spec.md §4.7 shape; MVP defaults chosen deliberately permissive (warn, not fail). */
35
+ export declare const DEFAULT_QUALITY_GATE_CONFIG: QualityGateConfig;
36
+ /** spec.md §24: "Telemetrie MUSS standardmäßig aus sein." */
37
+ export declare const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig;
38
+ /** portfolio.md §14.4 defaults, spelled out explicitly. */
39
+ export declare const DEFAULT_PORTFOLIO_CONFIG: PortfolioConfig;
40
+ /** Current config schema version this package produces and validates against. */
41
+ export declare const CURRENT_SCHEMA_VERSION: 1;
42
+ /**
43
+ * The complete product-default layer (spec.md §4.1 layer 6, lowest
44
+ * priority). Every optional sub-config `EffectiveUntangleConfigV1`
45
+ * guarantees present is defaulted here; `repositories` and
46
+ * `contextArtifacts` are deliberately absent (spec.md §4.3: no universal
47
+ * default repository set exists).
48
+ */
49
+ export declare const PRODUCT_DEFAULT_CONFIG: RawConfigLayer;
50
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,WAAW,EACX,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,EAAG,MAAe,CAAC;AAC/D,eAAO,MAAM,qBAAqB,EAAG,2BAAoC,CAAC;AAE1E,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,eAYtC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAOnC,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB,EAAE,aAGpC,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,oBAAoB,EAAE,WAelC,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,EAAE,YAanC,CAAC;AAEF,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,EAAE,WAOlC,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,EAAE,gBAKvC,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,EAAE,YAenC,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,2BAA2B,EAAE,iBAKzC,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,EAAE,eAItC,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,EAAE,eAuBtC,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,EAAG,CAAU,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,cAapC,CAAC"}