create-ailk 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/programmatic.js +44 -20
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +11 -0
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +120 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +7 -3
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +35 -3
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +4 -2
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/messages/en.json +16 -5
- package/templates/database/CHANGELOG.md +9 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
package/dist/programmatic.js
CHANGED
|
@@ -25,7 +25,7 @@ import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog
|
|
|
25
25
|
import { buildScaffoldMarkers, copyTemplate } from "./copy.js";
|
|
26
26
|
import { deriveArchitecture } from "./derive-architecture.js";
|
|
27
27
|
import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
|
|
28
|
-
import {
|
|
28
|
+
import { DESIGN_SEED_PAGES, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, siteIdentityName, } from "./design-home.js";
|
|
29
29
|
import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
|
|
30
30
|
import { resolveDesign, spliceThemeBlock, } from "./design-scaffold.js";
|
|
31
31
|
import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
|
|
@@ -330,6 +330,10 @@ export async function scaffold(opts) {
|
|
|
330
330
|
const designSeeds = resolvedDesign !== undefined
|
|
331
331
|
? designHomeSeeds(resolvedDesign, { designsDir: DESIGNS_DIR, catalog })
|
|
332
332
|
: [];
|
|
333
|
+
// #8661 D11 — and its contact seeds, read and validated the same way.
|
|
334
|
+
const designContactSeeds = resolvedDesign !== undefined
|
|
335
|
+
? designHomeSeeds(resolvedDesign, { designsDir: DESIGNS_DIR, catalog, page: "contact" })
|
|
336
|
+
: [];
|
|
333
337
|
// #6813 D7 — the resolved design only when the CALLER chose one (`opts.design`);
|
|
334
338
|
// a template's own declared design is never gated (#6778 option (c)).
|
|
335
339
|
const chosenDesign = opts.design !== undefined ? resolvedDesign : undefined;
|
|
@@ -719,21 +723,35 @@ export async function scaffold(opts) {
|
|
|
719
723
|
// every layer has landed, before the presets dress it. A home that is not
|
|
720
724
|
// the shape's placeholder is never written; `--design standard` (no
|
|
721
725
|
// seeds) leaves the shape's home byte-identical. The one
|
|
722
|
-
// `planDesignHome` call site in scaffold().
|
|
723
|
-
|
|
724
|
-
|
|
726
|
+
// `planDesignHome` call site in scaffold(). #8661 D11 — the contact page
|
|
727
|
+
// is planned right after the home, its heading named from the scaffold's
|
|
728
|
+
// `siteIdentity.name`.
|
|
729
|
+
const seededPages = [];
|
|
725
730
|
if (template !== undefined) {
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
731
|
+
const siteName = siteIdentityName(existsSync(siteMdxPath) ? readFileSync(siteMdxPath, "utf8") : undefined);
|
|
732
|
+
for (const [page, pageSeeds] of [
|
|
733
|
+
["home", designSeeds],
|
|
734
|
+
["contact", designContactSeeds],
|
|
735
|
+
]) {
|
|
736
|
+
const pagePath = join(result.targetDir, ...DESIGN_SEED_PAGES[page].path.split("/"));
|
|
737
|
+
const plan = planDesignHome({
|
|
738
|
+
page,
|
|
739
|
+
siteName,
|
|
740
|
+
current: existsSync(pagePath) ? readFileSync(pagePath, "utf8") : undefined,
|
|
741
|
+
shape: template.shape,
|
|
742
|
+
target: pageSeeds.find((seed) => seed.shape === template.shape),
|
|
743
|
+
shapeHome: readFileSync(shapeHomePath(template.shape, {
|
|
744
|
+
siteTemplatesDir: SITE_TEMPLATES_DIR,
|
|
745
|
+
templatesDir: TEMPLATES_DIR,
|
|
746
|
+
page,
|
|
747
|
+
}), "utf8"),
|
|
748
|
+
shippedSeeds: shippedHomeSeeds({ designsDir: DESIGNS_DIR, catalog, page }),
|
|
749
|
+
catalog: catalog(),
|
|
750
|
+
});
|
|
751
|
+
if (plan.outcome === "seeded" && plan.text !== undefined) {
|
|
752
|
+
writeFileSync(pagePath, plan.text);
|
|
753
|
+
seededPages.push(page);
|
|
754
|
+
}
|
|
737
755
|
}
|
|
738
756
|
}
|
|
739
757
|
// #6816 D6 step 10b — the design's section presets, written onto the
|
|
@@ -761,12 +779,18 @@ export async function scaffold(opts) {
|
|
|
761
779
|
}
|
|
762
780
|
}
|
|
763
781
|
// #7743 D6 — the manifest's `/` row records the seeded home's sections,
|
|
764
|
-
// as the presets left them.
|
|
782
|
+
// as the presets left them. #8661 — and the `/contact` row the seeded
|
|
783
|
+
// contact page's.
|
|
765
784
|
const rosterPath = join(result.targetDir, SITE_ROSTER_PATH);
|
|
766
|
-
if (
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
785
|
+
if (seededPages.length > 0 && existsSync(rosterPath)) {
|
|
786
|
+
const recorded = readFileSync(rosterPath, "utf8");
|
|
787
|
+
let manifestText = recorded;
|
|
788
|
+
for (const page of seededPages) {
|
|
789
|
+
const pagePath = join(result.targetDir, ...DESIGN_SEED_PAGES[page].path.split("/"));
|
|
790
|
+
manifestText = planHomeRecord(manifestText, readFileSync(pagePath, "utf8"), catalog(), page) ?? manifestText;
|
|
791
|
+
}
|
|
792
|
+
if (manifestText !== recorded)
|
|
793
|
+
writeFileSync(rosterPath, manifestText);
|
|
770
794
|
}
|
|
771
795
|
}
|
|
772
796
|
// #7838 D8 — a commerce spoke's own checkout: two routes, its config and
|
package/dist/site-roster.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CompiledDesignTheme } from "./design-compile.js";
|
|
2
|
+
import { type DesignSeedPage } from "./design-home.js";
|
|
2
3
|
import { type SectionLookCatalog } from "./design-presets.js";
|
|
3
4
|
import { type PageRosterRow } from "./module-architecture.js";
|
|
4
5
|
import type { ModuleName } from "./parse-args.js";
|
|
@@ -102,9 +103,10 @@ export declare const MANIFEST_OPTION_KEYS: readonly ["itemStyle", "actionsPositi
|
|
|
102
103
|
* `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
|
|
103
104
|
* when the record already says so, or the manifest has no `/` row with a
|
|
104
105
|
* `sections` key. The value is spliced in place at its own depth; no other
|
|
105
|
-
* byte moves.
|
|
106
|
+
* byte moves. `page: "contact"` records the `/contact` row from the contact
|
|
107
|
+
* page instead (#8661 D11).
|
|
106
108
|
*/
|
|
107
|
-
export declare function planHomeRecord(manifestText: string, homeText: string, catalog: SectionLookCatalog): string | undefined;
|
|
109
|
+
export declare function planHomeRecord(manifestText: string, homeText: string, catalog: SectionLookCatalog, page?: DesignSeedPage): string | undefined;
|
|
108
110
|
/**
|
|
109
111
|
* The generator's row for each of a module's `pageContributions`, in
|
|
110
112
|
* declaration order — read from the committed manifest of the first
|
package/dist/site-roster.js
CHANGED
|
@@ -28,7 +28,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
28
28
|
import { join, relative, sep } from "node:path";
|
|
29
29
|
import { isDeepStrictEqual } from "node:util";
|
|
30
30
|
import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-core.js";
|
|
31
|
-
import {
|
|
31
|
+
import { DESIGN_SEED_PAGES } from "./design-home.js";
|
|
32
32
|
import { locatePageSections } from "./design-presets.js";
|
|
33
33
|
import { diffTwoStrings } from "./lib/extract-module-bundle.js";
|
|
34
34
|
import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
@@ -296,14 +296,16 @@ function scalarValue(raw) {
|
|
|
296
296
|
* `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
|
|
297
297
|
* when the record already says so, or the manifest has no `/` row with a
|
|
298
298
|
* `sections` key. The value is spliced in place at its own depth; no other
|
|
299
|
-
* byte moves.
|
|
299
|
+
* byte moves. `page: "contact"` records the `/contact` row from the contact
|
|
300
|
+
* page instead (#8661 D11).
|
|
300
301
|
*/
|
|
301
|
-
export function planHomeRecord(manifestText, homeText, catalog) {
|
|
302
|
+
export function planHomeRecord(manifestText, homeText, catalog, page = "home") {
|
|
303
|
+
const { route, path } = DESIGN_SEED_PAGES[page];
|
|
302
304
|
const file = parseRosterFile(manifestText, SITE_ROSTER_PATH);
|
|
303
|
-
const row = file.rows.find((r) => r.value["route"] ===
|
|
305
|
+
const row = file.rows.find((r) => r.value["route"] === route);
|
|
304
306
|
if (row === undefined || !("sections" in row.value))
|
|
305
307
|
return undefined;
|
|
306
|
-
const sections = (locatePageSections(homeText,
|
|
308
|
+
const sections = (locatePageSections(homeText, path) ?? []).map((section) => {
|
|
307
309
|
const options = {};
|
|
308
310
|
for (const key of MANIFEST_OPTION_KEYS) {
|
|
309
311
|
const entry = section.entries.find((e) => e.key === key);
|
package/dist/starter-grid.js
CHANGED
|
@@ -117,6 +117,10 @@ const TEMPLATE_BASE_ROSTER = [
|
|
|
117
117
|
{ route: "/sign-up" },
|
|
118
118
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
119
119
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
120
|
+
{
|
|
121
|
+
route: "/working-agreement",
|
|
122
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
123
|
+
},
|
|
120
124
|
{
|
|
121
125
|
route: "/purchase-policy",
|
|
122
126
|
content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
|
|
@@ -235,6 +239,10 @@ const ORGANIZATION_SERVICE_ROWS = [
|
|
|
235
239
|
{ route: "/sign-up" },
|
|
236
240
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
237
241
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
242
|
+
{
|
|
243
|
+
route: "/working-agreement",
|
|
244
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
245
|
+
},
|
|
238
246
|
{
|
|
239
247
|
route: "/purchase-policy",
|
|
240
248
|
content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
|
|
@@ -267,6 +275,10 @@ const LOCAL_BUSINESS_SERVICE_ROWS = [
|
|
|
267
275
|
},
|
|
268
276
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
269
277
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
278
|
+
{
|
|
279
|
+
route: "/working-agreement",
|
|
280
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
281
|
+
},
|
|
270
282
|
];
|
|
271
283
|
/**
|
|
272
284
|
* `person-service`'s own rows (#6758 D1, D3), derived from
|
|
@@ -302,6 +314,10 @@ const PERSON_SERVICE_ROWS = [
|
|
|
302
314
|
},
|
|
303
315
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
304
316
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
317
|
+
{
|
|
318
|
+
route: "/working-agreement",
|
|
319
|
+
content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
|
|
320
|
+
},
|
|
305
321
|
];
|
|
306
322
|
/**
|
|
307
323
|
* Each buildable shape's standard roster (#6753 D3) — the rows an alias starts
|
package/dist/surfaces.js
CHANGED
|
@@ -331,6 +331,13 @@ export const GATEABLE_SURFACES = [
|
|
|
331
331
|
routes: [`${ROUTE_ROOT}/whitepapers`],
|
|
332
332
|
content: ["content/<locale>/Whitepaper"],
|
|
333
333
|
},
|
|
334
|
+
{
|
|
335
|
+
// #8661 D10 — the working agreement, module-less like `privacy` and
|
|
336
|
+
// `terms`: every shape roster names it right after /terms.
|
|
337
|
+
id: "working-agreement",
|
|
338
|
+
routes: [`${ROUTE_ROOT}/working-agreement`],
|
|
339
|
+
content: ["content/<locale>/WorkingAgreementPage"],
|
|
340
|
+
},
|
|
334
341
|
];
|
|
335
342
|
/** The recognized `--surfaces` vocabulary, sorted. */
|
|
336
343
|
export const RECOGNIZED_SURFACES = GATEABLE_SURFACES.map((s) => s.id);
|
|
@@ -552,6 +559,7 @@ export function isUnservedSurfaceContent(path, enabled) {
|
|
|
552
559
|
*/
|
|
553
560
|
export const ROUTE_SINCE = {
|
|
554
561
|
about: "0.0.0",
|
|
562
|
+
"about/[slug]": "0.42.0",
|
|
555
563
|
"blog/[[...slug]]": "0.0.0",
|
|
556
564
|
careers: "0.0.0",
|
|
557
565
|
"careers/[slug]": "0.0.0",
|
|
@@ -638,6 +646,9 @@ export const ROUTE_SINCE = {
|
|
|
638
646
|
"vs/[slug]": "0.28.0",
|
|
639
647
|
whitepapers: "0.0.0",
|
|
640
648
|
"whitepapers/[slug]": "0.0.0",
|
|
649
|
+
// #8661 D10 — the working-agreement page. Genuinely new, so it attaches at
|
|
650
|
+
// the fixed group's next release line, the same rule #7430 applied.
|
|
651
|
+
"working-agreement": "0.42.0",
|
|
641
652
|
// #5014 — the (authed) route GROUP: layout.tsx + sign-in-gate.tsx sit
|
|
642
653
|
// directly under "(authed)" (a Next.js route group — its parens are not a
|
|
643
654
|
// URL segment); account/ and waitlist/ are its two syncable sub-trees.
|
package/dist/sync-routes.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceVersionMap } from "./substitute.js";
|
|
2
|
+
/** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
|
|
3
|
+
export declare const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
|
|
2
4
|
export interface SyncRoutesOptions {
|
|
3
5
|
/**
|
|
4
6
|
* The already-scaffolded repo (absolute or relative). Must exist, be a
|
|
@@ -65,7 +67,32 @@ export interface SyncRoutesResult {
|
|
|
65
67
|
missingMessageNamespaces: string[];
|
|
66
68
|
/** Gateable surface ids the target does not carry, sorted. */
|
|
67
69
|
skippedSurfaces: string[];
|
|
70
|
+
/**
|
|
71
|
+
* When `apply` added a route the template exempts in its structured-data
|
|
72
|
+
* test and the site's copy of that test is site-edited (its bytes differ
|
|
73
|
+
* from the recorded hash), each template `NO_STRUCTURED_DATA_ROUTES` entry
|
|
74
|
+
* the site's test lacks, verbatim, with the added routes it covers (#8704).
|
|
75
|
+
* Sorted by entry; empty otherwise. The site's test is never written.
|
|
76
|
+
*/
|
|
77
|
+
structuredDataEntries: Array<{
|
|
78
|
+
routes: string[];
|
|
79
|
+
entry: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/** One `NO_STRUCTURED_DATA_ROUTES` element: its source text, dedented, and the routes it exempts. */
|
|
83
|
+
interface StructuredDataExemption {
|
|
84
|
+
route: string;
|
|
85
|
+
subtree: boolean;
|
|
86
|
+
text: string;
|
|
68
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
90
|
+
* brace scan that skips string literals and comments. Empty when the
|
|
91
|
+
* file declares no such array. It assumes what the template's array holds:
|
|
92
|
+
* object literals of string, boolean and identifier-array values — no regex
|
|
93
|
+
* literal or `${…}` interpolation.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseStructuredDataExemptions(source: string): StructuredDataExemption[];
|
|
69
96
|
/**
|
|
70
97
|
* The template's route directories, ROUTE_ROOT-relative and sorted.
|
|
71
98
|
*
|
|
@@ -99,3 +126,4 @@ export declare const SYNC_ROUTES_HELP = "create-ailk sync-routes \u2014 copy tem
|
|
|
99
126
|
* like `parseCliArgs`; at most one positional.
|
|
100
127
|
*/
|
|
101
128
|
export declare function parseSyncRoutesArgs(argv: string[]): SyncRoutesArgs;
|
|
129
|
+
export {};
|
package/dist/sync-routes.js
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* route root is copied once into a scratch dir and compared from there.
|
|
22
22
|
* - Never overwrite, never delete. A route dir the site has changed is
|
|
23
23
|
* reported as modified and left alone; `--apply` only creates directories
|
|
24
|
-
* that do not exist, so a second run is a no-op.
|
|
24
|
+
* that do not exist, so a second run is a no-op. When the site has edited
|
|
25
|
+
* its structured-data test, a route `--apply` adds that the template
|
|
26
|
+
* exempts there is reported with the exact `NO_STRUCTURED_DATA_ROUTES`
|
|
27
|
+
* entry to add — the test itself is never written (#8704).
|
|
25
28
|
*
|
|
26
29
|
* Pure module: no `import.meta.url`, so jest can import it directly. The
|
|
27
30
|
* package's own `templates/` dir is supplied by programmatic.ts.
|
|
@@ -35,9 +38,12 @@ import { appendEnvRows } from "./lib/declared-env.js";
|
|
|
35
38
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
36
39
|
import { substituteMarkers } from "./substitute.js";
|
|
37
40
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
41
|
+
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, sha256Hex } from "./template-record.js";
|
|
38
42
|
import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
|
|
39
43
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
40
44
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
45
|
+
/** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
|
|
46
|
+
export const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
|
|
41
47
|
/**
|
|
42
48
|
* `<surface id>/<entry name>` pairs that live directly under a GATEABLE
|
|
43
49
|
* surface's top-level directory but belong to a DIFFERENT module (#5366).
|
|
@@ -50,6 +56,114 @@ const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
|
50
56
|
const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
|
|
51
57
|
"projects/apply",
|
|
52
58
|
]);
|
|
59
|
+
/**
|
|
60
|
+
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
61
|
+
* brace scan that skips string literals and comments. Empty when the
|
|
62
|
+
* file declares no such array. It assumes what the template's array holds:
|
|
63
|
+
* object literals of string, boolean and identifier-array values — no regex
|
|
64
|
+
* literal or `${…}` interpolation.
|
|
65
|
+
*/
|
|
66
|
+
export function parseStructuredDataExemptions(source) {
|
|
67
|
+
const decl = source.indexOf("const NO_STRUCTURED_DATA_ROUTES");
|
|
68
|
+
if (decl === -1)
|
|
69
|
+
return [];
|
|
70
|
+
const open = /=\s*\[/.exec(source.slice(decl));
|
|
71
|
+
if (open === null)
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
for (let i = decl + open.index + open[0].length; i < source.length; i++) {
|
|
77
|
+
const ch = source[i];
|
|
78
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
79
|
+
for (i++; i < source.length && source[i] !== ch; i++) {
|
|
80
|
+
if (source[i] === "\\")
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
86
|
+
const eol = source.indexOf("\n", i);
|
|
87
|
+
i = eol === -1 ? source.length : eol;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
91
|
+
const end = source.indexOf("*/", i + 2);
|
|
92
|
+
i = end === -1 ? source.length : end + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === "{") {
|
|
96
|
+
if (depth === 0)
|
|
97
|
+
start = i;
|
|
98
|
+
depth++;
|
|
99
|
+
}
|
|
100
|
+
else if (ch === "}") {
|
|
101
|
+
depth--;
|
|
102
|
+
if (depth === 0 && start !== -1) {
|
|
103
|
+
const raw = source.slice(start, i + 1) + (source[i + 1] === "," ? "," : "");
|
|
104
|
+
const route = /\broute:\s*["']([^"']+)["']/.exec(raw)?.[1];
|
|
105
|
+
if (route !== undefined) {
|
|
106
|
+
const indent = /[ \t]*$/.exec(source.slice(source.lastIndexOf("\n", start) + 1, start))?.[0] ?? "";
|
|
107
|
+
const text = raw
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((line) => (line.startsWith(indent) ? line.slice(indent.length) : line))
|
|
110
|
+
.join("\n");
|
|
111
|
+
out.push({ route, subtree: /\bsubtree:\s*true\b/.test(raw), text });
|
|
112
|
+
}
|
|
113
|
+
start = -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "]" && depth === 0) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The first exemption covering `route` by the structured-data test's route
|
|
124
|
+
* rule (`route` itself, or under a `subtree` entry). An entry's `requires`
|
|
125
|
+
* strings are not checked: they key only the generated spoke checkout pages,
|
|
126
|
+
* which no template route dir — so no `--apply` — ever adds.
|
|
127
|
+
*/
|
|
128
|
+
function exemptionFor(exemptions, route) {
|
|
129
|
+
return exemptions.find((e) => route === e.route || (e.subtree && route.startsWith(`${e.route}/`)));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The template `NO_STRUCTURED_DATA_ROUTES` entries the site's structured-data
|
|
133
|
+
* test must gain for `added` routes (#8704). Empty unless that test is
|
|
134
|
+
* recorded and site-edited — an untouched copy is `create-ailk update`'s to
|
|
135
|
+
* bring forward, and an unrecorded one cannot be told apart from an edit.
|
|
136
|
+
*/
|
|
137
|
+
function structuredDataEntriesFor(targetAbs, templatesDir, added) {
|
|
138
|
+
const sitePath = join(targetAbs, STRUCTURED_DATA_TEST_PATH);
|
|
139
|
+
const templatePath = join(templatesDir, STRUCTURED_DATA_TEST_PATH);
|
|
140
|
+
const recordPath = join(targetAbs, TEMPLATE_RECORD_PATH);
|
|
141
|
+
if (added.length === 0 || !existsSync(sitePath) || !existsSync(templatePath) || !existsSync(recordPath)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
let recordedHash;
|
|
145
|
+
try {
|
|
146
|
+
recordedHash = parseTemplateRecord(readFileSync(recordPath, "utf8")).files[STRUCTURED_DATA_TEST_PATH]?.sha256;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const siteBytes = readFileSync(sitePath);
|
|
152
|
+
if (recordedHash === undefined || sha256Hex(siteBytes) === recordedHash)
|
|
153
|
+
return [];
|
|
154
|
+
const templateExemptions = parseStructuredDataExemptions(readFileSync(templatePath, "utf8"));
|
|
155
|
+
const siteExemptions = parseStructuredDataExemptions(siteBytes.toString("utf8"));
|
|
156
|
+
const byEntry = new Map();
|
|
157
|
+
for (const route of [...added].sort()) {
|
|
158
|
+
const needed = exemptionFor(templateExemptions, route);
|
|
159
|
+
if (needed === undefined || exemptionFor(siteExemptions, route) !== undefined)
|
|
160
|
+
continue;
|
|
161
|
+
byEntry.set(needed.text, [...(byEntry.get(needed.text) ?? []), route]);
|
|
162
|
+
}
|
|
163
|
+
return [...byEntry.entries()]
|
|
164
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
165
|
+
.map(([entry, routes]) => ({ routes, entry }));
|
|
166
|
+
}
|
|
53
167
|
/**
|
|
54
168
|
* The template's route directories, ROUTE_ROOT-relative and sorted.
|
|
55
169
|
*
|
|
@@ -276,6 +390,7 @@ export function syncRoutes(opts) {
|
|
|
276
390
|
appliedDependencies,
|
|
277
391
|
missingMessageNamespaces,
|
|
278
392
|
skippedSurfaces,
|
|
393
|
+
structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
|
|
279
394
|
};
|
|
280
395
|
}
|
|
281
396
|
/**
|
|
@@ -355,6 +470,13 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
355
470
|
lines.push(`[create-ailk] Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):`);
|
|
356
471
|
lines.push(...result.missingMessageNamespaces.map((ns) => ` ${ns}`));
|
|
357
472
|
}
|
|
473
|
+
if (result.structuredDataEntries.length > 0) {
|
|
474
|
+
lines.push(`[create-ailk] Also required — ${STRUCTURED_DATA_TEST_PATH} is edited here, so add these NO_STRUCTURED_DATA_ROUTES entries to it by hand (not written):`);
|
|
475
|
+
for (const { routes, entry } of result.structuredDataEntries) {
|
|
476
|
+
lines.push(` for ${routes.join(", ")}:`);
|
|
477
|
+
lines.push(...entry.split("\n").map((line) => ` ${line}`));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
358
480
|
if (result.missing.length > 0 && !apply) {
|
|
359
481
|
lines.push("Run `create-ailk sync-routes <target> --apply` to copy the missing routes.");
|
|
360
482
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface UpdateResult {
|
|
|
31
31
|
path: string;
|
|
32
32
|
reason: string;
|
|
33
33
|
}>;
|
|
34
|
+
/** Per file left under review or skipped, the template tests importing it that stay at the site's version (#8704). */
|
|
35
|
+
heldBack: Array<{
|
|
36
|
+
file: string;
|
|
37
|
+
tests: string[];
|
|
38
|
+
}>;
|
|
34
39
|
backfilled?: {
|
|
35
40
|
version: string;
|
|
36
41
|
files: number;
|
|
@@ -43,6 +48,6 @@ export interface UpdateArgs {
|
|
|
43
48
|
target: string | undefined;
|
|
44
49
|
help: boolean;
|
|
45
50
|
}
|
|
46
|
-
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\" \u2014
|
|
51
|
+
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\". Every template test that imports a file\nleft under \"Needs review\" or \"Skipped\" (by a relative import) is held at the\nsite's version with it, its record entry is not advanced, and it is listed\nunder \"Held back with <file>\" \u2014 reconcile the file, then run update again to\nbring its tests forward. Every other file in the run still updates.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. Commit before running, and review\nthe diff after.\n";
|
|
47
52
|
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
|
48
53
|
export declare function parseUpdateArgs(argv: string[]): UpdateArgs;
|
package/dist/update.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* version's copy — read from this package's own `templates/` when that is
|
|
10
10
|
* the running version, else re-materialised from the published npm tarball
|
|
11
11
|
* (`lib/fetch-template-snapshot.ts`) — and the running version's copy. A
|
|
12
|
-
* conflict leaves the file untouched and is listed under "Needs review".
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* conflict leaves the file untouched and is listed under "Needs review". Every
|
|
13
|
+
* template test that imports a file left under review or skipped is held at
|
|
14
|
+
* the site's version with it, so a run never leaves a test newer than the
|
|
15
|
+
* code it exercises (#8704). A site with no record gets one backfilled from
|
|
16
|
+
* `.ailk-site-manifest.json`'s `patternVersion` on its first run, then
|
|
17
|
+
* updates normally. Every refusal throws before the first write.
|
|
16
18
|
*
|
|
17
19
|
* Pure module: no `import.meta.url` — the CLI supplies `templatesDir`, the
|
|
18
20
|
* `adopt.ts` / `add-route.ts` split.
|
|
@@ -132,6 +134,50 @@ function containsKnownPath(prefix, knownPaths) {
|
|
|
132
134
|
}
|
|
133
135
|
return false;
|
|
134
136
|
}
|
|
137
|
+
/** A template test file: a `*.test.*` / `*.spec.*` module, or anything under `__tests__/`. */
|
|
138
|
+
const TEST_FILE = /(^|\/)__tests__\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
139
|
+
/** Module specifiers `import`/`export … from`, `import()`, `require()` and `jest.mock()` name. */
|
|
140
|
+
const IMPORT_SPECIFIER = /\bfrom\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\b(?:import|require|jest\.mock|jest\.requireActual)\s*\(\s*["']([^"']+)["']/g;
|
|
141
|
+
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
142
|
+
/**
|
|
143
|
+
* The record-style paths a relative specifier in `fromPath` can resolve to:
|
|
144
|
+
* the path as written, with each source extension, a `.js`-family suffix
|
|
145
|
+
* swapped for its TypeScript source, and as a directory's `index.*`.
|
|
146
|
+
*/
|
|
147
|
+
function relativeImportCandidates(fromPath, specifier) {
|
|
148
|
+
const parts = fromPath.split("/").slice(0, -1);
|
|
149
|
+
for (const segment of specifier.split("/")) {
|
|
150
|
+
if (segment === "" || segment === ".")
|
|
151
|
+
continue;
|
|
152
|
+
if (segment === "..")
|
|
153
|
+
parts.pop();
|
|
154
|
+
else
|
|
155
|
+
parts.push(segment);
|
|
156
|
+
}
|
|
157
|
+
const target = parts.join("/");
|
|
158
|
+
const out = [target, ...RESOLVE_EXTENSIONS.map((ext) => `${target}${ext}`)];
|
|
159
|
+
const jsSuffix = /\.([cm]?)jsx?$/.exec(target);
|
|
160
|
+
if (jsSuffix !== null) {
|
|
161
|
+
const stem = target.slice(0, -jsSuffix[0].length);
|
|
162
|
+
out.push(`${stem}.${jsSuffix[1]}ts`, `${stem}.tsx`);
|
|
163
|
+
}
|
|
164
|
+
out.push(...RESOLVE_EXTENSIONS.map((ext) => `${target}/index${ext}`));
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
/** The files of `held` that `text`, the source of `fromPath`, imports by relative specifier. */
|
|
168
|
+
function importedHeldFiles(fromPath, text, held) {
|
|
169
|
+
const out = new Set();
|
|
170
|
+
for (const match of text.matchAll(IMPORT_SPECIFIER)) {
|
|
171
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
172
|
+
if (specifier === undefined || !specifier.startsWith("."))
|
|
173
|
+
continue;
|
|
174
|
+
for (const candidate of relativeImportCandidates(fromPath, specifier)) {
|
|
175
|
+
if (held.has(candidate))
|
|
176
|
+
out.add(candidate);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [...out];
|
|
180
|
+
}
|
|
135
181
|
export async function updateSite(opts) {
|
|
136
182
|
const targetDir = resolve(opts.targetDir);
|
|
137
183
|
const version = opts.version;
|
|
@@ -391,37 +437,85 @@ export async function updateSite(opts) {
|
|
|
391
437
|
nextFiles[path] = { sha256: sha256Hex(newBytes), version };
|
|
392
438
|
added.push(path);
|
|
393
439
|
}
|
|
440
|
+
// ── Hold back the template tests of every file left alone (#8704) ────
|
|
441
|
+
// A test this run would move forward (update, merge, add, remove, or
|
|
442
|
+
// advance as current) that imports a file under review or skipped stays
|
|
443
|
+
// exactly as the site has it, and its row keeps its recorded version — so
|
|
444
|
+
// the tests on disk always match the code they exercise. The imports are
|
|
445
|
+
// read from both the site's copy and the running template's: either one
|
|
446
|
+
// exercising the held file is enough. A held test-support file
|
|
447
|
+
// (`__tests__/fixtures.ts`) holds the tests importing it in turn, until no
|
|
448
|
+
// further test is held.
|
|
449
|
+
const heldFiles = new Set([...review, ...skipped].map((row) => row.path));
|
|
450
|
+
const heldBackByFile = new Map();
|
|
451
|
+
const heldTests = new Set();
|
|
452
|
+
const moving = [...updated, ...merged, ...added, ...removed, ...current]
|
|
453
|
+
.filter((path) => TEST_FILE.test(path))
|
|
454
|
+
.sort();
|
|
455
|
+
for (let grew = heldFiles.size > 0; grew;) {
|
|
456
|
+
grew = false;
|
|
457
|
+
for (const path of moving) {
|
|
458
|
+
if (heldTests.has(path))
|
|
459
|
+
continue;
|
|
460
|
+
const sources = [];
|
|
461
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
462
|
+
if (existsSync(abs))
|
|
463
|
+
sources.push(readFileSync(abs, "utf8"));
|
|
464
|
+
const newBytes = newMap.get(path);
|
|
465
|
+
if (newBytes !== undefined)
|
|
466
|
+
sources.push(Buffer.from(newBytes).toString("utf8"));
|
|
467
|
+
const imported = new Set(sources.flatMap((text) => importedHeldFiles(path, text, heldFiles)));
|
|
468
|
+
if (imported.size === 0)
|
|
469
|
+
continue;
|
|
470
|
+
heldTests.add(path);
|
|
471
|
+
heldFiles.add(path);
|
|
472
|
+
grew = true;
|
|
473
|
+
for (const file of imported) {
|
|
474
|
+
heldBackByFile.set(file, [...(heldBackByFile.get(file) ?? []), path]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const path of heldTests) {
|
|
479
|
+
const recorded = record.files[path];
|
|
480
|
+
if (recorded !== undefined)
|
|
481
|
+
nextFiles[path] = recorded;
|
|
482
|
+
else
|
|
483
|
+
delete nextFiles[path];
|
|
484
|
+
}
|
|
485
|
+
const notHeld = (path) => !heldTests.has(path);
|
|
486
|
+
const heldBack = [...heldBackByFile.entries()]
|
|
487
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
488
|
+
.map(([file, tests]) => ({ file, tests }));
|
|
394
489
|
// ── Apply: writes and removals, then the record (D4 Order) ───────────
|
|
395
|
-
for (const write of writes) {
|
|
490
|
+
for (const write of writes.filter((w) => notHeld(w.path))) {
|
|
396
491
|
const abs = join(targetDir, ...write.path.split("/"));
|
|
397
492
|
mkdirSync(dirname(abs), { recursive: true });
|
|
398
493
|
writeFileSync(abs, write.bytes);
|
|
399
494
|
}
|
|
400
|
-
for (const path of deletes) {
|
|
495
|
+
for (const path of deletes.filter(notHeld)) {
|
|
401
496
|
rmSync(join(targetDir, ...path.split("/")));
|
|
402
497
|
}
|
|
403
498
|
const backfilled = backfillPatternVersion !== undefined
|
|
404
499
|
? { version: backfillPatternVersion, files: Object.keys(record.files).length }
|
|
405
500
|
: undefined;
|
|
406
|
-
const
|
|
407
|
-
updated.
|
|
408
|
-
merged.
|
|
409
|
-
added.
|
|
410
|
-
removed.
|
|
411
|
-
current.
|
|
501
|
+
const moved = {
|
|
502
|
+
updated: updated.filter(notHeld),
|
|
503
|
+
merged: merged.filter(notHeld),
|
|
504
|
+
added: added.filter(notHeld),
|
|
505
|
+
removed: removed.filter(notHeld),
|
|
506
|
+
current: current.filter(notHeld),
|
|
507
|
+
};
|
|
508
|
+
const recordChanged = backfilled !== undefined || Object.values(moved).some((paths) => paths.length > 0);
|
|
412
509
|
if (recordChanged) {
|
|
413
510
|
writeFileSync(recordPath, serializeTemplateRecord({ schema: 1, projectName: record.projectName, files: nextFiles }));
|
|
414
511
|
}
|
|
415
512
|
return {
|
|
416
513
|
targetDir,
|
|
417
514
|
version,
|
|
418
|
-
|
|
419
|
-
merged,
|
|
420
|
-
added,
|
|
421
|
-
removed,
|
|
422
|
-
current,
|
|
515
|
+
...moved,
|
|
423
516
|
review,
|
|
424
517
|
skipped,
|
|
518
|
+
heldBack,
|
|
425
519
|
...(backfilled !== undefined ? { backfilled } : {}),
|
|
426
520
|
};
|
|
427
521
|
}
|
|
@@ -448,6 +542,10 @@ export function formatUpdateReport(result) {
|
|
|
448
542
|
lines.push(`Needs review (${result.review.length}):`);
|
|
449
543
|
lines.push(...result.review.map((row) => ` ${row.path} — ${row.reason}`));
|
|
450
544
|
}
|
|
545
|
+
for (const { file, tests } of result.heldBack) {
|
|
546
|
+
lines.push(`Held back with ${file} (${tests.length}):`);
|
|
547
|
+
lines.push(...tests.map((path) => ` ${path}`));
|
|
548
|
+
}
|
|
451
549
|
if (result.skipped.length > 0) {
|
|
452
550
|
lines.push(`Skipped (${result.skipped.length}):`);
|
|
453
551
|
lines.push(...result.skipped.map((row) => ` ${row.path} — ${row.reason}`));
|
|
@@ -481,7 +579,11 @@ every template scaffold) to tell an untouched copy from an edit. An
|
|
|
481
579
|
untouched apps/web, apps/api or apps/mcp file whose template copy changed is
|
|
482
580
|
overwritten. An edited file whose template copy also changed is three-way
|
|
483
581
|
merged with \`git\` (git must be on PATH); a conflict leaves the file alone
|
|
484
|
-
and is listed under "Needs review"
|
|
582
|
+
and is listed under "Needs review". Every template test that imports a file
|
|
583
|
+
left under "Needs review" or "Skipped" (by a relative import) is held at the
|
|
584
|
+
site's version with it, its record entry is not advanced, and it is listed
|
|
585
|
+
under "Held back with <file>" — reconcile the file, then run update again to
|
|
586
|
+
bring its tests forward. Every other file in the run still updates.
|
|
485
587
|
A site with no record gets one on its first run, from the version its
|
|
486
588
|
.ailk-site-manifest.json already records. Commit before running, and review
|
|
487
589
|
the diff after.
|