@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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +99 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- 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
|
+
}
|