create-ailk 0.23.0 → 0.24.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 (96) hide show
  1. package/README.md +27 -14
  2. package/dist/add-module.js +6 -1
  3. package/dist/add-route.js +7 -1
  4. package/dist/derive-architecture.js +17 -0
  5. package/dist/lib/scaffold-render-sweep.d.ts +33 -0
  6. package/dist/lib/scaffold-render-sweep.js +45 -0
  7. package/dist/module-architecture.js +17 -0
  8. package/dist/sync-routes.d.ts +30 -4
  9. package/dist/sync-routes.js +271 -18
  10. package/package.json +3 -3
  11. package/templates/.env.example +4 -0
  12. package/templates/CLAUDE.md +4 -2
  13. package/templates/apps/api/.env.example +7 -0
  14. package/templates/apps/api/CLAUDE.md +25 -11
  15. package/templates/apps/api/src/lib/__mocks__/prisma.ts +4 -1
  16. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  17. package/templates/apps/api/src/lib/site.ts +4 -1
  18. package/templates/apps/api/src/lib/stripe.ts +3 -0
  19. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  20. package/templates/apps/api/src/openapi/spec.ts +333 -0
  21. package/templates/apps/api/src/routes/billing/plan-checkout.ts +18 -11
  22. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +126 -0
  23. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  24. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  25. package/templates/apps/api/src/routes/content/search.ts +16 -45
  26. package/templates/apps/api/src/routes/leads/index.ts +364 -298
  27. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  28. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  29. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  30. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  31. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +1 -0
  32. package/templates/apps/api/src/routes/webhooks/stripe.ts +2 -0
  33. package/templates/apps/api/src/server.ts +25 -0
  34. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  35. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +10 -0
  36. package/templates/apps/api/src/services/__tests__/person.test.ts +28 -0
  37. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +6 -0
  38. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  39. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  40. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  41. package/templates/apps/api/src/services/deliverable-fulfillment.ts +2 -7
  42. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +53 -0
  43. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +107 -0
  44. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +4 -0
  45. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +4 -0
  46. package/templates/apps/api/src/services/offerings/catalog.ts +10 -0
  47. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +23 -5
  48. package/templates/apps/api/src/services/offerings/connection.ts +83 -3
  49. package/templates/apps/api/src/services/offerings/index.ts +6 -6
  50. package/templates/apps/api/src/services/person.ts +23 -0
  51. package/templates/apps/api/src/services/project-listings.ts +43 -31
  52. package/templates/apps/mcp/CLAUDE.md +11 -5
  53. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  54. package/templates/apps/mcp/__tests__/http-transport.test.ts +91 -10
  55. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  56. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  57. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  58. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  59. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  60. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  61. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  62. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  63. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  64. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  65. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  66. package/templates/apps/mcp/package.json +1 -0
  67. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  68. package/templates/apps/mcp/src/http-transport.ts +47 -9
  69. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  70. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  71. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  72. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  73. package/templates/apps/mcp/src/tools/index.ts +22 -21
  74. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  75. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  76. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  77. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  78. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  79. package/templates/apps/web/CLAUDE.md +12 -3
  80. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  81. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  82. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  83. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +21 -4
  84. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  85. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  86. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  87. package/templates/database/CHANGELOG.md +20 -0
  88. package/templates/database/__tests__/seed.test.ts +33 -1
  89. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  90. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  91. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  92. package/templates/database/package.json +1 -1
  93. package/templates/database/seed.ts +27 -0
  94. package/templates/tools/lint-architecture.ts +205 -1
  95. package/versions.json +34 -34
  96. package/templates/apps/api/src/routes/content/slug.ts +0 -9
@@ -13,7 +13,17 @@
13
13
  * - the global and per-app `forbidden_patterns` (with `paths_excluded`);
14
14
  * - the `namespaces` boundaries inside `database/`, when declared:
15
15
  * cross-namespace relative imports and cross-namespace Prisma `@relation`
16
- * directives.
16
+ * directives;
17
+ * - the `upgradeable_consumer` rules, when declared — this site builds on
18
+ * the published `@working-theory/*` packages and never forks them:
19
+ * every `pnpm-workspace.yaml` glob is inside the declared `workspaces`;
20
+ * no `package.json` outside `node_modules` takes an in-scope name the
21
+ * scaffold did not create (`scaffold_packages`); no `patchedDependencies`
22
+ * entry (root `package.json` `pnpm` block or `pnpm-workspace.yaml`)
23
+ * patches an in-scope package; and every declaration of one in-scope
24
+ * package (dependencies, devDependencies, peerDependencies,
25
+ * optionalDependencies, `pnpm.overrides`, `resolutions`, workspace
26
+ * `overrides`) uses the same caret range.
17
27
  *
18
28
  * An import target is checked only when it is a workspace present in this
19
29
  * repo (`apps/*`, `database`); `@working-theory/*` libraries install from npm
@@ -45,10 +55,17 @@ interface AppRule {
45
55
  forbidden_patterns?: ForbiddenPattern[];
46
56
  }
47
57
 
58
+ interface UpgradeableConsumer {
59
+ package_scope?: string;
60
+ workspaces?: string[];
61
+ scaffold_packages?: string[];
62
+ }
63
+
48
64
  interface ArchConfig {
49
65
  apps?: Record<string, AppRule>;
50
66
  forbidden_patterns?: ForbiddenPattern[];
51
67
  namespaces?: { members?: Record<string, { path: string }> };
68
+ upgradeable_consumer?: UpgradeableConsumer;
52
69
  }
53
70
 
54
71
  interface Violation {
@@ -60,6 +77,9 @@ interface Violation {
60
77
 
61
78
  const ROOT = resolve(dirname(process.argv[1] ?? "."), "..");
62
79
  const SKIP_DIRS = new Set([
80
+ ".git",
81
+ ".pnpm-store",
82
+ ".vercel",
63
83
  "node_modules",
64
84
  "dist",
65
85
  ".next",
@@ -266,6 +286,189 @@ function checkNamespaces(cfg: ArchConfig): Violation[] {
266
286
  return out;
267
287
  }
268
288
 
289
+ // ── Upgradeable consumer (#9191) ─────────────────────────────────────────
290
+ //
291
+ // A copied, patched or pinned-off package stops taking upstream fixes, and
292
+ // whoever made the copy believes it is harmless. These rules refuse each form.
293
+
294
+ interface PackageJson {
295
+ name?: unknown;
296
+ pnpm?: { patchedDependencies?: unknown; overrides?: unknown };
297
+ resolutions?: unknown;
298
+ [field: string]: unknown;
299
+ }
300
+
301
+ interface Declaration {
302
+ file: string;
303
+ line: number;
304
+ name: string;
305
+ range: string;
306
+ field: string;
307
+ }
308
+
309
+ const DEP_FIELDS = [
310
+ "dependencies",
311
+ "devDependencies",
312
+ "peerDependencies",
313
+ "optionalDependencies",
314
+ ] as const;
315
+ const CARET_RANGE = /^\^\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?$/;
316
+
317
+ /** The 1-based line of the first line containing `needle`, else 1. */
318
+ function lineOfText(text: string, needle: string): number {
319
+ const i = text.split("\n").findIndex((line) => line.includes(needle));
320
+ return i === -1 ? 1 : i + 1;
321
+ }
322
+
323
+ /**
324
+ * The 1-based line of `key` as a map key at or after the first line holding
325
+ * `section` (a field label), so a package declared in two fields of one file
326
+ * reports each at its own line. Falls back to the first mention of `key`.
327
+ */
328
+ function lineOfKey(text: string, section: string, key: string): number {
329
+ const lines = text.split("\n");
330
+ const start = Math.max(0, lines.findIndex((line) => line.includes(section)));
331
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
332
+ const keyRe = new RegExp(`(?:^|[\\s{,])["']?${escaped}["']?\\s*:`);
333
+ const i = lines.findIndex((line, n) => n >= start && keyRe.test(line));
334
+ return i === -1 ? lineOfText(text, key) : i + 1;
335
+ }
336
+
337
+ /** A map-valued field as string entries, or none. */
338
+ function stringEntries(value: unknown): [string, string][] {
339
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
340
+ return Object.entries(value).filter(
341
+ (e): e is [string, string] => typeof e[1] === "string",
342
+ );
343
+ }
344
+
345
+ /** A patch or override key (`@s/p@1.2.3`, `a>@s/p@^1`) → the package it targets. */
346
+ function packageOfKey(key: string): string {
347
+ const target = key.slice(key.lastIndexOf(">") + 1);
348
+ const at = target.lastIndexOf("@");
349
+ return at > 0 ? target.slice(0, at) : target;
350
+ }
351
+
352
+ /** True when `glob` reaches nothing outside one of the `allowed` workspace globs. */
353
+ function insideWorkspaces(glob: string, allowed: string[]): boolean {
354
+ const g = glob.replace(/^\.\//, "").replace(/\/$/, "");
355
+ return allowed.some((a) => {
356
+ if (g === a) return true;
357
+ if (!a.endsWith("/*")) return false;
358
+ const prefix = a.slice(0, -1);
359
+ const rest = g.slice(prefix.length);
360
+ return g.startsWith(prefix) && rest !== "" && !rest.includes("/") && !rest.includes("**");
361
+ });
362
+ }
363
+
364
+ function checkUpgradeableConsumer(cfg: ArchConfig): Violation[] {
365
+ const uc = cfg.upgradeable_consumer;
366
+ if (!uc) return [];
367
+ const scope = uc.package_scope ?? "@working-theory/";
368
+ const workspaces = uc.workspaces ?? [];
369
+ const created = new Set(uc.scaffold_packages ?? []);
370
+ const inScope = (name: string) => name.startsWith(scope);
371
+ const out: Violation[] = [];
372
+ const decls: Declaration[] = [];
373
+
374
+ const patches = (file: string, text: string, value: unknown) => {
375
+ for (const [key] of stringEntries(value)) {
376
+ if (!inScope(packageOfKey(key))) continue;
377
+ out.push({
378
+ file,
379
+ line: lineOfKey(text, "patchedDependencies", key),
380
+ rule: `patchedDependencies patches "${key}"`,
381
+ reason:
382
+ "Fix: remove the patch — a patched package stops taking upstream fixes. Wrap or configure it from your app, or report the defect upstream.",
383
+ });
384
+ }
385
+ };
386
+ const declare = (file: string, text: string, field: string, value: unknown, byKey: boolean) => {
387
+ for (const [key, range] of stringEntries(value)) {
388
+ const name = byKey ? packageOfKey(key) : key;
389
+ if (!inScope(name)) continue;
390
+ // A scaffold's own workspace may be linked locally; anything else may not.
391
+ if (created.has(name) && range.startsWith("workspace:")) continue;
392
+ const line = lineOfKey(text, field.slice(field.lastIndexOf(".") + 1), key);
393
+ decls.push({ file, line, name, range, field });
394
+ }
395
+ };
396
+
397
+ const wsPath = join(ROOT, "pnpm-workspace.yaml");
398
+ if (existsSync(wsPath)) {
399
+ const text = readFileSync(wsPath, "utf8");
400
+ const ws = (parseYaml(text) ?? {}) as {
401
+ packages?: unknown;
402
+ patchedDependencies?: unknown;
403
+ overrides?: unknown;
404
+ };
405
+ for (const glob of Array.isArray(ws.packages) ? ws.packages : []) {
406
+ if (typeof glob !== "string" || glob.startsWith("!")) continue;
407
+ if (insideWorkspaces(glob, workspaces)) continue;
408
+ out.push({
409
+ file: wsPath,
410
+ line: lineOfText(text, glob),
411
+ rule: `workspace glob "${glob}" is outside this site's workspaces (${workspaces.join(", ")})`,
412
+ reason: `Fix: remove it. A site consumes ${scope}* from npm — compose, wrap or configure the published package from an app instead of keeping a local package tree.`,
413
+ });
414
+ }
415
+ patches(wsPath, text, ws.patchedDependencies);
416
+ declare(wsPath, text, "overrides", ws.overrides, true);
417
+ }
418
+
419
+ for (const file of walk(ROOT, /^package\.json$/)) {
420
+ const text = readFileSync(file, "utf8");
421
+ let pkg: PackageJson;
422
+ try {
423
+ pkg = JSON.parse(text) as PackageJson;
424
+ } catch {
425
+ continue; // pnpm itself refuses an unparseable manifest
426
+ }
427
+ if (typeof pkg.name === "string" && inScope(pkg.name) && !created.has(pkg.name)) {
428
+ out.push({
429
+ file,
430
+ line: lineOfText(text, `"${pkg.name}"`),
431
+ rule: `local package named "${pkg.name}" — not a package this scaffold created (${[...created].join(", ")})`,
432
+ reason: `Fix: delete the local copy and depend on the published ${pkg.name}; build on it by composing, wrapping or configuring it through its documented extension points — never copy it out of node_modules.`,
433
+ });
434
+ }
435
+ for (const field of DEP_FIELDS) declare(file, text, field, pkg[field], false);
436
+ declare(file, text, "pnpm.overrides", pkg.pnpm?.overrides, true);
437
+ declare(file, text, "resolutions", pkg.resolutions, true);
438
+ patches(file, text, pkg.pnpm?.patchedDependencies);
439
+ }
440
+
441
+ const byName = new Map<string, Declaration[]>();
442
+ for (const d of decls) {
443
+ if (!CARET_RANGE.test(d.range)) {
444
+ const example = /^\d/.test(d.range) ? `^${d.range}` : "^<version>";
445
+ out.push({
446
+ file: d.file,
447
+ line: d.line,
448
+ rule: `${d.name} is "${d.range}" in ${d.field}, not a caret range`,
449
+ reason: `Fix: declare it as a caret range (e.g. "${example}") so upgrades flow; never pin it, link it or point it at a local copy.`,
450
+ });
451
+ continue;
452
+ }
453
+ const list = byName.get(d.name) ?? [];
454
+ list.push(d);
455
+ byName.set(d.name, list);
456
+ }
457
+ for (const [name, list] of byName) {
458
+ if (new Set(list.map((d) => d.range)).size < 2) continue;
459
+ const where = list.map((d) => `${rel(d.file)} ${d.field} "${d.range}"`).join(", ");
460
+ for (const d of list) {
461
+ out.push({
462
+ file: d.file,
463
+ line: d.line,
464
+ rule: `${name} is declared on different ranges: ${where}`,
465
+ reason: `Fix: align every declaration of ${name} on one caret range.`,
466
+ });
467
+ }
468
+ }
469
+ return out;
470
+ }
471
+
269
472
  function main(): void {
270
473
  const yamlPath = join(ROOT, "architecture.yaml");
271
474
  if (!existsSync(yamlPath)) {
@@ -297,6 +500,7 @@ function main(): void {
297
500
  }
298
501
  }
299
502
  violations.push(...checkNamespaces(cfg));
503
+ violations.push(...checkUpgradeableConsumer(cfg));
300
504
 
301
505
  if (violations.length === 0) {
302
506
  console.log("architecture: OK");
package/versions.json CHANGED
@@ -1,53 +1,53 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.44.0",
3
- "@working-theory/aeo": "0.44.0",
4
- "@working-theory/agent-context": "0.44.0",
2
+ "@working-theory/admin-ui": "0.45.0",
3
+ "@working-theory/aeo": "0.45.0",
4
+ "@working-theory/agent-context": "0.45.0",
5
5
  "@working-theory/agent-contract": "0.1.0",
6
6
  "@working-theory/agent-memory": "0.0.0",
7
7
  "@working-theory/ai": "0.1.0",
8
- "@working-theory/analytics": "0.44.0",
9
- "@working-theory/api-client": "0.44.0",
8
+ "@working-theory/analytics": "0.45.0",
9
+ "@working-theory/api-client": "0.45.0",
10
10
  "@working-theory/cda-filesystem": "0.0.0",
11
- "@working-theory/cli": "0.44.0",
12
- "@working-theory/common": "0.44.0",
11
+ "@working-theory/cli": "0.45.0",
12
+ "@working-theory/common": "0.45.0",
13
13
  "@working-theory/consent": "0.1.0",
14
- "@working-theory/content-adapters": "0.44.0",
15
- "@working-theory/content-projection": "0.44.0",
16
- "@working-theory/email": "0.44.0",
14
+ "@working-theory/content-adapters": "0.45.0",
15
+ "@working-theory/content-projection": "0.45.0",
16
+ "@working-theory/email": "0.45.0",
17
17
  "@working-theory/env": "0.0.0",
18
- "@working-theory/eslint-config": "0.44.0",
19
- "@working-theory/fumadocs-schema": "0.44.0",
18
+ "@working-theory/eslint-config": "0.45.0",
19
+ "@working-theory/fumadocs-schema": "0.45.0",
20
20
  "@working-theory/ids": "0.15.0",
21
- "@working-theory/jsonld": "0.44.0",
22
- "@working-theory/license": "0.44.0",
23
- "@working-theory/metadata": "0.44.0",
24
- "@working-theory/observability": "0.44.0",
25
- "@working-theory/page-renderer": "0.44.0",
21
+ "@working-theory/jsonld": "0.45.0",
22
+ "@working-theory/license": "0.45.0",
23
+ "@working-theory/metadata": "0.45.0",
24
+ "@working-theory/observability": "0.45.0",
25
+ "@working-theory/page-renderer": "0.45.0",
26
26
  "@working-theory/plugin-mcp": "0.1.3",
27
- "@working-theory/schema": "0.44.0",
28
- "@working-theory/schema-pack-registry": "0.44.0",
29
- "@working-theory/schema-validator": "0.44.0",
30
- "@working-theory/sections-registry": "0.44.0",
31
- "@working-theory/site-config": "0.44.0",
32
- "@working-theory/spoke-checkout": "0.7.0",
33
- "@working-theory/stripe-client": "0.44.0",
34
- "@working-theory/templates": "0.44.0",
35
- "@working-theory/theme": "0.44.0",
36
- "@working-theory/ui": "0.44.0",
37
- "@working-theory/validation": "0.44.0",
38
- "@working-theory/web-i18n": "0.44.0",
27
+ "@working-theory/schema": "0.45.0",
28
+ "@working-theory/schema-pack-registry": "0.45.0",
29
+ "@working-theory/schema-validator": "0.45.0",
30
+ "@working-theory/sections-registry": "0.45.0",
31
+ "@working-theory/site-config": "0.45.0",
32
+ "@working-theory/spoke-checkout": "0.7.1",
33
+ "@working-theory/stripe-client": "0.45.0",
34
+ "@working-theory/templates": "0.45.0",
35
+ "@working-theory/theme": "0.45.0",
36
+ "@working-theory/ui": "0.45.0",
37
+ "@working-theory/validation": "0.45.0",
38
+ "@working-theory/web-i18n": "0.45.0",
39
39
  "@working-theory/billing-client": "0.0.0",
40
40
  "@working-theory/branding": "0.0.0",
41
41
  "@working-theory/license-client": "0.0.0",
42
42
  "@working-theory/license-manifest": "0.0.0",
43
43
  "@working-theory/partner-session": "0.0.0",
44
44
  "@working-theory/preview-protocol": "0.0.0",
45
- "@working-theory/ui-commerce": "0.23.22",
46
- "@working-theory/ui-docs-blog": "0.23.22",
47
- "@working-theory/ui-scheduling": "0.24.2",
48
- "@working-theory/ui-signal-collectors": "0.27.0",
45
+ "@working-theory/ui-commerce": "0.23.23",
46
+ "@working-theory/ui-docs-blog": "0.23.23",
47
+ "@working-theory/ui-scheduling": "0.24.3",
48
+ "@working-theory/ui-signal-collectors": "0.27.1",
49
49
  "@working-theory/web": "0.0.0",
50
50
  "@working-theory/api": "1.0.0",
51
51
  "@working-theory/mcp": "0.0.0",
52
- "@working-theory/database": "0.44.0"
52
+ "@working-theory/database": "0.45.0"
53
53
  }
@@ -1,9 +0,0 @@
1
- /**
2
- * Slug allowlist for D14a content-write routes.
3
- *
4
- * Re-exported from `@working-theory/content-adapters` (single source of truth, per
5
- * issue #398). The adapter sanitizes slug + type before any path
6
- * construction or git argument; this regex is the route-layer fail-fast
7
- * mirror so malformed input gets a 400 before reaching the adapter.
8
- */
9
- export { SLUG_PATTERN } from "@working-theory/content-adapters";