@monster-orange/den 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  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 +104 -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,247 @@
1
+ /**
2
+ * Rule B — synchronous service dependencies (HTTP / JWKS).
3
+ *
4
+ * Three corroborating signals, all caller→callee:
5
+ * - SDK imports: go.mod / package.json / pyproject.toml referencing a peer's SDK.
6
+ * - env URLs: *_API_URL / *_SERVICE_URL / *_JWKS_URL / *_URL whose key token or
7
+ * value host resolves to a peer (matched via the alias map).
8
+ * - IaC stack refs: Pulumi `pulumi_<svc>_project_name` / `<svc>__internal_dns_hostname`.
9
+ *
10
+ * All edges are emitted `soft` here; blocking is finalized in derive-graph via the
11
+ * central `edges:` override (the source of truth for functional-hard edges).
12
+ */
13
+
14
+ import type { AliasResolver } from "./alias.js";
15
+ import type {
16
+ Confidence,
17
+ DerivedEdge,
18
+ EdgeKind,
19
+ ScannedFile,
20
+ ScannedRepo,
21
+ } from "./types.js";
22
+ import { evidenceAt, isCommentedLine, lineNumberAt } from "./text.js";
23
+
24
+ const SDK_FILES = /(^|\/)(package\.json|go\.mod|pyproject\.toml)$/;
25
+ const ENV_FILES = /(^|\/)(stack\.yml|\.env)/;
26
+ const IAC_FILE = /\.py$/;
27
+
28
+ // Longer suffixes first so e.g. SHOP_BACKEND_INTERNAL_URL → token "shop-backend".
29
+ const URL_KEY =
30
+ /\b([A-Z0-9_]+?)_(API_URL|SERVICE_URL|JWKS_URL|BASE_URL|INTERNAL_URL|ENDPOINT_URL|ENDPOINT|URL|HOST|BASE)\b/g;
31
+ const URL_VALUE = /https?:\/\/([a-z0-9][a-z0-9.-]*)/g;
32
+ const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ /** `github.com/<org>/<repo>` for any configured SDK org (none → never matches). */
34
+ const goSdk = (orgs: string[]) =>
35
+ orgs.length
36
+ ? new RegExp(
37
+ `github\\.com\\/(?:${orgs.map(escapeRe).join("|")})\\/([a-z0-9-]+)`,
38
+ "g",
39
+ )
40
+ : null;
41
+ /** `@<scope>/<pkg>` for any configured npm scope (none → never matches). */
42
+ const npmSdk = (scopes: string[]) =>
43
+ scopes.length
44
+ ? new RegExp(`@(?:${scopes.map(escapeRe).join("|")})\\/([a-z0-9-]+)`, "g")
45
+ : null;
46
+ const PY_SDK = /\b([a-z][a-z0-9-]*-(?:sdk|events|client))\b/g;
47
+ const PULUMI_REF =
48
+ /(?:pulumi_([a-z0-9_]+)_(?:project_name|stack_name)|([a-z0-9_]+)__internal_dns_hostname)/g;
49
+
50
+ interface Candidate {
51
+ token: string;
52
+ kind: EdgeKind;
53
+ signal: string;
54
+ index: number;
55
+ confidence: Confidence;
56
+ }
57
+
58
+ /** matchAll results always carry an index; this just narrows the type. */
59
+ const at = (m: RegExpMatchArray): number => m.index ?? 0;
60
+
61
+ /**
62
+ * The assigned value on the line containing `index` — text after the first
63
+ * `:` or `=`, trimmed, surrounding quotes stripped. `""`, `KEY:`, and
64
+ * `KEY=` all read as empty.
65
+ */
66
+ function lineValueAt(content: string, index: number): string {
67
+ const start = content.lastIndexOf("\n", index - 1) + 1;
68
+ let end = content.indexOf("\n", index);
69
+ if (end === -1) end = content.length;
70
+ const line = content.slice(start, end);
71
+ const sep = line.search(/[:=]/);
72
+ if (sep === -1) return "";
73
+ return line
74
+ .slice(sep + 1)
75
+ .trim()
76
+ .replace(/^["']|["']$/g, "")
77
+ .trim();
78
+ }
79
+
80
+ /**
81
+ * Compact evidence for an SDK-import edge: `<file>:<line> <name> <version>`
82
+ * instead of the raw line — npm SDK deps resolve to a long api-specs `.tgz`
83
+ * URL that swamps the graph. The version is the last semver-ish token on the
84
+ * line (works for `…-0.2.18.tgz`, go `v0.2.6`, and npm `^1.2.3`).
85
+ */
86
+ function sdkEvidence(file: ScannedFile, index: number, token: string): string {
87
+ const start = file.content.lastIndexOf("\n", index - 1) + 1;
88
+ let end = file.content.indexOf("\n", index);
89
+ if (end === -1) end = file.content.length;
90
+ const line = file.content.slice(start, end);
91
+ // Core semver + optional hyphenated pre-release only — a hyphen won't attach
92
+ // a file extension the way `[-.]` did (`…-0.1.14.tgz` → "0.1.14", not
93
+ // "0.1.14.tgz"), while `1.2.3-rc1` is still captured whole.
94
+ const versions = line.match(/\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9.]+)?/g);
95
+ const version = versions?.[versions.length - 1];
96
+ return `${file.rel}:${lineNumberAt(file.content, index)} ${token}${version ? ` ${version}` : ""}`;
97
+ }
98
+
99
+ /**
100
+ * go.mod hygiene: only DIRECT `require` entries are real dependencies. Skips
101
+ * transitive modules (`// indirect`) and `replace`/`exclude` directive lines —
102
+ * a replace target is a local override, not a new callee.
103
+ */
104
+ function isDirectGoRequire(content: string, index: number): boolean {
105
+ const start = content.lastIndexOf("\n", index - 1) + 1;
106
+ let end = content.indexOf("\n", index);
107
+ if (end === -1) end = content.length;
108
+ const line = content.slice(start, end);
109
+ if (/\/\/\s*indirect\b/.test(line)) return false;
110
+ const head = line.trimStart();
111
+ return (
112
+ !head.startsWith("replace") &&
113
+ !head.startsWith("exclude") &&
114
+ !line.includes("=>")
115
+ );
116
+ }
117
+
118
+ function pushEdge(
119
+ out: DerivedEdge[],
120
+ repo: ScannedRepo,
121
+ alias: AliasResolver,
122
+ file: ScannedFile,
123
+ c: Candidate,
124
+ ): void {
125
+ const to = alias.resolve(c.token);
126
+ if (!to || to === repo.repoName || to === alias.infraRepo) return;
127
+ if (isCommentedLine(file.content, c.index)) return; // skip commented-out config
128
+ out.push({
129
+ from: repo.repoName,
130
+ to,
131
+ kind: c.kind,
132
+ blocking: "soft",
133
+ signal: c.signal,
134
+ evidence:
135
+ c.signal === "http:sdk-import"
136
+ ? sdkEvidence(file, c.index, c.token)
137
+ : evidenceAt(file, c.index),
138
+ confidence: c.confidence,
139
+ });
140
+ }
141
+
142
+ function scanFile(
143
+ out: DerivedEdge[],
144
+ repo: ScannedRepo,
145
+ alias: AliasResolver,
146
+ file: ScannedFile,
147
+ ): void {
148
+ const add = (c: Candidate) => pushEdge(out, repo, alias, file, c);
149
+
150
+ if (SDK_FILES.test(file.rel)) {
151
+ const NPM_SDK = npmSdk(alias.npmScopes);
152
+ const GO_SDK = goSdk(alias.sdkOrgs);
153
+ if (NPM_SDK)
154
+ for (const m of file.content.matchAll(NPM_SDK))
155
+ add({
156
+ token: m[1],
157
+ kind: "http",
158
+ signal: "http:sdk-import",
159
+ index: at(m),
160
+ confidence: "high",
161
+ });
162
+ if (GO_SDK)
163
+ for (const m of file.content.matchAll(GO_SDK)) {
164
+ if (
165
+ file.rel.endsWith("go.mod") &&
166
+ !isDirectGoRequire(file.content, at(m))
167
+ )
168
+ continue;
169
+ add({
170
+ token: m[1],
171
+ kind: "http",
172
+ signal: "http:sdk-import",
173
+ index: at(m),
174
+ confidence: "high",
175
+ });
176
+ }
177
+ if (file.rel.endsWith("pyproject.toml"))
178
+ for (const m of file.content.matchAll(PY_SDK))
179
+ add({
180
+ token: m[1],
181
+ kind: "http",
182
+ signal: "http:sdk-import",
183
+ index: at(m),
184
+ confidence: "high",
185
+ });
186
+ }
187
+
188
+ if (ENV_FILES.test(file.rel)) {
189
+ // stack.yml declares keys once with an EMPTY value (`SHOP_JWKS_URL:`) and
190
+ // fills them in per-env sections below. Group matches per key and cite the
191
+ // first occurrence that actually carries a value, so the graph evidence
192
+ // reads `TOKEN_SERVICE_URL: http://tokens` instead of a bare key.
193
+ const bestByKey = new Map<string, RegExpMatchArray>();
194
+ for (const m of file.content.matchAll(URL_KEY)) {
195
+ if (isCommentedLine(file.content, at(m))) continue;
196
+ const cur = bestByKey.get(m[0]);
197
+ if (!cur) {
198
+ bestByKey.set(m[0], m);
199
+ } else if (
200
+ lineValueAt(file.content, at(cur)) === "" &&
201
+ lineValueAt(file.content, at(m)) !== ""
202
+ ) {
203
+ bestByKey.set(m[0], m);
204
+ }
205
+ }
206
+ for (const m of bestByKey.values()) {
207
+ const kind: EdgeKind = m[2].includes("JWKS") ? "jwks" : "http";
208
+ add({
209
+ token: m[1],
210
+ kind,
211
+ signal: "http:env-url",
212
+ index: at(m),
213
+ confidence: "high",
214
+ });
215
+ }
216
+ for (const m of file.content.matchAll(URL_VALUE)) {
217
+ const host = m[1].split(/[.:]/)[0];
218
+ add({
219
+ token: host,
220
+ kind: "http",
221
+ signal: "http:env-url",
222
+ index: at(m),
223
+ confidence: "medium",
224
+ });
225
+ }
226
+ }
227
+
228
+ if (IAC_FILE.test(file.rel)) {
229
+ for (const m of file.content.matchAll(PULUMI_REF))
230
+ add({
231
+ token: m[1] ?? m[2],
232
+ kind: "http",
233
+ signal: "http:iac-stack-ref",
234
+ index: at(m),
235
+ confidence: "medium",
236
+ });
237
+ }
238
+ }
239
+
240
+ export function deriveHttpEdges(
241
+ repo: ScannedRepo,
242
+ alias: AliasResolver,
243
+ ): DerivedEdge[] {
244
+ const out: DerivedEdge[] = [];
245
+ for (const file of repo.files) scanFile(out, repo, alias, file);
246
+ return out;
247
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Rule A — the shared local-infra repo (`graph.infra` in the org config).
3
+ *
4
+ * When an org runs shared local infra (databases, cloud emulators, …) from one
5
+ * repo, backends must not start until it is healthy. A repo whose infra files
6
+ * (stack.yml, manifest, Makefile, env files) match a configured signal gets a
7
+ * single hard `repo → <infra repo>` edge. No `graph.infra`: no edges.
8
+ */
9
+
10
+ import { product } from "../../product.js";
11
+ import type { AliasResolver } from "./alias.js";
12
+ import type { DerivedEdge, ScannedRepo } from "./types.js";
13
+ import { evidenceAt, isCommentedLine } from "./text.js";
14
+
15
+ const escapeRe = (s: string): string =>
16
+ s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+
18
+ /** Files that carry local-infra wiring. */
19
+ const infraFiles = (): RegExp =>
20
+ new RegExp(
21
+ `(^|/)(stack\\.yml|${product().manifestFiles.map(escapeRe).join("|")}|Makefile|\\.env)`,
22
+ );
23
+
24
+ export function deriveInfraEdges(
25
+ repo: ScannedRepo,
26
+ alias: AliasResolver,
27
+ ): DerivedEdge[] {
28
+ const infra = alias.infraRepo;
29
+ if (!infra || repo.repoName === infra) return [];
30
+ if (!alias.knownRepos.has(infra)) return [];
31
+ // Configured signals are strong (concrete wiring); a bare mention of the
32
+ // infra repo's name is weak and only used when no strong signal is found.
33
+ const STRONG_SIGNALS = alias.infraSignals;
34
+ const WEAK_SIGNALS = [new RegExp(`\\b${escapeRe(infra)}\\b`)];
35
+
36
+ const filesRe = infraFiles();
37
+ const infraFilesOf = repo.files.filter((f) => filesRe.test(f.rel));
38
+ const edgeAt = (file: (typeof infraFilesOf)[number], index: number) => [
39
+ {
40
+ from: repo.repoName,
41
+ to: infra,
42
+ kind: "infra" as const,
43
+ blocking: "hard" as const,
44
+ signal: "infra",
45
+ evidence: evidenceAt(file, index),
46
+ confidence: "high" as const,
47
+ },
48
+ ];
49
+
50
+ // Signal-major: exhaust the strong signals across every file first, so a
51
+ // concrete `:4566` in one file wins over a bare-name mention in another.
52
+ for (const signals of [STRONG_SIGNALS, WEAK_SIGNALS]) {
53
+ for (const re of signals) {
54
+ for (const file of infraFilesOf) {
55
+ for (const m of file.content.matchAll(new RegExp(re, "g"))) {
56
+ const index = m.index ?? 0;
57
+ if (isCommentedLine(file.content, index)) continue;
58
+ return edgeAt(file, index);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return [];
64
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Rule D — readiness-probe dependencies (the "read the /readyz code" signal).
3
+ *
4
+ * A peer service referenced inside a DEDICATED readiness/health handler
5
+ * (`readyz_get`, `healthz_get`, `health.go`, `health_router.py`, …) is, by
6
+ * convention, a startup gate: the service declares it isn't ready until that
7
+ * peer is reachable. So we emit a HARD edge for it.
8
+ *
9
+ * This is forward-looking: today every surveyed `/readyz` checks only its own
10
+ * DB/redis, so this rule yields nothing. It activates automatically once a team
11
+ * adopts the convention (make `/readyz` 200 only when hard deps are reachable).
12
+ * We deliberately scan only dedicated handler files — never broad files like
13
+ * main.py — to avoid false positives. Comments are blanked before matching, and
14
+ * bare single-word aliases ("app", "token", "backend") only count as URL hosts —
15
+ * in prose they are far too ambiguous to mint a hard startup gate from.
16
+ */
17
+
18
+ import type { AliasResolver } from "./alias.js";
19
+ import type { DerivedEdge, ScannedRepo } from "./types.js";
20
+ import { evidenceAt } from "./text.js";
21
+
22
+ const HANDLER_FILE = /(readyz|healthz|health_router|(^|\/)health\.go$)/;
23
+ const TOKEN = /[A-Za-z][A-Za-z0-9_.-]+/g;
24
+ const URL_HOST = /https?:\/\/([a-z0-9][a-z0-9.-]*)/g;
25
+ /** Multi-segment tokens (`files-api`, `shop.web`, `token_service`) only. */
26
+ const MULTI_SEGMENT = /[-._]/;
27
+
28
+ /**
29
+ * Where an inline `//` or `#` comment starts on `line`, or -1. A `//` preceded
30
+ * by `:` is a URL scheme separator (`http://…`), not a comment.
31
+ */
32
+ function commentStart(line: string): number {
33
+ let slashes = line.indexOf("//");
34
+ while (slashes > 0 && line[slashes - 1] === ":") {
35
+ slashes = line.indexOf("//", slashes + 1);
36
+ }
37
+ const hash = line.indexOf("#");
38
+ if (slashes === -1) return hash;
39
+ if (hash === -1) return slashes;
40
+ return Math.min(slashes, hash);
41
+ }
42
+
43
+ /**
44
+ * Blank full-line AND inline comments with spaces. Lengths and newlines are
45
+ * preserved so match indexes still map onto the original content for evidence.
46
+ */
47
+ function blankComments(content: string): string {
48
+ return content
49
+ .split("\n")
50
+ .map((line) => {
51
+ const cut = commentStart(line);
52
+ return cut === -1
53
+ ? line
54
+ : line.slice(0, cut) + " ".repeat(line.length - cut);
55
+ })
56
+ .join("\n");
57
+ }
58
+
59
+ export function deriveReadinessEdges(
60
+ repo: ScannedRepo,
61
+ alias: AliasResolver,
62
+ ): DerivedEdge[] {
63
+ const out: DerivedEdge[] = [];
64
+ const seen = new Set<string>();
65
+ for (const file of repo.files) {
66
+ if (!HANDLER_FILE.test(file.rel)) continue;
67
+ const code = blankComments(file.content);
68
+ const emit = (token: string, index: number): void => {
69
+ const to = alias.resolve(token);
70
+ if (!to || to === repo.repoName || to === alias.infraRepo || seen.has(to))
71
+ return;
72
+ seen.add(to);
73
+ out.push({
74
+ from: repo.repoName,
75
+ to,
76
+ kind: "http",
77
+ blocking: "hard",
78
+ signal: "readiness-probe",
79
+ evidence: evidenceAt(file, index),
80
+ confidence: "high",
81
+ });
82
+ };
83
+ // A URL host is structured context — a bare word counts there
84
+ // (`http://tokens:8080/healthz` really does gate on the tokens service).
85
+ for (const m of code.matchAll(URL_HOST)) {
86
+ emit(m[1].split(/[.:]/)[0], m.index ?? 0);
87
+ }
88
+ // Everywhere else only multi-segment tokens count: bare alias words like
89
+ // "app" or "token" are just as often prose, and every readiness edge is a
90
+ // HARD startup gate — too costly to mint from a stray word.
91
+ for (const m of code.matchAll(TOKEN)) {
92
+ if (MULTI_SEGMENT.test(m[0])) emit(m[0], m.index ?? 0);
93
+ }
94
+ }
95
+ return out;
96
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Filesystem scanning helpers for dependency derivation.
3
+ *
4
+ * Discovery reads a small, fixed allow-list of files per repo (manifests, SDK
5
+ * descriptors, env config, root Makefile, IaC) — never a full-repo walk — so a
6
+ * cold derive of one repo is a few dozen small reads (tens of ms). The same
7
+ * file list is the cache fingerprint key (see cache.ts).
8
+ */
9
+
10
+ import {
11
+ heldManifestPath,
12
+ repoManifestPath,
13
+ } from "../../manifest/read-manifest.js";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import type { ScannedFile, ScannedRepo } from "./types.js";
17
+
18
+ import { product } from "../../product.js";
19
+ /** Root-relative files probed in every repo (those that exist are read). */
20
+ const ROOT_CANDIDATES = [
21
+ `${product().manifestYml}`,
22
+ `${product().manifestJson}`,
23
+ "package.json",
24
+ "go.mod",
25
+ "pyproject.toml",
26
+ "stack.yml",
27
+ "Makefile",
28
+ ".env",
29
+ ".env.example",
30
+ ".env.local.example",
31
+ ".env.op",
32
+ // shop keeps its backend config one level down.
33
+ "apps/backend/.env",
34
+ "apps/backend/.env.example",
35
+ "apps/backend/env.example",
36
+ "apps/backend/stack.yml",
37
+ ] as const;
38
+
39
+ /** Directories whose `.py` files carry IaC (Pulumi/EventBridge) signals. */
40
+ const IAC_DIRS: Array<{ dir: string; depth: number }> = [
41
+ { dir: "pulumi", depth: 1 },
42
+ { dir: "infra", depth: 2 },
43
+ ];
44
+
45
+ /**
46
+ * Conventional, DEDICATED readiness/health-handler files (stack layout). A peer
47
+ * service referenced here is a startup gate (see rules-readiness.ts). We list
48
+ * exact paths — never broad files like main.py — to avoid false positives. The
49
+ * Python package dir varies, so it is resolved with a one-level `src/*` glob.
50
+ */
51
+ const GO_HEALTH_HANDLERS = [
52
+ "internal/apis/health/health.go",
53
+ "internal/apis/health/readyz_get.go",
54
+ "internal/apis/health/healthz_get.go",
55
+ "internal/apis/shop/readyz_get.go",
56
+ "internal/apis/shop/healthz_get.go",
57
+ ];
58
+ const PY_HEALTH_HANDLERS = [
59
+ "internal/apis/health/readyz_get.py",
60
+ "internal/apis/health/healthz_get.py",
61
+ "internal/routers/health_router.py",
62
+ ];
63
+
64
+ function healthHandlerFiles(repoPath: string): string[] {
65
+ const out: string[] = [];
66
+ for (const rel of GO_HEALTH_HANDLERS) {
67
+ const abs = path.join(repoPath, rel);
68
+ if (fs.existsSync(abs)) out.push(abs);
69
+ }
70
+ let pkgs: fs.Dirent[];
71
+ try {
72
+ pkgs = fs.readdirSync(path.join(repoPath, "src"), { withFileTypes: true });
73
+ } catch {
74
+ pkgs = [];
75
+ }
76
+ for (const pkg of pkgs) {
77
+ if (!pkg.isDirectory()) continue;
78
+ for (const rel of PY_HEALTH_HANDLERS) {
79
+ const abs = path.join(repoPath, "src", pkg.name, rel);
80
+ if (fs.existsSync(abs)) out.push(abs);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** A repo "exists" for graph purposes if it has any of these markers. */
87
+ export function hasRepoMarker(dir: string): boolean {
88
+ return (
89
+ fs.existsSync(path.join(dir, `${product().manifestYml}`)) ||
90
+ fs.existsSync(path.join(dir, `${product().manifestJson}`)) ||
91
+ (fs.existsSync(dir) && heldManifestPath(dir) !== null) ||
92
+ fs.existsSync(path.join(dir, "Makefile")) ||
93
+ fs.existsSync(path.join(dir, ".git"))
94
+ );
95
+ }
96
+
97
+ export function readFileSafe(abs: string): string | null {
98
+ try {
99
+ return fs.readFileSync(abs, "utf-8");
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function listPyFiles(dir: string, depth: number): string[] {
106
+ const out: string[] = [];
107
+ let entries: fs.Dirent[];
108
+ try {
109
+ entries = fs.readdirSync(dir, { withFileTypes: true });
110
+ } catch {
111
+ return out;
112
+ }
113
+ for (const e of entries) {
114
+ if (e.name.startsWith(".")) continue;
115
+ const p = path.join(dir, e.name);
116
+ if (e.isDirectory()) {
117
+ if (depth > 0) out.push(...listPyFiles(p, depth - 1));
118
+ } else if (e.isFile() && e.name.endsWith(".py")) {
119
+ out.push(p);
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Absolute paths of the input files that exist for a repo — the exact set the
127
+ * rules read and the cache fingerprints. Order is stable for a given tree.
128
+ */
129
+ export function repoInputFiles(repoPath: string): string[] {
130
+ const files: string[] = [];
131
+ for (const rel of ROOT_CANDIDATES) {
132
+ const abs = path.join(repoPath, rel);
133
+ if (fs.existsSync(abs)) files.push(abs);
134
+ }
135
+ for (const { dir, depth } of IAC_DIRS) {
136
+ files.push(...listPyFiles(path.join(repoPath, dir), depth));
137
+ }
138
+ files.push(...healthHandlerFiles(repoPath));
139
+ // A den-held manifest stands in for a missing .den.yml.
140
+ if (!repoManifestPath(repoPath)) {
141
+ const held = heldManifestPath(repoPath);
142
+ if (held) files.push(held);
143
+ }
144
+ return files;
145
+ }
146
+
147
+ /** Read all of a repo's input files into memory for the rules to scan. */
148
+ export function scanRepo(repoName: string, repoPath: string): ScannedRepo {
149
+ const files: ScannedFile[] = [];
150
+ for (const abs of repoInputFiles(repoPath)) {
151
+ const content = readFileSafe(abs);
152
+ if (content === null) continue;
153
+ // A held manifest lives outside the repo; rules see it as .den.yml.
154
+ const rel = abs.startsWith(repoPath + path.sep)
155
+ ? path.relative(repoPath, abs)
156
+ : `${product().manifestYml}`;
157
+ files.push({ abs, rel, content });
158
+ }
159
+ return { repoName, repoPath, files };
160
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Small text helpers shared by the derivation rules — turning a regex match
3
+ * into human-readable `file:line snippet` evidence.
4
+ */
5
+
6
+ import type { ScannedFile } from "./types.js";
7
+
8
+ export function lineNumberAt(content: string, index: number): number {
9
+ let line = 1;
10
+ for (let i = 0; i < index && i < content.length; i++) {
11
+ if (content[i] === "\n") line++;
12
+ }
13
+ return line;
14
+ }
15
+
16
+ /** The trimmed source line containing `index`, capped for display. */
17
+ function lineTextAt(content: string, index: number): string {
18
+ const start = content.lastIndexOf("\n", index - 1) + 1;
19
+ let end = content.indexOf("\n", index);
20
+ if (end === -1) end = content.length;
21
+ const text = content.slice(start, end).trim();
22
+ return text.length > 160 ? `${text.slice(0, 157)}…` : text;
23
+ }
24
+
25
+ export function evidenceAt(file: ScannedFile, index: number): string {
26
+ return `${file.rel}:${lineNumberAt(file.content, index)} ${lineTextAt(file.content, index)}`;
27
+ }
28
+
29
+ /**
30
+ * Whether the source line containing `index` is commented out (`#` for
31
+ * yaml/env/toml/python, `//` for go/js). Used to skip false-positive matches on
32
+ * commented-out config like `# FILES_API_URL=...`.
33
+ */
34
+ export function isCommentedLine(content: string, index: number): boolean {
35
+ const start = content.lastIndexOf("\n", index - 1) + 1;
36
+ let end = content.indexOf("\n", index);
37
+ if (end === -1) end = content.length;
38
+ const line = content.slice(start, end).trimStart();
39
+ return line.startsWith("#") || line.startsWith("//");
40
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Types for the derived dependency graph (`den graph` / derived `den up`).
3
+ *
4
+ * The graph is derived from real signals in each repo (SDK imports, service
5
+ * URLs, IaC stack refs, EventBridge rules) rather than trusting each repo's
6
+ * hand-written `.den.yml` `depends_on`. See derive-graph.ts for the engine.
7
+ */
8
+
9
+ /**
10
+ * What kind of dependency an edge represents — this drives whether `den up`
11
+ * orders on it:
12
+ * infra — shared local infra (infra-stack / LocalStack). Hard foundation.
13
+ * http — synchronous HTTP call to a peer service.
14
+ * jwks — peer's JWKS endpoint used for JWT verification (a kind of http).
15
+ * event — EventBridge producer→consumer data-flow. Shown, never ordered.
16
+ */
17
+ export type EdgeKind = "infra" | "http" | "jwks" | "event";
18
+
19
+ /**
20
+ * Whether the edge constrains startup order:
21
+ * hard — `from`'s `make up` fails / `from` is non-functional unless `to` is up first.
22
+ * soft — started as part of the dev stack but does not constrain order.
23
+ */
24
+ export type EdgeBlocking = "hard" | "soft";
25
+
26
+ export type Confidence = "high" | "medium" | "low";
27
+
28
+ export interface DerivedEdge {
29
+ /** Source repo-system name (catalog/repo key). */
30
+ from: string;
31
+ /** Target repo-system name (alias-normalized). */
32
+ to: string;
33
+ kind: EdgeKind;
34
+ blocking: EdgeBlocking;
35
+ /** Rule id that produced the edge, e.g. "http:env-url" or "event:eventbridge-rule". */
36
+ signal: string;
37
+ /** Human-readable evidence: file (relative to repo) + short snippet. */
38
+ evidence: string;
39
+ confidence: Confidence;
40
+ /**
41
+ * Whether this edge constrains startup ORDER. Tolerant edges (JWKS auth,
42
+ * redirect URLs, reconcilers) are still shown and still pull the target into
43
+ * the start set, but don't gate sequence. Defaults to true when omitted.
44
+ */
45
+ ordering?: boolean;
46
+ }
47
+
48
+ export interface DerivedGraph {
49
+ /** Repo-system names present in the graph. */
50
+ nodes: string[];
51
+ /** Deduped edges between nodes. */
52
+ edges: DerivedEdge[];
53
+ }
54
+
55
+ /** A file read from a repo during discovery (content cached for the rules). */
56
+ export interface ScannedFile {
57
+ /** Path relative to the repo root (used in evidence + filtering). */
58
+ rel: string;
59
+ /** Absolute path. */
60
+ abs: string;
61
+ content: string;
62
+ }
63
+
64
+ /** A repo's scanned inputs, passed to the rule functions. */
65
+ export interface ScannedRepo {
66
+ repoName: string;
67
+ repoPath: string;
68
+ files: ScannedFile[];
69
+ }