@monster-orange/den 0.0.0 → 0.2.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 (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Derived dependency graph engine — the single entrypoint shared by
3
+ * `den graph` and (Phase 2) derived `den up`.
4
+ *
5
+ * Discovery runs on every invocation (never a stale hand-maintained graph) but
6
+ * stays fast: closure-scoped BFS from the seed, targeted in-process parsing, a
7
+ * per-repo `mtime` cache, and parallel-friendly per-repo derivation. Edge
8
+ * hard/soft is finalized from the central `config/den.yml` `edges:` override
9
+ * (the source of truth for functional-hard edges code doesn't expose).
10
+ */
11
+
12
+ import * as path from "path";
13
+ import { readCatalog } from "../catalog.js";
14
+ import { getReposRoot, readDenConfig } from "../../den-config.js";
15
+ import type { GraphConfig } from "../../den-config.js";
16
+ import type {
17
+ ResolvedService,
18
+ ServiceCatalog,
19
+ WorkspaceConfig,
20
+ } from "../../types/index.js";
21
+ import {
22
+ getActiveWorkspace,
23
+ getWorkspaceByName,
24
+ resolveServiceSource,
25
+ } from "../workspaces.js";
26
+ import { buildAliasResolver, type AliasResolver } from "./alias.js";
27
+ import {
28
+ emptyCache,
29
+ readGraphCache,
30
+ repoFingerprint,
31
+ writeGraphCache,
32
+ } from "./cache.js";
33
+ import { deriveComposeEdges } from "./rules-compose.js";
34
+ import { deriveEventEdges } from "./rules-events.js";
35
+ import { deriveHttpEdges } from "./rules-http.js";
36
+ import { deriveInfraEdges } from "./rules-infra.js";
37
+ import { deriveReadinessEdges } from "./rules-readiness.js";
38
+ import { hasRepoMarker, scanRepo } from "./scan.js";
39
+ import type { DerivedEdge, DerivedGraph } from "./types.js";
40
+
41
+ import { product } from "../../product.js";
42
+ export interface DeriveOptions {
43
+ /** "all" derives every known repo; "closure" BFS-derives the seed's deps. */
44
+ scope: "all" | "closure";
45
+ /** Required for scope "closure": the repo-system to start from. */
46
+ seed?: string;
47
+ /** Ignore cached per-repo results and re-derive from disk. */
48
+ refresh?: boolean;
49
+ /**
50
+ * Derive against this workspace's checkouts instead of the focused one. With
51
+ * several workspaces active, a repo name can point at a different checkout
52
+ * per workspace (or be absent), so the graph is only meaningful relative to
53
+ * one workspace.
54
+ */
55
+ workspace?: string;
56
+ }
57
+
58
+ type EdgeOverride = Record<string, "hard" | "soft" | "ignore">;
59
+
60
+ interface DeriveContext {
61
+ alias: AliasResolver;
62
+ edgesOverride: EdgeOverride;
63
+ catalog: ServiceCatalog;
64
+ workspace: WorkspaceConfig | null;
65
+ workspaceName: string;
66
+ reposRoot: string;
67
+ }
68
+
69
+ /**
70
+ * @param workspaceName Workspace whose checkouts repos resolve against. Falls
71
+ * back to the focused workspace, and to it again when the name is unknown.
72
+ */
73
+ export function buildContext(workspaceName?: string): DeriveContext {
74
+ const config = readDenConfig();
75
+ const graph: GraphConfig | undefined = config.graph;
76
+ const active =
77
+ (workspaceName ? getWorkspaceByName(workspaceName) : null) ??
78
+ getActiveWorkspace();
79
+ const catalog = readCatalog();
80
+ return {
81
+ alias: buildAliasResolver(graph, Object.keys(catalog)),
82
+ edgesOverride: graph?.edges ?? {},
83
+ catalog,
84
+ workspace: active?.config ?? null,
85
+ workspaceName: active?.name ?? "default",
86
+ reposRoot: getReposRoot(),
87
+ };
88
+ }
89
+
90
+ /** Where den will boot a repo from: a managed clone or the dev sibling. */
91
+ export interface RepoSource {
92
+ /** True = managed clone (`.den-services/...`); false = dev sibling. */
93
+ managed: boolean;
94
+ /** Absolute path of the configured location (clone or dev checkout). */
95
+ path: string;
96
+ /** Whether that path is present on disk (a managed repo may not be cloned yet). */
97
+ exists: boolean;
98
+ }
99
+
100
+ /**
101
+ * The active workspace's entry for a repo-system. Tries the bare repo name, then
102
+ * the first "repo/subsystem" address (multi-service repos are addressed per
103
+ * subsystem). Returns null when the repo isn't in the workspace/catalog.
104
+ */
105
+ function configuredService(
106
+ repoName: string,
107
+ ctx: DeriveContext,
108
+ ): ResolvedService | null {
109
+ if (!ctx.workspace || !ctx.catalog[repoName]) return null;
110
+ const direct = resolveServiceSource(
111
+ repoName,
112
+ ctx.workspaceName,
113
+ ctx.workspace,
114
+ ctx.catalog,
115
+ );
116
+ if (direct) return direct;
117
+ const subAddr = Object.keys(ctx.workspace.services).find((a) =>
118
+ a.startsWith(`${repoName}/`),
119
+ );
120
+ return subAddr
121
+ ? resolveServiceSource(
122
+ subAddr,
123
+ ctx.workspaceName,
124
+ ctx.workspace,
125
+ ctx.catalog,
126
+ )
127
+ : null;
128
+ }
129
+
130
+ /**
131
+ * The CONFIGURED boot location for a repo-system (for `den graph` annotation):
132
+ * the workspace entry's mode/path if present (with `exists` flagging an
133
+ * un-cloned managed repo), else the sibling under repos_root.
134
+ */
135
+ export function repoSource(
136
+ repoName: string,
137
+ ctx: DeriveContext = buildContext(),
138
+ ): RepoSource | null {
139
+ const svc = configuredService(repoName, ctx);
140
+ if (svc?.repoPath) {
141
+ return {
142
+ managed: svc.mode === "managed",
143
+ path: svc.repoPath,
144
+ exists: hasRepoMarker(svc.repoPath),
145
+ };
146
+ }
147
+ const sibling = path.join(ctx.reposRoot, repoName);
148
+ return hasRepoMarker(sibling)
149
+ ? { managed: false, path: sibling, exists: true }
150
+ : null;
151
+ }
152
+
153
+ /**
154
+ * Absolute path den READS a repo-system from. Prefers the configured location
155
+ * when it exists on disk; if a managed clone isn't present yet, falls back
156
+ * to the dev sibling so discovery still has something to scan.
157
+ */
158
+ export function resolveRepoPath(
159
+ repoName: string,
160
+ ctx: DeriveContext = buildContext(),
161
+ ): string | null {
162
+ const svc = configuredService(repoName, ctx);
163
+ if (svc?.repoPath && hasRepoMarker(svc.repoPath)) return svc.repoPath;
164
+ const sibling = path.join(ctx.reposRoot, repoName);
165
+ return hasRepoMarker(sibling) ? sibling : null;
166
+ }
167
+
168
+ /** Edges produced by scanning one repo (cached by input-file fingerprint). */
169
+ function deriveRepoEdges(
170
+ repoName: string,
171
+ repoPath: string,
172
+ ctx: DeriveContext,
173
+ cache: ReturnType<typeof readGraphCache>,
174
+ refresh: boolean,
175
+ ): DerivedEdge[] {
176
+ const fingerprint = repoFingerprint(repoPath);
177
+ // Keyed by checkout, not just repo name: active workspaces can point the same
178
+ // repo name at different checkouts, which would otherwise evict each other.
179
+ const key = `${repoName}@${repoPath}`;
180
+ const cached = cache.repos[key];
181
+ if (!refresh && cached && cached.fingerprint === fingerprint) {
182
+ return cached.edges;
183
+ }
184
+ const repo = scanRepo(repoName, repoPath);
185
+ const edges = [
186
+ ...deriveInfraEdges(repo, ctx.alias),
187
+ ...deriveHttpEdges(repo, ctx.alias),
188
+ ...deriveReadinessEdges(repo, ctx.alias),
189
+ ...deriveEventEdges(repo, ctx.alias),
190
+ ...deriveComposeEdges(repo, ctx.alias),
191
+ ];
192
+ cache.repos[key] = { fingerprint, edges };
193
+ return edges;
194
+ }
195
+
196
+ const KIND_ORDER: Record<DerivedEdge["kind"], number> = {
197
+ infra: 0,
198
+ http: 1,
199
+ jwks: 2,
200
+ event: 3,
201
+ };
202
+ const CONF_RANK: Record<DerivedEdge["confidence"], number> = {
203
+ high: 3,
204
+ medium: 2,
205
+ low: 1,
206
+ };
207
+ const BLOCKING_RANK: Record<DerivedEdge["blocking"], number> = {
208
+ hard: 2,
209
+ soft: 1,
210
+ };
211
+
212
+ /**
213
+ * Collapse duplicates of (from,to,kind). Hard beats soft (a startup gate must
214
+ * never be silently dropped in favor of a soft signal to the same peer), then
215
+ * higher confidence wins; ties keep the first-inserted edge.
216
+ */
217
+ function dedupe(edges: DerivedEdge[]): DerivedEdge[] {
218
+ const best = new Map<string, DerivedEdge>();
219
+ for (const e of edges) {
220
+ const key = `${e.from}->${e.to}:${e.kind}`;
221
+ const prev = best.get(key);
222
+ if (
223
+ !prev ||
224
+ BLOCKING_RANK[e.blocking] > BLOCKING_RANK[prev.blocking] ||
225
+ (e.blocking === prev.blocking &&
226
+ CONF_RANK[e.confidence] > CONF_RANK[prev.confidence])
227
+ ) {
228
+ best.set(key, e);
229
+ }
230
+ }
231
+ // A JWKS URL also matches as plain http via its value host — when a pair has a
232
+ // jwks edge, drop the redundant http one so the dep is purely jwks (auth).
233
+ const jwksPairs = new Set<string>();
234
+ for (const e of best.values())
235
+ if (e.kind === "jwks") jwksPairs.add(`${e.from}->${e.to}`);
236
+ return [...best.values()]
237
+ .filter((e) => !(e.kind === "http" && jwksPairs.has(`${e.from}->${e.to}`)))
238
+ .sort(
239
+ (a, b) =>
240
+ a.from.localeCompare(b.from) ||
241
+ a.to.localeCompare(b.to) ||
242
+ KIND_ORDER[a.kind] - KIND_ORDER[b.kind],
243
+ );
244
+ }
245
+
246
+ /** Apply the central per-edge override: hard/soft reclassify; ignore = non-ordering. */
247
+ function applyOverrideTo(
248
+ edge: DerivedEdge,
249
+ override: EdgeOverride,
250
+ ): DerivedEdge {
251
+ const o = override[`${edge.from}->${edge.to}`];
252
+ if (!o) return edge;
253
+ if (o === "ignore") return { ...edge, ordering: false };
254
+ if (edge.kind === "event") return edge; // events aren't reclassified hard/soft
255
+ return { ...edge, blocking: o };
256
+ }
257
+
258
+ /**
259
+ * Pure graph assembly — separated from filesystem/config so it can be unit
260
+ * tested. Callers inject `deriveEdges` (per-repo discovery) and `repoExists`.
261
+ */
262
+ export interface AssembleOptions {
263
+ scope: "all" | "closure";
264
+ seed?: string;
265
+ knownRepos: Set<string>;
266
+ edgesOverride: EdgeOverride;
267
+ /** Edges produced by scanning one repo (may include incoming event edges). */
268
+ deriveEdges: (repoName: string) => DerivedEdge[];
269
+ /** Whether a repo is present on disk. */
270
+ repoExists: (repoName: string) => boolean;
271
+ /**
272
+ * Whether a repo is configured in the workspace even if not on disk (e.g. a
273
+ * managed entry whose clone hasn't been created yet). Such repos stay
274
+ * graph NODES — with config-override edges only — so a fresh install renders
275
+ * the full workspace instead of an empty graph. Edge scanning still requires
276
+ * `repoExists`.
277
+ */
278
+ isConfigured?: (repoName: string) => boolean;
279
+ /** Repos that are event sinks — edges INTO these are reclassified to `event`. */
280
+ eventSinks?: Set<string>;
281
+ }
282
+
283
+ /**
284
+ * An edge into an event-sink (e.g. notes-api, an EventBridge consumer) is a
285
+ * producer→sink EVENT flow — its SDK publishes events, it isn't a sync call. So
286
+ * reclassify it as `event` (soft, non-gating) rather than http/jwks.
287
+ */
288
+ function reclassifyEventSink(
289
+ edge: DerivedEdge,
290
+ eventSinks: Set<string>,
291
+ ): DerivedEdge {
292
+ if (edge.kind === "event" || !eventSinks.has(edge.to)) return edge;
293
+ return {
294
+ ...edge,
295
+ kind: "event",
296
+ blocking: "soft",
297
+ signal: "event:sdk-publish",
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Synthetic edges from the central `edges:` override that discovery did not
303
+ * already produce for `repoName`. This makes the override the source of truth:
304
+ * a hard edge the code doesn't expose (or that a managed clone lacks) is still
305
+ * present, participates in closure BFS, and orders startup.
306
+ */
307
+ function overrideEdgesFor(
308
+ repoName: string,
309
+ existing: DerivedEdge[],
310
+ override: EdgeOverride,
311
+ knownRepos: Set<string>,
312
+ ): DerivedEdge[] {
313
+ const out: DerivedEdge[] = [];
314
+ const prefix = `${repoName}->`;
315
+ for (const [key, blocking] of Object.entries(override)) {
316
+ if (blocking === "ignore") continue; // ignore only annotates existing edges
317
+ if (!key.startsWith(prefix)) continue;
318
+ const to = key.slice(prefix.length);
319
+ if (to === repoName || !knownRepos.has(to)) continue;
320
+ const already = existing.some(
321
+ (e) => e.from === repoName && e.to === to && e.kind !== "event",
322
+ );
323
+ if (already) continue;
324
+ out.push({
325
+ from: repoName,
326
+ to,
327
+ kind: "http",
328
+ blocking,
329
+ signal: "override:config",
330
+ evidence: `${product().configFile} graph.edges`,
331
+ confidence: "high",
332
+ });
333
+ }
334
+ return out;
335
+ }
336
+
337
+ export function assembleGraph(opts: AssembleOptions): DerivedGraph {
338
+ const derived = new Map<string, DerivedEdge[]>();
339
+ const derive = (repoName: string): DerivedEdge[] => {
340
+ let edges = derived.get(repoName);
341
+ if (edges) return edges;
342
+ const base = opts.repoExists(repoName) ? opts.deriveEdges(repoName) : [];
343
+ edges = [
344
+ ...base,
345
+ ...overrideEdgesFor(repoName, base, opts.edgesOverride, opts.knownRepos),
346
+ ];
347
+ derived.set(repoName, edges);
348
+ return edges;
349
+ };
350
+
351
+ let nodes: Set<string>;
352
+ if (opts.scope === "closure") {
353
+ if (!opts.seed)
354
+ throw new Error("assembleGraph: scope 'closure' requires a seed");
355
+ nodes = new Set([opts.seed]);
356
+ const queue = [opts.seed];
357
+ while (queue.length > 0) {
358
+ const current = queue.shift()!;
359
+ for (const e of derive(current)) {
360
+ // Follow only outgoing, non-event edges to expand the start set.
361
+ if (e.from !== current || e.kind === "event") continue;
362
+ if (!nodes.has(e.to)) {
363
+ nodes.add(e.to);
364
+ queue.push(e.to);
365
+ }
366
+ }
367
+ }
368
+ } else {
369
+ nodes = new Set(
370
+ [...opts.knownRepos].filter(
371
+ (repo) => opts.repoExists(repo) || (opts.isConfigured?.(repo) ?? false),
372
+ ),
373
+ );
374
+ for (const repo of nodes) derive(repo);
375
+ }
376
+
377
+ // Aggregate every derived edge whose endpoints are both in the node set.
378
+ const eventSinks = opts.eventSinks ?? new Set<string>();
379
+ const all: DerivedEdge[] = [];
380
+ for (const edges of derived.values()) {
381
+ for (const e of edges) {
382
+ if (nodes.has(e.from) && nodes.has(e.to)) {
383
+ // An explicit hard/soft config override is authoritative — don't let
384
+ // event-sink reclassification downgrade it back to event/soft.
385
+ const o = opts.edgesOverride[`${e.from}->${e.to}`];
386
+ const withOverride = applyOverrideTo(e, opts.edgesOverride);
387
+ all.push(
388
+ o === "hard" || o === "soft"
389
+ ? withOverride
390
+ : reclassifyEventSink(withOverride, eventSinks),
391
+ );
392
+ }
393
+ }
394
+ }
395
+ return { nodes: [...nodes].sort(), edges: dedupe(all) };
396
+ }
397
+
398
+ export function deriveGraph(opts: DeriveOptions): DerivedGraph {
399
+ const ctx = buildContext(opts.workspace);
400
+ const cache = opts.refresh ? emptyCache() : readGraphCache();
401
+ const refresh = opts.refresh ?? false;
402
+
403
+ const graph = assembleGraph({
404
+ scope: opts.scope,
405
+ seed: opts.seed,
406
+ knownRepos: ctx.alias.knownRepos,
407
+ edgesOverride: ctx.edgesOverride,
408
+ eventSinks: ctx.alias.eventSinks,
409
+ repoExists: (repo) => resolveRepoPath(repo, ctx) !== null,
410
+ isConfigured: (repo) => configuredService(repo, ctx) !== null,
411
+ deriveEdges: (repo) => {
412
+ const repoPath = resolveRepoPath(repo, ctx);
413
+ return repoPath
414
+ ? deriveRepoEdges(repo, repoPath, ctx, cache, refresh)
415
+ : [];
416
+ },
417
+ });
418
+
419
+ writeGraphCache(cache);
420
+ return graph;
421
+ }