create-ailk 0.18.0 → 0.19.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 (167) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +4 -1
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/lib/license-key-files.d.ts +6 -3
  9. package/dist/lib/license-key-files.js +8 -2
  10. package/dist/lib/scaffold-render-sweep.d.ts +9 -0
  11. package/dist/lib/scaffold-render-sweep.js +24 -3
  12. package/dist/license-set.d.ts +1 -1
  13. package/dist/license-set.js +13 -8
  14. package/dist/module-architecture.js +119 -0
  15. package/dist/programmatic.js +3 -0
  16. package/dist/spoke-checkout.d.ts +11 -3
  17. package/dist/spoke-checkout.js +62 -12
  18. package/dist/starter-grid.d.ts +1 -1
  19. package/dist/surfaces.js +140 -1
  20. package/dist/template-manifest.d.ts +4 -3
  21. package/dist/topology.d.ts +7 -2
  22. package/dist/topology.js +60 -3
  23. package/package.json +5 -4
  24. package/templates/.env.example +3 -3
  25. package/templates/.github/workflows/ci.yml +5 -0
  26. package/templates/apps/api/CLAUDE.md +25 -7
  27. package/templates/apps/api/LICENSE +6 -5
  28. package/templates/apps/api/NOTICE +2 -0
  29. package/templates/apps/api/jest.config.cjs +4 -29
  30. package/templates/apps/api/package.json +1 -0
  31. package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
  32. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  33. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  34. package/templates/apps/api/src/lib/stripe.ts +81 -0
  35. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
  36. package/templates/apps/api/src/middleware/auth.ts +206 -46
  37. package/templates/apps/api/src/routes/leads/index.ts +4 -0
  38. package/templates/apps/api/src/server.ts +107 -0
  39. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
  40. package/templates/apps/api/src/services/lead-routing.ts +30 -2
  41. package/templates/apps/api/vercel.json +2 -2
  42. package/templates/apps/mcp/.env.example +10 -0
  43. package/templates/apps/mcp/CLAUDE.md +9 -4
  44. package/templates/apps/mcp/LICENSE +6 -5
  45. package/templates/apps/mcp/NOTICE +2 -0
  46. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  47. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  48. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  49. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  50. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  51. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  52. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  53. package/templates/apps/mcp/jest.config.cjs +3 -44
  54. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  55. package/templates/apps/mcp/src/boot.ts +5 -23
  56. package/templates/apps/mcp/src/http-transport.ts +93 -73
  57. package/templates/apps/mcp/src/index.ts +2 -0
  58. package/templates/apps/mcp/src/request-context.ts +52 -0
  59. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  60. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  61. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  62. package/templates/apps/web/LICENSE +6 -5
  63. package/templates/apps/web/NOTICE +2 -0
  64. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  65. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
  66. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  69. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
  70. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  72. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
  73. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  74. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  75. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
  76. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
  77. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  78. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  79. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
  80. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  81. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  82. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  83. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  84. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  85. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  86. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  87. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  88. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  89. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  92. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  93. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  94. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  96. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  97. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
  98. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  99. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  101. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  102. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
  103. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  106. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  108. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  111. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  114. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  115. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  116. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  117. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  118. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  119. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  120. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  121. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  122. package/templates/apps/web/auth.ts +5 -1
  123. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  124. package/templates/apps/web/lib/account-config.ts +188 -52
  125. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  126. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  127. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  128. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  129. package/templates/apps/web/lib/topology.generated.ts +24 -0
  130. package/templates/apps/web/messages/en.json +156 -35
  131. package/templates/database/CHANGELOG.md +21 -0
  132. package/templates/database/LICENSE +6 -5
  133. package/templates/database/NOTICE +2 -0
  134. package/templates/database/README.md +1 -1
  135. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  136. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  137. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  138. package/templates/database/__tests__/seed.test.ts +54 -2
  139. package/templates/database/inbox/schema.prisma +90 -15
  140. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  141. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  142. package/templates/database/package.json +3 -2
  143. package/templates/database/seed.ts +34 -2
  144. package/versions.json +36 -36
  145. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  146. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  147. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  148. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  149. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  150. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  151. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  152. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  153. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  154. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  155. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  156. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  157. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  158. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  159. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  160. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  161. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  162. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  163. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  164. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  165. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  166. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  167. package/templates/database/__tests__/build.test.ts +0 -131
@@ -1,346 +0,0 @@
1
- /**
2
- * Tests for the audit MCP tool handler.
3
- *
4
- * Acceptance criteria verified here:
5
- * - auditInputSchema validates slug input (with regex enforcement)
6
- * - auditInputSchema validates url input
7
- * - auditInputSchema rejects invalid slugs (uppercase, slashes, dots)
8
- * - ailkAudit returns score + perRule + perPage + meta for a slug input
9
- * - ailkAudit returns an error result for a cross-domain URL
10
- * - Output validates against auditOutputSchema
11
- * - No console.* calls in handler (structural test)
12
- * - No fetch() call in tool file (structural test)
13
- *
14
- * Phase 3 U0 (#294): adds a sanity-check that purchaseInputSchema relocated
15
- * to @working-theory/validation (per ADR 0013) is reachable and parses the same input
16
- * shape the inline schema parsed pre-relocation.
17
- */
18
-
19
- import {
20
- mkdirSync,
21
- mkdtempSync,
22
- readFileSync,
23
- rmSync,
24
- writeFileSync,
25
- } from "node:fs";
26
- import { tmpdir } from "node:os";
27
- import { resolve, join } from "node:path";
28
-
29
- import { READINESS_SCAN_TYPES } from "@working-theory/aeo";
30
-
31
- import {
32
- auditInputSchema,
33
- auditOutputSchema,
34
- purchaseInputSchema,
35
- } from "@working-theory/validation";
36
-
37
- import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
38
- import { readStripeTaxSignal } from "../src/lib/stripe-tax-signal.js";
39
- import { ailkAudit } from "../src/tools/audit.js";
40
-
41
- const REPO_ROOT = resolve(__dirname, "../../..");
42
- const CONTENT_ROOT = join(REPO_ROOT, "content");
43
- const TEST_BASE_URL = "https://example.ailk.test";
44
-
45
- function withBaseUrl(
46
- url: string,
47
- fn: () => Promise<unknown>,
48
- ): Promise<unknown> {
49
- const prev = process.env.AILK_BASE_URL;
50
- process.env.AILK_BASE_URL = url;
51
- return fn().finally(() => {
52
- if (prev === undefined) delete process.env.AILK_BASE_URL;
53
- else process.env.AILK_BASE_URL = prev;
54
- });
55
- }
56
-
57
- // ── Input schema tests ────────────────────────────────────────────────────────
58
-
59
- describe("auditInputSchema", () => {
60
- it("accepts a valid slug", () => {
61
- const result = auditInputSchema.safeParse({
62
- slug: "home",
63
- type: "HomePage",
64
- });
65
- expect(result.success).toBe(true);
66
- });
67
-
68
- it("accepts slug without explicit type", () => {
69
- const result = auditInputSchema.safeParse({ slug: "home" });
70
- expect(result.success).toBe(true);
71
- });
72
-
73
- it("accepts a url string", () => {
74
- const result = auditInputSchema.safeParse({
75
- url: "https://example.com/page",
76
- });
77
- expect(result.success).toBe(true);
78
- });
79
-
80
- it("rejects slug with uppercase letters", () => {
81
- const result = auditInputSchema.safeParse({ slug: "LaunchingAilk" });
82
- expect(result.success).toBe(false);
83
- });
84
-
85
- it("rejects slug with forward slash (path traversal guard)", () => {
86
- const result = auditInputSchema.safeParse({ slug: "some/path" });
87
- expect(result.success).toBe(false);
88
- });
89
-
90
- it("rejects slug with dots (path traversal guard)", () => {
91
- const result = auditInputSchema.safeParse({ slug: "../etc" });
92
- expect(result.success).toBe(false);
93
- });
94
-
95
- it("rejects empty slug", () => {
96
- const result = auditInputSchema.safeParse({ slug: "" });
97
- expect(result.success).toBe(false);
98
- });
99
-
100
- it("rejects non-url string in url field", () => {
101
- const result = auditInputSchema.safeParse({ url: "not-a-url" });
102
- expect(result.success).toBe(false);
103
- });
104
-
105
- it("rejects empty object (neither slug nor url)", () => {
106
- const result = auditInputSchema.safeParse({});
107
- expect(result.success).toBe(false);
108
- });
109
- });
110
-
111
- // ── Handler happy-path tests ──────────────────────────────────────────────────
112
-
113
- describe("ailkAudit handler — slug input", () => {
114
- it("returns score, perRule, perPage, meta for a known HomePage slug", async () => {
115
- const result = await ailkAudit(
116
- { slug: "home", type: "HomePage" },
117
- CONTENT_ROOT,
118
- );
119
- expect(typeof result.score).toBe("number");
120
- expect(result.score).toBeGreaterThanOrEqual(0);
121
- expect(result.score).toBeLessThanOrEqual(100);
122
- expect(Array.isArray(result.perRule)).toBe(true);
123
- expect(Array.isArray(result.perPage)).toBe(true);
124
- expect(typeof result.meta).toBe("object");
125
- expect(typeof result.meta.auditedAt).toBe("string");
126
- expect(typeof result.meta.ruleSet).toBe("string");
127
- expect(typeof result.meta.locale).toBe("string");
128
- });
129
-
130
- it("perRule entries have ruleId, name, weight, status fields", async () => {
131
- const result = await ailkAudit(
132
- { slug: "home", type: "HomePage" },
133
- CONTENT_ROOT,
134
- );
135
- for (const rule of result.perRule) {
136
- expect(typeof rule.ruleId).toBe("string");
137
- expect(typeof rule.name).toBe("string");
138
- expect(typeof rule.weight).toBe("number");
139
- expect(rule.status === "pass" || rule.status === "fail").toBe(true);
140
- }
141
- });
142
-
143
- it("perPage entries have slug and type fields", async () => {
144
- const result = await ailkAudit(
145
- { slug: "home", type: "HomePage" },
146
- CONTENT_ROOT,
147
- );
148
- expect(result.perPage).toHaveLength(1);
149
- expect(result.perPage[0].slug).toBe("home");
150
- expect(result.perPage[0].type).toBe("HomePage");
151
- });
152
-
153
- it("output validates against auditOutputSchema", async () => {
154
- const result = await ailkAudit(
155
- { slug: "home", type: "HomePage" },
156
- CONTENT_ROOT,
157
- );
158
- const parsed = auditOutputSchema.safeParse(result);
159
- expect(parsed.success).toBe(true);
160
- });
161
- });
162
-
163
- describe("ailkAudit handler — url input (same-origin)", () => {
164
- it("returns audit result for a same-origin URL", async () => {
165
- const result = (await withBaseUrl(TEST_BASE_URL, () =>
166
- ailkAudit({ url: `${TEST_BASE_URL}/HomePage/home` }, CONTENT_ROOT),
167
- )) as Awaited<ReturnType<typeof ailkAudit>>;
168
- expect(typeof result.score).toBe("number");
169
- expect(result.perPage[0].slug).toBe("home");
170
- });
171
- });
172
-
173
- describe("ailkAudit handler — cross-domain URL", () => {
174
- it("throws for a cross-domain URL (no fetch attempted)", async () => {
175
- await expect(
176
- withBaseUrl(TEST_BASE_URL, () =>
177
- ailkAudit(
178
- { url: "https://evil.example.com/HomePage/page" },
179
- CONTENT_ROOT,
180
- ),
181
- ),
182
- ).rejects.toThrow(/out of scope|use `ailk audit`/i);
183
- });
184
- });
185
-
186
- // ── Structural guards ─────────────────────────────────────────────────────────
187
-
188
- describe("structural guards", () => {
189
- it("apps/mcp/src/tools/audit.ts does not call console.*", () => {
190
- const src = readFileSync(
191
- resolve(__dirname, "../src/tools/audit.ts"),
192
- "utf-8",
193
- );
194
- expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
195
- });
196
-
197
- it("apps/mcp/src/tools/audit.ts does not call fetch()", () => {
198
- const src = readFileSync(
199
- resolve(__dirname, "../src/tools/audit.ts"),
200
- "utf-8",
201
- );
202
- // Strip comments before checking — JSDoc may mention fetch() as a negative assertion.
203
- const codeOnly = src
204
- .replace(/\/\*[\s\S]*?\*\//g, "")
205
- .replace(/\/\/[^\n]*/g, "");
206
- expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
207
- });
208
- });
209
-
210
- // ── Schema relocation parity (Phase 3 U0 / ADR 0013) ──────────────────────────
211
-
212
- describe("purchaseInputSchema relocated to @working-theory/validation", () => {
213
- it("parses the same shape the inline schema parsed pre-relocation", () => {
214
- const result = purchaseInputSchema.safeParse({
215
- productCode: "price_test_123",
216
- quantity: 2,
217
- });
218
- expect(result.success).toBe(true);
219
- });
220
-
221
- it("rejects an empty productCode", () => {
222
- const result = purchaseInputSchema.safeParse({ productCode: "" });
223
- expect(result.success).toBe(false);
224
- });
225
- });
226
-
227
- // ── #5949 — the agent-readiness rules on the MCP audit path ───────────────────
228
-
229
- const T1_ROOT = resolve(
230
- __dirname,
231
- "../../../packages/aeo/__tests__/fixtures/t1-site",
232
- );
233
- const T1_CONTENT_ROOT = join(T1_ROOT, "content");
234
-
235
- const READINESS_RULE_IDS = [
236
- "policies-present",
237
- "offer-on-priced-page",
238
- "tax-code-when-stripe-tax",
239
- ];
240
-
241
- describe("audit — readiness rules (#5949)", () => {
242
- it("AC-1: the T1 fixture site passes all three through the MCP tool", async () => {
243
- const result = await ailkAudit(
244
- { slug: "widget", type: "Product" },
245
- T1_CONTENT_ROOT,
246
- T1_ROOT,
247
- );
248
- for (const id of READINESS_RULE_IDS) {
249
- const row = result.perRule.find((r) => r.ruleId === id);
250
- expect(row).toBeDefined();
251
- expect(row!.status).toBe("pass");
252
- }
253
- });
254
-
255
- it("the tool's rule set is 9 rows and still validates against auditOutputSchema", async () => {
256
- const result = await ailkAudit(
257
- { slug: "widget", type: "Product" },
258
- T1_CONTENT_ROOT,
259
- T1_ROOT,
260
- );
261
- expect(result.perRule).toHaveLength(9);
262
- expect(auditOutputSchema.safeParse(result).success).toBe(true);
263
- });
264
-
265
- it("D5: the site scan is the five READINESS_SCAN_TYPES and no more", async () => {
266
- const input = await buildSinglePageAuditInput(
267
- { slug: "widget", type: "Product" },
268
- T1_CONTENT_ROOT,
269
- T1_ROOT,
270
- );
271
- const scanned = new Set(input.readiness!.sitePages.map((p) => p.type));
272
- expect([...scanned].sort()).toEqual([...READINESS_SCAN_TYPES].sort());
273
- // The fixture publishes a HomePage; it is deliberately outside the universe.
274
- expect(scanned.has("HomePage")).toBe(false);
275
- expect(input.readiness!.sitePages.find((p) => p.slug === "widget")).toEqual(
276
- {
277
- slug: "widget",
278
- type: "Product",
279
- price: 49,
280
- taxCode: "txcd_99999999",
281
- },
282
- );
283
- });
284
-
285
- it("D5 fail-open-to-pass: a site the scan finds nothing in yields sitePages: [] and three passes", async () => {
286
- const dir = mkdtempSync(join(tmpdir(), "ailk-mcp-readiness-"));
287
- try {
288
- mkdirSync(join(dir, "content", "en", "HomePage"), { recursive: true });
289
- writeFileSync(
290
- join(dir, "content", "en", "HomePage", "index.mdx"),
291
- [
292
- "---",
293
- 'type: "HomePage"',
294
- 'slug: "index"',
295
- 'title: "Nothing for sale"',
296
- 'description: "A site the readiness scan finds no priced page in."',
297
- "---",
298
- "",
299
- "Body.",
300
- ].join("\n"),
301
- "utf8",
302
- );
303
-
304
- const input = await buildSinglePageAuditInput(
305
- { slug: "index", type: "HomePage" },
306
- join(dir, "content"),
307
- dir,
308
- );
309
- expect(input.readiness!.sitePages).toEqual([]);
310
-
311
- const result = await ailkAudit(
312
- { slug: "index", type: "HomePage" },
313
- join(dir, "content"),
314
- dir,
315
- );
316
- for (const id of READINESS_RULE_IDS) {
317
- expect(result.perRule.find((r) => r.ruleId === id)!.status).toBe(
318
- "pass",
319
- );
320
- }
321
- } finally {
322
- rmSync(dir, { recursive: true, force: true });
323
- }
324
- });
325
-
326
- it("the projectRoot seam defaults to cwd when omitted (existing call sites unchanged)", async () => {
327
- const result = await ailkAudit(
328
- { slug: "home", type: "HomePage" },
329
- CONTENT_ROOT,
330
- );
331
- expect(result.perRule).toHaveLength(9);
332
- expect(auditOutputSchema.safeParse(result).success).toBe(true);
333
- });
334
- });
335
-
336
- describe("readStripeTaxSignal — wiring smoke test (#6012)", () => {
337
- // Not a duplicate of packages/aeo's own loadCommerceConfig suite (which
338
- // covers the reader's parse/extract arms in isolation, against synthetic
339
- // tmpdirs): this proves the EXPORTED apps/mcp wrapper itself is wired
340
- // correctly — the directory probe delegating to loadCommerceConfig — against
341
- // a real fixture file, since #6012 deleted the rest of this describe block
342
- // as duplicate coverage but nothing else here exercises readStripeTaxSignal.
343
- it("reads the T1 fixture's own project.yaml — Stripe Tax on", () => {
344
- expect(readStripeTaxSignal(T1_ROOT)).toBe(true);
345
- });
346
- });
@@ -1,325 +0,0 @@
1
- /**
2
- * MCP ↔ Server-Action parity test.
3
- *
4
- * Today: trivially passes (no Server Actions in apps/web, no MCP tools yet).
5
- * Phase 3: every Server Action in apps/web has a matching MCP tool.
6
- *
7
- * Convention (locked in spec §3):
8
- * - Server Actions live at `apps/web/**\/*.actions.ts` and export a symbol
9
- * named `^[a-z][a-zA-Z0-9]*Action$` (e.g. `captureLeadAction`).
10
- * - MCP tools live at `apps/mcp/src/tools/<tool>.ts` and export a symbol
11
- * named `^ailk[A-Z][a-zA-Z0-9]*$` (e.g. `ailkCaptureLead`).
12
- * - Mapping: `<name>Action` ↔ `ailk<Name>` (PascalCase of the action prefix).
13
- *
14
- * The harness exposes `computeParityFailures(actions, tools)` as a pure helper
15
- * so the populated/empty/divergent cases can be exercised without filesystem.
16
- * The integration case enumerates the real filesystem and runs the same helper.
17
- */
18
-
19
- import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
20
- import { resolve, join } from "node:path";
21
-
22
- import { getMcpToolRegistrations } from "@working-theory/metadata";
23
-
24
- const REPO_ROOT = resolve(__dirname, "../../..");
25
- const WEB_ROOT = join(REPO_ROOT, "apps/web");
26
- const MCP_TOOLS_ROOT = join(REPO_ROOT, "apps/mcp/src/tools");
27
-
28
- const ACTION_FILE_RE = /\.actions\.ts$/;
29
- const ACTION_EXPORT_RE =
30
- /export\s+(?:async\s+)?(?:function|const)\s+([a-z][a-zA-Z0-9]*Action)\b/g;
31
- const TOOL_EXPORT_RE =
32
- /export\s+(?:async\s+)?(?:function|const)\s+(ailk[A-Z][a-zA-Z0-9]*)\b/g;
33
-
34
- function walk(dir: string, out: string[] = []): string[] {
35
- if (!existsSync(dir)) return out;
36
- for (const entry of readdirSync(dir)) {
37
- if (entry === "node_modules" || entry === "dist" || entry === ".next")
38
- continue;
39
- const p = join(dir, entry);
40
- const s = statSync(p);
41
- if (s.isDirectory()) walk(p, out);
42
- else out.push(p);
43
- }
44
- return out;
45
- }
46
-
47
- export function discoverServerActions(webRoot: string = WEB_ROOT): Set<string> {
48
- const found = new Set<string>();
49
- for (const file of walk(webRoot)) {
50
- if (!ACTION_FILE_RE.test(file)) continue;
51
- const content = readFileSync(file, "utf-8");
52
- for (const m of content.matchAll(ACTION_EXPORT_RE)) {
53
- found.add(m[1]);
54
- }
55
- }
56
- return found;
57
- }
58
-
59
- export function discoverMcpTools(
60
- toolsRoot: string = MCP_TOOLS_ROOT,
61
- ): Set<string> {
62
- const found = new Set<string>();
63
- if (!existsSync(toolsRoot)) return found;
64
- for (const file of walk(toolsRoot)) {
65
- if (!file.endsWith(".ts")) continue;
66
- const content = readFileSync(file, "utf-8");
67
- for (const m of content.matchAll(TOOL_EXPORT_RE)) {
68
- found.add(m[1]);
69
- }
70
- }
71
- return found;
72
- }
73
-
74
- /**
75
- * Map an action symbol (e.g. `captureLeadAction`) to its expected MCP tool
76
- * symbol (`ailkCaptureLead`).
77
- */
78
- export function actionToToolName(action: string): string {
79
- // Strip trailing "Action" and capitalize first char.
80
- const base = action.replace(/Action$/, "");
81
- if (base.length === 0) return "ailk";
82
- return `ailk${base[0].toUpperCase()}${base.slice(1)}`;
83
- }
84
-
85
- /**
86
- * Map a catalog tool name (e.g. `get_agent_context`) to its handler symbol
87
- * (`ailkGetAgentContext`) — the ADR 0013 naming the harness discovers.
88
- */
89
- export function toolNameToHandler(toolName: string): string {
90
- const pascal = toolName
91
- .split("_")
92
- .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
93
- .join("");
94
- return `ailk${pascal}`;
95
- }
96
-
97
- /**
98
- * The signed-in `/mcp/workspace` handlers (#3591 D13–D16), derived from the
99
- * catalog's `surface: "workspace"` entries — never a hand-kept list or count.
100
- * They sit outside this Server-Action harness, not inside MCP_ONLY_TOOLS: each
101
- * has an apps/api twin route, proven by the API ↔ MCP parity guard
102
- * (`tests/parity/api-parity/kinds/site.ts`), and no web Server Action.
103
- */
104
- export function workspaceSurfaceHandlers(): Set<string> {
105
- return new Set(
106
- getMcpToolRegistrations({ surface: "workspace" }).map((reg) =>
107
- toolNameToHandler(reg.name),
108
- ),
109
- );
110
- }
111
-
112
- /**
113
- * MCP-only tools — tools that have no Web Server Action equivalent because
114
- * they are pure-read content tools. Adding a Server Action wrapper would be
115
- * meaningless: these tools exist to serve agent consumers, not web UI flows.
116
- *
117
- * Each entry is the camelCase MCP handler name (e.g. ailkListPages).
118
- * Extend this list in subsequent issues (I3–I5) as more read-only tools land.
119
- *
120
- * Note: this allow-list uses the symbol name convention (`ailk<PascalVerb>`),
121
- * not the MCP tool name convention (`list_pages`). The parity harness operates
122
- * on symbol names discovered via regex from tool source files.
123
- */
124
- const MCP_ONLY_TOOLS = new Set([
125
- "ailkListPages", // list_pages — pure read, no action equivalent (I2)
126
- "ailkReadContent", // read_content — pure read, no action equivalent (I2)
127
- "ailkSearchContent", // search_content — pure read, no action equivalent (I4)
128
- "ailkGetSiteInfo", // get_site_info — pure read, no action equivalent (I4)
129
- "ailkGetBusinessHours", // get_business_hours — pure read, no action equivalent (I4)
130
- "ailkGetLocations", // get_locations — pure read, no action equivalent (I4)
131
- "ailkGetOfferings", // get_offerings — pure read, no action equivalent (I4)
132
- "ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
133
- "ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
134
- "ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
135
- "ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
136
- "ailkCreateProject", // create_project — agent-only project creator; no Server Action equivalent (#299)
137
- "ailkGetFlow", // get_flow — agent-callable flow-engine read; no web Server Action (#3956 D7)
138
- "ailkListAvailability", // list_availability — anonymous scheduling read via MCP; the booker section is a client island calling apps/api directly, not a Server Action (#4416)
139
- "ailkGetEventMeta", // get_event_meta — meta-only scheduling read via MCP; a tile-rendering page is an RSC calling apps/api via api-client, not a Server Action (#4633)
140
- "ailkCreateBooking", // create_booking — anonymous scheduling write via MCP; same posture as subscribe_newsletter/request_callback (#4416)
141
- // ailkSubmitFlowStep REMOVED (#3957 D8) — submitFlowStepAction is its web
142
- // Server Action twin (apps/web/app/[locale]/flows/[slug]/flow.actions.ts).
143
- ]);
144
-
145
- /**
146
- * Server Actions that are intentionally WEB-ONLY — a RECORDED exception to the
147
- * API ↔ MCP parity contract, not a gap. The mirror of MCP_ONLY_TOOLS: an
148
- * action listed here is excluded from missing-tool reporting.
149
- *
150
- * Note: this allow-list uses the action's exported symbol name
151
- * (`<verb>Action`), exactly as discovered from `apps/web/**\/*.actions.ts`.
152
- */
153
- const WEB_ONLY_ACTIONS = new Set([
154
- // The recommender's email moment (#4841 → POST /v1/flows/:slug/save and
155
- // /checkout; consumed by the web Server Actions in #4842's
156
- // apps/web/app/[locale]/flows/[slug]/checkout.actions.ts). No MCP twin —
157
- // the recorded exception in apps/api/CLAUDE.md (the `/v1/flows/:slug/save`
158
- // + `/checkout` row, #4841): "an agent walking a flow has no email to give
159
- // and no card to pay with; the anonymous MCP surface is identity-blind."
160
- "startCheckoutAction", // checkout.actions.ts — Pay → Stripe TEST-mode hosted Checkout (#4842 D7)
161
- "saveRecommendationAction", // checkout.actions.ts — "Email me this plan" → flows.save (#4842 D7)
162
- ]);
163
-
164
- /**
165
- * Pure parity computation. Given the set of action symbols and the set of
166
- * MCP tool symbols, return the missing-tool and orphan-tool sets.
167
- *
168
- * Tools in MCP_ONLY_TOOLS are excluded from orphan reporting — they are
169
- * intentionally MCP-only and have no Server Action counterpart. Actions in
170
- * WEB_ONLY_ACTIONS are excluded from missing-tool reporting — they are
171
- * intentionally web-only and have no MCP tool counterpart.
172
- */
173
- export function computeParityFailures(
174
- actions: Set<string>,
175
- tools: Set<string>,
176
- ): { missingTools: string[]; orphanTools: string[] } {
177
- const expected = new Set<string>();
178
- for (const a of actions) {
179
- if (WEB_ONLY_ACTIONS.has(a)) continue;
180
- expected.add(actionToToolName(a));
181
- }
182
-
183
- const missingTools: string[] = [];
184
- for (const e of expected) if (!tools.has(e)) missingTools.push(e);
185
-
186
- const orphanTools: string[] = [];
187
- for (const t of tools) {
188
- if (!expected.has(t) && !MCP_ONLY_TOOLS.has(t)) orphanTools.push(t);
189
- }
190
-
191
- missingTools.sort();
192
- orphanTools.sort();
193
- return { missingTools, orphanTools };
194
- }
195
-
196
- // ── Tests ────────────────────────────────────────────────────────────────────
197
-
198
- describe("MCP ↔ Server-Action parity (helper)", () => {
199
- it("trivially passes when both sides are empty", () => {
200
- const r = computeParityFailures(new Set(), new Set());
201
- expect(r.missingTools).toEqual([]);
202
- expect(r.orphanTools).toEqual([]);
203
- });
204
-
205
- it("reports missing MCP tools when an action has no matching tool", () => {
206
- const actions = new Set(["captureLeadAction"]);
207
- const tools = new Set<string>();
208
- const r = computeParityFailures(actions, tools);
209
- expect(r.missingTools).toEqual(["ailkCaptureLead"]);
210
- expect(r.orphanTools).toEqual([]);
211
- });
212
-
213
- it("reports orphan MCP tools when a tool has no matching action", () => {
214
- const actions = new Set<string>();
215
- const tools = new Set(["ailkPurchase"]);
216
- const r = computeParityFailures(actions, tools);
217
- expect(r.missingTools).toEqual([]);
218
- expect(r.orphanTools).toEqual(["ailkPurchase"]);
219
- });
220
-
221
- it("passes when actions and tools are paired", () => {
222
- const actions = new Set(["captureLeadAction", "purchaseAction"]);
223
- const tools = new Set(["ailkCaptureLead", "ailkPurchase"]);
224
- const r = computeParityFailures(actions, tools);
225
- expect(r.missingTools).toEqual([]);
226
- expect(r.orphanTools).toEqual([]);
227
- });
228
-
229
- it("skips WEB_ONLY_ACTIONS when computing missing tools (the recorded #4841 exception)", () => {
230
- const actions = new Set([
231
- "captureLeadAction",
232
- "startCheckoutAction",
233
- "saveRecommendationAction",
234
- ]);
235
- const tools = new Set(["ailkCaptureLead"]);
236
- const r = computeParityFailures(actions, tools);
237
- expect(r.missingTools).toEqual([]);
238
- expect(r.orphanTools).toEqual([]);
239
- });
240
-
241
- it("maps action names to tool names by stripping Action and prefixing ailk", () => {
242
- expect(actionToToolName("captureLeadAction")).toBe("ailkCaptureLead");
243
- expect(actionToToolName("purchaseAction")).toBe("ailkPurchase");
244
- expect(actionToToolName("auditAction")).toBe("ailkAudit");
245
- });
246
-
247
- it("maps catalog tool names to handler symbols by PascalCasing and prefixing ailk", () => {
248
- expect(toolNameToHandler("get_agent_context")).toBe("ailkGetAgentContext");
249
- expect(toolNameToHandler("mark_inbox_message_done")).toBe(
250
- "ailkMarkInboxMessageDone",
251
- );
252
- });
253
- });
254
-
255
- describe("MCP ↔ Server-Action parity (filesystem)", () => {
256
- it("post-#4842 state: five Server Actions (purchaseAction + captureLeadAction + submitFlowStepAction + the two web-only startCheckoutAction + saveRecommendationAction), twenty-three MCP tools, zero orphans", () => {
257
- const actions = discoverServerActions();
258
- const discovered = discoverMcpTools();
259
- const workspaceHandlers = workspaceSurfaceHandlers();
260
- // The anonymous-surface handlers — what this harness has always counted.
261
- // The signed-in handlers are set aside and checked against the catalog
262
- // below, so a handler the catalog does not name lands here and fails.
263
- const tools = new Set(
264
- [...discovered].filter((t) => !workspaceHandlers.has(t)),
265
- );
266
- const r = computeParityFailures(actions, tools);
267
-
268
- // After D14a Stage 1 Unit C (#55) + #3956 (get_flow/submit_flow_step) +
269
- // #3957 (submitFlowStepAction — the web Server Action twin of
270
- // ailkSubmitFlowStep): twenty tools + three paired Server Actions.
271
- // - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
272
- // - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
273
- // - ailkSubscribeNewsletter: paired with subscribeNewsletterAction
274
- // (apps/web/app/[locale]/newsletter/newsletter.actions.ts, #6730 D9).
275
- // - ailkSubmitFlowStep: paired with submitFlowStepAction
276
- // (apps/web/app/[locale]/flows/[slug]/flow.actions.ts, #3957).
277
- // - startCheckoutAction, saveRecommendationAction: WEB-ONLY (#4842 D7,
278
- // apps/web/app/[locale]/flows/[slug]/checkout.actions.ts) — the HTTP
279
- // twins are POST /v1/flows/:slug/checkout and /save (#4841); no MCP
280
- // twin by the recorded apps/api/CLAUDE.md exception (an agent walking
281
- // a flow has no email to give and no card to pay with; the anonymous
282
- // MCP surface is identity-blind). In WEB_ONLY_ACTIONS.
283
- // - ailkListPages, ailkReadContent: MCP-only (pure read; I2)
284
- // - ailkSearchContent, ailkGetSiteInfo, ailkGetBusinessHours,
285
- // ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
286
- // MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
287
- // - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
288
- // - ailkRequestCallback: MCP-only anonymous-write tool (B7; in
289
- // MCP_ONLY_TOOLS allow-list).
290
- // - ailkCreateProject: MCP-only agent project creator (#299; in MCP_ONLY_TOOLS).
291
- // - ailkCreatePage, ailkUpdatePage, ailkDeletePage: RETIRED by #5853
292
- // (D7). Their HTTP twins — the untenanted /v1/content writes — were
293
- // deleted in the same PR, so keeping the tools would have left three
294
- // MCP tools with no HTTP equivalent, a direct breach of the parity rule
295
- // this file enforces. Their replacement is identity-BOUND
296
- // (/v1/workspaces/:workspaceId/content), which the identity-blind
297
- // anonymous MCP surface can never carry (D15); the authed MCP surface
298
- // is #3591.
299
- // - ailkGetFlow: MCP-only agent-callable flow-engine read; HTTP twin is
300
- // apps/api, not a Server Action (#3956 D7) — the page fetch in
301
- // apps/web/app/[locale]/flows/[slug]/page.tsx is a direct api-client
302
- // call in an RSC, not a Server Action.
303
- // - ailkListAvailability, ailkCreateBooking: MCP-only scheduling twins of
304
- // GET /v1/schedule/availability and POST /v1/schedule/book (#4416). The
305
- // `booker` section is a "use client" island that fetches those routes
306
- // directly (the ContactForm/StepFlow pattern) — no Server Action, so
307
- // the HTTP twin is apps/api.
308
- // - ailkGetEventMeta: MCP-only meta-only scheduling read, twin of
309
- // GET /v1/schedule/event-meta (#4633) — same posture as the other
310
- // scheduling tools.
311
- expect(actions.size).toBe(6);
312
- expect(tools.size).toBe(20);
313
- expect(r.missingTools).toEqual([]);
314
- expect(r.orphanTools).toEqual([]);
315
- });
316
-
317
- it("every catalog workspace-surface tool has its handler under src/tools/, and no other signed-in handler exists (#3591 D16)", () => {
318
- const discovered = discoverMcpTools();
319
- const workspaceHandlers = workspaceSurfaceHandlers();
320
- expect(workspaceHandlers.size).toBeGreaterThan(0);
321
- expect(
322
- [...discovered].filter((t) => workspaceHandlers.has(t)).sort(),
323
- ).toEqual([...workspaceHandlers].sort());
324
- });
325
- });