Skip to content

Commit cbc4d87

Browse files
fix: address all 5 bugs plus design/feature improvements across engine and shared packages
Co-authored-by: connortessaro <116526628+connortessaro@users.noreply.github.com> Agent-Logs-Url: https://github.com/connortessaro/shopify-web-replicator/sessions/c23ba258-cfae-4305-bc3f-445838cf5c17
1 parent e15083c commit cbc4d87

File tree

12 files changed

+418
-204
lines changed

12 files changed

+418
-204
lines changed

apps/api/src/services/integration-report-generator.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe("ShopifyIntegrationReportGenerator", () => {
153153
summary: "All deterministic integration checks passed for Example Storefront."
154154
});
155155
await expect(readFile(join(themeRoot, "config/generated-integration-report.json"), "utf8")).resolves.toContain(
156-
"\"generated_artifacts\""
156+
"\"generatedArtifacts\""
157157
);
158158
});
159159

apps/api/src/services/replication-pipeline.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,84 @@ describe("ReplicationPipeline", () => {
556556
])
557557
});
558558
});
559+
560+
it("still writes the integration report when validation fails (Bug 3)", async () => {
561+
const dataRoot = await mkdtemp(join(tmpdir(), "shopify-web-replicator-jobs-"));
562+
const themeRoot = await mkdtemp(join(tmpdir(), "shopify-web-replicator-theme-"));
563+
tempDirectories.push(dataRoot, themeRoot);
564+
565+
const repository = new SqliteJobRepository(join(dataRoot, "replicator.db"));
566+
const job = createReplicationJob({
567+
referenceUrl: "https://example.com",
568+
notes: "Landing page MVP"
569+
});
570+
571+
await repository.save(job);
572+
573+
const pipeline = new ReplicationPipeline({
574+
repository,
575+
analyzer: {
576+
async analyze({ referenceUrl, notes }) {
577+
return {
578+
sourceUrl: referenceUrl,
579+
pageType: "landing_page",
580+
title: "Example Storefront",
581+
summary: notes
582+
? `Prepared deterministic analysis for Example Storefront. Operator notes: ${notes}`
583+
: "Prepared deterministic analysis for Example Storefront.",
584+
analyzedAt: "2026-03-20T12:01:00.000Z",
585+
recommendedSections: ["hero", "cta"]
586+
} satisfies ReferenceAnalysis;
587+
}
588+
},
589+
mapper: {
590+
async map({ analysis, referenceUrl, notes }) {
591+
return {
592+
sourceUrl: referenceUrl,
593+
title: analysis.title,
594+
summary: notes
595+
? `Mapped ${analysis.title} into the stable generated reference section. Operator notes: ${notes}`
596+
: `Mapped ${analysis.title} into the stable generated reference section.`,
597+
mappedAt: "2026-03-20T12:02:00.000Z",
598+
templatePath: "templates/page.generated-reference.json",
599+
sectionPath: "sections/generated-reference.liquid",
600+
sections: [
601+
{
602+
id: "hero-1",
603+
type: "hero",
604+
heading: "Example heading",
605+
body: "Example body copy",
606+
ctaLabel: "Review generated output",
607+
ctaHref: "/pages/generated-reference"
608+
}
609+
]
610+
} satisfies ThemeMapping;
611+
}
612+
},
613+
generator: new ShopifyThemeGenerator(themeRoot),
614+
storeSetupGenerator: new ShopifyStoreSetupGenerator(themeRoot),
615+
commerceGenerator: new ShopifyCommerceWiringGenerator(themeRoot),
616+
integrationGenerator: new ShopifyIntegrationReportGenerator(themeRoot),
617+
themeValidator: {
618+
async validate() {
619+
return {
620+
status: "failed",
621+
summary: "Theme check failed: missing required liquid tag.",
622+
checkedAt: "2026-03-20T12:03:00.000Z"
623+
} satisfies ThemeCheckResult;
624+
}
625+
}
626+
});
627+
628+
await pipeline.process(job.id);
629+
630+
const savedJob = await repository.getById(job.id);
631+
632+
expect(savedJob?.status).toBe("failed");
633+
expect(savedJob?.integration).toBeDefined();
634+
635+
await expect(
636+
readFile(join(themeRoot, "config/generated-integration-report.json"), "utf8")
637+
).resolves.toContain("\"commerce_snippet_reference\"");
638+
});
559639
});

apps/api/src/services/theme-generator.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ describe("ShopifyThemeGenerator", () => {
8484
await expect(readFile(join(themeRoot, "templates/page.generated-reference.json"), "utf8")).resolves.toContain(
8585
'"type": "generated-reference"'
8686
);
87+
await expect(readFile(join(themeRoot, "templates/page.generated-reference.json"), "utf8")).resolves.not.toContain(
88+
'"mapping_summary"'
89+
);
8790
});
8891

8992
it("writes product-page outputs with a product form into product-specific stable files", async () => {
@@ -242,4 +245,102 @@ describe("ShopifyThemeGenerator", () => {
242245
'"type": "generated-reference"'
243246
);
244247
});
248+
249+
it("guards the product-page eyebrow with a blank check, includes a product image, and omits mapping_summary", async () => {
250+
const themeRoot = await mkdtemp(join(tmpdir(), "shopify-web-replicator-theme-"));
251+
tempDirectories.push(themeRoot);
252+
253+
const generator = new ShopifyThemeGenerator(themeRoot);
254+
const analysis: ReferenceAnalysis = {
255+
sourceUrl: "https://example.com/products/trail-pack",
256+
referenceHost: "example.com",
257+
pageType: "product_page",
258+
title: "Trail Pack",
259+
summary: "Product page summary.",
260+
analyzedAt: "2026-03-20T12:00:00.000Z",
261+
recommendedSections: ["product_detail"]
262+
};
263+
const mapping: ThemeMapping = {
264+
sourceUrl: "https://example.com/products/trail-pack",
265+
title: "Trail Pack",
266+
summary: "Product mapping summary.",
267+
mappedAt: "2026-03-20T12:01:00.000Z",
268+
templatePath: "templates/product.generated-reference.json",
269+
sectionPath: "sections/generated-product-reference.liquid",
270+
sections: [
271+
{
272+
id: "product-hero-1",
273+
type: "product_detail",
274+
heading: "Trail Pack",
275+
body: "Durable pack copy",
276+
ctaLabel: "Add to cart",
277+
ctaHref: "/cart"
278+
}
279+
]
280+
};
281+
282+
await generator.generate({ analysis, mapping });
283+
284+
const sectionContent = await readFile(join(themeRoot, "sections/generated-product-reference.liquid"), "utf8");
285+
const templateContent = await readFile(join(themeRoot, "templates/product.generated-reference.json"), "utf8");
286+
287+
// Bug 4 fix: eyebrow is wrapped in a blank check
288+
expect(sectionContent).toContain("{% if section.settings.eyebrow != blank %}");
289+
290+
// Feature 2 fix: product image is rendered
291+
expect(sectionContent).toContain("product.featured_image");
292+
293+
// Feature 1 fix: mapping_summary is absent from section HTML and schema settings
294+
expect(sectionContent).not.toContain("section.settings.mapping_summary");
295+
expect(sectionContent).not.toContain('"mapping_summary"');
296+
297+
// Feature 1 fix: mapping_summary is absent from the template JSON
298+
expect(templateContent).not.toContain('"mapping_summary"');
299+
});
300+
301+
it("renders collection-page product cards as linked <a> elements and omits mapping_summary", async () => {
302+
const themeRoot = await mkdtemp(join(tmpdir(), "shopify-web-replicator-theme-"));
303+
tempDirectories.push(themeRoot);
304+
305+
const generator = new ShopifyThemeGenerator(themeRoot);
306+
const analysis: ReferenceAnalysis = {
307+
sourceUrl: "https://example.com/collections/summer-gear",
308+
referenceHost: "example.com",
309+
pageType: "collection_page",
310+
title: "Summer Gear",
311+
summary: "Collection page summary.",
312+
analyzedAt: "2026-03-20T12:00:00.000Z",
313+
recommendedSections: ["collection_grid"]
314+
};
315+
const mapping: ThemeMapping = {
316+
sourceUrl: "https://example.com/collections/summer-gear",
317+
title: "Summer Gear",
318+
summary: "Collection mapping summary.",
319+
mappedAt: "2026-03-20T12:01:00.000Z",
320+
templatePath: "templates/collection.generated-reference.json",
321+
sectionPath: "sections/generated-collection-reference.liquid",
322+
sections: [
323+
{
324+
id: "collection-1",
325+
type: "collection_grid",
326+
heading: "Summer Gear",
327+
body: "Collection copy"
328+
}
329+
]
330+
};
331+
332+
await generator.generate({ analysis, mapping });
333+
334+
const sectionContent = await readFile(join(themeRoot, "sections/generated-collection-reference.liquid"), "utf8");
335+
const templateContent = await readFile(join(themeRoot, "templates/collection.generated-reference.json"), "utf8");
336+
337+
// Bug 5 fix: cards use <a href="{{ product.url }}"> instead of <article>
338+
expect(sectionContent).toContain('<a class="generated-collection-reference__card" href="{{ product.url }}">');
339+
expect(sectionContent).not.toContain("<article");
340+
341+
// Feature 1 fix: mapping_summary is absent from section HTML, schema settings, and template JSON
342+
expect(sectionContent).not.toContain("section.settings.mapping_summary");
343+
expect(sectionContent).not.toContain('"mapping_summary"');
344+
expect(templateContent).not.toContain('"mapping_summary"');
345+
});
245346
});

packages/engine/src/orchestrator.ts

Lines changed: 46 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import type {
22
AppRuntimeConfig,
3-
PageType,
4-
ReferenceAnalysis,
53
ReferenceIntake,
64
ReplicationJob,
7-
ReplicationJobSummary,
8-
StoreSetupPlan,
9-
ThemeCheckResult,
10-
ThemeMapping
5+
ReplicationJobSummary
116
} from "@shopify-web-replicator/shared";
127
import { createReplicationJob } from "@shopify-web-replicator/shared";
138

@@ -22,64 +17,7 @@ import { ShopifyStoreSetupGenerator } from "./services/store-setup-generator.js"
2217
import { DeterministicThemeMapper } from "./services/theme-mapper.js";
2318
import { ShopifyThemeGenerator } from "./services/theme-generator.js";
2419
import { ShopifyThemeValidator } from "./services/theme-validator.js";
25-
26-
type Analyzer = {
27-
analyze(input: { referenceUrl: string; pageType?: PageType; notes?: string }): Promise<ReferenceAnalysis>;
28-
};
29-
30-
type Mapper = {
31-
map(input: { analysis: ReferenceAnalysis; referenceUrl: string; notes?: string }): Promise<ThemeMapping>;
32-
};
33-
34-
type Generator = {
35-
generate(input: {
36-
analysis: ReferenceAnalysis;
37-
mapping: ThemeMapping;
38-
}): Promise<{
39-
artifacts: ReplicationJob["artifacts"];
40-
generation: NonNullable<ReplicationJob["generation"]>;
41-
}>;
42-
};
43-
44-
type StoreSetupGenerator = {
45-
generate(input: {
46-
analysis: ReferenceAnalysis;
47-
mapping: ThemeMapping;
48-
}): Promise<{
49-
artifact: ReplicationJob["artifacts"][number];
50-
storeSetup: StoreSetupPlan;
51-
}>;
52-
};
53-
54-
type CommerceGenerator = {
55-
generate(input: {
56-
analysis: ReferenceAnalysis;
57-
mapping: ThemeMapping;
58-
storeSetup: StoreSetupPlan;
59-
}): Promise<{
60-
artifact: ReplicationJob["artifacts"][number];
61-
commerce: NonNullable<ReplicationJob["commerce"]>;
62-
}>;
63-
};
64-
65-
type IntegrationGenerator = {
66-
generate(input: {
67-
analysis: ReferenceAnalysis;
68-
mapping: ThemeMapping;
69-
generation: NonNullable<ReplicationJob["generation"]>;
70-
storeSetup: StoreSetupPlan;
71-
commerce: NonNullable<ReplicationJob["commerce"]>;
72-
artifacts: ReplicationJob["artifacts"];
73-
validation: ThemeCheckResult;
74-
}): Promise<{
75-
artifact: ReplicationJob["artifacts"][number];
76-
integration: NonNullable<ReplicationJob["integration"]>;
77-
}>;
78-
};
79-
80-
type ThemeValidator = {
81-
validate(): Promise<ThemeCheckResult>;
82-
};
20+
import type { Analyzer, CommerceGenerator, Generator, IntegrationGenerator, Mapper, StoreSetupGenerator, ThemeValidator } from "./services/types.js";
8321

8422
export interface ReplicationHandoff {
8523
job: ReplicationJob;
@@ -169,6 +107,50 @@ export class ReplicationOrchestrator {
169107
return job;
170108
}
171109

110+
async retryJob(jobId: string): Promise<ReplicationHandoff> {
111+
const job = await this.repository.getById(jobId);
112+
113+
if (!job) {
114+
throw new Error(`Missing job ${jobId}`);
115+
}
116+
117+
if (job.status !== "failed") {
118+
throw new Error(`Job ${jobId} is not in a failed state`);
119+
}
120+
121+
for (const stage of job.stages) {
122+
if (stage.status === "failed") {
123+
stage.status = "pending";
124+
delete stage.errorMessage;
125+
delete stage.summary;
126+
}
127+
}
128+
129+
const firstPendingStage = job.stages.find((stage) => stage.status === "pending");
130+
if (firstPendingStage) {
131+
firstPendingStage.status = "current";
132+
job.currentStage = firstPendingStage.name;
133+
}
134+
135+
job.status = "in_progress";
136+
delete job.error;
137+
138+
job.artifacts = job.artifacts.map((artifact) =>
139+
artifact.status === "failed" ? { ...artifact, status: "pending" } : artifact
140+
);
141+
142+
job.updatedAt = new Date().toISOString();
143+
await this.repository.save(job);
144+
145+
const updatedJob = await this.runJob(jobId);
146+
147+
return {
148+
job: updatedJob,
149+
runtime: this.#runtime,
150+
nextActions: buildNextActions(updatedJob)
151+
};
152+
}
153+
172154
async replicateStorefront(intake: ReferenceIntake): Promise<ReplicationHandoff> {
173155
const created = await this.createJob(intake);
174156
const job = await this.runJob(created.jobId);

packages/engine/src/services/integration-report-generator.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,25 +225,25 @@ export class ShopifyIntegrationReportGenerator {
225225
outputPath,
226226
`${JSON.stringify(
227227
{
228-
generated_by: "Shopify Web Replicator",
229-
checked_at: checkedAt,
230-
source_url: analysis.sourceUrl,
231-
page_type: analysis.pageType,
228+
generatedBy: "Shopify Web Replicator",
229+
checkedAt: checkedAt,
230+
sourceUrl: analysis.sourceUrl,
231+
pageType: analysis.pageType,
232232
title: analysis.title,
233233
summary: integration.summary,
234-
mapping_summary: mapping.summary,
235-
section_path: generation.sectionPath,
236-
template_path: generation.templatePath,
237-
store_setup_config_path: storeSetup.configPath,
238-
commerce_snippet_path: commerce.snippetPath,
239-
generated_artifacts: artifacts.map((artifact) => ({
234+
mappingSummary: mapping.summary,
235+
sectionPath: generation.sectionPath,
236+
templatePath: generation.templatePath,
237+
storeSetupConfigPath: storeSetup.configPath,
238+
commerceSnippetPath: commerce.snippetPath,
239+
generatedArtifacts: artifacts.map((artifact) => ({
240240
kind: artifact.kind,
241241
path: artifact.path,
242242
status: artifact.status,
243243
description: artifact.description,
244-
last_written_at: artifact.lastWrittenAt
244+
lastWrittenAt: artifact.lastWrittenAt
245245
})),
246-
commerce_entrypoints: commerce.entrypoints,
246+
commerceEntrypoints: commerce.entrypoints,
247247
validation,
248248
checks
249249
},

0 commit comments

Comments
 (0)