@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,369 @@
1
+ function authTokenExpression(target) {
2
+ return target === "sveltekit"
3
+ ? 'publicEnv.PUBLIC_TOPOGRAM_AUTH_TOKEN || ""'
4
+ : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_TOKEN") || readPublicEnv("VITE_PUBLIC_TOPOGRAM_AUTH_TOKEN")';
5
+ }
6
+
7
+ function apiBaseExpression(target, defaultApiBaseUrl) {
8
+ return target === "sveltekit"
9
+ ? `publicEnv.PUBLIC_TOPOGRAM_API_BASE_URL || "${defaultApiBaseUrl}"`
10
+ : `readPublicEnv("PUBLIC_TOPOGRAM_API_BASE_URL") || readPublicEnv("VITE_PUBLIC_TOPOGRAM_API_BASE_URL") || "${defaultApiBaseUrl}"`;
11
+ }
12
+
13
+ function publicEnvPreamble(target) {
14
+ return target === "sveltekit"
15
+ ? 'import { env as publicEnv } from "$env/dynamic/public";\n'
16
+ : "";
17
+ }
18
+
19
+ function publicEnvHelper(target) {
20
+ if (target === "sveltekit") {
21
+ return "";
22
+ }
23
+ return `function readPublicEnv(name: string) {
24
+ return import.meta.env[name] || "";
25
+ }
26
+
27
+ `;
28
+ }
29
+
30
+ export function renderVisibilityModule(target) {
31
+ const preamble = target === "sveltekit" ? `${publicEnvPreamble(target)}\n` : publicEnvPreamble(target);
32
+ return `${preamble}export interface VisibilityRule {
33
+ predicate?: string | null;
34
+ value?: string | null;
35
+ claimValue?: string | null;
36
+ ownershipField?: string | null;
37
+ capability?: { id?: string | null } | null;
38
+ }
39
+
40
+ export interface VisibilityPrincipalOverride {
41
+ userId?: string | null;
42
+ permissions?: string | string[] | null;
43
+ roles?: string | string[] | null;
44
+ claims?: Record<string, unknown> | null;
45
+ isAdmin?: boolean | string | null;
46
+ }
47
+
48
+ interface AuthPrincipal {
49
+ userId: string;
50
+ permissions: Set<string>;
51
+ roles: Set<string>;
52
+ claims: Record<string, unknown>;
53
+ isAdmin: boolean;
54
+ }
55
+
56
+ ${publicEnvHelper(target)}function authToken() {
57
+ return ${authTokenExpression(target)};
58
+ }
59
+
60
+ function csvValues(value: string) {
61
+ return value
62
+ .split(",")
63
+ .map((entry) => entry.trim())
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function normalizeOverrideList(value: string | string[] | null | undefined) {
68
+ if (Array.isArray(value)) {
69
+ return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
70
+ }
71
+ if (typeof value === "string") {
72
+ return csvValues(value);
73
+ }
74
+ return [];
75
+ }
76
+
77
+ function readBoolean(value: string) {
78
+ return value === "true" || value === "1" || value === "yes";
79
+ }
80
+
81
+ function parseClaimsJson(raw: string) {
82
+ if (!raw) return {};
83
+ try {
84
+ const parsed = JSON.parse(raw);
85
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
86
+ } catch {
87
+ return {};
88
+ }
89
+ }
90
+
91
+ function decodeJwtPayload(token: string) {
92
+ const parts = token.split(".");
93
+ if (parts.length < 2) return null;
94
+ try {
95
+ const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
96
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
97
+ return JSON.parse(atob(padded));
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ function principalFromJwt(token: string): AuthPrincipal | null {
104
+ const payload = decodeJwtPayload(token);
105
+ if (!payload || typeof payload !== "object") return null;
106
+ return {
107
+ userId: typeof payload.sub === "string" ? payload.sub : "",
108
+ permissions: new Set(Array.isArray(payload.permissions) ? payload.permissions.filter((value: unknown): value is string => typeof value === "string") : []),
109
+ roles: new Set(Array.isArray(payload.roles) ? payload.roles.filter((value: unknown): value is string => typeof value === "string") : []),
110
+ claims: payload as Record<string, unknown>,
111
+ isAdmin: payload.admin === true
112
+ };
113
+ }
114
+
115
+ function currentPrincipal(overrides?: VisibilityPrincipalOverride | null): AuthPrincipal | null {
116
+ const token = authToken();
117
+ const jwtPrincipal = token ? principalFromJwt(token) : null;
118
+ const envClaims = parseClaimsJson(${target === "sveltekit" ? 'publicEnv.PUBLIC_TOPOGRAM_AUTH_CLAIMS || ""' : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_CLAIMS")'});
119
+ const userId = overrides?.userId || ${target === "sveltekit" ? "publicEnv.PUBLIC_TOPOGRAM_AUTH_USER_ID" : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_USER_ID")'} || jwtPrincipal?.userId || "";
120
+ const permissions = new Set([
121
+ ...normalizeOverrideList(overrides?.permissions),
122
+ ...csvValues(${target === "sveltekit" ? 'publicEnv.PUBLIC_TOPOGRAM_AUTH_PERMISSIONS || ""' : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_PERMISSIONS")'}),
123
+ ...Array.from(jwtPrincipal?.permissions || [])
124
+ ]);
125
+ const roles = new Set([
126
+ ...normalizeOverrideList(overrides?.roles),
127
+ ...csvValues(${target === "sveltekit" ? 'publicEnv.PUBLIC_TOPOGRAM_AUTH_ROLES || publicEnv.PUBLIC_TOPOGRAM_AUTH_ROLE || ""' : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_ROLES") || readPublicEnv("PUBLIC_TOPOGRAM_AUTH_ROLE")'}),
128
+ ...Array.from(jwtPrincipal?.roles || [])
129
+ ]);
130
+ const isAdmin = (
131
+ typeof overrides?.isAdmin === "boolean"
132
+ ? overrides.isAdmin
133
+ : readBoolean(String(overrides?.isAdmin || ""))
134
+ ) || readBoolean(${target === "sveltekit" ? 'publicEnv.PUBLIC_TOPOGRAM_AUTH_ADMIN || ""' : 'readPublicEnv("PUBLIC_TOPOGRAM_AUTH_ADMIN")'}) || jwtPrincipal?.isAdmin === true;
135
+
136
+ const claims = {
137
+ ...(jwtPrincipal?.claims || {}),
138
+ ...envClaims,
139
+ ...(overrides?.claims || {})
140
+ };
141
+
142
+ if (!token && !userId && permissions.size === 0 && roles.size === 0 && Object.keys(claims).length === 0 && !isAdmin) {
143
+ return null;
144
+ }
145
+
146
+ return { userId, permissions, roles, claims, isAdmin };
147
+ }
148
+
149
+ function claimMatches(principal: AuthPrincipal, claim: string | null | undefined, claimValue: string | null | undefined) {
150
+ if (!claim) return true;
151
+ const value = principal.claims[claim];
152
+ if (value == null) return false;
153
+ if (!claimValue) {
154
+ return value !== false && value !== "";
155
+ }
156
+ return String(value) === claimValue;
157
+ }
158
+
159
+ function ownerIdFromResource(resource: Record<string, unknown> | null | undefined, ownershipField?: string | null) {
160
+ if (!resource || typeof resource !== "object") {
161
+ return "";
162
+ }
163
+
164
+ if (ownershipField) {
165
+ const explicitValue = resource[ownershipField];
166
+ return typeof explicitValue === "string" ? explicitValue : "";
167
+ }
168
+
169
+ for (const field of ["owner_id", "assignee_id", "author_id", "user_id", "created_by_user_id"]) {
170
+ const value = resource[field];
171
+ if (typeof value === "string" && value.length > 0) {
172
+ return value;
173
+ }
174
+ }
175
+
176
+ return "";
177
+ }
178
+
179
+ export function canShowAction(
180
+ rule: VisibilityRule | null | undefined,
181
+ resource?: Record<string, unknown> | null,
182
+ overrides?: VisibilityPrincipalOverride | null
183
+ ) {
184
+ if (!rule) return true;
185
+
186
+ const principal = currentPrincipal(overrides);
187
+ if (!principal) {
188
+ return true;
189
+ }
190
+
191
+ if (rule.predicate === "permission") {
192
+ if (!rule.value) return true;
193
+ return principal.permissions.has("*") || principal.permissions.has(rule.value);
194
+ }
195
+
196
+ if (rule.predicate === "ownership") {
197
+ if (!rule.value || rule.value === "none") return true;
198
+ if (rule.value === "owner_or_admin" && principal.isAdmin) return true;
199
+ return ownerIdFromResource(resource, rule.ownershipField) === principal.userId;
200
+ }
201
+
202
+ if (rule.predicate === "claim") {
203
+ return claimMatches(principal, rule.value, rule.claimValue);
204
+ }
205
+
206
+ return true;
207
+ }
208
+ `;
209
+ }
210
+
211
+ export function renderApiClientModule(target, webReference, options = {}) {
212
+ const defaultApiBaseUrl = webReference.defaultApiBaseUrl || "http://localhost:3000";
213
+ const supportsDownload = Boolean(options.supportsDownload);
214
+ const contractsImportPath = target === "sveltekit" ? "$lib/topogram/api-contracts.json" : "../topogram/api-contracts.json";
215
+ return `${publicEnvPreamble(target)}import apiContracts from "${contractsImportPath}";
216
+
217
+ type Fetcher = typeof fetch;
218
+ type ApiContract = (typeof apiContracts)[keyof typeof apiContracts];
219
+ type RequestOptions = {
220
+ headers?: Record<string, string>;
221
+ };
222
+
223
+ ${target === "react" ? "" : publicEnvHelper(target)}function apiBase() {
224
+ return ${target === "react"
225
+ ? `import.meta.env.PUBLIC_TOPOGRAM_API_BASE_URL || import.meta.env.VITE_PUBLIC_TOPOGRAM_API_BASE_URL || "${defaultApiBaseUrl}"`
226
+ : apiBaseExpression(target, defaultApiBaseUrl)};
227
+ }
228
+
229
+ function authToken() {
230
+ return ${target === "react"
231
+ ? 'import.meta.env.PUBLIC_TOPOGRAM_AUTH_TOKEN || import.meta.env.VITE_PUBLIC_TOPOGRAM_AUTH_TOKEN || ""'
232
+ : authTokenExpression(target)};
233
+ }
234
+
235
+ function buildPath(contract: ApiContract, input: Record<string, unknown>) {
236
+ let path = contract.endpoint.path;
237
+ for (const field of contract.requestContract?.transport.path || []) {
238
+ const raw = input[field.name];
239
+ path = path.replace(\`:\${field.transport.wireName}\`, encodeURIComponent(String(raw ?? "")));
240
+ }
241
+ const params = new URLSearchParams();
242
+ for (const field of contract.requestContract?.transport.query || []) {
243
+ const raw = input[field.name];
244
+ if (raw !== undefined && raw !== null && raw !== "") {
245
+ params.set(field.transport.wireName, String(raw));
246
+ }
247
+ }
248
+ const query = params.toString();
249
+ return query ? \`\${path}?\${query}\` : path;
250
+ }
251
+
252
+ export async function requestCapability(fetcher: Fetcher, capabilityId: keyof typeof apiContracts, input: Record<string, unknown> = {}, options: RequestOptions = {}) {
253
+ const contract = apiContracts[capabilityId];
254
+ const url = new URL(buildPath(contract, input), apiBase()).toString();
255
+ const headers = new Headers();
256
+ for (const [name, value] of Object.entries(options.headers || {})) {
257
+ headers.set(name, value);
258
+ }
259
+ if ((contract.endpoint.authz || []).length > 0 && authToken() && !headers.has("Authorization")) {
260
+ headers.set("Authorization", "Bearer " + authToken());
261
+ }
262
+ let body: string | undefined;
263
+ if ((contract.requestContract?.transport.body || []).length > 0) {
264
+ headers.set("content-type", "application/json");
265
+ const payload: Record<string, unknown> = {};
266
+ for (const field of contract.requestContract?.transport.body || []) {
267
+ if (input[field.name] !== undefined) {
268
+ payload[field.transport.wireName] = input[field.name];
269
+ }
270
+ }
271
+ body = JSON.stringify(payload);
272
+ }
273
+ const response = await fetcher(url, { method: contract.endpoint.method, headers, body });
274
+ if (!response.ok) {
275
+ const detail = await response.text();
276
+ const error = new Error(\`\${contract.endpoint.method} \${contract.endpoint.path} failed (\${response.status}): \${detail}\`) as Error & { status?: number; detail?: string };
277
+ error.status = response.status;
278
+ error.detail = detail;
279
+ throw error;
280
+ }
281
+ if (response.status === 204) {
282
+ return null;
283
+ }
284
+ ${supportsDownload ? 'if ((contract.endpoint.download || []).length > 0) {\n return response.arrayBuffer();\n }\n ' : ""}return response.json();
285
+ }
286
+
287
+ export async function listPrimaryResources(fetcher: Fetcher, input: Record<string, unknown> = {}) {
288
+ return requestCapability(fetcher, "${webReference.client.capabilityIds.list}", input);
289
+ }
290
+ export async function ${webReference.client.functionNames.list}(fetcher: Fetcher, input: Record<string, unknown> = {}) {
291
+ return listPrimaryResources(fetcher, input);
292
+ }
293
+
294
+ export async function getPrimaryResource(fetcher: Fetcher, primary_id: string) {
295
+ return requestCapability(fetcher, "${webReference.client.capabilityIds.get}", { ${webReference.client.primaryParam}: primary_id });
296
+ }
297
+ export async function ${webReference.client.functionNames.get}(fetcher: Fetcher, primary_id: string) {
298
+ return getPrimaryResource(fetcher, primary_id);
299
+ }
300
+
301
+ export async function createPrimaryResource(fetcher: Fetcher, input: Record<string, unknown>, options: RequestOptions = {}) {
302
+ return requestCapability(fetcher, "${webReference.client.capabilityIds.create}", input, options);
303
+ }
304
+ export async function ${webReference.client.functionNames.create}(fetcher: Fetcher, input: Record<string, unknown>, options: RequestOptions = {}) {
305
+ return createPrimaryResource(fetcher, input, options);
306
+ }
307
+
308
+ export async function updatePrimaryResource(fetcher: Fetcher, primary_id: string, input: Record<string, unknown> = {}, options: RequestOptions = {}) {
309
+ return requestCapability(fetcher, "${webReference.client.capabilityIds.update}", { ${webReference.client.primaryParam}: primary_id, ...input }, options);
310
+ }
311
+ export async function ${webReference.client.functionNames.update}(fetcher: Fetcher, primary_id: string, input: Record<string, unknown> = {}, options: RequestOptions = {}) {
312
+ return updatePrimaryResource(fetcher, primary_id, input, options);
313
+ }
314
+
315
+ export async function terminalPrimaryAction(fetcher: Fetcher, primary_id: string, input: Record<string, unknown> = {}, options: RequestOptions = {}) {
316
+ return requestCapability(fetcher, "${webReference.client.capabilityIds.terminal}", { ${webReference.client.primaryParam}: primary_id, ...input }, options);
317
+ }
318
+ export async function ${webReference.client.functionNames.terminal}(fetcher: Fetcher, primary_id: string, input: Record<string, unknown> = {}, options: RequestOptions = {}) {
319
+ return terminalPrimaryAction(fetcher, primary_id, input, options);
320
+ }
321
+ ${(webReference.client.extraFunctions || []).map((entry) => {
322
+ const params = entry.primaryParam
323
+ ? `fetcher: Fetcher, ${entry.primaryParam}: string, input: Record<string, unknown> = {}, options: RequestOptions = {}`
324
+ : "fetcher: Fetcher, input: Record<string, unknown> = {}, options: RequestOptions = {}";
325
+ const payload = entry.primaryParam
326
+ ? `{ ${entry.primaryParam}, ...input }`
327
+ : "input";
328
+ return `
329
+ export async function ${entry.name}(${params}) {
330
+ return requestCapability(fetcher, "${entry.capabilityId}", ${payload}, options);
331
+ }`;
332
+ }).join("\n")}
333
+ `;
334
+ }
335
+
336
+ export function renderLookupModule(target, defaultApiBaseUrl) {
337
+ const preamble = target === "sveltekit" ? `${publicEnvPreamble(target)}\n` : publicEnvPreamble(target);
338
+ const authTokenHelper = `function authToken() {
339
+ return ${target === "react"
340
+ ? 'import.meta.env.PUBLIC_TOPOGRAM_AUTH_TOKEN || import.meta.env.VITE_PUBLIC_TOPOGRAM_AUTH_TOKEN || ""'
341
+ : authTokenExpression(target)};
342
+ }
343
+
344
+ `;
345
+ return `${preamble}export interface LookupOption {
346
+ value: string;
347
+ label: string;
348
+ }
349
+
350
+ ${target === "react" ? "" : publicEnvHelper(target)}function apiBase() {
351
+ return ${target === "react"
352
+ ? `import.meta.env.PUBLIC_TOPOGRAM_API_BASE_URL || import.meta.env.VITE_PUBLIC_TOPOGRAM_API_BASE_URL || "${defaultApiBaseUrl}"`
353
+ : apiBaseExpression(target, defaultApiBaseUrl)};
354
+ }
355
+
356
+ ${authTokenHelper}export async function listLookupOptions(fetcher: typeof fetch, route: string): Promise<LookupOption[]> {
357
+ const headers = new Headers();
358
+ if (authToken()) {
359
+ headers.set("Authorization", "Bearer " + authToken());
360
+ }
361
+ const response = await fetcher(new URL(route, apiBase()).toString(), { headers });
362
+ if (!response.ok) {
363
+ const detail = await response.text();
364
+ throw new Error(\`Lookup request failed (\${response.status}): \${detail}\`);
365
+ }
366
+ return response.json();
367
+ }
368
+ `;
369
+ }
@@ -0,0 +1,28 @@
1
+ function indentBlock(block, spaces) {
2
+ const indent = " ".repeat(spaces);
3
+ return block
4
+ .trim()
5
+ .split("\n")
6
+ .map((line) => (line ? `${indent}${line}` : ""))
7
+ .join("\n");
8
+ }
9
+
10
+ export function renderSvelteKitRedirectingAction({
11
+ actionName,
12
+ signature,
13
+ prelude,
14
+ tryStatement,
15
+ catchReturn,
16
+ successStatement
17
+ }) {
18
+ return ` ${actionName}: async (${signature}) => {
19
+ ${indentBlock(prelude, 4)}
20
+
21
+ try {
22
+ ${indentBlock(tryStatement, 6)}
23
+ } catch (error) {
24
+ ${indentBlock(catchReturn, 6)}
25
+ }
26
+ ${successStatement}
27
+ }`;
28
+ }
@@ -0,0 +1,234 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {{ id?: string, name?: string }} ComponentReference
5
+ * @typedef {{ component?: ComponentReference, region?: string }} ComponentUsage
6
+ * @typedef {{ patterns?: string[] }} ComponentContract
7
+ * @typedef {Record<string, ComponentContract>} ComponentContractMap
8
+ * @typedef {{ itemsExpression?: string, componentContracts?: ComponentContractMap, useTypescript?: boolean }} RenderOptions
9
+ * @typedef {{ components?: ComponentUsage[] }} ScreenContract
10
+ */
11
+
12
+ /**
13
+ * @param {unknown} value
14
+ * @returns {string}
15
+ */
16
+ function escapeHtml(value) {
17
+ return String(value ?? "")
18
+ .replace(/&/g, "&amp;")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+ .replace(/"/g, "&quot;");
22
+ }
23
+
24
+ /**
25
+ * @param {ComponentUsage} usage
26
+ * @returns {string}
27
+ */
28
+ function componentName(usage) {
29
+ return usage?.component?.name || usage?.component?.id || "Component";
30
+ }
31
+
32
+ /**
33
+ * @param {ComponentUsage} usage
34
+ * @returns {string}
35
+ */
36
+ function componentId(usage) {
37
+ return usage?.component?.id || "component";
38
+ }
39
+
40
+ /**
41
+ * @param {ComponentUsage} usage
42
+ * @param {ComponentContractMap | undefined} componentContracts
43
+ * @returns {string[]}
44
+ */
45
+ function componentPatterns(usage, componentContracts) {
46
+ const id = usage?.component?.id;
47
+ const contract = id ? componentContracts?.[id] : null;
48
+ return Array.isArray(contract?.patterns) ? contract.patterns : [];
49
+ }
50
+
51
+ /**
52
+ * @param {ComponentUsage} usage
53
+ * @param {ComponentContractMap | undefined} componentContracts
54
+ * @param {string} pattern
55
+ * @returns {boolean}
56
+ */
57
+ function hasPattern(usage, componentContracts, pattern) {
58
+ return componentPatterns(usage, componentContracts).includes(pattern);
59
+ }
60
+
61
+ /**
62
+ * @param {ComponentUsage} usage
63
+ * @param {RenderOptions} options
64
+ * @returns {string}
65
+ */
66
+ function renderSummaryStats(usage, options) {
67
+ const items = options.itemsExpression || "data.result.items";
68
+ return `<section class="component-card component-summary" data-topogram-component="${escapeHtml(componentId(usage))}">
69
+ <div>
70
+ <p class="component-eyebrow">Component</p>
71
+ <h2>${escapeHtml(componentName(usage))}</h2>
72
+ </div>
73
+ <div class="summary-grid">
74
+ <div>
75
+ <strong>{${items}.length}</strong>
76
+ <span>Total</span>
77
+ </div>
78
+ <div>
79
+ <strong>{Object.keys(${items}[0] ?? {}).length}</strong>
80
+ <span>Fields</span>
81
+ </div>
82
+ <div>
83
+ <strong>{${items}.filter((item) => item && (item.id ?? item.uuid ?? item.key)).length}</strong>
84
+ <span>Identified</span>
85
+ </div>
86
+ </div>
87
+ </section>`;
88
+ }
89
+
90
+ /**
91
+ * @param {ComponentUsage} usage
92
+ * @param {RenderOptions} options
93
+ * @returns {string}
94
+ */
95
+ function renderCollectionTable(usage, options) {
96
+ const items = options.itemsExpression || "data.result.items";
97
+ return `<div class="component-card component-table" data-topogram-component="${escapeHtml(componentId(usage))}">
98
+ <div class="component-header">
99
+ <div>
100
+ <p class="component-eyebrow">Component</p>
101
+ <h2>${escapeHtml(componentName(usage))}</h2>
102
+ </div>
103
+ <span class="badge">{${items}.length} items</span>
104
+ </div>
105
+ <div class="table-wrap component-table-wrap">
106
+ <table class="resource-table data-grid">
107
+ <thead>
108
+ <tr>
109
+ {#each Object.keys(${items}[0] ?? {}).slice(0, 4) as field}
110
+ <th>{field}</th>
111
+ {/each}
112
+ </tr>
113
+ </thead>
114
+ <tbody>
115
+ {#each ${items} as item}
116
+ <tr>
117
+ {#each Object.keys(${items}[0] ?? {}).slice(0, 4) as field}
118
+ <td>{String(item?.[field] ?? "")}</td>
119
+ {/each}
120
+ </tr>
121
+ {/each}
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ </div>`;
126
+ }
127
+
128
+ /**
129
+ * @param {ComponentUsage} usage
130
+ * @param {RenderOptions} options
131
+ * @returns {string}
132
+ */
133
+ function renderBoard(usage, options) {
134
+ const items = options.itemsExpression || "data.result.items";
135
+ return `<div class="component-card component-board" data-topogram-component="${escapeHtml(componentId(usage))}">
136
+ <div class="component-header">
137
+ <div>
138
+ <p class="component-eyebrow">Component</p>
139
+ <h2>${escapeHtml(componentName(usage))}</h2>
140
+ </div>
141
+ </div>
142
+ <div class="board-grid">
143
+ {#each Array.from(new Set(${items}.map((item) => item?.status ?? item?.state ?? item?.stage ?? item?.category ?? "items"))) as group}
144
+ <section class="board-column">
145
+ <h3>{group}</h3>
146
+ {#each ${items}.filter((item) => (item?.status ?? item?.state ?? item?.stage ?? item?.category ?? "items") === group) as item}
147
+ <div class="board-card">{item.title ?? item.name ?? item.label ?? item.message ?? item.id ?? JSON.stringify(item)}</div>
148
+ {/each}
149
+ </section>
150
+ {/each}
151
+ </div>
152
+ </div>`;
153
+ }
154
+
155
+ /**
156
+ * @param {ComponentUsage} usage
157
+ * @param {RenderOptions} options
158
+ * @returns {string}
159
+ */
160
+ function renderCalendar(usage, options) {
161
+ const items = options.itemsExpression || "data.result.items";
162
+ return `<div class="component-card component-calendar" data-topogram-component="${escapeHtml(componentId(usage))}">
163
+ <div class="component-header">
164
+ <div>
165
+ <p class="component-eyebrow">Component</p>
166
+ <h2>${escapeHtml(componentName(usage))}</h2>
167
+ </div>
168
+ </div>
169
+ <div class="calendar-list">
170
+ {#each ${items}.filter((item) => item?.date || item?.due_at || item?.dueAt || item?.created_at || item?.createdAt || item?.updated_at || item?.updatedAt) as item}
171
+ <div class="calendar-card">
172
+ <span>{item.date ?? item.due_at ?? item.dueAt ?? item.created_at ?? item.createdAt ?? item.updated_at ?? item.updatedAt}</span>
173
+ <strong>{item.title ?? item.name ?? item.label ?? item.message ?? item.id ?? JSON.stringify(item)}</strong>
174
+ </div>
175
+ {/each}
176
+ </div>
177
+ </div>`;
178
+ }
179
+
180
+ /**
181
+ * @param {ComponentUsage} usage
182
+ * @param {RenderOptions} options
183
+ * @returns {string}
184
+ */
185
+ function renderUsage(usage, options) {
186
+ const componentContracts = options.componentContracts || {};
187
+ if (hasPattern(usage, componentContracts, "summary_stats")) {
188
+ return renderSummaryStats(usage, options);
189
+ }
190
+ if (hasPattern(usage, componentContracts, "board_view")) {
191
+ return renderBoard(usage, options);
192
+ }
193
+ if (hasPattern(usage, componentContracts, "calendar_view")) {
194
+ return renderCalendar(usage, options);
195
+ }
196
+ if (hasPattern(usage, componentContracts, "resource_table") || hasPattern(usage, componentContracts, "data_grid_view")) {
197
+ return renderCollectionTable(usage, options);
198
+ }
199
+ return "";
200
+ }
201
+
202
+ /**
203
+ * @param {ScreenContract} screen
204
+ * @param {string} region
205
+ * @returns {ComponentUsage[]}
206
+ */
207
+ export function svelteKitComponentUsagesForRegion(screen, region) {
208
+ return (screen?.components || []).filter((usage) => usage?.region === region);
209
+ }
210
+
211
+ /**
212
+ * @param {ScreenContract} screen
213
+ * @param {string} region
214
+ * @returns {boolean}
215
+ */
216
+ export function hasSvelteKitComponentRegion(screen, region) {
217
+ return svelteKitComponentUsagesForRegion(screen, region).length > 0;
218
+ }
219
+
220
+ /**
221
+ * @param {ScreenContract} screen
222
+ * @param {string} region
223
+ * @param {RenderOptions} [options]
224
+ * @returns {string}
225
+ */
226
+ export function renderSvelteKitComponentRegion(screen, region, options = {}) {
227
+ const rendered = svelteKitComponentUsagesForRegion(screen, region)
228
+ .map((usage) => renderUsage(usage, options))
229
+ .filter(Boolean);
230
+ if (rendered.length === 0) {
231
+ return "";
232
+ }
233
+ return rendered.join("\n");
234
+ }