create-ailk 0.10.0 → 0.11.1
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 +7 -2
- package/dist/cli.js +5 -1
- package/dist/content-payloads.d.ts +37 -0
- package/dist/content-payloads.js +64 -0
- package/dist/lib/fetch-module.d.ts +3 -3
- package/dist/lib/fetch-module.js +11 -5
- package/dist/lib/fetch-template.d.ts +1 -1
- package/dist/lib/fetch-template.js +7 -4
- package/dist/lib/scaffold-render-sweep.d.ts +16 -0
- package/dist/lib/scaffold-render-sweep.js +52 -3
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +43 -12
- package/dist/parse-args.d.ts +2 -0
- package/dist/parse-args.js +7 -0
- package/dist/surfaces.js +2 -3
- package/package.json +3 -3
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +4 -3
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +8 -8
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +8 -8
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +8 -8
- package/templates/.claude/settings.json +12 -0
- package/templates/.env.example +1 -5
- package/templates/CLAUDE.md +38 -22
- package/templates/README.md +12 -12
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/mcp/CLAUDE.md +23 -10
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +160 -17
- package/templates/apps/mcp/__tests__/index.test.ts +118 -1
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +425 -5
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +67 -3
- package/templates/apps/mcp/__tests__/server.test.ts +162 -1
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/index.ts +35 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +193 -24
- package/templates/apps/mcp/src/server.ts +77 -5
- package/templates/apps/mcp/src/tools/purchase.ts +1 -1
- package/templates/apps/web/CLAUDE.md +1 -1
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +15 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +10 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/for/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +1 -15
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +4 -16
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +3 -15
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +1 -14
- package/templates/apps/web/src/lib/commerce-claim.ts +15 -6
- package/templates/content/_site.mdx +23 -4
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +34 -34
- package/templates/.claude/skills/README.md +0 -37
- package/templates/.claude/skills/add-auth/SKILL.md +0 -313
- package/templates/.claude/skills/add-auth/references/env-vars.md +0 -106
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +0 -180
- package/templates/.claude/skills/add-page/SKILL.md +0 -260
- package/templates/.claude/skills/add-page/references/page-types.md +0 -123
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +0 -87
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +0 -99
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +0 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +0 -43
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +0 -52
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +0 -38
- package/templates/.claude/skills/check-site/SKILL.md +0 -245
- package/templates/.claude/skills/check-site/references/common-errors.md +0 -116
- package/templates/.claude/skills/check-site/references/findings-glossary.md +0 -140
- package/templates/.claude/skills/check-site/references/severity-levels.md +0 -114
- package/templates/.claude/skills/fill-page/SKILL.md +0 -199
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +0 -80
- package/templates/.claude/skills/fill-page/references/section-sources.md +0 -41
- package/templates/.claude/skills/setup-database/SKILL.md +0 -156
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +0 -154
- package/templates/.claude/skills/setup-deploy/SKILL.md +0 -312
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +0 -204
- package/templates/.claude/skills/setup-deploy/references/vercel-json-template.md +0 -98
- package/templates/.claude/skills/update-page/SKILL.md +0 -117
- package/templates/.claude/skills/update-page/scripts/update-page.ts +0 -640
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +0 -31
- package/templates/.claude/skills/upgrade-plan/SKILL.md +0 -121
- package/templates/.claude/skills/upgrade-plan/references/options.md +0 -24
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +0 -527
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +0 -75
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +0 -44
package/README.md
CHANGED
|
@@ -275,8 +275,13 @@ gate runs before anything is copied (`scaffold()` in `src/programmatic.ts`), so
|
|
|
275
275
|
a refused run leaves no partial scaffold behind.
|
|
276
276
|
|
|
277
277
|
`AILK_MODULE_REGISTRY_URL` is optional: `fetchModule` uses an explicitly passed
|
|
278
|
-
`registryUrl`, then that variable, then
|
|
279
|
-
`DEFAULT_AILK_MODULE_REGISTRY_URL
|
|
278
|
+
`registryUrl`, then that variable, then `https://api.ailaunchkit.ai/v1/modules`
|
|
279
|
+
(`DEFAULT_AILK_MODULE_REGISTRY_URL`, `src/lib/fetch-module.ts`).
|
|
280
|
+
`AILK_TEMPLATE_REGISTRY_URL` is optional the same way: `fetchTemplate` uses an
|
|
281
|
+
explicitly passed `registryUrl`, then that variable, then
|
|
282
|
+
`https://api.ailaunchkit.ai/v1/templates` (`DEFAULT_AILK_TEMPLATE_REGISTRY_URL`,
|
|
283
|
+
`src/lib/fetch-template.ts`). Neither client follows a redirect: a 3xx from
|
|
284
|
+
either registry fails the fetch before any second request is sent.
|
|
280
285
|
|
|
281
286
|
```bash
|
|
282
287
|
# free-only — no key read, no network call
|
package/dist/cli.js
CHANGED
|
@@ -46,7 +46,7 @@ import { buildScaffoldMarkers } from "./copy.js";
|
|
|
46
46
|
import { assertReleased } from "./lib/module-license-gate.js";
|
|
47
47
|
import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
|
|
48
48
|
import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
|
|
49
|
-
import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
49
|
+
import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
50
50
|
import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
51
51
|
import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
|
|
52
52
|
import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
|
|
@@ -381,6 +381,10 @@ async function main() {
|
|
|
381
381
|
lines.push("Open http://localhost:3000 to see the home page.");
|
|
382
382
|
lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
|
|
383
383
|
lines.push("");
|
|
384
|
+
lines.push("Claude Code: the AI Launch Kit skills come from the ailk plugin. Trust this folder and Claude Code loads it, or run in a session:");
|
|
385
|
+
for (const line of PLUGIN_INSTALL_LINES)
|
|
386
|
+
lines.push(` ${line}`);
|
|
387
|
+
lines.push("");
|
|
384
388
|
lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
|
|
385
389
|
(surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
|
|
386
390
|
lines.push("Add another with `create-ailk add-route <id>`, or re-run with --surfaces <id>.");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The issuer's content kinds (spec 7110 § R1), pinned equal to CONTENT_KINDS by the issuer's test. */
|
|
2
|
+
export declare const CONTENT_PAYLOAD_KINDS: readonly ["skill", "profile", "template-set", "instructions"];
|
|
3
|
+
export type ContentPayloadKind = (typeof CONTENT_PAYLOAD_KINDS)[number];
|
|
4
|
+
export interface ContentPayload {
|
|
5
|
+
readonly kind: ContentPayloadKind;
|
|
6
|
+
/** ^[a-z0-9][a-z0-9-]*$ — the route's :name and the store's path segment. */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** The ONE paid feature the issuer's CONTENT_FEATURES row maps this name to. */
|
|
9
|
+
readonly feature: string;
|
|
10
|
+
/** SemVer — the content version, never create-ailk's (spec 7131 § D1). */
|
|
11
|
+
readonly version: string;
|
|
12
|
+
/**
|
|
13
|
+
* 64-hex `contentDigest` of `files` at `version` (§ D5). A change to any
|
|
14
|
+
* payload byte needs a new `version` AND a new `digest`; the build refuses
|
|
15
|
+
* until this agrees with the tree.
|
|
16
|
+
*/
|
|
17
|
+
readonly digest: string;
|
|
18
|
+
/** Repo-relative, POSIX; each file's source is `${sourceRoot}/${file}`. */
|
|
19
|
+
readonly sourceRoot: string;
|
|
20
|
+
/** Archive paths, POSIX, relative, sorted, unique — listed, never globbed. */
|
|
21
|
+
readonly files: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export declare const CONTENT_PAYLOADS: readonly ContentPayload[];
|
|
24
|
+
/** The producer-side cap on one payload's archive (spec 7131 § D6). */
|
|
25
|
+
export declare const MAX_CONTENT_PAYLOAD_BYTES: number;
|
|
26
|
+
/** The keyed route's public base; `AILK_CONTENT_REGISTRY_URL` overrides it. */
|
|
27
|
+
export declare const DEFAULT_AILK_CONTENT_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/content";
|
|
28
|
+
/**
|
|
29
|
+
* sha256 over the sorted `${path}\0${sha256hex(bytes)}\n` lines (§ D5).
|
|
30
|
+
* Covers archive paths and file bytes, never the archive's own bytes — gzip
|
|
31
|
+
* and tar headers differ between machines and must not decide whether the
|
|
32
|
+
* content changed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function contentDigest(files: ReadonlyArray<{
|
|
35
|
+
path: string;
|
|
36
|
+
bytes: Uint8Array;
|
|
37
|
+
}>): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content-payload manifest (spec 7131 § D2): every licence-gated content
|
|
3
|
+
* payload the issuer's keyed route `GET /v1/content/:kind/:name/:version`
|
|
4
|
+
* serves (spec 7110) — its kind, name, feature, content version, content
|
|
5
|
+
* digest and source files. The ONE place the builder
|
|
6
|
+
* (`scripts/build-content-payloads.mjs`), its `--verify` leg and the issuer's
|
|
7
|
+
* `CONTENT_FEATURES` agreement test read.
|
|
8
|
+
*
|
|
9
|
+
* A build-time registry, not programmatic API: deliberately not exported from
|
|
10
|
+
* `src/index.ts`. It carries names, a feature, a version and a digest — never
|
|
11
|
+
* a payload byte — so the public tarball's `dist/content-payloads.js` ships no
|
|
12
|
+
* paid content (spec 7131 § 7, AC-SPB-05).
|
|
13
|
+
*
|
|
14
|
+
* create-ailk is `oss`-tier and cannot import the issuer or the licence
|
|
15
|
+
* manifest, so the kind list is a pinned COPY of the issuer's `CONTENT_KINDS`
|
|
16
|
+
* and `feature` is a plain string; the issuer's `module-features.test.ts`
|
|
17
|
+
* fails when either drifts.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
/** The issuer's content kinds (spec 7110 § R1), pinned equal to CONTENT_KINDS by the issuer's test. */
|
|
21
|
+
export const CONTENT_PAYLOAD_KINDS = [
|
|
22
|
+
"skill",
|
|
23
|
+
"profile",
|
|
24
|
+
"template-set",
|
|
25
|
+
"instructions",
|
|
26
|
+
];
|
|
27
|
+
export const CONTENT_PAYLOADS = Object.freeze([
|
|
28
|
+
Object.freeze({
|
|
29
|
+
kind: "template-set",
|
|
30
|
+
name: "scaffold-commerce",
|
|
31
|
+
feature: "commerce",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
digest: "baa4ac6e86ec7e3b367eb67e77c3407da90c7ff9ef51d5f4e1185615194543ee",
|
|
34
|
+
sourceRoot: "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce",
|
|
35
|
+
files: Object.freeze([
|
|
36
|
+
"references/lookup-keys-template.md",
|
|
37
|
+
"references/marketplace-brand-pages.md",
|
|
38
|
+
"templates/checkout-route.template.ts",
|
|
39
|
+
"templates/invoice-route.template.ts",
|
|
40
|
+
"templates/marketplace-cta-route.tsx.tmpl",
|
|
41
|
+
"templates/payment-link-route.template.ts",
|
|
42
|
+
"templates/portal-route.template.ts",
|
|
43
|
+
"templates/stripe-config.template.ts",
|
|
44
|
+
"templates/subscription-route.template.ts",
|
|
45
|
+
]),
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
/** The producer-side cap on one payload's archive (spec 7131 § D6). */
|
|
49
|
+
export const MAX_CONTENT_PAYLOAD_BYTES = 256 * 1024;
|
|
50
|
+
/** The keyed route's public base; `AILK_CONTENT_REGISTRY_URL` overrides it. */
|
|
51
|
+
export const DEFAULT_AILK_CONTENT_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/content";
|
|
52
|
+
const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
53
|
+
/**
|
|
54
|
+
* sha256 over the sorted `${path}\0${sha256hex(bytes)}\n` lines (§ D5).
|
|
55
|
+
* Covers archive paths and file bytes, never the archive's own bytes — gzip
|
|
56
|
+
* and tar headers differ between machines and must not decide whether the
|
|
57
|
+
* content changed.
|
|
58
|
+
*/
|
|
59
|
+
export function contentDigest(files) {
|
|
60
|
+
const lines = files
|
|
61
|
+
.map(({ path, bytes }) => `${path}\0${sha256Hex(bytes)}\n`)
|
|
62
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
63
|
+
return sha256Hex(lines.join(""));
|
|
64
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/
|
|
1
|
+
export declare const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/modules";
|
|
2
2
|
export interface FetchModuleOptions {
|
|
3
3
|
moduleName: string;
|
|
4
4
|
/** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
|
|
@@ -11,10 +11,10 @@ export interface FetchModuleOptions {
|
|
|
11
11
|
fetchImpl?: typeof fetch;
|
|
12
12
|
}
|
|
13
13
|
export declare class FetchModuleError extends Error {
|
|
14
|
-
/** True iff the failure was a license-gating signal (HTTP 401). */
|
|
14
|
+
/** True iff the failure was a license-gating signal (HTTP 401 or 403). */
|
|
15
15
|
readonly isLicenseError: boolean;
|
|
16
16
|
constructor(message: string,
|
|
17
|
-
/** True iff the failure was a license-gating signal (HTTP 401). */
|
|
17
|
+
/** True iff the failure was a license-gating signal (HTTP 401 or 403). */
|
|
18
18
|
isLicenseError?: boolean);
|
|
19
19
|
}
|
|
20
20
|
export declare function fetchModule(options: FetchModuleOptions): Promise<Uint8Array>;
|
package/dist/lib/fetch-module.js
CHANGED
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
* distribution only.
|
|
10
10
|
* - Empty-token guard before any network call (defense in depth — the
|
|
11
11
|
* caller should already have gated on an empty/absent key).
|
|
12
|
-
* - Status-code mapping: 401 → license invalid/revoked/lapsed,
|
|
13
|
-
*
|
|
12
|
+
* - Status-code mapping: 401 → license invalid/revoked/lapsed, 403 →
|
|
13
|
+
* not included / term ended, 404 → included, but that release's bundle
|
|
14
|
+
* not yet uploaded, other 4xx/5xx → upstream body. The issuer answers
|
|
15
|
+
* 403 before 404 (spec 7110 § D4), so a 404 reaches only a key that
|
|
16
|
+
* holds the module's feature.
|
|
17
|
+
* - Default registry: the product API gateway (`api.ailaunchkit.ai`, routed
|
|
18
|
+
* to the licence issuer's `/v1/modules`); `AILK_MODULE_REGISTRY_URL`
|
|
19
|
+
* overrides it.
|
|
14
20
|
*
|
|
15
21
|
* Deliberately DOES NOT depend on `@working-theory/license` or
|
|
16
22
|
* `@working-theory/cli` — both are `publishConfig.access: "restricted"` and
|
|
@@ -37,7 +43,7 @@
|
|
|
37
43
|
*/
|
|
38
44
|
import pkg from "../../package.json" with { type: "json" };
|
|
39
45
|
import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
|
|
40
|
-
export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://ailaunchkit.ai/
|
|
46
|
+
export const DEFAULT_AILK_MODULE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/modules";
|
|
41
47
|
/**
|
|
42
48
|
* This package's own `package.json` `"version"`. Deliberately a static JSON
|
|
43
49
|
* import (`resolveJsonModule`), not `import.meta.url` + `fileURLToPath` (the
|
|
@@ -53,7 +59,7 @@ const CREATE_AILK_VERSION = pkg.version;
|
|
|
53
59
|
export class FetchModuleError extends Error {
|
|
54
60
|
isLicenseError;
|
|
55
61
|
constructor(message,
|
|
56
|
-
/** True iff the failure was a license-gating signal (HTTP 401). */
|
|
62
|
+
/** True iff the failure was a license-gating signal (HTTP 401 or 403). */
|
|
57
63
|
isLicenseError = false) {
|
|
58
64
|
super(message);
|
|
59
65
|
this.isLicenseError = isLicenseError;
|
|
@@ -108,7 +114,7 @@ export async function fetchModule(options) {
|
|
|
108
114
|
/* isLicenseError */ true);
|
|
109
115
|
}
|
|
110
116
|
if (response.status === 404) {
|
|
111
|
-
throw new FetchModuleError(`"${moduleName}" module content not yet available — ping us at hello@ailaunchkit.ai`);
|
|
117
|
+
throw new FetchModuleError(`"${moduleName}" module content not yet available for create-ailk ${version} — your licence includes it, but that release's bundle has not been uploaded yet; ping us at hello@ailaunchkit.ai`);
|
|
112
118
|
}
|
|
113
119
|
if (!response.ok) {
|
|
114
120
|
const body = await safeReadBody(response);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/
|
|
1
|
+
export declare const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/templates";
|
|
2
2
|
export interface FetchTemplateOptions {
|
|
3
3
|
templateName: string;
|
|
4
4
|
/** JWT used as `Authorization: Bearer <token>`. Must be non-empty. */
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* D1, § 6).
|
|
4
4
|
*
|
|
5
5
|
* The client half of `GET /v1/templates/:name/:version` (#5480's route),
|
|
6
|
-
* reached
|
|
6
|
+
* reached at `https://api.ailaunchkit.ai/v1/templates/:name/:version` through
|
|
7
|
+
* the product API gateway. Posture is
|
|
7
8
|
* `fetch-module.ts`'s, mirrored one-for-one rather than generalised into a
|
|
8
9
|
* shared helper: bearer-JWT auth, `redirect: "error"` so a redirected
|
|
9
10
|
* response cannot drop the bearer token onto an unintended host, an
|
|
10
11
|
* empty-token guard before any network call, and the same status mapping —
|
|
11
12
|
* 401 → licence invalid/revoked/lapsed, 403 → not included / term ended,
|
|
12
|
-
* 404 →
|
|
13
|
+
* 404 → included, but that release's bundle not yet uploaded, other non-2xx
|
|
14
|
+
* → the upstream body. The issuer answers 403 before 404 (spec 7110 § D4),
|
|
15
|
+
* so a 404 reaches only a key that holds the template's whole union.
|
|
13
16
|
*
|
|
14
17
|
* ONE word differs, and it is the whole reason this is a sibling rather
|
|
15
18
|
* than a second call site of `fetchModule` (spec 5481 § 3 DD5): the 404
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
*/
|
|
28
31
|
import pkg from "../../package.json" with { type: "json" };
|
|
29
32
|
import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
|
|
30
|
-
export const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://ailaunchkit.ai/
|
|
33
|
+
export const DEFAULT_AILK_TEMPLATE_REGISTRY_URL = "https://api.ailaunchkit.ai/v1/templates";
|
|
31
34
|
/**
|
|
32
35
|
* This package's own `package.json` `"version"` — a static JSON import for
|
|
33
36
|
* the reason `fetch-module.ts`'s `CREATE_AILK_VERSION` doc gives: `import.meta`
|
|
@@ -95,7 +98,7 @@ export async function fetchTemplate(options) {
|
|
|
95
98
|
/* isLicenseError */ true);
|
|
96
99
|
}
|
|
97
100
|
if (response.status === 404) {
|
|
98
|
-
throw new FetchTemplateError(`"${templateName}" template content not yet available — ping us at hello@ailaunchkit.ai`);
|
|
101
|
+
throw new FetchTemplateError(`"${templateName}" template content not yet available for create-ailk ${version} — your licence includes it, but that release's bundle has not been uploaded yet; ping us at hello@ailaunchkit.ai`);
|
|
99
102
|
}
|
|
100
103
|
if (!response.ok) {
|
|
101
104
|
const body = await safeReadBody(response);
|
|
@@ -103,3 +103,19 @@ export declare function assessConfigGatedRoute(input: {
|
|
|
103
103
|
* a socket) throws rather than being silently followed or skipped.
|
|
104
104
|
*/
|
|
105
105
|
export declare function listPublicFiles(dir: string): string[];
|
|
106
|
+
/** The slug the sweep requests to prove a detail route 404s an unknown slug. */
|
|
107
|
+
export declare const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
|
|
108
|
+
/** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
|
|
109
|
+
export declare function detailRoutePrefix(route: string): string | null;
|
|
110
|
+
/** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
|
|
111
|
+
export declare function listSeedSlugs(dir: string): string[];
|
|
112
|
+
/** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
|
|
113
|
+
export declare function assessUnknownSlug(input: {
|
|
114
|
+
route: string;
|
|
115
|
+
hops: RedirectHop[];
|
|
116
|
+
status: number | null;
|
|
117
|
+
}): SweepVerdict;
|
|
118
|
+
/** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
|
|
119
|
+
export declare function assessPrerenderManifest(manifest: {
|
|
120
|
+
dynamicRoutes?: Record<string, unknown>;
|
|
121
|
+
}): SweepVerdict;
|
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
* not cost a templates build, an install and a `next build`. Each is a pure
|
|
14
14
|
* function of what the driver observed, so
|
|
15
15
|
* `__tests__/verify-scaffold-render-sweep.test.ts` drives them directly. The
|
|
16
|
-
* driver loads this module from `dist/`. Only `listPublicFiles`
|
|
17
|
-
* filesystem, and none of it touches the network.
|
|
16
|
+
* driver loads this module from `dist/`. Only `listPublicFiles` and
|
|
17
|
+
* `listSeedSlugs` touch the filesystem, and none of it touches the network.
|
|
18
|
+
*
|
|
19
|
+
* #7379 added two sweeps. A starter roster's detail row that names a seed
|
|
20
|
+
* directory (`/features/[slug]` → `content/en/Feature`) is probed: each seed
|
|
21
|
+
* must render, and `UNKNOWN_SLUG` must answer 404. And the build's
|
|
22
|
+
* `prerender-manifest.json` must register no `/[locale]/…` route as an
|
|
23
|
+
* on-demand prerender — under the cookie-reading locale layout, such a route
|
|
24
|
+
* answers 500 for every slug, seeded or not.
|
|
18
25
|
*/
|
|
19
|
-
import { readdirSync } from "node:fs";
|
|
26
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
20
27
|
import { join } from "node:path";
|
|
21
28
|
/** More hops than this is a redirect chain, not a render. */
|
|
22
29
|
export const MAX_REDIRECTS = 5;
|
|
@@ -234,3 +241,45 @@ function listFilesUnder(dir, relPrefix) {
|
|
|
234
241
|
}
|
|
235
242
|
return out;
|
|
236
243
|
}
|
|
244
|
+
/** The slug the sweep requests to prove a detail route 404s an unknown slug. */
|
|
245
|
+
export const UNKNOWN_SLUG = "ailk-unknown-slug-7379";
|
|
246
|
+
/** `/features/[slug]` → `/features`; null unless `[slug]` is the one dynamic segment and the last. */
|
|
247
|
+
export function detailRoutePrefix(route) {
|
|
248
|
+
const match = /^((?:\/[^/[\]]+)+)\/\[slug\]$/.exec(route);
|
|
249
|
+
return match?.[1] ?? null;
|
|
250
|
+
}
|
|
251
|
+
/** The sorted basenames (sans `.mdx`) of the `*.mdx` files directly in `dir`; [] when `dir` is absent. */
|
|
252
|
+
export function listSeedSlugs(dir) {
|
|
253
|
+
if (!existsSync(dir))
|
|
254
|
+
return [];
|
|
255
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
256
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".mdx"))
|
|
257
|
+
.map((entry) => entry.name.slice(0, -".mdx".length))
|
|
258
|
+
.sort();
|
|
259
|
+
}
|
|
260
|
+
/** An unknown slug must answer exactly 404, with no redirect. A 500 is the #7379 failure; a 200 means no 404 branch ran. */
|
|
261
|
+
export function assessUnknownSlug(input) {
|
|
262
|
+
const { route, hops, status } = input;
|
|
263
|
+
const first = hops[0];
|
|
264
|
+
if (first !== undefined) {
|
|
265
|
+
return { ok: false, reason: `${route} -> ${first.status} redirect (an unknown slug must answer 404, not redirect)` };
|
|
266
|
+
}
|
|
267
|
+
if (status === 200)
|
|
268
|
+
return { ok: false, reason: `${route} -> 200 (no 404 branch ran for an unknown slug)` };
|
|
269
|
+
if (status !== 404)
|
|
270
|
+
return { ok: false, reason: `${route} -> status ${status} (expected 404 for an unknown slug)` };
|
|
271
|
+
return { ok: true, reason: "404" };
|
|
272
|
+
}
|
|
273
|
+
/** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
|
|
274
|
+
export function assessPrerenderManifest(manifest) {
|
|
275
|
+
const locale = Object.keys(manifest.dynamicRoutes ?? {})
|
|
276
|
+
.filter((route) => route.startsWith("/[locale]/"))
|
|
277
|
+
.sort();
|
|
278
|
+
if (locale.length > 0) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
reason: `on-demand prerender under the cookie-reading locale layout (500s every slug, #7379): ${locale.join(", ")}`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, reason: "no /[locale] on-demand prerender route" };
|
|
285
|
+
}
|
|
@@ -212,6 +212,15 @@ export interface ModuleSlice {
|
|
|
212
212
|
* leak class `scripts/check-oss-strip.sh` exists to catch (#5644).
|
|
213
213
|
*/
|
|
214
214
|
installRoot?: string;
|
|
215
|
+
/**
|
|
216
|
+
* Paths this module's keyed content payloads occupy (src/content-payloads.ts):
|
|
217
|
+
* each payload file's monorepo source AND the scaffold path its skill's fetch
|
|
218
|
+
* writes it to. Never delivered — no build, extract or patch step reads this
|
|
219
|
+
* field. Declared so the paid-path leak guards, which derive from
|
|
220
|
+
* `ownedFiles`/`installMap`, still recognise a leaked copy after the files
|
|
221
|
+
* left `installMap` (#7133).
|
|
222
|
+
*/
|
|
223
|
+
contentPayloadPaths?: string[];
|
|
215
224
|
/**
|
|
216
225
|
* Plain text `extractModuleBundle` prints to stdout after a successful
|
|
217
226
|
* merge, with C0/C1 control characters (other than newline) stripped
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, readFileSync } from "node:fs";
|
|
25
25
|
import { join, sep } from "node:path";
|
|
26
|
+
import { CONTENT_PAYLOADS, } from "./content-payloads.js";
|
|
26
27
|
import { importClosure } from "./lib/import-graph.js";
|
|
27
28
|
import { PAID_MODULES } from "./lib/module-tier.js";
|
|
28
29
|
import { RECOGNIZED_MODULES } from "./parse-args.js";
|
|
@@ -183,6 +184,21 @@ const ASSET_STORE_FILES = [
|
|
|
183
184
|
* tools/lint-architecture.ts fails on (#6649).
|
|
184
185
|
*/
|
|
185
186
|
const WEB_AUTH_ADAPTER_IMPORT = "database";
|
|
187
|
+
/**
|
|
188
|
+
* A content payload's paths for `ModuleSlice.contentPayloadPaths` (#7133):
|
|
189
|
+
* each file's `${sourceRoot}/${file}` and the `${destRoot}/${file}` its
|
|
190
|
+
* skill's keyed fetch writes it to — read off `CONTENT_PAYLOADS`, never
|
|
191
|
+
* hand-listed, so the leak guards cannot drift from the manifest.
|
|
192
|
+
*/
|
|
193
|
+
function payloadPaths(kind, name, destRoot) {
|
|
194
|
+
const payload = CONTENT_PAYLOADS.find((p) => p.kind === kind && p.name === name);
|
|
195
|
+
if (!payload)
|
|
196
|
+
throw new Error(`no content payload ${kind}/${name}`);
|
|
197
|
+
return payload.files.flatMap((file) => [
|
|
198
|
+
`${payload.sourceRoot}/${file}`,
|
|
199
|
+
`${destRoot}/${file}`,
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
186
202
|
/**
|
|
187
203
|
* The per-module slices. Authored for every `RECOGNIZED_MODULES` value; only
|
|
188
204
|
* `marketing` (the default) is validated end-to-end against a golden.
|
|
@@ -1661,31 +1677,31 @@ export const MODULE_SLICES = {
|
|
|
1661
1677
|
// it sits outside build-templates.mjs's SNAPSHOT_DIRS/overlay, so
|
|
1662
1678
|
// ownedFiles/paidModuleTemplateExclusions cannot reach it) to
|
|
1663
1679
|
// `module-templates/commerce/.claude/skills/scaffold-commerce/`, and
|
|
1664
|
-
// delivered into a keyed scaffold by `installMap` instead.
|
|
1680
|
+
// delivered into a keyed scaffold by `installMap` instead. Five literal
|
|
1665
1681
|
// entries, not one directory entry: `extractModuleBundle`'s
|
|
1666
1682
|
// `validateInstallMap` requires every source to be a FILE the bundle
|
|
1667
1683
|
// produced (`extract-module-bundle.ts`'s `!sourceStat.isFile()` check) —
|
|
1668
|
-
// a directory source is rejected outright.
|
|
1684
|
+
// a directory source is rejected outright. The skill ships as steps only
|
|
1685
|
+
// (#7133): its `templates/` and `references/` are the keyed content
|
|
1686
|
+
// payload `template-set/scaffold-commerce` (src/content-payloads.ts),
|
|
1687
|
+
// which `scripts/fetch-templates.ts` fetches with the site's licence key;
|
|
1688
|
+
// `link/` carries byte copies of the link path's two files, which need no
|
|
1689
|
+
// key.
|
|
1669
1690
|
installMap: {
|
|
1670
1691
|
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/SKILL.md": ".claude/skills/scaffold-commerce/SKILL.md",
|
|
1671
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/
|
|
1672
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/
|
|
1673
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/
|
|
1674
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts": ".claude/skills/scaffold-commerce/templates/invoice-route.template.ts",
|
|
1675
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl": ".claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl",
|
|
1676
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts": ".claude/skills/scaffold-commerce/templates/payment-link-route.template.ts",
|
|
1677
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/portal-route.template.ts": ".claude/skills/scaffold-commerce/templates/portal-route.template.ts",
|
|
1678
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts": ".claude/skills/scaffold-commerce/templates/stripe-config.template.ts",
|
|
1679
|
-
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts": ".claude/skills/scaffold-commerce/templates/subscription-route.template.ts",
|
|
1692
|
+
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/link/marketplace-brand-pages.md": ".claude/skills/scaffold-commerce/link/marketplace-brand-pages.md",
|
|
1693
|
+
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/link/marketplace-cta-route.tsx.tmpl": ".claude/skills/scaffold-commerce/link/marketplace-cta-route.tsx.tmpl",
|
|
1694
|
+
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/scripts/fetch-templates.ts": ".claude/skills/scaffold-commerce/scripts/fetch-templates.ts",
|
|
1680
1695
|
"packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce/scripts/stripe-catalog.ts": ".claude/skills/scaffold-commerce/scripts/stripe-catalog.ts",
|
|
1681
1696
|
// The commerce-boundary rule (#6039/#6070, this entry #6041): ships to
|
|
1682
1697
|
// a commerce-keyed scaffold's project root so an agent working there
|
|
1683
1698
|
// reads the money/live-mode/personal-data classification before it
|
|
1684
|
-
// acts. One more literal entry alongside the
|
|
1699
|
+
// acts. One more literal entry alongside the five scaffold-commerce
|
|
1685
1700
|
// skill entries above, for the same reason — a directory source is
|
|
1686
1701
|
// rejected by `validateInstallMap`.
|
|
1687
1702
|
"packages/create-ailk/module-templates/commerce/.claude/rules/commerce-boundary.md": ".claude/rules/commerce-boundary.md",
|
|
1688
1703
|
},
|
|
1704
|
+
contentPayloadPaths: payloadPaths("template-set", "scaffold-commerce", ".claude/skills/scaffold-commerce"),
|
|
1689
1705
|
// patches (#5581 U5, D5) — delivers the extracted component package to a
|
|
1690
1706
|
// keyed scaffold's apps/web, mirroring the workspace slice's identical
|
|
1691
1707
|
// merge-keys op above.
|
|
@@ -1833,12 +1849,27 @@ export function coOwnedModuleDestinations() {
|
|
|
1833
1849
|
// see that entry, and sync-routes' AC-5 coverage test, which reads this
|
|
1834
1850
|
// list so the resulting ROUTE_SINCE-vs-template absence is justified
|
|
1835
1851
|
// rather than reported stale.
|
|
1852
|
+
// - the three app-side suites of the API ↔ MCP parity guard (#7135, spec
|
|
1853
|
+
// docs/specs/7135-an-api-mcp-parity-guard-spec.md D11) — file entries,
|
|
1854
|
+
// matched exactly by `isExcludedTemplatePath`. Each imports the guard's
|
|
1855
|
+
// core from the monorepo's root `tests/parity/api-parity/` by relative
|
|
1856
|
+
// path, which a scaffold does not carry. A test that imports a slice file
|
|
1857
|
+
// is a dependent, not an import-graph companion, so
|
|
1858
|
+
// `paidModuleTemplateExclusions` never sweeps these along.
|
|
1859
|
+
// - apps/mcp/__tests__/pro-tool-call-gate.test.ts (#7315, spec
|
|
1860
|
+
// docs/specs/7315-a-gated-pro-tool-re-spec.md D4) — same reason as the
|
|
1861
|
+
// three suites above: it imports `SITE_ARGS` from the monorepo's root
|
|
1862
|
+
// `tests/parity/` tree, which no scaffold carries.
|
|
1836
1863
|
export const EXCLUDE_TEMPLATE_DIRS = [
|
|
1837
1864
|
"apps/web/__tests__",
|
|
1838
1865
|
"apps/web/app/__tests__",
|
|
1839
1866
|
"apps/web/app/[locale]/__tests__",
|
|
1840
1867
|
"apps/web/app/[locale]/(authed)/edit",
|
|
1841
1868
|
"apps/web/content",
|
|
1869
|
+
"apps/api/src/__tests__/api-parity.test.ts",
|
|
1870
|
+
"apps/api/src/__tests__/api-parity-paid.test.ts",
|
|
1871
|
+
"apps/mcp/__tests__/api-parity.test.ts",
|
|
1872
|
+
"apps/mcp/__tests__/pro-tool-call-gate.test.ts",
|
|
1842
1873
|
];
|
|
1843
1874
|
export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
|
|
1844
1875
|
const roots = new Set(snapshotRoots);
|
package/dist/parse-args.d.ts
CHANGED
|
@@ -103,6 +103,8 @@ export interface ParseEnv {
|
|
|
103
103
|
/** A human can answer a prompt: `process.stdin.isTTY === true` in cli.ts. */
|
|
104
104
|
interactive: boolean;
|
|
105
105
|
}
|
|
106
|
+
/** The two lines that install the `ailk` plugin in a Claude Code session (#7298). */
|
|
107
|
+
export declare const PLUGIN_INSTALL_LINES: readonly ["/plugin marketplace add working-theory-labs/claude-plugins", "/plugin install ailk@working-theory"];
|
|
106
108
|
export declare const helpText: string;
|
|
107
109
|
/**
|
|
108
110
|
* Parse `create-ailk` argv. `env.interactive` defaults to false —
|
package/dist/parse-args.js
CHANGED
|
@@ -65,6 +65,11 @@ export class ParseError extends Error {
|
|
|
65
65
|
this.name = "ParseError";
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
/** The two lines that install the `ailk` plugin in a Claude Code session (#7298). */
|
|
69
|
+
export const PLUGIN_INSTALL_LINES = [
|
|
70
|
+
"/plugin marketplace add working-theory-labs/claude-plugins",
|
|
71
|
+
"/plugin install ailk@working-theory",
|
|
72
|
+
];
|
|
68
73
|
export const helpText = `create-ailk — scaffold a new AI Launch Kit site.
|
|
69
74
|
|
|
70
75
|
Usage:
|
|
@@ -166,6 +171,8 @@ Next steps after a successful scaffold:
|
|
|
166
171
|
cd <target-dir>
|
|
167
172
|
pnpm install
|
|
168
173
|
pnpm dev
|
|
174
|
+
Then, in Claude Code, install the ailk plugin:
|
|
175
|
+
${PLUGIN_INSTALL_LINES.map((line) => ` ${line}`).join("\n")}
|
|
169
176
|
|
|
170
177
|
Reference: ADR 0007 — \`pnpm create ailk@latest\` is the canonical install path.
|
|
171
178
|
`;
|
package/dist/surfaces.js
CHANGED
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The default (`--modules marketing`) enables NO gateable surface: every one of
|
|
22
22
|
* them is a claim about the business that the scaffolder cannot make. The path
|
|
23
|
-
* from there is the
|
|
24
|
-
* `templates/.claude/skills/`.
|
|
23
|
+
* from there is the `/ailk:add-page` skill in the `ailk` Claude Code plugin.
|
|
25
24
|
*
|
|
26
25
|
* Zero runtime dependencies — create-ailk is a published, standalone CLI.
|
|
27
26
|
*
|
|
@@ -1047,7 +1046,7 @@ export function renderNavConfig(enabled, options) {
|
|
|
1047
1046
|
// below it, the nav is yours and add-module reports the change it wanted
|
|
1048
1047
|
// instead.
|
|
1049
1048
|
//
|
|
1050
|
-
// Adding a link: scaffold the page (the
|
|
1049
|
+
// Adding a link: scaffold the page (the \`/ailk:add-page\` skill), then add an
|
|
1051
1050
|
// entry below with a \`labelKey\` that exists in the \`nav\` namespace of
|
|
1052
1051
|
// \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
|
|
1053
1052
|
// them with the active locale at render.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"tar": "^7.5.21"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@working-theory/schema": "0.
|
|
31
|
-
"@working-theory/templates": "0.
|
|
30
|
+
"@working-theory/schema": "0.30.0",
|
|
31
|
+
"@working-theory/templates": "0.30.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -366,10 +366,11 @@ Every content route emits one `application/ld+json` script whose `@graph` carrie
|
|
|
366
366
|
| Node | `@id` | Built from | Links |
|
|
367
367
|
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
368
368
|
| Identity (`Organization` · `Person` · `LocalBusiness`) | `<origin>/#organization` (`#person` for a Person) | `siteIdentity` `name` · `url` · `logo`/`image` · `description` · `sameAs` | — |
|
|
369
|
+
| Identity (`SoftwareApplication`) | `<origin>/#product` | `siteIdentity` fields; `logo` emitted as `image` | `publisher` → the maker's own `@id` (`siteIdentity.publisher`), never a node here; takes no secondary |
|
|
369
370
|
| Secondary (optional) | `<origin>/#product` (a `SoftwareApplication`) · `<origin>/#organization` (a Person's company) | `siteIdentity.secondary` | product `publisher` → `#organization`, `featureList` from the product page's (`/software/product`) feature items; a Person `worksFor` its company, which has the Person as `founder` |
|
|
370
|
-
| `WebSite` | `<origin>/#website` | `siteName` · `baseUrl` · the render locale | `publisher` → the identity node
|
|
371
|
+
| `WebSite` | `<origin>/#website` | `siteName` · `baseUrl` · the render locale | `publisher` → the identity node (the maker, for a `SoftwareApplication` identity) |
|
|
371
372
|
|
|
372
|
-
Each page's primary node is `isPartOf` the `WebSite`; the home page's `mainEntity` is the secondary when declared, else the identity. A testimonials `Review` points its `itemReviewed` at the identity node, declared in the same graph. Free declares no secondary, so a fresh site emits the Organization and the WebSite. `/sign-in` emits no JSON-LD.
|
|
373
|
+
Each page's primary node is `isPartOf` the `WebSite`; the home page's `mainEntity` is the secondary when declared, else the identity (`#product` for a `SoftwareApplication` identity, #7103). A testimonials `Review` points its `itemReviewed` at the identity node, declared in the same graph. Free declares no secondary, so a fresh site emits the Organization and the WebSite. `/sign-in` emits no JSON-LD.
|
|
373
374
|
|
|
374
375
|
## 2a. State and persistence (every interactive element)
|
|
375
376
|
|
|
@@ -466,7 +467,7 @@ reversed (`docs/decisions/starter-roster-page-set.md` D4, D7).
|
|
|
466
467
|
|
|
467
468
|
### 3b. Site entity (operator-disposed 2026-09-24)
|
|
468
469
|
|
|
469
|
-
`derive-roster.py` names `SoftwareApplication` as the main entity for this template's inputs. The template deliberately declares the **Organization** as the site's primary node and the software as its **secondary** node (`siteIdentity.secondary`, kind `SoftwareApplication`, #6597): the Free template is a company's own site about its product, so the entity the site is _about_ is the maker. The inverse — `SoftwareApplication` primary — is for a product whose
|
|
470
|
+
`derive-roster.py` names `SoftwareApplication` as the main entity for this template's inputs. The template deliberately declares the **Organization** as the site's primary node and the software as its **secondary** node (`siteIdentity.secondary`, kind `SoftwareApplication`, #6597): the Free template is a company's own site about its product, so the entity the site is _about_ is the maker. The inverse — `SoftwareApplication` primary — is for a product whose maker has its own site (Positionary): express it as `siteIdentity.kind: SoftwareApplication` with a required `publisher` naming the maker's stable `@id` on that site (e.g. `https://salessmyth.com/#organization`, #7103), so the product is `<origin>/#product` and the maker is a reference, never a second Organization node here. A fresh Free scaffold declares no `secondary` until the owner names the product; the overlay `_site.mdx` documents the field.
|
|
470
471
|
|
|
471
472
|
## 4. Component inventory (Free, by tier)
|
|
472
473
|
|
|
@@ -11,29 +11,29 @@ schema:
|
|
|
11
11
|
name: "Your Company"
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
# {First job}
|
|
14
|
+
# \{First job}
|
|
15
15
|
|
|
16
|
-
{Open with the situation a reader is in when they come looking for this. Two or
|
|
16
|
+
\{Open with the situation a reader is in when they come looking for this. Two or
|
|
17
17
|
three sentences, no more — the detail belongs under the headings below.}
|
|
18
18
|
|
|
19
19
|
## What it covers
|
|
20
20
|
|
|
21
|
-
- {The first thing you actually do}
|
|
22
|
-
- {The second thing you actually do}
|
|
23
|
-
- {The third thing you actually do}
|
|
21
|
+
- \{The first thing you actually do}
|
|
22
|
+
- \{The second thing you actually do}
|
|
23
|
+
- \{The third thing you actually do}
|
|
24
24
|
|
|
25
25
|
## How it runs
|
|
26
26
|
|
|
27
|
-
{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
27
|
+
\{One paragraph on the shape of the visit: how it is scheduled, how long it
|
|
28
28
|
takes, who turns up, and what you need from the customer beforehand.}
|
|
29
29
|
|
|
30
30
|
## What you end up with
|
|
31
31
|
|
|
32
|
-
{One paragraph on the result. Be concrete — describe the property when the
|
|
32
|
+
\{One paragraph on the result. Be concrete — describe the property when the
|
|
33
33
|
crew drives away.}
|
|
34
34
|
|
|
35
35
|
## What it costs
|
|
36
36
|
|
|
37
|
-
{One line on the pricing shape — flat rate, hourly, or by the job — and what it
|
|
37
|
+
\{One line on the pricing shape — flat rate, hourly, or by the job — and what it
|
|
38
38
|
excludes. Replace with your real numbers, or delete this section and let a
|
|
39
39
|
call do it.}
|