create-ailk 0.8.0 → 0.10.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 +71 -3
- package/component-catalog.json +279 -45
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +104 -5
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +48 -32
- package/dist/lib/module-license-gate.js +117 -8
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +67 -23
- package/dist/parse-args.d.ts +12 -2
- package/dist/parse-args.js +74 -13
- package/dist/programmatic.d.ts +30 -6
- package/dist/programmatic.js +216 -19
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +141 -8
- package/dist/starter-grid.d.ts +16 -63
- package/dist/starter-grid.js +102 -84
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +200 -83
- package/dist/template-manifest.d.ts +30 -13
- package/dist/template-manifest.js +53 -28
- package/package.json +6 -3
- package/release-scope.json +25 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +1 -1
- package/site-templates/designs/standard/DESIGN.md +98 -6
- package/site-templates/marketing-site/DESIGN.md +98 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
- package/site-templates/marketing-site-free/DESIGN.md +98 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/saas-product/DESIGN.md +98 -6
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
- package/site-templates/shapes/person-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
- package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
- package/templates/.claude/agents/web.md +10 -8
- package/templates/.claude/rules/architecture.md +17 -7
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-auth/SKILL.md +142 -90
- package/templates/.claude/skills/add-auth/references/env-vars.md +32 -17
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +17 -8
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +94 -86
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +51 -18
- package/templates/.claude/skills/check-site/references/common-errors.md +12 -9
- package/templates/.claude/skills/check-site/references/findings-glossary.md +4 -4
- package/templates/.claude/skills/check-site/references/severity-levels.md +3 -3
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-deploy/SKILL.md +75 -38
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.env.example +23 -2
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +98 -6
- package/templates/README.md +50 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +35 -19
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +14 -13
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +23 -9
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +40 -1
- package/templates/content/en/HomePage/home.mdx +2 -6
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +4 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +0 -506
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -175
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** One row of theme's contrast audit, as the audit script prints it. */
|
|
2
|
+
export interface ContrastRow {
|
|
3
|
+
name: string;
|
|
4
|
+
bg: string;
|
|
5
|
+
fg: string;
|
|
6
|
+
ratio: number | null;
|
|
7
|
+
threshold: number;
|
|
8
|
+
status: "pass" | "fail" | "unmeasurable";
|
|
9
|
+
}
|
|
10
|
+
export type DesignContrastVerdict = {
|
|
11
|
+
ok: true;
|
|
12
|
+
pairs: number;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
pairs: number;
|
|
16
|
+
failing: ContrastRow[];
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether `design`'s derived token set passes WCAG AA. Every row whose
|
|
21
|
+
* status is not `pass` fails it; an audit of no pair never passes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assessDesignContrast(design: string, results: ContrastRow[]): DesignContrastVerdict;
|
|
24
|
+
/** `__tests__/fixtures/page-jsonld.ts`'s stdout: page path → its composed JSON-LD. */
|
|
25
|
+
export type ComposedPages = Record<string, {
|
|
26
|
+
jsonld: string;
|
|
27
|
+
sectionsValid: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
/** One changed node: the page and its JSON-pointer path (or a `(page …)` pseudo-path). */
|
|
30
|
+
export interface DriftNode {
|
|
31
|
+
page: string;
|
|
32
|
+
path: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every node that differs between two compositions, sorted by page then path.
|
|
36
|
+
* A removed or added page, and a page whose sections no longer validate after,
|
|
37
|
+
* are one pseudo-path node each; a `logo` subtree is never compared.
|
|
38
|
+
*/
|
|
39
|
+
export declare function jsonLdDrift(before: ComposedPages, after: ComposedPages): DriftNode[];
|
|
40
|
+
export type JsonLdDriftVerdict = {
|
|
41
|
+
ok: true;
|
|
42
|
+
pages: number;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
pages: number;
|
|
46
|
+
changed: DriftNode[];
|
|
47
|
+
reason: string;
|
|
48
|
+
};
|
|
49
|
+
/** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
|
|
50
|
+
export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
|
|
51
|
+
export interface MeasureApplyDesignDriftOptions {
|
|
52
|
+
/** create-ailk's built `dist/cli.js`. */
|
|
53
|
+
cliPath: string;
|
|
54
|
+
/** The monorepo's `tsx` binary. */
|
|
55
|
+
tsxPath: string;
|
|
56
|
+
/** `__tests__/fixtures/page-jsonld.ts` — the one production composer, run from source. */
|
|
57
|
+
composerPath: string;
|
|
58
|
+
/** Its tsconfig, passed as `TSX_TSCONFIG_PATH`. */
|
|
59
|
+
composerTsconfig: string;
|
|
60
|
+
/** The design `apply-design` applies (a name or a package path). */
|
|
61
|
+
design: string;
|
|
62
|
+
/** An existing empty directory; the drift site is scaffolded at `<workDir>/site`. */
|
|
63
|
+
workDir: string;
|
|
64
|
+
/** The loopback registry the CLI resolves modules and templates from. */
|
|
65
|
+
registryUrl?: string;
|
|
66
|
+
/** Written to the site's own `ailk.key`, which `apply-design` gates a non-Free design on. */
|
|
67
|
+
licenseKey?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scaffold a keyless `organization-product` site on `standard`, compose its
|
|
71
|
+
* JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
|
|
72
|
+
* exit at any step throws naming the step and the child's stderr.
|
|
73
|
+
*/
|
|
74
|
+
export declare function measureApplyDesignDrift(opts: MeasureApplyDesignDriftOptions): {
|
|
75
|
+
before: ComposedPages;
|
|
76
|
+
after: ComposedPages;
|
|
77
|
+
applyStdout: string;
|
|
78
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two design checks a design-wearing run of `scripts/verify-scaffold.mjs`
|
|
3
|
+
* runs before install (#6823, spec
|
|
4
|
+
* docs/specs/6823-the-matrix-runs-the-derived-spec.md D3).
|
|
5
|
+
*
|
|
6
|
+
* CONTRAST — `assessDesignContrast` turns the rows
|
|
7
|
+
* `packages/theme/scripts/audit-site-theme-contrast.ts` prints (theme's
|
|
8
|
+
* `auditTokenSetContrast` over the design's derived token set) into a verdict
|
|
9
|
+
* naming every non-passing `{bg, fg}` pair and the design. The WCAG math and
|
|
10
|
+
* the pair list stay theme's; this file only decides pass/fail.
|
|
11
|
+
*
|
|
12
|
+
* DRIFT — `jsonLdDrift` / `assessJsonLdDrift` compare every page's composed
|
|
13
|
+
* JSON-LD before and after `apply-design`, excluding the logo image (the PRD
|
|
14
|
+
* metric "structural drift after apply"), and `measureApplyDesignDrift`
|
|
15
|
+
* gathers the two snapshots (the `renderRoute` precedent, #6821 D4): it
|
|
16
|
+
* touches the filesystem and processes only through the paths it is handed,
|
|
17
|
+
* so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
|
|
18
|
+
* procedure. Nothing here imports `@working-theory/theme`.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
function describePair(row) {
|
|
24
|
+
const measured = row.status === "unmeasurable" || row.ratio === null
|
|
25
|
+
? "unmeasurable"
|
|
26
|
+
: `${row.ratio.toFixed(2)}:1 < ${row.threshold}:1`;
|
|
27
|
+
return `${row.name} {bg ${row.bg}, fg ${row.fg}} ${measured}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether `design`'s derived token set passes WCAG AA. Every row whose
|
|
31
|
+
* status is not `pass` fails it; an audit of no pair never passes.
|
|
32
|
+
*/
|
|
33
|
+
export function assessDesignContrast(design, results) {
|
|
34
|
+
const pairs = results.length;
|
|
35
|
+
if (pairs === 0) {
|
|
36
|
+
return { ok: false, pairs, failing: [], reason: `design ${design}: no contrast pair was audited` };
|
|
37
|
+
}
|
|
38
|
+
const failing = results.filter((row) => row.status !== "pass");
|
|
39
|
+
if (failing.length === 0)
|
|
40
|
+
return { ok: true, pairs };
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
pairs,
|
|
44
|
+
failing,
|
|
45
|
+
reason: `design ${design}: ${failing.length} of ${pairs} contrast pair(s) below WCAG AA — ${failing
|
|
46
|
+
.map(describePair)
|
|
47
|
+
.join("; ")}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** schema.org's logo property — the one subtree the PRD's "excluding the logo image" skips. */
|
|
51
|
+
const LOGO_KEY = "logo";
|
|
52
|
+
const kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
53
|
+
function walk(before, after, path, out) {
|
|
54
|
+
const kind = kindOf(before);
|
|
55
|
+
if (kind !== kindOf(after)) {
|
|
56
|
+
out.push(path);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (kind === "array") {
|
|
60
|
+
const a = before;
|
|
61
|
+
const b = after;
|
|
62
|
+
if (a.length !== b.length)
|
|
63
|
+
out.push(path);
|
|
64
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
65
|
+
walk(a[i], b[i], `${path}/${i}`, out);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (kind === "object") {
|
|
69
|
+
const a = before;
|
|
70
|
+
const b = after;
|
|
71
|
+
for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
|
|
72
|
+
if (key === LOGO_KEY)
|
|
73
|
+
continue;
|
|
74
|
+
walk(a[key], b[key], `${path}/${key}`, out);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (before !== after)
|
|
79
|
+
out.push(path);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every node that differs between two compositions, sorted by page then path.
|
|
83
|
+
* A removed or added page, and a page whose sections no longer validate after,
|
|
84
|
+
* are one pseudo-path node each; a `logo` subtree is never compared.
|
|
85
|
+
*/
|
|
86
|
+
export function jsonLdDrift(before, after) {
|
|
87
|
+
const nodes = [];
|
|
88
|
+
for (const page of [...new Set([...Object.keys(before), ...Object.keys(after)])]) {
|
|
89
|
+
const b = before[page];
|
|
90
|
+
const a = after[page];
|
|
91
|
+
if (a === undefined) {
|
|
92
|
+
nodes.push({ page, path: "(page removed)" });
|
|
93
|
+
}
|
|
94
|
+
else if (b === undefined) {
|
|
95
|
+
nodes.push({ page, path: "(page added)" });
|
|
96
|
+
}
|
|
97
|
+
else if (!a.sectionsValid) {
|
|
98
|
+
nodes.push({ page, path: "(sections invalid)" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const paths = [];
|
|
102
|
+
walk(JSON.parse(b.jsonld), JSON.parse(a.jsonld), "", paths);
|
|
103
|
+
for (const path of paths)
|
|
104
|
+
nodes.push({ page, path: path === "" ? "/" : path });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
108
|
+
return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
|
|
109
|
+
}
|
|
110
|
+
/** How many changed nodes a drift reason lists before it stops. */
|
|
111
|
+
const REASON_NODE_LIMIT = 10;
|
|
112
|
+
/** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
|
|
113
|
+
export function assessJsonLdDrift(design, before, after) {
|
|
114
|
+
const pages = Object.keys(before).length;
|
|
115
|
+
if (pages === 0) {
|
|
116
|
+
return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
|
|
117
|
+
}
|
|
118
|
+
const changed = jsonLdDrift(before, after);
|
|
119
|
+
if (changed.length === 0)
|
|
120
|
+
return { ok: true, pages };
|
|
121
|
+
const listed = changed
|
|
122
|
+
.slice(0, REASON_NODE_LIMIT)
|
|
123
|
+
.map((n) => `${n.page} ${n.path}`)
|
|
124
|
+
.join("; ");
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
pages,
|
|
128
|
+
changed,
|
|
129
|
+
reason: `design ${design}: ${changed.length} JSON-LD node(s) changed by apply-design — ${listed}${changed.length > REASON_NODE_LIMIT ? "; …" : ""}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Scaffold a keyless `organization-product` site on `standard`, compose its
|
|
134
|
+
* JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
|
|
135
|
+
* exit at any step throws naming the step and the child's stderr.
|
|
136
|
+
*/
|
|
137
|
+
export function measureApplyDesignDrift(opts) {
|
|
138
|
+
const baseEnv = { ...process.env };
|
|
139
|
+
delete baseEnv["AILK_LICENSE_KEY"];
|
|
140
|
+
const registryEnv = opts.registryUrl === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { AILK_MODULE_REGISTRY_URL: opts.registryUrl, AILK_TEMPLATE_REGISTRY_URL: opts.registryUrl };
|
|
143
|
+
const site = join(opts.workDir, "site");
|
|
144
|
+
const exec = (label, cmd, args, env) => {
|
|
145
|
+
try {
|
|
146
|
+
return execFileSync(cmd, args, {
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
env: { ...baseEnv, ...env },
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const { stderr, message } = err;
|
|
155
|
+
throw new Error(`drift: ${label} failed: ${(stderr || message || "").trim()}`, { cause: err });
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const compose = (label) => {
|
|
159
|
+
const stdout = exec(label, opts.tsxPath, [opts.composerPath, site], { TSX_TSCONFIG_PATH: opts.composerTsconfig });
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(stdout);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new Error(`drift: ${label} printed no JSON: ${err.message}`, { cause: err });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
exec("scaffold organization-product on standard", process.execPath, [opts.cliPath, site, "--shape", "organization-product", "--design", "standard"], registryEnv);
|
|
168
|
+
if (opts.licenseKey !== undefined)
|
|
169
|
+
writeFileSync(join(site, "ailk.key"), `${opts.licenseKey}\n`);
|
|
170
|
+
const before = compose("compose JSON-LD before apply-design");
|
|
171
|
+
const applyStdout = exec(`apply-design ${opts.design}`, process.execPath, [opts.cliPath, "apply-design", opts.design, "--target", site], opts.registryUrl === undefined ? {} : { AILK_MODULE_REGISTRY_URL: opts.registryUrl });
|
|
172
|
+
const after = compose("compose JSON-LD after apply-design");
|
|
173
|
+
return { before, after, applyStdout };
|
|
174
|
+
}
|
|
@@ -6,6 +6,15 @@ export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
|
|
|
6
6
|
* describes the rule, never the value.
|
|
7
7
|
*/
|
|
8
8
|
export declare function assertLicenseKeyShape(key: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Write `content` only when it differs, with mode 0600 either way. An existing
|
|
11
|
+
* file is tightened BEFORE the write — `mode` applies only on creation, so a
|
|
12
|
+
* looser pre-existing `.env` would otherwise hold the key for a moment at its
|
|
13
|
+
* old permissions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeOwnerOnly(path: string, content: string): void;
|
|
16
|
+
/** `text` plus `line`, starting on a fresh line. */
|
|
17
|
+
export declare function appendLine(text: string, line: string): string;
|
|
9
18
|
/**
|
|
10
19
|
* Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
|
|
11
20
|
* `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
|
|
@@ -48,7 +48,7 @@ export function assertLicenseKeyShape(key) {
|
|
|
48
48
|
* looser pre-existing `.env` would otherwise hold the key for a moment at its
|
|
49
49
|
* old permissions.
|
|
50
50
|
*/
|
|
51
|
-
function writeOwnerOnly(path, content) {
|
|
51
|
+
export function writeOwnerOnly(path, content) {
|
|
52
52
|
mkdirSync(dirname(path), { recursive: true });
|
|
53
53
|
if (existsSync(path))
|
|
54
54
|
chmodSync(path, 0o600);
|
|
@@ -58,7 +58,7 @@ function writeOwnerOnly(path, content) {
|
|
|
58
58
|
chmodSync(path, 0o600);
|
|
59
59
|
}
|
|
60
60
|
/** `text` plus `line`, starting on a fresh line. */
|
|
61
|
-
function appendLine(text, line) {
|
|
61
|
+
export function appendLine(text, line) {
|
|
62
62
|
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
|
|
63
63
|
return `${text}${sep}${line}\n`;
|
|
64
64
|
}
|
|
@@ -1,28 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paid-module license gate (K1, #5089 — Flavor 3). Orchestrates
|
|
3
|
-
* `module-tier.ts`'s classification + `fetch-module.ts`'s authenticated
|
|
4
|
-
* fetch into the three honest states the CLI must surface:
|
|
5
|
-
*
|
|
6
|
-
* (a) no key + a paid module requested → conversion hint, non-zero, ZERO
|
|
7
|
-
* network (mirrors `install-skill.ts`'s free-tier short-circuit).
|
|
8
|
-
* (b) a lapsed/invalid key + a paid module → "your licence does not cover
|
|
9
|
-
* this", non-zero (the module distribution's own 401 — see
|
|
10
|
-
* `fetch-module.ts`'s file doc for why there is no local JWT
|
|
11
|
-
* pre-check here).
|
|
12
|
-
* (c) a valid key → every requested paid module's bundle bytes, ready for
|
|
13
|
-
* `extract-module-bundle.ts` to merge into the scaffold.
|
|
14
|
-
*
|
|
15
|
-
* A Free-only request (no paid modules) never reaches the key check at all —
|
|
16
|
-
* `resolvePaidModules` returns immediately, before touching `process.env` or
|
|
17
|
-
* the network, so a bare/all-Free scaffold is provably unaffected by this
|
|
18
|
-
* gate (K1's regression floor).
|
|
19
|
-
*
|
|
20
|
-
* Also carries the design gate (#6760): `assertDesignAllowed` refuses a
|
|
21
|
-
* non-`standard` design at the `free` level, naming the design and the
|
|
22
|
-
* run's level, before `scaffold()`'s first disk write.
|
|
23
|
-
*/
|
|
24
1
|
import type { ModuleName } from "../parse-args.js";
|
|
25
|
-
import type { StarterDesign } from "../template-manifest.js";
|
|
26
2
|
import { type Level } from "./module-tier.js";
|
|
27
3
|
/**
|
|
28
4
|
* Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
|
|
@@ -39,18 +15,58 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
|
|
|
39
15
|
export declare class LicenseRequiredError extends Error {
|
|
40
16
|
constructor(message: string);
|
|
41
17
|
}
|
|
42
|
-
/**
|
|
43
|
-
export
|
|
18
|
+
/** The five axes `release-scope.json` declares (spec 6977 D3). */
|
|
19
|
+
export type ReleaseAxis = "templates" | "shapes" | "designs" | "modules" | "levels";
|
|
20
|
+
/** `gated` and `deferred` are both refused; only a `gated` one gets negative checks. */
|
|
21
|
+
export type ReleaseStatus = "released" | "gated" | "deferred";
|
|
22
|
+
export type ReleaseScope = Readonly<Record<ReleaseAxis, Readonly<Record<string, ReleaseStatus>>>>;
|
|
23
|
+
/**
|
|
24
|
+
* The package-root `release-scope.json` (shipped in `files`). A static JSON
|
|
25
|
+
* import, not an `import.meta.url` read, for `fetch-module.ts`'s reason: jest
|
|
26
|
+
* loads this module directly. `release-scope.test.ts` pins every registry
|
|
27
|
+
* name to an entry here (R5).
|
|
28
|
+
*/
|
|
29
|
+
export declare const RELEASE_SCOPE: ReleaseScope;
|
|
30
|
+
/** Thrown for a surface outside the release scope — a refusal, not a licence state. */
|
|
31
|
+
export declare class ReleaseScopeError extends Error {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refuse any of `names` that `scope[axis]` marks `gated` or `deferred`, with
|
|
36
|
+
* the not-available message `parse-args.ts` gives a reserved shape. A name
|
|
37
|
+
* the scope does not list (a customer's own design, say) is not refused.
|
|
38
|
+
* Bypassed when `AILK_RELEASE_SCOPE=ignore` — this package's jest setup and
|
|
39
|
+
* an operator's own run; a published CLI never sets it.
|
|
40
|
+
*
|
|
41
|
+
* Called on the scaffold path only (`scaffold()`, `cli.ts`, and the level
|
|
42
|
+
* check in `resolveModuleSet`) — never from `parseModules`, so `add-module`
|
|
43
|
+
* and `--sync-architecture` keep working on a site that already carries a
|
|
44
|
+
* gated module.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertReleased(axis: ReleaseAxis, names: readonly (string | undefined)[], scope?: ReleaseScope): void;
|
|
47
|
+
/**
|
|
48
|
+
* Design names every level renders (#6760 D1). Anything else in
|
|
49
|
+
* `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
|
|
50
|
+
* `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
|
|
51
|
+
* `assertDesignAllowed`'s `free` parameter carries that verdict in.
|
|
52
|
+
*/
|
|
53
|
+
export declare const FREE_DESIGNS: readonly string[];
|
|
44
54
|
/** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
|
|
45
55
|
export declare const DESIGN_LEVELS: readonly Level[];
|
|
46
56
|
/**
|
|
47
|
-
* Refuse a non
|
|
48
|
-
* and synchronous — takes the level
|
|
49
|
-
* resolved; never reads a key or
|
|
50
|
-
* both checks below are allow-lists,
|
|
51
|
-
* rather than granting it).
|
|
57
|
+
* Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
|
|
58
|
+
* path design #6813 D7). Pure and synchronous — takes the level
|
|
59
|
+
* `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
|
|
60
|
+
* derives a level itself (D2, fail-closed: both checks below are allow-lists,
|
|
61
|
+
* so an unrecognized `level` refuses rather than granting it).
|
|
62
|
+
*
|
|
63
|
+
* @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
|
|
64
|
+
* (a design NAME's own answer), so a caller passing a name never changes
|
|
65
|
+
* behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
|
|
66
|
+
* identity rule) passes it explicitly: a byte-identical copy of a shipped
|
|
67
|
+
* Free design is Free; any other path is not, whatever it is named.
|
|
52
68
|
*/
|
|
53
|
-
export declare function assertDesignAllowed(design:
|
|
69
|
+
export declare function assertDesignAllowed(design: string | undefined, level: Level, free?: boolean): void;
|
|
54
70
|
export interface ResolvePaidModulesOptions {
|
|
55
71
|
/** The requested module set's paid subset (see `module-tier.ts`). */
|
|
56
72
|
paidModules: ModuleName[];
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paid-module license gate (K1, #5089 — Flavor 3). Orchestrates
|
|
3
|
+
* `module-tier.ts`'s classification + `fetch-module.ts`'s authenticated
|
|
4
|
+
* fetch into the three honest states the CLI must surface:
|
|
5
|
+
*
|
|
6
|
+
* (a) no key + a paid module requested → conversion hint, non-zero, ZERO
|
|
7
|
+
* network (mirrors `install-skill.ts`'s free-tier short-circuit).
|
|
8
|
+
* (b) a lapsed/invalid key + a paid module → "your licence does not cover
|
|
9
|
+
* this", non-zero (the module distribution's own 401 — see
|
|
10
|
+
* `fetch-module.ts`'s file doc for why there is no local JWT
|
|
11
|
+
* pre-check here).
|
|
12
|
+
* (c) a valid key → every requested paid module's bundle bytes, ready for
|
|
13
|
+
* `extract-module-bundle.ts` to merge into the scaffold.
|
|
14
|
+
*
|
|
15
|
+
* A Free-only request (no paid modules) never reaches the key check at all —
|
|
16
|
+
* `resolvePaidModules` returns immediately, before touching `process.env` or
|
|
17
|
+
* the network, so a bare/all-Free scaffold is provably unaffected by this
|
|
18
|
+
* gate (K1's regression floor).
|
|
19
|
+
*
|
|
20
|
+
* Also carries the design gate (#6760): `assertDesignAllowed` refuses a
|
|
21
|
+
* non-`standard` design at the `free` level, naming the design and the
|
|
22
|
+
* run's level, before `scaffold()`'s first disk write.
|
|
23
|
+
*
|
|
24
|
+
* And the release-scope gate (#6984, spec 6977 D3): `assertReleased` refuses
|
|
25
|
+
* a template, shape, design, module or level that `release-scope.json` marks
|
|
26
|
+
* `gated` or `deferred`, on the scaffold path only.
|
|
27
|
+
*/
|
|
28
|
+
import releaseScopeJson from "../../release-scope.json" with { type: "json" };
|
|
1
29
|
import { FetchModuleError, fetchModule } from "./fetch-module.js";
|
|
2
30
|
import { assertLicenseKeyShape } from "./license-key-files.js";
|
|
3
31
|
import { ADD_ONS, LEVEL_LABEL, LEVEL_MODULES, OPT_IN_MODULES, PAID_MODULES, classifyModules, composeModuleSet, levelOf, } from "./module-tier.js";
|
|
@@ -19,19 +47,98 @@ export class LicenseRequiredError extends Error {
|
|
|
19
47
|
this.name = "LicenseRequiredError";
|
|
20
48
|
}
|
|
21
49
|
}
|
|
22
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The package-root `release-scope.json` (shipped in `files`). A static JSON
|
|
52
|
+
* import, not an `import.meta.url` read, for `fetch-module.ts`'s reason: jest
|
|
53
|
+
* loads this module directly. `release-scope.test.ts` pins every registry
|
|
54
|
+
* name to an entry here (R5).
|
|
55
|
+
*/
|
|
56
|
+
export const RELEASE_SCOPE = releaseScopeJson;
|
|
57
|
+
const AXIS_NOUN = {
|
|
58
|
+
templates: "template",
|
|
59
|
+
shapes: "shape",
|
|
60
|
+
designs: "design",
|
|
61
|
+
modules: "module",
|
|
62
|
+
levels: "level",
|
|
63
|
+
};
|
|
64
|
+
/** Thrown for a surface outside the release scope — a refusal, not a licence state. */
|
|
65
|
+
export class ReleaseScopeError extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "ReleaseScopeError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const RELEASE_STATUSES = ["released", "gated", "deferred"];
|
|
72
|
+
/**
|
|
73
|
+
* Refuse a scope carrying a status other than `released`, `gated` or
|
|
74
|
+
* `deferred` — fail closed: a misspelled status never reads as released.
|
|
75
|
+
*/
|
|
76
|
+
function assertValidScope(scope) {
|
|
77
|
+
for (const [axis, statuses] of Object.entries(scope)) {
|
|
78
|
+
for (const [name, status] of Object.entries(statuses)) {
|
|
79
|
+
if (!RELEASE_STATUSES.includes(status)) {
|
|
80
|
+
throw new ReleaseScopeError(`release-scope.json ${axis}."${name}" is "${String(status)}" — expected one of: ${RELEASE_STATUSES.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Refuse any of `names` that `scope[axis]` marks `gated` or `deferred`, with
|
|
87
|
+
* the not-available message `parse-args.ts` gives a reserved shape. A name
|
|
88
|
+
* the scope does not list (a customer's own design, say) is not refused.
|
|
89
|
+
* Bypassed when `AILK_RELEASE_SCOPE=ignore` — this package's jest setup and
|
|
90
|
+
* an operator's own run; a published CLI never sets it.
|
|
91
|
+
*
|
|
92
|
+
* Called on the scaffold path only (`scaffold()`, `cli.ts`, and the level
|
|
93
|
+
* check in `resolveModuleSet`) — never from `parseModules`, so `add-module`
|
|
94
|
+
* and `--sync-architecture` keep working on a site that already carries a
|
|
95
|
+
* gated module.
|
|
96
|
+
*/
|
|
97
|
+
export function assertReleased(axis, names, scope = RELEASE_SCOPE) {
|
|
98
|
+
if (process.env["AILK_RELEASE_SCOPE"] === "ignore")
|
|
99
|
+
return;
|
|
100
|
+
assertValidScope(scope);
|
|
101
|
+
const statuses = scope[axis];
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
if (name === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
const status = statuses[name];
|
|
106
|
+
if (status !== "gated" && status !== "deferred")
|
|
107
|
+
continue;
|
|
108
|
+
const released = Object.keys(statuses).filter((n) => statuses[n] === "released");
|
|
109
|
+
// A level comes from the key, never a flag — so it points at what the run CAN choose.
|
|
110
|
+
const choose = axis === "levels"
|
|
111
|
+
? `Your licence key's level is ${name}; name the modules you want with --modules (one of: ${Object.keys(scope.modules)
|
|
112
|
+
.filter((m) => scope.modules[m] === "released")
|
|
113
|
+
.join(", ")}).`
|
|
114
|
+
: `Choose one of: ${released.join(", ")}.`;
|
|
115
|
+
throw new ReleaseScopeError(`${AXIS_NOUN[axis]} "${name}" is not available yet. ${choose}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Design names every level renders (#6760 D1). Anything else in
|
|
120
|
+
* `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
|
|
121
|
+
* `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
|
|
122
|
+
* `assertDesignAllowed`'s `free` parameter carries that verdict in.
|
|
123
|
+
*/
|
|
23
124
|
export const FREE_DESIGNS = ["standard"];
|
|
24
125
|
/** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
|
|
25
126
|
export const DESIGN_LEVELS = ["pro", "pro-plus"];
|
|
26
127
|
/**
|
|
27
|
-
* Refuse a non
|
|
28
|
-
* and synchronous — takes the level
|
|
29
|
-
* resolved; never reads a key or
|
|
30
|
-
* both checks below are allow-lists,
|
|
31
|
-
* rather than granting it).
|
|
128
|
+
* Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
|
|
129
|
+
* path design #6813 D7). Pure and synchronous — takes the level
|
|
130
|
+
* `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
|
|
131
|
+
* derives a level itself (D2, fail-closed: both checks below are allow-lists,
|
|
132
|
+
* so an unrecognized `level` refuses rather than granting it).
|
|
133
|
+
*
|
|
134
|
+
* @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
|
|
135
|
+
* (a design NAME's own answer), so a caller passing a name never changes
|
|
136
|
+
* behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
|
|
137
|
+
* identity rule) passes it explicitly: a byte-identical copy of a shipped
|
|
138
|
+
* Free design is Free; any other path is not, whatever it is named.
|
|
32
139
|
*/
|
|
33
|
-
export function assertDesignAllowed(design, level) {
|
|
34
|
-
if (design === undefined ||
|
|
140
|
+
export function assertDesignAllowed(design, level, free = design !== undefined && FREE_DESIGNS.includes(design)) {
|
|
141
|
+
if (design === undefined || free)
|
|
35
142
|
return;
|
|
36
143
|
if (DESIGN_LEVELS.includes(level))
|
|
37
144
|
return;
|
|
@@ -212,6 +319,8 @@ export async function resolveModuleSet(options) {
|
|
|
212
319
|
return { modules, bundles };
|
|
213
320
|
}
|
|
214
321
|
const { level, bundles } = await deriveLevel(options);
|
|
322
|
+
// #6984 — a gated level (Pro Plus) is refused before its defaults compose.
|
|
323
|
+
assertReleased("levels", [level]);
|
|
215
324
|
const modules = composeModuleSet(namedList, level);
|
|
216
325
|
const { paid } = classifyModules(modules);
|
|
217
326
|
const missing = paid.filter((m) => !bundles.has(m));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** What the driver observed of one route's render. */
|
|
2
|
+
export interface RenderObservation {
|
|
3
|
+
/** The requested route, e.g. `/` or `/about`. */
|
|
4
|
+
route: string;
|
|
5
|
+
/** The message `page.goto` threw with, when it threw. */
|
|
6
|
+
gotoError?: string;
|
|
7
|
+
/** The status of the response `page.goto` resolved; undefined when it resolved none. */
|
|
8
|
+
status: number | undefined;
|
|
9
|
+
/** `page.url()` after navigation settled. */
|
|
10
|
+
finalUrl: string;
|
|
11
|
+
/** The screenshot's size on disk; undefined when none was written. */
|
|
12
|
+
pngBytes: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export type RenderVerdict = {
|
|
15
|
+
ok: true;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
reason: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether `route` rendered. Checked in order: a navigation error (a
|
|
22
|
+
* net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
|
|
23
|
+
* status other than 200, a final pathname other than the requested route (a
|
|
24
|
+
* redirect elsewhere is not a render of this route), and a missing or empty
|
|
25
|
+
* PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assessRender(observation: RenderObservation): RenderVerdict;
|
|
28
|
+
/** The slice of a Playwright `Page` that `renderRoute` uses. */
|
|
29
|
+
export interface RenderPage {
|
|
30
|
+
goto(url: string, options: {
|
|
31
|
+
waitUntil: "load";
|
|
32
|
+
timeout: number;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
status(): number;
|
|
35
|
+
} | null>;
|
|
36
|
+
url(): string;
|
|
37
|
+
screenshot(options: {
|
|
38
|
+
path: string;
|
|
39
|
+
fullPage: boolean;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** How long one navigation may take before it counts as a failed render. */
|
|
43
|
+
export declare const RENDER_TIMEOUT_MS = 60000;
|
|
44
|
+
/**
|
|
45
|
+
* Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
|
|
46
|
+
* `assessRender`'s verdict on what happened. `status` comes from the response
|
|
47
|
+
* `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
|
|
48
|
+
* requested URL, never a side `fetch` — so a redirect cannot pass as a render.
|
|
49
|
+
* A failed navigation takes no screenshot; a screenshot that throws counts
|
|
50
|
+
* as no PNG written — never a stale one left at `pngPath` by an earlier run.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderRoute(page: RenderPage, base: string, route: string, pngPath: string): Promise<RenderVerdict>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict on one browser render of a scaffold route (#6821, spec
|
|
3
|
+
* docs/specs/6821-the-shape-design-render-matrix-spec.md D4).
|
|
4
|
+
*
|
|
5
|
+
* A design leg of `scripts/verify-scaffold.mjs` renders its scaffold's home
|
|
6
|
+
* route and one inner route in Chromium and stores a screenshot of each — the
|
|
7
|
+
* shape × design render matrix. A render that did not happen must fail the
|
|
8
|
+
* leg, and proving that rule must not cost a scaffold build, so it lives here
|
|
9
|
+
* as a pure function (`assessRender`, the `api-boot-assertion.ts` precedent)
|
|
10
|
+
* plus the one function that gathers its facts (`renderRoute`), which touches
|
|
11
|
+
* Playwright only through the `page` it is handed. The driver loads both from
|
|
12
|
+
* `dist/`; `__tests__/verify-scaffold-render.test.ts` drives them with a fake
|
|
13
|
+
* page. Nothing here imports `@playwright/test`.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, rmSync, statSync } from "node:fs";
|
|
16
|
+
/**
|
|
17
|
+
* Decide whether `route` rendered. Checked in order: a navigation error (a
|
|
18
|
+
* net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
|
|
19
|
+
* status other than 200, a final pathname other than the requested route (a
|
|
20
|
+
* redirect elsewhere is not a render of this route), and a missing or empty
|
|
21
|
+
* PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
|
|
22
|
+
*/
|
|
23
|
+
export function assessRender(observation) {
|
|
24
|
+
const { route, gotoError, status, finalUrl, pngBytes } = observation;
|
|
25
|
+
if (gotoError !== undefined) {
|
|
26
|
+
return { ok: false, reason: `navigation failed: ${gotoError}` };
|
|
27
|
+
}
|
|
28
|
+
if (status !== 200) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: status === undefined ? "navigation resolved no response" : `final status ${status}, expected 200`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const finalPath = new URL(finalUrl).pathname;
|
|
35
|
+
if (finalPath !== route) {
|
|
36
|
+
return { ok: false, reason: `requested "${route}" but the page settled on "${finalPath}"` };
|
|
37
|
+
}
|
|
38
|
+
if (typeof pngBytes !== "number" || pngBytes <= 0) {
|
|
39
|
+
return { ok: false, reason: pngBytes === 0 ? "the screenshot PNG is empty" : "no screenshot PNG was written" };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
/** How long one navigation may take before it counts as a failed render. */
|
|
44
|
+
export const RENDER_TIMEOUT_MS = 60_000;
|
|
45
|
+
/**
|
|
46
|
+
* Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
|
|
47
|
+
* `assessRender`'s verdict on what happened. `status` comes from the response
|
|
48
|
+
* `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
|
|
49
|
+
* requested URL, never a side `fetch` — so a redirect cannot pass as a render.
|
|
50
|
+
* A failed navigation takes no screenshot; a screenshot that throws counts
|
|
51
|
+
* as no PNG written — never a stale one left at `pngPath` by an earlier run.
|
|
52
|
+
*/
|
|
53
|
+
export async function renderRoute(page, base, route, pngPath) {
|
|
54
|
+
let status;
|
|
55
|
+
try {
|
|
56
|
+
const response = await page.goto(base + route, { waitUntil: "load", timeout: RENDER_TIMEOUT_MS });
|
|
57
|
+
status = response === null ? undefined : response.status();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return assessRender({
|
|
61
|
+
route,
|
|
62
|
+
gotoError: err instanceof Error ? err.message : String(err),
|
|
63
|
+
status: undefined,
|
|
64
|
+
finalUrl: page.url(),
|
|
65
|
+
pngBytes: undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
rmSync(pngPath, { force: true });
|
|
69
|
+
try {
|
|
70
|
+
await page.screenshot({ path: pngPath, fullPage: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Falls through to the missing-PNG check below.
|
|
74
|
+
}
|
|
75
|
+
const pngBytes = existsSync(pngPath) ? statSync(pngPath).size : undefined;
|
|
76
|
+
return assessRender({ route, status, finalUrl: page.url(), pngBytes });
|
|
77
|
+
}
|