create-ailk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/component-catalog.json +220 -8
- package/component-catalog.md +21 -9
- package/dist/add-module.js +9 -5
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +63 -25
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +30 -14
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +124 -1
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +125 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +20 -7
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +139 -3
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +192 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +40 -12
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/database/CHANGELOG.md +47 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/seed.test.ts +72 -2
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +60 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
package/dist/sync-routes.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceVersionMap } from "./substitute.js";
|
|
2
|
+
/** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
|
|
3
|
+
export declare const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
|
|
2
4
|
export interface SyncRoutesOptions {
|
|
3
5
|
/**
|
|
4
6
|
* The already-scaffolded repo (absolute or relative). Must exist, be a
|
|
@@ -65,7 +67,32 @@ export interface SyncRoutesResult {
|
|
|
65
67
|
missingMessageNamespaces: string[];
|
|
66
68
|
/** Gateable surface ids the target does not carry, sorted. */
|
|
67
69
|
skippedSurfaces: string[];
|
|
70
|
+
/**
|
|
71
|
+
* When `apply` added a route the template exempts in its structured-data
|
|
72
|
+
* test and the site's copy of that test is site-edited (its bytes differ
|
|
73
|
+
* from the recorded hash), each template `NO_STRUCTURED_DATA_ROUTES` entry
|
|
74
|
+
* the site's test lacks, verbatim, with the added routes it covers (#8704).
|
|
75
|
+
* Sorted by entry; empty otherwise. The site's test is never written.
|
|
76
|
+
*/
|
|
77
|
+
structuredDataEntries: Array<{
|
|
78
|
+
routes: string[];
|
|
79
|
+
entry: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/** One `NO_STRUCTURED_DATA_ROUTES` element: its source text, dedented, and the routes it exempts. */
|
|
83
|
+
interface StructuredDataExemption {
|
|
84
|
+
route: string;
|
|
85
|
+
subtree: boolean;
|
|
86
|
+
text: string;
|
|
68
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
90
|
+
* brace scan that skips string literals and comments. Empty when the
|
|
91
|
+
* file declares no such array. It assumes what the template's array holds:
|
|
92
|
+
* object literals of string, boolean and identifier-array values — no regex
|
|
93
|
+
* literal or `${…}` interpolation.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseStructuredDataExemptions(source: string): StructuredDataExemption[];
|
|
69
96
|
/**
|
|
70
97
|
* The template's route directories, ROUTE_ROOT-relative and sorted.
|
|
71
98
|
*
|
|
@@ -99,3 +126,4 @@ export declare const SYNC_ROUTES_HELP = "create-ailk sync-routes \u2014 copy tem
|
|
|
99
126
|
* like `parseCliArgs`; at most one positional.
|
|
100
127
|
*/
|
|
101
128
|
export declare function parseSyncRoutesArgs(argv: string[]): SyncRoutesArgs;
|
|
129
|
+
export {};
|
package/dist/sync-routes.js
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* route root is copied once into a scratch dir and compared from there.
|
|
22
22
|
* - Never overwrite, never delete. A route dir the site has changed is
|
|
23
23
|
* reported as modified and left alone; `--apply` only creates directories
|
|
24
|
-
* that do not exist, so a second run is a no-op.
|
|
24
|
+
* that do not exist, so a second run is a no-op. When the site has edited
|
|
25
|
+
* its structured-data test, a route `--apply` adds that the template
|
|
26
|
+
* exempts there is reported with the exact `NO_STRUCTURED_DATA_ROUTES`
|
|
27
|
+
* entry to add — the test itself is never written (#8704).
|
|
25
28
|
*
|
|
26
29
|
* Pure module: no `import.meta.url`, so jest can import it directly. The
|
|
27
30
|
* package's own `templates/` dir is supplied by programmatic.ts.
|
|
@@ -35,9 +38,12 @@ import { appendEnvRows } from "./lib/declared-env.js";
|
|
|
35
38
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
36
39
|
import { substituteMarkers } from "./substitute.js";
|
|
37
40
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
41
|
+
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, sha256Hex } from "./template-record.js";
|
|
38
42
|
import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
|
|
39
43
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
40
44
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
45
|
+
/** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
|
|
46
|
+
export const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
|
|
41
47
|
/**
|
|
42
48
|
* `<surface id>/<entry name>` pairs that live directly under a GATEABLE
|
|
43
49
|
* surface's top-level directory but belong to a DIFFERENT module (#5366).
|
|
@@ -50,6 +56,115 @@ const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
|
50
56
|
const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
|
|
51
57
|
"projects/apply",
|
|
52
58
|
]);
|
|
59
|
+
/**
|
|
60
|
+
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
61
|
+
* brace scan that skips string literals and comments. Empty when the
|
|
62
|
+
* file declares no such array. It assumes what the template's array holds:
|
|
63
|
+
* object literals of string, boolean and identifier-array values — no regex
|
|
64
|
+
* literal or `${…}` interpolation.
|
|
65
|
+
*/
|
|
66
|
+
export function parseStructuredDataExemptions(source) {
|
|
67
|
+
const decl = source.indexOf("const NO_STRUCTURED_DATA_ROUTES");
|
|
68
|
+
if (decl === -1)
|
|
69
|
+
return [];
|
|
70
|
+
const open = /=\s*\[/.exec(source.slice(decl));
|
|
71
|
+
if (open === null)
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
for (let i = decl + open.index + open[0].length; i < source.length; i++) {
|
|
77
|
+
const ch = source[i];
|
|
78
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
79
|
+
for (i++; i < source.length && source[i] !== ch; i++) {
|
|
80
|
+
if (source[i] === "\\")
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
86
|
+
const eol = source.indexOf("\n", i);
|
|
87
|
+
i = eol === -1 ? source.length : eol;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
91
|
+
const end = source.indexOf("*/", i + 2);
|
|
92
|
+
i = end === -1 ? source.length : end + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === "{") {
|
|
96
|
+
if (depth === 0)
|
|
97
|
+
start = i;
|
|
98
|
+
depth++;
|
|
99
|
+
}
|
|
100
|
+
else if (ch === "}") {
|
|
101
|
+
depth--;
|
|
102
|
+
if (depth === 0 && start !== -1) {
|
|
103
|
+
const raw = source.slice(start, i + 1) + (source[i + 1] === "," ? "," : "");
|
|
104
|
+
const route = /\broute:\s*["']([^"']+)["']/.exec(raw)?.[1];
|
|
105
|
+
if (route !== undefined) {
|
|
106
|
+
const indent = /[ \t]*$/.exec(source.slice(source.lastIndexOf("\n", start) + 1, start))?.[0] ?? "";
|
|
107
|
+
const text = raw
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((line) => (line.startsWith(indent) ? line.slice(indent.length) : line))
|
|
110
|
+
.join("\n");
|
|
111
|
+
out.push({ route, subtree: /\bsubtree:\s*true\b/.test(raw), text });
|
|
112
|
+
}
|
|
113
|
+
start = -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "]" && depth === 0) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The first exemption covering `route` by the structured-data test's route
|
|
124
|
+
* rule (`route` itself, or under a `subtree` entry). An entry's `requires`
|
|
125
|
+
* strings are not checked: they key only the standard spoke checkout pages
|
|
126
|
+
* (#8748), which `surfaceForRouteDir` leaves unclassified, so no `--apply`
|
|
127
|
+
* ever adds them.
|
|
128
|
+
*/
|
|
129
|
+
function exemptionFor(exemptions, route) {
|
|
130
|
+
return exemptions.find((e) => route === e.route || (e.subtree && route.startsWith(`${e.route}/`)));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The template `NO_STRUCTURED_DATA_ROUTES` entries the site's structured-data
|
|
134
|
+
* test must gain for `added` routes (#8704). Empty unless that test is
|
|
135
|
+
* recorded and site-edited — an untouched copy is `create-ailk update`'s to
|
|
136
|
+
* bring forward, and an unrecorded one cannot be told apart from an edit.
|
|
137
|
+
*/
|
|
138
|
+
function structuredDataEntriesFor(targetAbs, templatesDir, added) {
|
|
139
|
+
const sitePath = join(targetAbs, STRUCTURED_DATA_TEST_PATH);
|
|
140
|
+
const templatePath = join(templatesDir, STRUCTURED_DATA_TEST_PATH);
|
|
141
|
+
const recordPath = join(targetAbs, TEMPLATE_RECORD_PATH);
|
|
142
|
+
if (added.length === 0 || !existsSync(sitePath) || !existsSync(templatePath) || !existsSync(recordPath)) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
let recordedHash;
|
|
146
|
+
try {
|
|
147
|
+
recordedHash = parseTemplateRecord(readFileSync(recordPath, "utf8")).files[STRUCTURED_DATA_TEST_PATH]?.sha256;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const siteBytes = readFileSync(sitePath);
|
|
153
|
+
if (recordedHash === undefined || sha256Hex(siteBytes) === recordedHash)
|
|
154
|
+
return [];
|
|
155
|
+
const templateExemptions = parseStructuredDataExemptions(readFileSync(templatePath, "utf8"));
|
|
156
|
+
const siteExemptions = parseStructuredDataExemptions(siteBytes.toString("utf8"));
|
|
157
|
+
const byEntry = new Map();
|
|
158
|
+
for (const route of [...added].sort()) {
|
|
159
|
+
const needed = exemptionFor(templateExemptions, route);
|
|
160
|
+
if (needed === undefined || exemptionFor(siteExemptions, route) !== undefined)
|
|
161
|
+
continue;
|
|
162
|
+
byEntry.set(needed.text, [...(byEntry.get(needed.text) ?? []), route]);
|
|
163
|
+
}
|
|
164
|
+
return [...byEntry.entries()]
|
|
165
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
166
|
+
.map(([entry, routes]) => ({ routes, entry }));
|
|
167
|
+
}
|
|
53
168
|
/**
|
|
54
169
|
* The template's route directories, ROUTE_ROOT-relative and sorted.
|
|
55
170
|
*
|
|
@@ -276,6 +391,7 @@ export function syncRoutes(opts) {
|
|
|
276
391
|
appliedDependencies,
|
|
277
392
|
missingMessageNamespaces,
|
|
278
393
|
skippedSurfaces,
|
|
394
|
+
structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
|
|
279
395
|
};
|
|
280
396
|
}
|
|
281
397
|
/**
|
|
@@ -355,6 +471,13 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
355
471
|
lines.push(`[create-ailk] Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):`);
|
|
356
472
|
lines.push(...result.missingMessageNamespaces.map((ns) => ` ${ns}`));
|
|
357
473
|
}
|
|
474
|
+
if (result.structuredDataEntries.length > 0) {
|
|
475
|
+
lines.push(`[create-ailk] Also required — ${STRUCTURED_DATA_TEST_PATH} is edited here, so add these NO_STRUCTURED_DATA_ROUTES entries to it by hand (not written):`);
|
|
476
|
+
for (const { routes, entry } of result.structuredDataEntries) {
|
|
477
|
+
lines.push(` for ${routes.join(", ")}:`);
|
|
478
|
+
lines.push(...entry.split("\n").map((line) => ` ${line}`));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
358
481
|
if (result.missing.length > 0 && !apply) {
|
|
359
482
|
lines.push("Run `create-ailk sync-routes <target> --apply` to copy the missing routes.");
|
|
360
483
|
}
|
|
@@ -3,9 +3,10 @@ export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
|
3
3
|
/**
|
|
4
4
|
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
5
5
|
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
6
|
-
* registry,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* registry, the four spoke seams, and a commerce spoke's checkout config,
|
|
7
|
+
* which the checkout writer stamps and the site owns from then on (#8748
|
|
8
|
+
* D2). Composed from each writer's own exported path constant, so a renamed
|
|
9
|
+
* constant carries its path along here too (#8522 D2).
|
|
9
10
|
*/
|
|
10
11
|
export declare const REGENERATED_APP_PATHS: readonly string[];
|
|
11
12
|
/**
|
package/dist/template-record.js
CHANGED
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
import { PAID_SECTIONS_PATH } from "./paid-sections.js";
|
|
22
22
|
import { SITE_CHROME_PATH } from "./site-chrome.js";
|
|
23
23
|
import { SITE_FONTS_PATH } from "./site-fonts.js";
|
|
24
|
+
import { SPOKE_CHECKOUT_CONFIG_PATH } from "./spoke-checkout.js";
|
|
24
25
|
import { NAV_CONFIG_PATH } from "./surfaces.js";
|
|
25
26
|
import { SPOKE_SEAM_FILES } from "./topology.js";
|
|
26
27
|
/** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
|
|
@@ -28,9 +29,10 @@ export const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
|
28
29
|
/**
|
|
29
30
|
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
30
31
|
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
31
|
-
* registry,
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* registry, the four spoke seams, and a commerce spoke's checkout config,
|
|
33
|
+
* which the checkout writer stamps and the site owns from then on (#8748
|
|
34
|
+
* D2). Composed from each writer's own exported path constant, so a renamed
|
|
35
|
+
* constant carries its path along here too (#8522 D2).
|
|
34
36
|
*/
|
|
35
37
|
export const REGENERATED_APP_PATHS = [
|
|
36
38
|
NAV_CONFIG_PATH,
|
|
@@ -38,6 +40,7 @@ export const REGENERATED_APP_PATHS = [
|
|
|
38
40
|
SITE_FONTS_PATH,
|
|
39
41
|
PAID_SECTIONS_PATH,
|
|
40
42
|
...SPOKE_SEAM_FILES,
|
|
43
|
+
SPOKE_CHECKOUT_CONFIG_PATH,
|
|
41
44
|
];
|
|
42
45
|
const REGENERATED_SET = new Set(REGENERATED_APP_PATHS);
|
|
43
46
|
const RECORDABLE_APP_ROOT = /^apps\/(web|api|mcp)\//;
|
package/dist/topology.js
CHANGED
|
@@ -174,6 +174,29 @@ export function isRemovableClosureMember(path, opts = {}) {
|
|
|
174
174
|
return (p.split("/").includes("__tests__") ||
|
|
175
175
|
prefixes.some((prefix) => underPrefix(p, prefix)));
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* The relative module specifiers a file reaches that are NOT static imports:
|
|
179
|
+
* a jest module reference (`jest.mock("../auth")`, `doMock`, `unmock`,
|
|
180
|
+
* `setMock`, `requireActual`, `requireMock`, `createMockFromModule`) or a
|
|
181
|
+
* `require(...)`. A suite that mocks a seed by path ships a dangling
|
|
182
|
+
* reference on a spoke exactly as one that imports it would (#8758), yet the
|
|
183
|
+
* shared import walk (`relativeImportsOf`) sees only `import`/`export ... from`
|
|
184
|
+
* and `import()` — it is shared with the module-slice lints, so the extra
|
|
185
|
+
* forms are read here rather than widening it for them.
|
|
186
|
+
*/
|
|
187
|
+
function referencedModulesOf(absPath) {
|
|
188
|
+
const specs = relativeImportsOf(absPath);
|
|
189
|
+
let src;
|
|
190
|
+
try {
|
|
191
|
+
src = readFileSync(absPath, "utf8");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return specs;
|
|
195
|
+
}
|
|
196
|
+
for (const m of src.matchAll(/\b(?:jest\s*\.\s*(?:mock|doMock|unmock|setMock|requireActual|requireMock|createMockFromModule)|require)\s*\(\s*["'`](\.[^"'`]*)["'`]/g))
|
|
197
|
+
specs.push(m[1]);
|
|
198
|
+
return specs;
|
|
199
|
+
}
|
|
177
200
|
/**
|
|
178
201
|
* Every path a spoke does not carry, computed over the built `templates/`
|
|
179
202
|
* tree: the removed roots, the seeds that apply, and every source file under
|
|
@@ -209,7 +232,7 @@ export function spokeExclusions(templatesDir, opts) {
|
|
|
209
232
|
for (const file of candidates) {
|
|
210
233
|
if (closed.has(file))
|
|
211
234
|
continue;
|
|
212
|
-
for (const spec of
|
|
235
|
+
for (const spec of referencedModulesOf(join(templatesDir, file))) {
|
|
213
236
|
const target = resolveRelativeImport(file, spec, candidates);
|
|
214
237
|
if (target !== undefined && closed.has(target)) {
|
|
215
238
|
closed.add(file);
|
|
@@ -899,6 +922,7 @@ export function renderSpokeHardFloors(opts) {
|
|
|
899
922
|
" reason: Stripe webhook route — payment state requires human review",
|
|
900
923
|
" - paths:",
|
|
901
924
|
' - "apps/web/app/api/checkout/**"',
|
|
925
|
+
' - "apps/web/lib/checkout.ts"',
|
|
902
926
|
' - "apps/web/lib/checkout-config.ts"',
|
|
903
927
|
' - ".github/workflows/checkout-e2e.yml"',
|
|
904
928
|
" reason: Spoke checkout — the buy route, the price map and the checkout e2e workflow require human review",
|
package/dist/update.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface UpdateResult {
|
|
|
31
31
|
path: string;
|
|
32
32
|
reason: string;
|
|
33
33
|
}>;
|
|
34
|
+
/** Per file left under review or skipped, the template tests importing it that stay at the site's version (#8704). */
|
|
35
|
+
heldBack: Array<{
|
|
36
|
+
file: string;
|
|
37
|
+
tests: string[];
|
|
38
|
+
}>;
|
|
34
39
|
backfilled?: {
|
|
35
40
|
version: string;
|
|
36
41
|
files: number;
|
|
@@ -43,6 +48,6 @@ export interface UpdateArgs {
|
|
|
43
48
|
target: string | undefined;
|
|
44
49
|
help: boolean;
|
|
45
50
|
}
|
|
46
|
-
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\" \u2014
|
|
51
|
+
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\". Every template test that imports a file\nleft under \"Needs review\" or \"Skipped\" (by a relative import) is held at the\nsite's version with it, its record entry is not advanced, and it is listed\nunder \"Held back with <file>\" \u2014 reconcile the file, then run update again to\nbring its tests forward. Every other file in the run still updates.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. Commit before running, and review\nthe diff after.\n";
|
|
47
52
|
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
|
48
53
|
export declare function parseUpdateArgs(argv: string[]): UpdateArgs;
|
package/dist/update.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* version's copy — read from this package's own `templates/` when that is
|
|
10
10
|
* the running version, else re-materialised from the published npm tarball
|
|
11
11
|
* (`lib/fetch-template-snapshot.ts`) — and the running version's copy. A
|
|
12
|
-
* conflict leaves the file untouched and is listed under "Needs review".
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* conflict leaves the file untouched and is listed under "Needs review". Every
|
|
13
|
+
* template test that imports a file left under review or skipped is held at
|
|
14
|
+
* the site's version with it, so a run never leaves a test newer than the
|
|
15
|
+
* code it exercises (#8704). A site with no record gets one backfilled from
|
|
16
|
+
* `.ailk-site-manifest.json`'s `patternVersion` on its first run, then
|
|
17
|
+
* updates normally. Every refusal throws before the first write.
|
|
16
18
|
*
|
|
17
19
|
* Pure module: no `import.meta.url` — the CLI supplies `templatesDir`, the
|
|
18
20
|
* `adopt.ts` / `add-route.ts` split.
|
|
@@ -26,6 +28,7 @@ import { fetchTemplateSnapshot } from "./lib/fetch-template-snapshot.js";
|
|
|
26
28
|
import { mergeThreeWay } from "./lib/merge-three-way.js";
|
|
27
29
|
import { ParseError } from "./parse-args.js";
|
|
28
30
|
import { SITE_ROSTER_PATH } from "./site-roster.js";
|
|
31
|
+
import { SPOKE_CHECKOUT_TEMPLATE_PATHS } from "./spoke-checkout.js";
|
|
29
32
|
import { excludedRoutePaths } from "./surfaces.js";
|
|
30
33
|
import { TEMPLATE_RECORD_PATH, isRecordable, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
31
34
|
import { TopologyError, isSpokeExcluded, readSpokeShape, spokeExclusions, } from "./topology.js";
|
|
@@ -132,6 +135,50 @@ function containsKnownPath(prefix, knownPaths) {
|
|
|
132
135
|
}
|
|
133
136
|
return false;
|
|
134
137
|
}
|
|
138
|
+
/** A template test file: a `*.test.*` / `*.spec.*` module, or anything under `__tests__/`. */
|
|
139
|
+
const TEST_FILE = /(^|\/)__tests__\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
140
|
+
/** Module specifiers `import`/`export … from`, `import()`, `require()` and `jest.mock()` name. */
|
|
141
|
+
const IMPORT_SPECIFIER = /\bfrom\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\b(?:import|require|jest\.mock|jest\.requireActual)\s*\(\s*["']([^"']+)["']/g;
|
|
142
|
+
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
143
|
+
/**
|
|
144
|
+
* The record-style paths a relative specifier in `fromPath` can resolve to:
|
|
145
|
+
* the path as written, with each source extension, a `.js`-family suffix
|
|
146
|
+
* swapped for its TypeScript source, and as a directory's `index.*`.
|
|
147
|
+
*/
|
|
148
|
+
function relativeImportCandidates(fromPath, specifier) {
|
|
149
|
+
const parts = fromPath.split("/").slice(0, -1);
|
|
150
|
+
for (const segment of specifier.split("/")) {
|
|
151
|
+
if (segment === "" || segment === ".")
|
|
152
|
+
continue;
|
|
153
|
+
if (segment === "..")
|
|
154
|
+
parts.pop();
|
|
155
|
+
else
|
|
156
|
+
parts.push(segment);
|
|
157
|
+
}
|
|
158
|
+
const target = parts.join("/");
|
|
159
|
+
const out = [target, ...RESOLVE_EXTENSIONS.map((ext) => `${target}${ext}`)];
|
|
160
|
+
const jsSuffix = /\.([cm]?)jsx?$/.exec(target);
|
|
161
|
+
if (jsSuffix !== null) {
|
|
162
|
+
const stem = target.slice(0, -jsSuffix[0].length);
|
|
163
|
+
out.push(`${stem}.${jsSuffix[1]}ts`, `${stem}.tsx`);
|
|
164
|
+
}
|
|
165
|
+
out.push(...RESOLVE_EXTENSIONS.map((ext) => `${target}/index${ext}`));
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
/** The files of `held` that `text`, the source of `fromPath`, imports by relative specifier. */
|
|
169
|
+
function importedHeldFiles(fromPath, text, held) {
|
|
170
|
+
const out = new Set();
|
|
171
|
+
for (const match of text.matchAll(IMPORT_SPECIFIER)) {
|
|
172
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
173
|
+
if (specifier === undefined || !specifier.startsWith("."))
|
|
174
|
+
continue;
|
|
175
|
+
for (const candidate of relativeImportCandidates(fromPath, specifier)) {
|
|
176
|
+
if (held.has(candidate))
|
|
177
|
+
out.add(candidate);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return [...out];
|
|
181
|
+
}
|
|
135
182
|
export async function updateSite(opts) {
|
|
136
183
|
const targetDir = resolve(opts.targetDir);
|
|
137
184
|
const version = opts.version;
|
|
@@ -202,9 +249,13 @@ export async function updateSite(opts) {
|
|
|
202
249
|
}
|
|
203
250
|
const excludedRoutes = excludedRoutePaths(header.surfaces);
|
|
204
251
|
const spokeExclusionList = spoke !== undefined ? spokeExclusions(opts.templatesDir, spoke) : undefined;
|
|
252
|
+
// #8748 D3 — the standard spoke checkout is a commerce spoke's alone.
|
|
253
|
+
const sellsAsSpoke = spoke !== undefined && header.modules.includes("commerce");
|
|
205
254
|
const outOfScope = (destRel) => {
|
|
206
255
|
if (isExcluded(destRel, excludedRoutes))
|
|
207
256
|
return true;
|
|
257
|
+
if (!sellsAsSpoke && isExcluded(destRel, SPOKE_CHECKOUT_TEMPLATE_PATHS))
|
|
258
|
+
return true;
|
|
208
259
|
if (spoke !== undefined && spokeExclusionList !== undefined) {
|
|
209
260
|
return isSpokeExcluded(destRel, { ...spoke, exclusions: spokeExclusionList });
|
|
210
261
|
}
|
|
@@ -391,37 +442,85 @@ export async function updateSite(opts) {
|
|
|
391
442
|
nextFiles[path] = { sha256: sha256Hex(newBytes), version };
|
|
392
443
|
added.push(path);
|
|
393
444
|
}
|
|
445
|
+
// ── Hold back the template tests of every file left alone (#8704) ────
|
|
446
|
+
// A test this run would move forward (update, merge, add, remove, or
|
|
447
|
+
// advance as current) that imports a file under review or skipped stays
|
|
448
|
+
// exactly as the site has it, and its row keeps its recorded version — so
|
|
449
|
+
// the tests on disk always match the code they exercise. The imports are
|
|
450
|
+
// read from both the site's copy and the running template's: either one
|
|
451
|
+
// exercising the held file is enough. A held test-support file
|
|
452
|
+
// (`__tests__/fixtures.ts`) holds the tests importing it in turn, until no
|
|
453
|
+
// further test is held.
|
|
454
|
+
const heldFiles = new Set([...review, ...skipped].map((row) => row.path));
|
|
455
|
+
const heldBackByFile = new Map();
|
|
456
|
+
const heldTests = new Set();
|
|
457
|
+
const moving = [...updated, ...merged, ...added, ...removed, ...current]
|
|
458
|
+
.filter((path) => TEST_FILE.test(path))
|
|
459
|
+
.sort();
|
|
460
|
+
for (let grew = heldFiles.size > 0; grew;) {
|
|
461
|
+
grew = false;
|
|
462
|
+
for (const path of moving) {
|
|
463
|
+
if (heldTests.has(path))
|
|
464
|
+
continue;
|
|
465
|
+
const sources = [];
|
|
466
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
467
|
+
if (existsSync(abs))
|
|
468
|
+
sources.push(readFileSync(abs, "utf8"));
|
|
469
|
+
const newBytes = newMap.get(path);
|
|
470
|
+
if (newBytes !== undefined)
|
|
471
|
+
sources.push(Buffer.from(newBytes).toString("utf8"));
|
|
472
|
+
const imported = new Set(sources.flatMap((text) => importedHeldFiles(path, text, heldFiles)));
|
|
473
|
+
if (imported.size === 0)
|
|
474
|
+
continue;
|
|
475
|
+
heldTests.add(path);
|
|
476
|
+
heldFiles.add(path);
|
|
477
|
+
grew = true;
|
|
478
|
+
for (const file of imported) {
|
|
479
|
+
heldBackByFile.set(file, [...(heldBackByFile.get(file) ?? []), path]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const path of heldTests) {
|
|
484
|
+
const recorded = record.files[path];
|
|
485
|
+
if (recorded !== undefined)
|
|
486
|
+
nextFiles[path] = recorded;
|
|
487
|
+
else
|
|
488
|
+
delete nextFiles[path];
|
|
489
|
+
}
|
|
490
|
+
const notHeld = (path) => !heldTests.has(path);
|
|
491
|
+
const heldBack = [...heldBackByFile.entries()]
|
|
492
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
493
|
+
.map(([file, tests]) => ({ file, tests }));
|
|
394
494
|
// ── Apply: writes and removals, then the record (D4 Order) ───────────
|
|
395
|
-
for (const write of writes) {
|
|
495
|
+
for (const write of writes.filter((w) => notHeld(w.path))) {
|
|
396
496
|
const abs = join(targetDir, ...write.path.split("/"));
|
|
397
497
|
mkdirSync(dirname(abs), { recursive: true });
|
|
398
498
|
writeFileSync(abs, write.bytes);
|
|
399
499
|
}
|
|
400
|
-
for (const path of deletes) {
|
|
500
|
+
for (const path of deletes.filter(notHeld)) {
|
|
401
501
|
rmSync(join(targetDir, ...path.split("/")));
|
|
402
502
|
}
|
|
403
503
|
const backfilled = backfillPatternVersion !== undefined
|
|
404
504
|
? { version: backfillPatternVersion, files: Object.keys(record.files).length }
|
|
405
505
|
: undefined;
|
|
406
|
-
const
|
|
407
|
-
updated.
|
|
408
|
-
merged.
|
|
409
|
-
added.
|
|
410
|
-
removed.
|
|
411
|
-
current.
|
|
506
|
+
const moved = {
|
|
507
|
+
updated: updated.filter(notHeld),
|
|
508
|
+
merged: merged.filter(notHeld),
|
|
509
|
+
added: added.filter(notHeld),
|
|
510
|
+
removed: removed.filter(notHeld),
|
|
511
|
+
current: current.filter(notHeld),
|
|
512
|
+
};
|
|
513
|
+
const recordChanged = backfilled !== undefined || Object.values(moved).some((paths) => paths.length > 0);
|
|
412
514
|
if (recordChanged) {
|
|
413
515
|
writeFileSync(recordPath, serializeTemplateRecord({ schema: 1, projectName: record.projectName, files: nextFiles }));
|
|
414
516
|
}
|
|
415
517
|
return {
|
|
416
518
|
targetDir,
|
|
417
519
|
version,
|
|
418
|
-
|
|
419
|
-
merged,
|
|
420
|
-
added,
|
|
421
|
-
removed,
|
|
422
|
-
current,
|
|
520
|
+
...moved,
|
|
423
521
|
review,
|
|
424
522
|
skipped,
|
|
523
|
+
heldBack,
|
|
425
524
|
...(backfilled !== undefined ? { backfilled } : {}),
|
|
426
525
|
};
|
|
427
526
|
}
|
|
@@ -448,6 +547,10 @@ export function formatUpdateReport(result) {
|
|
|
448
547
|
lines.push(`Needs review (${result.review.length}):`);
|
|
449
548
|
lines.push(...result.review.map((row) => ` ${row.path} — ${row.reason}`));
|
|
450
549
|
}
|
|
550
|
+
for (const { file, tests } of result.heldBack) {
|
|
551
|
+
lines.push(`Held back with ${file} (${tests.length}):`);
|
|
552
|
+
lines.push(...tests.map((path) => ` ${path}`));
|
|
553
|
+
}
|
|
451
554
|
if (result.skipped.length > 0) {
|
|
452
555
|
lines.push(`Skipped (${result.skipped.length}):`);
|
|
453
556
|
lines.push(...result.skipped.map((row) => ` ${row.path} — ${row.reason}`));
|
|
@@ -481,7 +584,11 @@ every template scaffold) to tell an untouched copy from an edit. An
|
|
|
481
584
|
untouched apps/web, apps/api or apps/mcp file whose template copy changed is
|
|
482
585
|
overwritten. An edited file whose template copy also changed is three-way
|
|
483
586
|
merged with \`git\` (git must be on PATH); a conflict leaves the file alone
|
|
484
|
-
and is listed under "Needs review"
|
|
587
|
+
and is listed under "Needs review". Every template test that imports a file
|
|
588
|
+
left under "Needs review" or "Skipped" (by a relative import) is held at the
|
|
589
|
+
site's version with it, its record entry is not advanced, and it is listed
|
|
590
|
+
under "Held back with <file>" — reconcile the file, then run update again to
|
|
591
|
+
bring its tests forward. Every other file in the run still updates.
|
|
485
592
|
A site with no record gets one on its first run, from the version its
|
|
486
593
|
.ailk-site-manifest.json already records. Commit before running, and review
|
|
487
594
|
the diff after.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.43.0",
|
|
32
|
+
"@working-theory/templates": "0.43.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "_base",
|
|
3
3
|
"requires": [],
|
|
4
|
-
"$comment": "The BASE LAYER (#5618 D1) — not a template. Every declared template reaches this tree by `extends: \"_base\"`; scaffold() installs it through installBundleTree before any module bundle and before the template's own tree, only when --template is named. It carries the seed content every template brings itself and no module owns: /pricing, /privacy
|
|
5
|
-
"postInstall": "Base layer installed: seed copy for /pricing, /privacy and /
|
|
4
|
+
"$comment": "The BASE LAYER (#5618 D1) — not a template. Every declared template reaches this tree by `extends: \"_base\"`; scaffold() installs it through installBundleTree before any module bundle and before the template's own tree, only when --template is named. It carries the seed content every template brings itself and no module owns: /pricing, /privacy, /terms (spec 5462 D16) and /working-agreement (#8661 D10). No installMap: every file here is unmapped and lands at its own path under the scaffold root's content/, so a template overrides a base page by shipping the same path. Never published as a registry bundle; the leading underscore is the skip signal for the bundle producer and the site-templates/ directory scans.",
|
|
5
|
+
"postInstall": "Base layer installed: seed copy for /pricing, /privacy, /terms and /working-agreement is in content/en/ - replace the placeholder copy before you launch."
|
|
6
6
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: "WorkingAgreementPage"
|
|
3
|
+
slug: "working-agreement"
|
|
4
|
+
title: "Working Agreement"
|
|
5
|
+
description: "Placeholder working agreement for Your Company. Replace every section with how you actually work with a client before you launch."
|
|
6
|
+
sections:
|
|
7
|
+
[
|
|
8
|
+
{
|
|
9
|
+
"id": "working-agreement-document",
|
|
10
|
+
"type": "document-chassis",
|
|
11
|
+
"data":
|
|
12
|
+
{
|
|
13
|
+
"headline": "Working Agreement",
|
|
14
|
+
"layout": "article",
|
|
15
|
+
"updated": "Last updated {Month Day, Year}",
|
|
16
|
+
"navGroupLabel": "Contents",
|
|
17
|
+
"sections":
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
"id": "placeholder-notice",
|
|
21
|
+
"heading": "This is placeholder text",
|
|
22
|
+
"blocks":
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
"kind": "paragraph",
|
|
26
|
+
"text": "This page is starter content that shipped with your site. It is not a contract, it binds nobody, and it is not legal advice. Replace every section below with how Your Company actually works with a client, and have someone qualified read it before you publish.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"kind": "paragraph",
|
|
30
|
+
"text": "Until you do, leave this notice in place so nobody mistakes the page for a finished agreement.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "what-to-write-here",
|
|
36
|
+
"heading": "What belongs on this page",
|
|
37
|
+
"blocks":
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
"kind": "paragraph",
|
|
41
|
+
"text": "A working agreement sets out how an engagement runs day to day. Write each part about the work you actually do, and keep the sentences short enough that a client can follow them.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"kind": "list",
|
|
45
|
+
"listStyle": "number",
|
|
46
|
+
"items":
|
|
47
|
+
[
|
|
48
|
+
"How you work with a client, from the first conversation to the last delivery.",
|
|
49
|
+
"What each side provides, and when.",
|
|
50
|
+
"Timelines, and how and how often you communicate.",
|
|
51
|
+
"How changes and cancellations are handled.",
|
|
52
|
+
"How the agreement ends, and what happens to the work when it does.",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "contact",
|
|
59
|
+
"heading": "Questions about this agreement",
|
|
60
|
+
"blocks":
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
"kind": "paragraph",
|
|
64
|
+
"text": "Replace this with the address you actually monitor — for example hello@example.com — so a client with a question about the agreement has somewhere to send it.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"kind": "paragraph",
|
|
68
|
+
"text": "Replace this with the date this agreement last changed, and say what happens when it changes again.",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
{/* Rendered from frontmatter `sections[]`. Placeholder seed content — this is not a working agreement and not legal advice. Replace every section before you launch. */}
|