@untangle-code/file-graph 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 (130) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +284 -2
  4. package/THIRD_PARTY_LICENSES.md +29 -0
  5. package/dist/assignment.d.ts +15 -0
  6. package/dist/assignment.d.ts.map +1 -0
  7. package/dist/assignment.js +75 -0
  8. package/dist/assignment.js.map +1 -0
  9. package/dist/boundaries/endpoints.d.ts +11 -0
  10. package/dist/boundaries/endpoints.d.ts.map +1 -0
  11. package/dist/boundaries/endpoints.js +195 -0
  12. package/dist/boundaries/endpoints.js.map +1 -0
  13. package/dist/boundaries/index.d.ts +9 -0
  14. package/dist/boundaries/index.d.ts.map +1 -0
  15. package/dist/boundaries/index.js +93 -0
  16. package/dist/boundaries/index.js.map +1 -0
  17. package/dist/boundaries/requests.d.ts +12 -0
  18. package/dist/boundaries/requests.d.ts.map +1 -0
  19. package/dist/boundaries/requests.js +311 -0
  20. package/dist/boundaries/requests.js.map +1 -0
  21. package/dist/edges.d.ts +49 -0
  22. package/dist/edges.d.ts.map +1 -0
  23. package/dist/edges.js +223 -0
  24. package/dist/edges.js.map +1 -0
  25. package/dist/file-index.d.ts +37 -0
  26. package/dist/file-index.d.ts.map +1 -0
  27. package/dist/file-index.js +103 -0
  28. package/dist/file-index.js.map +1 -0
  29. package/dist/graph.d.ts +160 -0
  30. package/dist/graph.d.ts.map +1 -0
  31. package/dist/graph.js +448 -0
  32. package/dist/graph.js.map +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +48 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/metrics.d.ts +67 -0
  38. package/dist/metrics.d.ts.map +1 -0
  39. package/dist/metrics.js +272 -0
  40. package/dist/metrics.js.map +1 -0
  41. package/dist/package-catalog.d.ts +43 -0
  42. package/dist/package-catalog.d.ts.map +1 -0
  43. package/dist/package-catalog.js +136 -0
  44. package/dist/package-catalog.js.map +1 -0
  45. package/dist/paths.d.ts +56 -0
  46. package/dist/paths.d.ts.map +1 -0
  47. package/dist/paths.js +129 -0
  48. package/dist/paths.js.map +1 -0
  49. package/dist/persistence.d.ts +83 -0
  50. package/dist/persistence.d.ts.map +1 -0
  51. package/dist/persistence.js +538 -0
  52. package/dist/persistence.js.map +1 -0
  53. package/dist/resolve/candidates.d.ts +52 -0
  54. package/dist/resolve/candidates.d.ts.map +1 -0
  55. package/dist/resolve/candidates.js +202 -0
  56. package/dist/resolve/candidates.js.map +1 -0
  57. package/dist/resolve/package-exports.d.ts +31 -0
  58. package/dist/resolve/package-exports.d.ts.map +1 -0
  59. package/dist/resolve/package-exports.js +229 -0
  60. package/dist/resolve/package-exports.js.map +1 -0
  61. package/dist/resolve/pipeline.d.ts +11 -0
  62. package/dist/resolve/pipeline.d.ts.map +1 -0
  63. package/dist/resolve/pipeline.js +109 -0
  64. package/dist/resolve/pipeline.js.map +1 -0
  65. package/dist/resolve/resolvers.d.ts +25 -0
  66. package/dist/resolve/resolvers.d.ts.map +1 -0
  67. package/dist/resolve/resolvers.js +367 -0
  68. package/dist/resolve/resolvers.js.map +1 -0
  69. package/dist/restore.d.ts +75 -0
  70. package/dist/restore.d.ts.map +1 -0
  71. package/dist/restore.js +136 -0
  72. package/dist/restore.js.map +1 -0
  73. package/dist/topology/compose.d.ts +27 -0
  74. package/dist/topology/compose.d.ts.map +1 -0
  75. package/dist/topology/compose.js +208 -0
  76. package/dist/topology/compose.js.map +1 -0
  77. package/dist/topology/jsonc.d.ts +38 -0
  78. package/dist/topology/jsonc.d.ts.map +1 -0
  79. package/dist/topology/jsonc.js +137 -0
  80. package/dist/topology/jsonc.js.map +1 -0
  81. package/dist/topology/load.d.ts +61 -0
  82. package/dist/topology/load.d.ts.map +1 -0
  83. package/dist/topology/load.js +436 -0
  84. package/dist/topology/load.js.map +1 -0
  85. package/dist/topology/manifest.d.ts +48 -0
  86. package/dist/topology/manifest.d.ts.map +1 -0
  87. package/dist/topology/manifest.js +109 -0
  88. package/dist/topology/manifest.js.map +1 -0
  89. package/dist/topology/project-config.d.ts +39 -0
  90. package/dist/topology/project-config.d.ts.map +1 -0
  91. package/dist/topology/project-config.js +150 -0
  92. package/dist/topology/project-config.js.map +1 -0
  93. package/dist/topology/workspace-definition.d.ts +26 -0
  94. package/dist/topology/workspace-definition.d.ts.map +1 -0
  95. package/dist/topology/workspace-definition.js +104 -0
  96. package/dist/topology/workspace-definition.js.map +1 -0
  97. package/dist/traverse.d.ts +49 -0
  98. package/dist/traverse.d.ts.map +1 -0
  99. package/dist/traverse.js +167 -0
  100. package/dist/traverse.js.map +1 -0
  101. package/dist/types.d.ts +332 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +2 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +63 -3
  106. package/src/assignment.ts +83 -0
  107. package/src/boundaries/endpoints.ts +273 -0
  108. package/src/boundaries/index.ts +118 -0
  109. package/src/boundaries/requests.ts +411 -0
  110. package/src/edges.ts +334 -0
  111. package/src/file-index.ts +127 -0
  112. package/src/graph.ts +591 -0
  113. package/src/index.ts +58 -0
  114. package/src/metrics.ts +350 -0
  115. package/src/package-catalog.ts +145 -0
  116. package/src/paths.ts +138 -0
  117. package/src/persistence.ts +746 -0
  118. package/src/resolve/candidates.ts +230 -0
  119. package/src/resolve/package-exports.ts +268 -0
  120. package/src/resolve/pipeline.ts +140 -0
  121. package/src/resolve/resolvers.ts +468 -0
  122. package/src/restore.ts +201 -0
  123. package/src/topology/compose.ts +268 -0
  124. package/src/topology/jsonc.ts +147 -0
  125. package/src/topology/load.ts +644 -0
  126. package/src/topology/manifest.ts +132 -0
  127. package/src/topology/project-config.ts +183 -0
  128. package/src/topology/workspace-definition.ts +115 -0
  129. package/src/traverse.ts +232 -0
  130. package/src/types.ts +372 -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,285 @@
1
- # Temporary Holding Version
1
+ # @untangle-code/file-graph
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's file dependency graph (spec.md §14, architecture.md §16).
4
+
5
+ It answers three questions:
6
+
7
+ 1. **Which file depends on which?** — `dependenciesOf(fileId)`
8
+ 2. **What could break if I change this file?** — `dependentsOf(fileId)`
9
+ 3. **How are a monorepo's subprojects connected?** — `loadWorkspaceTopology(...)`,
10
+ at **two** levels: packages (`workspace:` protocol, `exports` maps) **and**
11
+ container services (`depends_on`, shared networks).
12
+
13
+ The package does not parse source itself. It receives the `RawImport` lists produced by a
14
+ `LanguageAdapter` (`@untangle-code/parser-registry`), so the parser can be replaced independently.
15
+
16
+ `analyzeHttpBoundaries` also processes the parser package's `HttpBoundaryFileFactsV1`
17
+ AST projections. These HTTP relationships form a separate report; they do not add import
18
+ or synchronous call edges. Associations require explicit `HttpServiceBindingV1`
19
+ declarations; unknown origins, methods, registrations and multiple candidates are not
20
+ resolved through guesses. Static route construction does not confirm that the route is
21
+ actually active in the deployed ASGI process. The local entry point and its limits are
22
+ described in [feature alignment](../../docs/FEATURE_ALIGNMENT.md#frontend-und-backend-gemeinsam-prüfen).
23
+
24
+ ---
25
+
26
+ <a id="benutzung"></a>
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import { NodeFileSystem } from "@untangle-code/scanner/node-fs";
32
+ import {
33
+ aliasRulesFromConfigs,
34
+ buildFileGraph,
35
+ loadWorkspaceTopology,
36
+ } from "@untangle-code/file-graph";
37
+
38
+ const fs = new NodeFileSystem();
39
+ const repositories = [{ repoId, refId, rootPath: "/abs/path/to/repo" }];
40
+
41
+ // 1. Read topology (the ONLY part that touches the file system)
42
+ const { topology, projectConfigs } = await loadWorkspaceTopology(fs, {
43
+ workspaceId,
44
+ generationId,
45
+ repositories,
46
+ });
47
+
48
+ // 2. Build the graph (pure, without I/O)
49
+ const graph = buildFileGraph({
50
+ workspaceId,
51
+ graphGeneration: generationId,
52
+ repositories,
53
+ files, // FileImportsV1[] from the scanner and parser
54
+ topology,
55
+ projectAliases: aliasRulesFromConfigs(projectConfigs),
56
+ });
57
+
58
+ // 3. Query the graph
59
+ const impact = graph.dependentsOf(fileId, { maxDepth: 12 });
60
+ const metrics = graph.metrics();
61
+ const snapshot = graph.snapshot(); // serializable, deterministic
62
+
63
+ // 4. Update incrementally instead of rebuilding
64
+ graph.applyChanges([{ kind: "modified", file: changedFile }]);
65
+ ```
66
+
67
+ Persist through the existing `GraphStore` port:
68
+
69
+ ```ts
70
+ import { readGraphSnapshot, writeGraphSnapshot } from "@untangle-code/file-graph";
71
+
72
+ await writeGraphSnapshot(store, graph.snapshot(), writeId);
73
+ const { snapshot, issues } = await readGraphSnapshot(store, {
74
+ workspaceId,
75
+ generationId,
76
+ });
77
+ ```
78
+
79
+ ---
80
+
81
+ <a id="aufbau"></a>
82
+
83
+ ## Structure
84
+
85
+ | Module | Responsibility |
86
+ | --- | --- |
87
+ | `paths.ts` | Repository-relative POSIX path arithmetic; `joinRelative` returns `null` instead of clamping at the root |
88
+ | `file-index.ts` | Lookup table of all known files — the sole source for resolution |
89
+ | `topology/` | `package.json`, `tsconfig*.json` (with `extends`), `pnpm-workspace.yaml`, composition files |
90
+ | `resolve/` | Candidate generation, `exports`/`imports` maps, the eight resolvers, the pipeline |
91
+ | `edges.ts` | `RawImport` → `FileEdgeV1`, boundary calculation, skip ledger |
92
+ | `metrics.ts` | Tarjan SCCs, cycles, orphans, centrality, distributions — all iterative |
93
+ | `graph.ts` | Node construction, traversal, incremental updates |
94
+ | `persistence.ts` | Mapping to `GraphShardV1` through the existing `GraphStore` port |
95
+
96
+ ### Resolver pipeline
97
+
98
+ In the order specified by architecture.md §16.2:
99
+
100
+ ```
101
+ relative-path → runtime-builtin → subpath-import (#) → python-module
102
+ → project-alias → configured-alias → workspace-package → installed-package
103
+ ```
104
+
105
+ A resolver that claims an import but finds **no** match does **not** end the search — the
106
+ next applicable resolver can try. An unresolved edge is created only after every applicable
107
+ resolver has been tried, and it records the trace of all attempts.
108
+
109
+ ---
110
+
111
+ <a id="entscheidungen"></a>
112
+
113
+ ## Decisions
114
+
115
+ **1. No separate storage port.** `GraphStore` from `@untangle-code/domain` already provides
116
+ exactly the three collections a file graph needs: `graph_files`, `graph_indices` and
117
+ `graph_meta`, with a contract suite in `@untangle-code/testkit` and two adapters. A second
118
+ port would describe the same semantics twice. Instead, `persistence.ts` maps the graph to
119
+ `GraphShardV1` — one shard per file, so incremental updates write back exactly the files
120
+ that were recomputed. Each logical shard ID is namespaced with a length-prefixed workspace
121
+ ID; the generation remains the store's explicit version dimension.
122
+
123
+ **2. A dedicated traversal type.** `TraversalResultV1` from `domain` uses `*SymbolIds` field
124
+ names, which would be incorrect for files. `FileTraversalResultV1` keeps the mandatory parts
125
+ (`TraversalLimitsV1`, `TraversalDirection`, `TraversalTruncationReason`) unchanged, gives the
126
+ fields file-specific names and adds `cycles`. No domain type was renamed: three other agents
127
+ were building against `domain` at the same time.
128
+
129
+ **3. `unresolvedImportCount` counts by `resolution.status`, not `kind`.** The `FileNodeV1`
130
+ documentation comment says “edges with `kind: "unresolved"`”, but the normative rule in
131
+ `validateFileEdge` requires an unresolvable import to retain `kind: "import"`. Counting by kind
132
+ would produce 0 in every real run, concealing exactly what the metric is meant to reveal
133
+ (E4). The count therefore uses resolution status.
134
+
135
+ **4. Resolution without file system access.** All module resolution operates against
136
+ `FileIndex`. Resolution that reads from disk is neither reproducible nor testable without a
137
+ real repository. Reads occur only in `topology/load.ts`, and even there use `FileSystemPort`,
138
+ never `node:fs`.
139
+
140
+ **5. Three reverse mappings this repository needs.**
141
+
142
+ - *ESM extension*: TypeScript under `NodeNext` writes `./registry.js` and means `registry.ts`.
143
+ The source file's language determines which extension is tried first: the source extension
144
+ wins in TypeScript, the written extension in JavaScript.
145
+ - *Build output*: an `exports` map points to `dist/index.js`; the project configuration's
146
+ `rootDir`/`outDir` maps it back to `src/index.ts`.
147
+ - *Condition order*: runtime conditions (`node`, `import`, `require`, `default`) precede
148
+ `types`, because a `types` match points to a `.d.ts` file absent from the source index.
149
+
150
+ **6. Two kinds of service edges.** `depends_on` is directed and answers “who needs whom
151
+ first”; `shared-network` is undirected (represented as two opposing edges) and answers
152
+ “who can reach whom”. Services without their own `networks` declaration join the implicit
153
+ `default` network. Omitting this would report zero reachability edges for the most common
154
+ composition files, even though every service can reach every other service.
155
+
156
+ **7. A package name in the catalog IS a workspace edge**, even without a `workspace:` prefix.
157
+ pnpm resolves `"^1.0.0"` to a local package when its version matches; checking only the protocol
158
+ would lose those edges.
159
+
160
+ **8. An established YAML reader (`yaml`, licensed under ISC).** An ad hoc parser would be
161
+ particularly costly here: composition files use anchor references, block scalars and both
162
+ forms of `depends_on`. `@untangle-code/context-artifacts` has its own `yaml-lite`, but it is
163
+ internal to that package and designed to split text into blocks rather than read a structure.
164
+
165
+ **9. No overlap with `context-artifacts`.** That package splits composition files into
166
+ *retrieval chunks* (one chunk per service). This package reads *topology* from the same file:
167
+ startup order, networks, build contexts and file → service associations. These are two
168
+ different questions about the same file; there is no shared code or duplicate assertion.
169
+
170
+ **10. A tolerant JSON reader instead of `JSON.parse`.** Project configurations may contain
171
+ comments and trailing commas (this repository's `tsconfig.base.json` does). Throwing an error
172
+ here would silently remove every path alias from a package.
173
+
174
+ ---
175
+
176
+ <a id="e4-in-diesem-paket"></a>
177
+
178
+ ## E4 in this package
179
+
180
+ “No silent skipping.” Specifically:
181
+
182
+ | Case | Behavior |
183
+ | --- | --- |
184
+ | Unresolvable import | Retained as an **edge** with `kind: "import"`, `status: "unresolved"`, a named reason, mandatory detail text and the list of checked candidates |
185
+ | Import without a specifier | No edge is possible (`rawSpecifier` must never be empty) → **ledger entry** with source range |
186
+ | Import list not extracted | `importsMissingReason` → **ledger entry**; otherwise an unparsed file would appear to have no imports |
187
+ | Unreadable manifest/composition file | **Ledger entry**; a subproject never silently disappears from the topology |
188
+ | `depends_on` names an unknown service | **Warning** in the ledger, no invented edge |
189
+ | Cycle list reaches its budget | `truncationReason` on the metrics |
190
+ | Traversal reaches its budget | `truncated` + `truncationReason` |
191
+ | Cycle during traversal | `cycles` **and** `revisitSkippedFileIds` — reported, not broken |
192
+
193
+ Checked candidates serve more than diagnostics: incremental updates use them to recompute
194
+ **exactly** the files whose resolution could change when a file is added.
195
+
196
+ ---
197
+
198
+ <a id="inkrementelle-fortschreibung"></a>
199
+
200
+ ## Incremental updates
201
+
202
+ - `modified` (content changed, path unchanged): **only** the file itself. Its existence did not
203
+ change, so no other file's resolution could have changed.
204
+ - `added` / `removed`: also every file whose resolution depends on this path's existence,
205
+ identified through unresolved edges' `rejectedCandidates`, previous dependents and the
206
+ shadowing case (a new `util.ts` shadows an existing `util.js`).
207
+
208
+ Tests measure every claim against a **complete rebuild**: after each incremental update,
209
+ the snapshot must match, character for character, the result of rebuilding the same file state.
210
+
211
+ ---
212
+
213
+ <a id="determinismus"></a>
214
+
215
+ ## Determinism
216
+
217
+ - Never `String#localeCompare` — only `compareCodepoints` from `@untangle-code/domain`.
218
+ - No `Date.now()`/`Math.random()` in result data. Traversal accepts an **injectable** clock for
219
+ its time budget; no time value ever enters the result.
220
+ - Every list, counter object and ledger entry has canonical ordering, so two runs produce the
221
+ same document and therefore the same hash. This is checked against this repository.
222
+
223
+ ---
224
+
225
+ ## Tests
226
+
227
+ ```bash
228
+ pnpm --filter @untangle-code/file-graph test
229
+ ```
230
+
231
+ `src/real-repo.test.ts` runs against **this** repository: the real scanner
232
+ (`@untangle-code/scanner`), real parser adapters (`@untangle-code/parser-registry`), and real
233
+ `package.json`, `tsconfig.json` and composition files. Fourteen packages, the `workspace:`
234
+ protocol, `exports` maps, inherited project configurations and ESM extensions in TypeScript
235
+ source form exactly the combination that can break resolution tested only against fixtures.
236
+
237
+ ---
238
+
239
+ <a id="bekannte-grenzen"></a>
240
+
241
+ ## Known limits
242
+
243
+ - **Language coverage.** The JavaScript/TypeScript family and relative Python imports are
244
+ complete. There are no resolvers yet for Go, Cargo, Maven, .NET or Composer (spec.md §14.3);
245
+ their imports remain unresolved **with** a named reason, rather than being discarded.
246
+ `ImportResolverV1` is the extension point.
247
+ - **Packages with the same name in different repositories.** The package catalog is keyed
248
+ by repository *and* name (`WorkspacePackageCatalogV1`, `package-catalog.ts`). A package-name
249
+ import is searched for in its **own** repository first. If none exists there and exactly one
250
+ exists elsewhere, a cross-repository edge is created; multiple external candidates produce
251
+ `ambiguous_candidates` instead of a guess. The name collision itself appears as
252
+ `ambiguous_resolution` in the topology's skip ledger. `boundary.crossPackage` compares
253
+ package *identity* (repository + directory), not its name. `cross-repo.test.ts` exercises a
254
+ workspace with multiple repositories; it does not replace a real multi-repository fixture.
255
+ - **Traversal with multiple roots.** A depth limit cuts off only the current branch. Only an
256
+ exhausted global budget (nodes, edges, time) stops traversal entirely, and roots never entered
257
+ are listed in `unvisitedRootFileIds`.
258
+ - **Loading snapshots.** `readGraphSnapshot` checks the `graph_meta` payload for its required
259
+ parts (`metrics`, `topology`, `skipped`). If any are missing it returns `null` **with** a named
260
+ issue, rather than a snapshot containing `undefined` fields. Missing file shards are named
261
+ through the path index and also checked against the metadata shard's expected counts
262
+ (`count_mismatch`); a smaller graph without an issue is not possible. The reader always
263
+ requires `{ workspaceId, generationId }`; the old string signature reads nothing. Global
264
+ shards from older versions are accessible only with `{ allowLegacyFallback: true }`, and
265
+ even then are strictly checked against both scope values.
266
+ - **`exports` conditions** are evaluated in a fixed order; custom conditions (`development`,
267
+ `browser`) must be passed as parameters.
268
+ - **Cycle enumeration** is exponential in the worst case and therefore uses a budget. An
269
+ exhausted budget is recorded in `truncationReason`; the list never claims completeness.
270
+
271
+ <a id="lizenz"></a>
272
+
273
+ ## License
274
+
275
+ **AGPL-3.0-only.** This package includes the full license text in [`LICENSE`](./LICENSE),
276
+ the rationale for the license choice in [`NOTICE`](./NOTICE), and the licenses of all
277
+ third-party production dependencies in [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md).
278
+ All three files are part of the npm tarball — AGPL-3.0-only §4 and §5 require every recipient
279
+ to receive the license, and a README alone is not a copy of the license.
280
+
281
+ Local use creates no obligation. Copyleft obligations apply to redistribution and provision
282
+ as a network service (AGPL §13).
283
+
284
+ The corresponding TypeScript source is included under `src/` in the same tarball; the full
285
+ repository is available at <https://github.com/lboel/untangle>.
@@ -0,0 +1,29 @@
1
+ # Third-party licenses — @untangle-code/file-graph
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
+ | `yaml` | 2.9.0 | ISC | <https://eemeli.org/yaml/> |
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,15 @@
1
+ import type { ContainerServiceV1, WorkspacePackageV1 } from "./types.js";
2
+ /** Paket, dem `path` im Repository `repoId` zugeordnet ist; `null`, wenn keines passt. */
3
+ export declare function assignPackage(packages: readonly WorkspacePackageV1[], repoId: string, path: string): WorkspacePackageV1 | null;
4
+ /**
5
+ * Container-Dienst, dem `path` zugeordnet ist.
6
+ *
7
+ * Zuordnungskriterium ist der Build-Kontext des Dienstes: was in seinem
8
+ * Kontextverzeichnis liegt, landet in seinem Abbild. Dienste ohne
9
+ * Build-Kontext (reine Abbild-Dienste wie eine Datenbank) bekommen KEINE
10
+ * Dateien zugeordnet — sie sind Knoten der Dienstebene, aber kein Quelltext
11
+ * gehoert zu ihnen, und ein Rateversuch waere hier schlechter als kein
12
+ * Ergebnis.
13
+ */
14
+ export declare function assignContainerService(services: readonly ContainerServiceV1[], repoId: string, path: string): ContainerServiceV1 | null;
15
+ //# sourceMappingURL=assignment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../src/assignment.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEzE,0FAA0F;AAC1F,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,kBAAkB,GAAG,IAAI,CAsB3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,kBAAkB,GAAG,IAAI,CAsB3B"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Zuordnung einer Datei zu Paket und Container-Dienst.
3
+ *
4
+ * Regel in beiden Faellen: das LAENGSTE passende Verzeichnis gewinnt. Ein
5
+ * Monorepo verschachtelt Pakete (`packages/a` und `packages/a/plugins/b`),
6
+ * und die Wurzel `""` passt immer — ohne „laengster Treffer" landete jede
7
+ * Datei im Wurzelpaket und die gesamte Paketebene waere wertlos.
8
+ *
9
+ * Bei gleicher Laenge entscheidet der Codepoint-Vergleich des Namens. Das
10
+ * kommt nur bei einer fehlerhaften Konfiguration vor (zwei Pakete mit
11
+ * demselben Verzeichnis), muss aber deterministisch enden.
12
+ */
13
+ import { compareCodepoints } from "@untangle-code/domain";
14
+ import { isWithin, normalizePath } from "./paths.js";
15
+ /** Paket, dem `path` im Repository `repoId` zugeordnet ist; `null`, wenn keines passt. */
16
+ export function assignPackage(packages, repoId, path) {
17
+ const target = normalizePath(path);
18
+ let best = null;
19
+ for (const entry of packages) {
20
+ if (entry.repoId !== repoId)
21
+ continue;
22
+ if (!isWithin(entry.packageDir, target))
23
+ continue;
24
+ if (best === null) {
25
+ best = entry;
26
+ continue;
27
+ }
28
+ if (entry.packageDir.length > best.packageDir.length) {
29
+ best = entry;
30
+ continue;
31
+ }
32
+ if (entry.packageDir.length === best.packageDir.length &&
33
+ compareCodepoints(entry.name, best.name) < 0) {
34
+ best = entry;
35
+ }
36
+ }
37
+ return best;
38
+ }
39
+ /**
40
+ * Container-Dienst, dem `path` zugeordnet ist.
41
+ *
42
+ * Zuordnungskriterium ist der Build-Kontext des Dienstes: was in seinem
43
+ * Kontextverzeichnis liegt, landet in seinem Abbild. Dienste ohne
44
+ * Build-Kontext (reine Abbild-Dienste wie eine Datenbank) bekommen KEINE
45
+ * Dateien zugeordnet — sie sind Knoten der Dienstebene, aber kein Quelltext
46
+ * gehoert zu ihnen, und ein Rateversuch waere hier schlechter als kein
47
+ * Ergebnis.
48
+ */
49
+ export function assignContainerService(services, repoId, path) {
50
+ const target = normalizePath(path);
51
+ let best = null;
52
+ for (const service of services) {
53
+ if (service.repoId !== repoId)
54
+ continue;
55
+ const context = service.buildContext;
56
+ if (context === undefined)
57
+ continue;
58
+ if (!isWithin(context, target))
59
+ continue;
60
+ const bestContext = best?.buildContext ?? "";
61
+ if (best === null) {
62
+ best = service;
63
+ continue;
64
+ }
65
+ if (context.length > bestContext.length) {
66
+ best = service;
67
+ continue;
68
+ }
69
+ if (context.length === bestContext.length && compareCodepoints(service.name, best.name) < 0) {
70
+ best = service;
71
+ }
72
+ }
73
+ return best;
74
+ }
75
+ //# sourceMappingURL=assignment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment.js","sourceRoot":"","sources":["../src/assignment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGrD,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAC3B,QAAuC,EACvC,MAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,IAAI,GAA8B,IAAI,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM;YAAE,SAAS;QACtC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC;YAAE,SAAS;QAClD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,IAAI,GAAG,KAAK,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;YACrD,IAAI,GAAG,KAAK,CAAC;YACb,SAAS;QACX,CAAC;QACD,IACE,KAAK,CAAC,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC,UAAU,CAAC,MAAM;YAClD,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAC5C,CAAC;YACD,IAAI,GAAG,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAuC,EACvC,MAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,IAAI,GAA8B,IAAI,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;YAAE,SAAS;QACxC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC;QACrC,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;YAAE,SAAS;QACzC,MAAM,WAAW,GAAG,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC;QAC7C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,IAAI,GAAG,OAAO,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;YACxC,IAAI,GAAG,OAAO,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5F,IAAI,GAAG,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { HttpBoundaryFileFactsV1, HttpBoundaryProvenanceV1, HttpEndpointV1, HttpServiceBindingV1 } from "@untangle-code/domain";
2
+ export declare function under(path: string, root: string): boolean;
3
+ export declare function provenance(file: HttpBoundaryFileFactsV1, range: HttpBoundaryProvenanceV1["range"], kind: HttpBoundaryProvenanceV1["kind"], name?: string): HttpBoundaryProvenanceV1;
4
+ export declare function collectEndpoints(files: readonly HttpBoundaryFileFactsV1[], services: readonly HttpServiceBindingV1[], signal?: AbortSignal): {
5
+ endpoints: HttpEndpointV1[];
6
+ limitations: {
7
+ path: string;
8
+ reason: string;
9
+ }[];
10
+ };
11
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../src/boundaries/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EAEd,oBAAoB,EACrB,MAAM,uBAAuB,CAAC;AAG/B,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,uBAAuB,EAC7B,KAAK,EAAE,wBAAwB,CAAC,OAAO,CAAC,EACxC,IAAI,EAAE,wBAAwB,CAAC,MAAM,CAAC,EACtC,IAAI,CAAC,EAAE,MAAM,GACZ,wBAAwB,CAQ1B;AAuGD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,uBAAuB,EAAE,EACzC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,MAAM,CAAC,EAAE,WAAW,GACnB;IAAE,SAAS,EAAE,cAAc,EAAE,CAAC;IAAC,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CA0IlF"}