@monster-orange/den 0.0.0 → 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 (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -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 +99 -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 +406 -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 +108 -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 +729 -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/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Startup ordering for derived `den up`/`down`.
3
+ *
4
+ * We want a dependencies-first order: for every edge `A → B` (A depends on /
5
+ * publishes to B), B starts before A. The infra-stack foundation lands first,
6
+ * leaf services next, and entrypoints/aggregators (shop, shop-web) last.
7
+ *
8
+ * The catch: soft edges form cycles (e.g. shop → notes-api for calls, and
9
+ * notes-api → shop for JWKS; control-plane → shop for its reconciler). A strict
10
+ * topo-sort can't linearize those. So we treat ordering as a feedback-arc-set
11
+ * problem: accept HARD edges first (they're acyclic), then add soft + event
12
+ * edges greedily — processing higher-fan-out sources first so an aggregator's
13
+ * "my deps first" constraints win and the weak back-edges into it (JWKS,
14
+ * reconciler) get dropped. The accepted edges form a DAG we topo-sort.
15
+ *
16
+ * `hardDepsOf` (hard, non-event edges) is used separately by `den up` for
17
+ * readiness gating — only hard deps are *waited on*; this ordering is the launch
18
+ * sequence.
19
+ */
20
+
21
+ import type { DerivedEdge, DerivedGraph } from "./types.js";
22
+
23
+ /** Hard, non-event edges — the only ones `den up` blocks (waits) on. */
24
+ function isHardOrderingEdge(e: DerivedEdge): boolean {
25
+ return e.kind !== "event" && e.blocking === "hard";
26
+ }
27
+
28
+ /** Hard dependency targets of a single repo (used for readiness blocking). */
29
+ export function hardDepsOf(graph: DerivedGraph, repo: string): string[] {
30
+ return graph.edges
31
+ .filter((e) => e.from === repo && isHardOrderingEdge(e))
32
+ .map((e) => e.to);
33
+ }
34
+
35
+ export interface OrderOptions {
36
+ /** The repo `den up` was invoked for. */
37
+ seed?: string;
38
+ /** Include the seed itself in the result (`--include`/`-i`). */
39
+ include?: boolean;
40
+ }
41
+
42
+ /** Out-degree (distinct dep targets) per node — higher = more of an aggregator. */
43
+ function outDegrees(graph: DerivedGraph): Map<string, number> {
44
+ const deg = new Map<string, number>(graph.nodes.map((n) => [n, 0]));
45
+ const seen = new Set<string>();
46
+ for (const e of graph.edges) {
47
+ if (e.from === e.to) continue;
48
+ const k = `${e.from}->${e.to}`;
49
+ if (seen.has(k)) continue;
50
+ seen.add(k);
51
+ deg.set(e.from, (deg.get(e.from) ?? 0) + 1);
52
+ }
53
+ return deg;
54
+ }
55
+
56
+ interface Constraint {
57
+ before: string; // must start before `after`
58
+ after: string;
59
+ hard: boolean;
60
+ srcDeg: number; // out-degree of the dependent (`after`) node
61
+ }
62
+
63
+ /**
64
+ * Dependencies-first launch order. Each edge `A → B` asks for B before A; cycles
65
+ * are broken by dropping soft edges (hard kept). Excludes the seed unless
66
+ * `include` is set.
67
+ */
68
+ export function orderForUp(
69
+ graph: DerivedGraph,
70
+ opts: OrderOptions = {},
71
+ ): string[] {
72
+ const nodes = graph.nodes;
73
+ const deg = outDegrees(graph);
74
+
75
+ // Event reachability: which nodes a producer's events flow to (transitively).
76
+ // A non-event edge B→A where A's events reach B is a tolerant *back-call*
77
+ // (B consumes A's events and calls A back during processing, e.g.
78
+ // jobs-runtime → shop) — it must not gate order, so the consumer can start
79
+ // before the producer it listens to.
80
+ const eventOut = new Map<string, string[]>();
81
+ for (const e of graph.edges)
82
+ if (e.kind === "event")
83
+ (eventOut.get(e.from) ?? eventOut.set(e.from, []).get(e.from)!).push(
84
+ e.to,
85
+ );
86
+ const eventReaches = (a: string, b: string): boolean => {
87
+ const stack = [...(eventOut.get(a) ?? [])];
88
+ const visited = new Set(stack);
89
+ while (stack.length > 0) {
90
+ const cur = stack.pop()!;
91
+ if (cur === b) return true;
92
+ for (const n of eventOut.get(cur) ?? [])
93
+ if (!visited.has(n)) {
94
+ visited.add(n);
95
+ stack.push(n);
96
+ }
97
+ }
98
+ return false;
99
+ };
100
+
101
+ // One constraint per distinct edge: edge A→B ⇒ "B before A". Tolerant edges
102
+ // (JWKS auth, `ignore` overrides, event back-calls) are excluded from ordering
103
+ // — they still start the target, just don't gate sequence. Event edges DO
104
+ // order (consumer before producer).
105
+ const seen = new Set<string>();
106
+ const constraints: Constraint[] = [];
107
+ for (const e of graph.edges) {
108
+ if (e.from === e.to) continue;
109
+ if (e.kind === "jwks" || e.ordering === false) continue;
110
+ if (e.kind !== "event" && eventReaches(e.to, e.from)) continue;
111
+ const key = `${e.to}<${e.from}`;
112
+ if (seen.has(key)) continue;
113
+ seen.add(key);
114
+ constraints.push({
115
+ before: e.to,
116
+ after: e.from,
117
+ hard: isHardOrderingEdge(e),
118
+ srcDeg: deg.get(e.from) ?? 0,
119
+ });
120
+ }
121
+
122
+ // Hard constraints first; then soft by dependent out-degree desc (aggregators
123
+ // win, so their back-edges get dropped), then by name for determinism.
124
+ constraints.sort(
125
+ (a, b) =>
126
+ Number(b.hard) - Number(a.hard) ||
127
+ b.srcDeg - a.srcDeg ||
128
+ a.after.localeCompare(b.after) ||
129
+ a.before.localeCompare(b.before),
130
+ );
131
+
132
+ // Greedily accept "before → after" edges that don't introduce a cycle.
133
+ const adj = new Map<string, Set<string>>(nodes.map((n) => [n, new Set()]));
134
+ const reaches = (src: string, dst: string): boolean => {
135
+ const stack = [src];
136
+ const visited = new Set([src]);
137
+ while (stack.length > 0) {
138
+ const cur = stack.pop()!;
139
+ if (cur === dst) return true;
140
+ for (const n of adj.get(cur) ?? [])
141
+ if (!visited.has(n)) {
142
+ visited.add(n);
143
+ stack.push(n);
144
+ }
145
+ }
146
+ return false;
147
+ };
148
+ for (const c of constraints) {
149
+ if (c.before === c.after || !adj.has(c.before) || !adj.has(c.after))
150
+ continue;
151
+ if (reaches(c.after, c.before)) continue; // accepting would cycle → drop
152
+ adj.get(c.before)!.add(c.after);
153
+ }
154
+
155
+ // Kahn topo-sort (before-first); name-sorted ready queue for determinism.
156
+ const indeg = new Map<string, number>(nodes.map((n) => [n, 0]));
157
+ for (const [, outs] of adj)
158
+ for (const t of outs) indeg.set(t, (indeg.get(t) ?? 0) + 1);
159
+
160
+ // Layer = longest accepted-dependency chain below a node (its deepest dep
161
+ // is layer+? no — deps are BEFORE it, so layer(n) = 1 + max(layer(dep))).
162
+ // The ready queue orders by (layer, name) rather than name alone: a pure
163
+ // name sort exiles nodes to a position their dependencies don't justify —
164
+ // tasks-api (deps: only infra-stack) sorted dead last in up-order purely
165
+ // because "t" loses to every other repo name, which put it FIRST in the
166
+ // display order, parked next to shop-web instead of beside the one thing it
167
+ // depends on. Layer-first keeps every node in the band its dependency depth
168
+ // earns; name only breaks ties within a band.
169
+ const layer = new Map<string, number>(nodes.map((n) => [n, 0]));
170
+ {
171
+ // Relaxation over the accepted DAG (adj: dep → dependent). Bounded by
172
+ // node count; the graph is small and acyclic by construction.
173
+ let changed = true;
174
+ let guard = 0;
175
+ while (changed && guard++ <= nodes.length + 1) {
176
+ changed = false;
177
+ for (const [dep, outs] of adj) {
178
+ for (const t of outs) {
179
+ const need = (layer.get(dep) ?? 0) + 1;
180
+ if ((layer.get(t) ?? 0) < need) {
181
+ layer.set(t, need);
182
+ changed = true;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ const byLayerThenName = (a: string, b: string): number =>
189
+ (layer.get(a) ?? 0) - (layer.get(b) ?? 0) || a.localeCompare(b);
190
+
191
+ const ready = nodes
192
+ .filter((n) => (indeg.get(n) ?? 0) === 0)
193
+ .sort(byLayerThenName);
194
+ const order: string[] = [];
195
+ while (ready.length > 0) {
196
+ const n = ready.shift()!;
197
+ order.push(n);
198
+ for (const m of [...(adj.get(n) ?? [])].sort()) {
199
+ indeg.set(m, (indeg.get(m) ?? 0) - 1);
200
+ if (indeg.get(m) === 0) {
201
+ ready.push(m);
202
+ ready.sort(byLayerThenName);
203
+ }
204
+ }
205
+ }
206
+ for (const n of nodes) if (!order.includes(n)) order.push(n); // safety
207
+
208
+ return opts.seed && !opts.include
209
+ ? order.filter((n) => n !== opts.seed)
210
+ : order;
211
+ }
212
+
213
+ /** Teardown order: reverse of the full startup order (always includes the seed). */
214
+ export function orderForDown(graph: DerivedGraph): string[] {
215
+ return orderForUp(graph, { include: true }).reverse();
216
+ }
217
+
218
+ /**
219
+ * The set `den up` should start: the seed's DEPENDENCIES — what it needs to
220
+ * run — not what consumes it. From the seed we follow every dependency edge
221
+ * (infra/http — hard AND soft, since those are the seed's own declared deps);
222
+ * BEYOND the seed we follow only HARD edges, so a single soft/tolerant dep
223
+ * doesn't drag its entire subtree into the bring-up. Two edge kinds are NEVER
224
+ * followed:
225
+ * - event — points at DOWNSTREAM consumers of the seed, which depend on it
226
+ * and must not start ahead of it.
227
+ * - jwks — a tolerant auth-key lookup, not a runtime dependency. Following
228
+ * it is how `den up notes-api` once booted shop's entire neighborhood
229
+ * (files-api, control-plane, jobs-runtime, shop-web) off the single
230
+ * scribe→shop JWKS edge.
231
+ *
232
+ * (`den down <seed>` tears down this same set in reverse.)
233
+ */
234
+ /**
235
+ * Everything `den up <seed>` operates on: the kept subgraph plus the
236
+ * dependencies-first launch order. Always the seed's DEPENDENCY closure —
237
+ * what it needs to run — never the downstream consumers/JWKS-provider world
238
+ * it feeds (`up notes-api` once booted shop + control-plane + jobs-runtime
239
+ * + shop-web through the tolerant scribe→shop JWKS edge). `include` controls
240
+ * whether the seed itself is started (`-x` starts only the dependencies).
241
+ */
242
+ /**
243
+ * The set "up with deps" should start: everything the seed sits ON TOP OF.
244
+ *
245
+ * Walks dependency edges outward from the seed and keeps only systems that are
246
+ * strictly LOWER than the seed in the global start order — never climbing. That
247
+ * positional rule is the whole idea, and it is what an engineer reads off the
248
+ * graph: start what is underneath me, not what is above me.
249
+ *
250
+ * Why positional rather than by edge kind (the rule this replaced):
251
+ *
252
+ * - It INCLUDES tolerant deps that sit below. `files-api --event-->
253
+ * notes-api` was skipped purely for being an event edge, yet `orderForUp`
254
+ * already ranks notes-api below files-api and
255
+ * the graph draws it there — so refusing to start it contradicted both the
256
+ * picture and the order.
257
+ * - It EXCLUDES tolerant deps that sit above, which kind-based rules let
258
+ * through: `links-api --http(soft)--> shop-web` dragged in shop-web,
259
+ * ten places higher up the stack.
260
+ * - It subsumes the Jul 14 incident — the scribe→shop JWKS edge that once
261
+ * booted shop's whole neighborhood — because shop is
262
+ * above notes-api and is refused on position, not on a special case.
263
+ *
264
+ * `jwks` and edges explicitly marked non-ordering are still skipped: they are
265
+ * not runtime dependencies at all, whichever direction they point.
266
+ */
267
+ export function startClosure(graph: DerivedGraph, seed: string): Set<string> {
268
+ const rank = new Map(orderForUp(graph).map((repo, i) => [repo, i]));
269
+ const seedRank = rank.get(seed);
270
+ const seen = new Set([seed]);
271
+ if (seedRank === undefined) return seen; // seed not in the graph — nothing to add
272
+
273
+ const outgoing = new Map<string, DerivedEdge[]>();
274
+ for (const e of graph.edges) {
275
+ (outgoing.get(e.from) ?? outgoing.set(e.from, []).get(e.from)!).push(e);
276
+ }
277
+
278
+ const queue = [seed];
279
+ while (queue.length > 0) {
280
+ const cur = queue.shift()!;
281
+ for (const e of outgoing.get(cur) ?? []) {
282
+ if (e.kind === "jwks" || e.ordering === false) continue;
283
+ const toRank = rank.get(e.to);
284
+ // Strictly below the SEED (not below `cur`): a transitive dep may rank
285
+ // above an intermediate node while still sitting under the seed.
286
+ if (toRank === undefined || toRank >= seedRank) continue;
287
+ if (!seen.has(e.to)) {
288
+ seen.add(e.to);
289
+ queue.push(e.to);
290
+ }
291
+ }
292
+ }
293
+ return seen;
294
+ }
295
+
296
+ /**
297
+ * Everything "up with deps" operates on: the downward closure plus its
298
+ * dependencies-first launch order.
299
+ */
300
+ export function deriveStartTargets(
301
+ full: DerivedGraph,
302
+ seed: string,
303
+ include = true,
304
+ ): { graph: DerivedGraph; order: string[] } {
305
+ const graph = restrictGraph(full, startClosure(full, seed));
306
+ return { graph, order: orderForUp(graph, { seed, include }) };
307
+ }
308
+
309
+ /**
310
+ * Apply the workspace auto-start policy to a derived up order.
311
+ *
312
+ * A system the engineer toggled auto-start OFF stays off when it is pulled in
313
+ * as somebody else's dependency — but never when it IS the seed, because that
314
+ * is the system they just asked to start. Same rule `den up <repo>` uses,
315
+ * kept here so the CLI and the web card action cannot drift.
316
+ */
317
+ export function filterClosureForStart(
318
+ order: string[],
319
+ seed: string,
320
+ noAutostart: ReadonlySet<string>,
321
+ ): { start: string[]; skippedOff: string[] } {
322
+ const start: string[] = [];
323
+ const skippedOff: string[] = [];
324
+ for (const repo of order) {
325
+ if (repo === seed || !noAutostart.has(repo)) start.push(repo);
326
+ else skippedOff.push(repo);
327
+ }
328
+ return { start, skippedOff };
329
+ }
330
+
331
+ /**
332
+ * The set `den down` should stop: the seed plus its DEPENDENTS — the
333
+ * services that would BREAK without it (hard, non-event edges into the seed,
334
+ * transitively). Its own dependencies and shared infra stay up: other running
335
+ * services use them, and tolerant edges (soft http, event, jwks) degrade
336
+ * gracefully rather than break. (`den down notes-api` once derived an
337
+ * 11-system teardown — infra-stack included — by spanning full directed
338
+ * reachability through the tolerant scribe→shop JWKS edge.)
339
+ */
340
+ /**
341
+ * Everything `den down <seed>` operates on: the kept subgraph plus the
342
+ * dependents-first stop order (reverse of startup — dependents stop before
343
+ * the seed they need).
344
+ */
345
+ /** Restrict a graph to a node set (keeping edges with both endpoints inside). */
346
+ export function restrictGraph(
347
+ graph: DerivedGraph,
348
+ keep: Set<string>,
349
+ ): DerivedGraph {
350
+ return {
351
+ nodes: graph.nodes.filter((n) => keep.has(n)),
352
+ edges: graph.edges.filter((e) => keep.has(e.from) && keep.has(e.to)),
353
+ };
354
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The alias resolver for this machine: built-in topology, the team's
3
+ * `graph.aliases`, and every repo in the engineer's catalog.
4
+ */
5
+ import { readDenConfig } from "../../den-config.js";
6
+ import { readCatalog } from "../catalog.js";
7
+ import { buildAliasResolver, type AliasResolver } from "./alias.js";
8
+
9
+ export function loadAliasResolver(): AliasResolver {
10
+ return buildAliasResolver(readDenConfig().graph, Object.keys(readCatalog()));
11
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Rule — declared compose dependencies.
3
+ *
4
+ * A repo's `.den.yml` `compose.uses` names the stacks it calls. That is an
5
+ * explicit declaration, so each entry is a hard http edge.
6
+ */
7
+ import { parse as parseYaml } from "yaml";
8
+ import type { AliasResolver } from "./alias.js";
9
+ import type { DerivedEdge, ScannedRepo } from "./types.js";
10
+
11
+ import { product } from "../../product.js";
12
+ export function deriveComposeEdges(
13
+ repo: ScannedRepo,
14
+ alias: AliasResolver,
15
+ ): DerivedEdge[] {
16
+ const file = repo.files.find((f) => f.rel === `${product().manifestYml}`);
17
+ if (!file) return [];
18
+ let uses: unknown;
19
+ try {
20
+ uses = (parseYaml(file.content) as { compose?: { uses?: unknown } })
21
+ ?.compose?.uses;
22
+ } catch {
23
+ return [];
24
+ }
25
+ if (!Array.isArray(uses)) return [];
26
+ const out: DerivedEdge[] = [];
27
+ for (const u of uses) {
28
+ if (typeof u !== "string") continue;
29
+ const to = alias.resolve(u);
30
+ if (!to || to === repo.repoName) continue;
31
+ out.push({
32
+ from: repo.repoName,
33
+ to,
34
+ kind: "http",
35
+ blocking: "hard",
36
+ signal: "compose:uses",
37
+ evidence: `${product().manifestYml} compose.uses: ${u}`,
38
+ confidence: "high",
39
+ });
40
+ }
41
+ return out;
42
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Rule C — event choreography (EventBridge).
3
+ *
4
+ * A consumer declares an EventBridge rule whose `event_pattern.source` names the
5
+ * upstream producer (e.g. `acme.orders`). Each source string is resolved
6
+ * through the org's `graph.aliases`, so no namespace is hard-wired.
7
+ * Scanning the consumer's IaC therefore recovers a directed producer→consumer
8
+ * flow edge — no `.den.yml` needed. Event edges are shown in the graph but
9
+ * never gate startup (the bus buffers), so they are always `soft`.
10
+ *
11
+ * Wildcard patterns (`{"prefix": "acme."}`) match many sources and can't be
12
+ * attributed to a single producer, so they are skipped.
13
+ */
14
+
15
+ import type { AliasResolver } from "./alias.js";
16
+ import type { DerivedEdge, ScannedRepo } from "./types.js";
17
+ import { evidenceAt, isCommentedLine } from "./text.js";
18
+
19
+ const IAC_FILE = /\.py$/;
20
+ const SOURCE_BLOCK = /["']source["']\s*:\s*\[([^\]]*)\]/g;
21
+ const SOURCE_STRING = /["']([A-Za-z0-9_.-]+)["']/g;
22
+
23
+ export function deriveEventEdges(
24
+ repo: ScannedRepo,
25
+ alias: AliasResolver,
26
+ ): DerivedEdge[] {
27
+ const out: DerivedEdge[] = [];
28
+ for (const file of repo.files) {
29
+ if (!IAC_FILE.test(file.rel)) continue;
30
+ for (const block of file.content.matchAll(SOURCE_BLOCK)) {
31
+ const group = block[1];
32
+ if (group.includes("prefix")) continue; // wildcard — unattributable
33
+ if (isCommentedLine(file.content, block.index ?? 0)) continue;
34
+ for (const src of group.matchAll(SOURCE_STRING)) {
35
+ const producer = alias.resolve(src[1]);
36
+ if (!producer || producer === repo.repoName) continue;
37
+ out.push({
38
+ from: producer,
39
+ to: repo.repoName,
40
+ kind: "event",
41
+ blocking: "soft",
42
+ signal: "event:eventbridge-rule",
43
+ evidence: evidenceAt(file, block.index ?? 0),
44
+ confidence: "high",
45
+ });
46
+ }
47
+ }
48
+ }
49
+ return out;
50
+ }