@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.273

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.
Files changed (132) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +4 -3
  3. package/src/lib/activity/ai-activity-actions.js +3 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +18 -0
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
  8. package/src/lib/components/ai-site-import-job.component.js +707 -0
  9. package/src/lib/components/ai-site-import-job.component.js.map +1 -0
  10. package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
  11. package/src/lib/components/ai-site-import-start.component.js +335 -0
  12. package/src/lib/components/ai-site-import-start.component.js.map +1 -0
  13. package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
  14. package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
  15. package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
  16. package/src/lib/components/ai-site-start-card.component.js +329 -252
  17. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  18. package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
  19. package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
  20. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
  21. package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
  22. package/src/lib/jobs/ai-job-drafts.js +24 -0
  23. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  24. package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
  25. package/src/lib/jobs/ai-job-import-step.js +809 -0
  26. package/src/lib/jobs/ai-job-import-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
  28. package/src/lib/jobs/ai-job-page-language.js +62 -4
  29. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  30. package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
  31. package/src/lib/jobs/ai-job-page-step.js +26 -4
  32. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-seo-step.js +8 -4
  34. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
  36. package/src/lib/jobs/ai-job-site-datasets.js +111 -15
  37. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  38. package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
  39. package/src/lib/jobs/ai-job-site-step.js +85 -11
  40. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  41. package/src/lib/jobs/ai-job-text-step.d.ts +15 -0
  42. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  43. package/src/lib/jobs/ai-jobs.d.ts +8 -0
  44. package/src/lib/jobs/ai-jobs.js +19 -3
  45. package/src/lib/jobs/ai-jobs.js.map +1 -1
  46. package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
  47. package/src/lib/jobs/ai-layout-owner-photos.js +49 -2
  48. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
  50. package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
  53. package/src/lib/jobs/ai-site-late-photos.js +486 -0
  54. package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
  55. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  56. package/src/lib/jobs/ai-site-publish.js +123 -5
  57. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  58. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
  59. package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
  60. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  61. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
  62. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
  63. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
  64. package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
  65. package/src/lib/layout-language/ai-layout-pictures.js +81 -19
  66. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  67. package/src/lib/model/ai-credit-estimate.d.ts +20 -0
  68. package/src/lib/model/ai-credit-estimate.js +13 -0
  69. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  70. package/src/lib/model/ai-job-activity.d.ts +2 -2
  71. package/src/lib/model/ai-job-activity.js +6 -4
  72. package/src/lib/model/ai-job-activity.js.map +1 -1
  73. package/src/lib/model/ai-job-notice.js +18 -4
  74. package/src/lib/model/ai-job-notice.js.map +1 -1
  75. package/src/lib/model/ai-jobs.types.d.ts +39 -2
  76. package/src/lib/model/ai-jobs.types.js +2 -1
  77. package/src/lib/model/ai-jobs.types.js.map +1 -1
  78. package/src/lib/model/ai-site-import-view.d.ts +130 -0
  79. package/src/lib/model/ai-site-import-view.js +182 -0
  80. package/src/lib/model/ai-site-import-view.js.map +1 -0
  81. package/src/lib/model/ai-site-import.d.ts +302 -0
  82. package/src/lib/model/ai-site-import.js +526 -0
  83. package/src/lib/model/ai-site-import.js.map +1 -0
  84. package/src/lib/model/ai-site-photos.d.ts +119 -0
  85. package/src/lib/model/ai-site-photos.js +128 -0
  86. package/src/lib/model/ai-site-photos.js.map +1 -0
  87. package/src/lib/model/ai-site-start.d.ts +22 -9
  88. package/src/lib/model/ai-site-start.js +64 -23
  89. package/src/lib/model/ai-site-start.js.map +1 -1
  90. package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
  91. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  92. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  93. package/src/lib/runtime/seo-fields.d.ts +29 -4
  94. package/src/lib/runtime/seo-fields.js +58 -11
  95. package/src/lib/runtime/seo-fields.js.map +1 -1
  96. package/src/lib/server/ai-jobs-route.js +11 -0
  97. package/src/lib/server/ai-jobs-route.js.map +1 -1
  98. package/src/lib/server/ai-site-import-route.d.ts +89 -0
  99. package/src/lib/server/ai-site-import-route.js +356 -0
  100. package/src/lib/server/ai-site-import-route.js.map +1 -0
  101. package/src/lib/server.js +14 -0
  102. package/src/lib/server.js.map +1 -1
  103. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
  104. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
  105. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
  106. package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
  107. package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
  108. package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
  109. package/src/lib/site-import/site-import-crawl.d.ts +163 -0
  110. package/src/lib/site-import/site-import-crawl.js +467 -0
  111. package/src/lib/site-import/site-import-crawl.js.map +1 -0
  112. package/src/lib/site-import/site-import-flag.d.ts +23 -0
  113. package/src/lib/site-import/site-import-flag.js +23 -0
  114. package/src/lib/site-import/site-import-flag.js.map +1 -0
  115. package/src/lib/site-import/site-import-html.d.ts +94 -0
  116. package/src/lib/site-import/site-import-html.js +462 -0
  117. package/src/lib/site-import/site-import-html.js.map +1 -0
  118. package/src/lib/site-import/site-import-images.d.ts +54 -0
  119. package/src/lib/site-import/site-import-images.js +204 -0
  120. package/src/lib/site-import/site-import-images.js.map +1 -0
  121. package/src/lib/site-import/site-import-page.d.ts +48 -0
  122. package/src/lib/site-import/site-import-page.js +150 -0
  123. package/src/lib/site-import/site-import-page.js.map +1 -0
  124. package/src/lib/site-import/site-import-review.d.ts +40 -0
  125. package/src/lib/site-import/site-import-review.js +149 -0
  126. package/src/lib/site-import/site-import-review.js.map +1 -0
  127. package/src/lib/site-import/site-import-robots.d.ts +52 -0
  128. package/src/lib/site-import/site-import-robots.js +126 -0
  129. package/src/lib/site-import/site-import-robots.js.map +1 -0
  130. package/src/lib/tools/ai-seo-tool.d.ts +25 -1
  131. package/src/lib/tools/ai-seo-tool.js +61 -0
  132. package/src/lib/tools/ai-seo-tool.js.map +1 -1
@@ -29,7 +29,7 @@ import { aiModelForStep } from "../providers/routing.js";
29
29
  import { registerAiJobAdmission } from "./ai-job-admission.js";
30
30
  import { aiOriginJobId, aiRecordedJobDraftId } from "./ai-job-draft-ids.js";
31
31
  import { readAiDraftNodes } from "./ai-job-drafts.js";
32
- import { AI_STOCK_PHOTO_AVOID_INPUT, AI_STOCK_PHOTO_JOB_PHOTOS_INPUT, AI_STOCK_PHOTO_PAGES_INPUT } from "./ai-layout-stock-photos.js";
32
+ import { AI_STOCK_PHOTO_AVOID_INPUT, AI_STOCK_PHOTO_JOB_PHOTOS_INPUT, AI_STOCK_PHOTO_LATE_PASS_INPUT, AI_STOCK_PHOTO_PAGES_INPUT } from "./ai-layout-stock-photos.js";
33
33
  import { AI_LAYOUT_SITE_ALIASES_INPUT, AI_LAYOUT_SITE_PAGES_INPUT, AI_LAYOUT_SITE_PAGES_MAX, aiLayoutIsHomeSlug, aiLayoutSitePagesOfPlan } from "./ai-job-layout-site-pages.js";
34
34
  import { aiPageSectionNodeId } from "./ai-job-page-sections.js";
35
35
  import { AI_LAYOUT_LISTINGS_INPUT, aiLayoutIsShopPage } from "../layout-language/ai-layout-listings.js";
@@ -47,6 +47,8 @@ import { AI_FORM_DATASET_MADE_INPUT } from "./ai-job-form-dataset.js";
47
47
  import { aiConfirmedPlan, aiUnspentOutcome } from "./ai-job-generation.js";
48
48
  import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
49
49
  import { AI_JOB_PAGE_STEP_MINIMUM_MS } from "./ai-job-page-budget.js";
50
+ import { aiSiteMergePhotos, aiSitePhotosOwed } from "../model/ai-site-photos.js";
51
+ import { AI_SITE_LATE_PHOTOS_MINIMUM_MS, aiSiteLatePhotos } from "./ai-site-late-photos.js";
50
52
  import { AI_SITE_INSPIRATION_SIGNALS_INPUT, aiSiteInspirationReleased } from "./ai-site-inspiration.js";
51
53
  import { aiSiteInspirationRefusal } from "../model/ai-site-inspiration.js";
52
54
  import { aiJobStepRunMinimumMs, aiJobStepRunnerFor, registerAiJobStep, registerAiJobStepPasses } from "./ai-jobs.js";
@@ -142,13 +144,18 @@ import { aiJobStepRunMinimumMs, aiJobStepRunnerFor, registerAiJobStep, registerA
142
144
  export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to build.';
143
145
  /** What a scaffold answers where the step that builds a page is not loaded. */ export const AI_SITE_NO_PAGE_STEP_COPY = 'Building a whole site is not available yet.';
144
146
  /** A unit that finished without reporting what it built; the scaffold cannot go on. */ export const AI_SITE_UNIT_EMPTY_COPY = 'Part of this site could not be built. Try the site brief again.';
147
+ /**
148
+ * The passes the late photo pass may add (AGL-3988): the one that hands the
149
+ * publish to it, its own, and one more where a beat's time ran out on it.
150
+ * That last one publishes whatever the pictures hold (`latePasses`).
151
+ */ export const AI_SITE_LATE_PHOTOS_PASSES = 3;
145
152
  /**
146
153
  * The most passes a scaffold's step may take: the largest plan it admits —
147
154
  * eight pages of eight sections, its three creations, a blog's first posts
148
- * (one a pass, the largest part) and a welcome email — with nothing to
149
- * spare, so a runner that never finishes is still bounded while a real site
150
- * is not.
151
- */ export const AI_SITE_MAX_PASSES = AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + AI_SITE_DATASETS_MAX + AI_SITE_EVENTS_MAX + AI_SITE_POSTS + 2;
155
+ * (one a pass, the largest part), a welcome email and the late photo pass
156
+ * (AGL-3988) — with nothing to spare, so a runner that never finishes is
157
+ * still bounded while a real site is not.
158
+ */ export const AI_SITE_MAX_PASSES = AI_SITE_PAGES.max * (AI_SITE_MAX_SECTIONS + 1) + CREATION_UNITS.length + AI_SITE_DATASETS_MAX + AI_SITE_EVENTS_MAX + AI_SITE_POSTS + 2 + AI_SITE_LATE_PHOTOS_PASSES;
152
159
  /**
153
160
  * The units a plan implies, in build order: the palette first, because a
154
161
  * member reads it while the pages are still building; then the layout every
@@ -546,6 +553,8 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
546
553
  // Which a picture the library fails takes before any starter (AGL-3988).
547
554
  const recordPhotos = aiSiteDatasetPlacedPhotoDetails((_job_outputs4 = job.outputs) != null ? _job_outputs4 : []);
548
555
  if (recordPhotos.length) unitInputs[AI_STOCK_PHOTO_JOB_PHOTOS_INPUT] = recordPhotos;
556
+ // And that the site's late photo pass fills what none of them fits yet (AGL-3988).
557
+ if (job.kind === 'site') unitInputs[AI_STOCK_PHOTO_LATE_PASS_INPUT] = true;
549
558
  }
550
559
  // The layout is built before the pages, so it is told them (AGL-3596): their
551
560
  // ids are minted on the plan, and the platform writes the header's links.
@@ -906,6 +915,10 @@ export const aiSiteJobAdmission = createAiSiteJobAdmission();
906
915
  built: true
907
916
  };
908
917
  }
918
+ /** The photos state a site job holds once its late photo pass is done: nothing left (AGL-3988). */ const AI_SITE_PHOTOS_SETTLED = {
919
+ pages: [],
920
+ lists: []
921
+ };
909
922
  /** A page the scaffold reported built that holds none of its plan's sections. */ export const AI_SITE_PAGE_NOT_WRITTEN_COPY = 'A page of this site was not written from its plan. Try the site brief again.';
910
923
  /**
911
924
  * Whether a page unit's draft holds the sections its plan named, under the
@@ -1004,7 +1017,7 @@ export const aiSiteJobAdmission = createAiSiteJobAdmission();
1004
1017
  }).filter((unit)=>unit.kind === 'store' && ledgered('store') || unit.kind === 'theme' || unit.kind === 'posts' && ledgered('posts') || unit.kind === 'dataset' || unit.kind === 'event' || unit.kind !== 'store' && runnerFor(unit.jobKind));
1005
1018
  }
1006
1019
  export function createAiJobSiteStep(deps = {}) {
1007
- var _deps_runnerFor, _deps_readNodes, _deps_publish, _deps_siteName, _deps_look, _deps_contentRefusal, _deps_datasetRefusal, _deps_eventRefusal, _deps_storePagesRefusal, _deps_publishPosts, _deps_dropCache, _deps_restoreBlogEntry;
1020
+ var _deps_runnerFor, _deps_readNodes, _deps_publish, _deps_siteName, _deps_look, _deps_contentRefusal, _deps_datasetRefusal, _deps_eventRefusal, _deps_storePagesRefusal, _deps_publishPosts, _deps_dropCache, _deps_restoreBlogEntry, _deps_latePhotos;
1008
1021
  const runnerFor = (_deps_runnerFor = deps.runnerFor) != null ? _deps_runnerFor : aiJobStepRunnerFor;
1009
1022
  const readNodes = (_deps_readNodes = deps.readNodes) != null ? _deps_readNodes : readAiDraftNodes;
1010
1023
  const publish = (_deps_publish = deps.publish) != null ? _deps_publish : aiPublishGuidedSite;
@@ -1017,8 +1030,9 @@ export function createAiJobSiteStep(deps = {}) {
1017
1030
  const publishPosts = (_deps_publishPosts = deps.publishPosts) != null ? _deps_publishPosts : aiPublishSitePosts;
1018
1031
  const dropCache = (_deps_dropCache = deps.dropCache) != null ? _deps_dropCache : dropPluginSiteCache;
1019
1032
  const restoreBlogEntry = (_deps_restoreBlogEntry = deps.restoreBlogEntry) != null ? _deps_restoreBlogEntry : aiRestoreGuidedBlogEntry;
1033
+ const latePhotos = (_deps_latePhotos = deps.latePhotos) != null ? _deps_latePhotos : aiSiteLatePhotos;
1020
1034
  return async (context)=>{
1021
- var _ref, _job_outputs, _deps_posts, _deps_products, _deps_dataset, _deps_event, _deps_storePages, _job_outputs1;
1035
+ var _ref, _job_outputs, _job_sitePhotos, _deps_posts, _deps_products, _deps_dataset, _deps_event, _deps_storePages, _job_outputs1;
1022
1036
  var _job_items, _rows_get, _rows_get1, _rows_get2, _rows_get3;
1023
1037
  const { job } = context;
1024
1038
  // The scaffold asks no model of its own. What it names where it spends
@@ -1059,6 +1073,17 @@ export function createAiJobSiteStep(deps = {}) {
1059
1073
  ]));
1060
1074
  const next = aiBuildNextUnit(ledgerUnits, ledger);
1061
1075
  /**
1076
+ * The pictures the start left on a starter (AGL-3988): what the job
1077
+ * holds, with what this pass's unit reported merged in once it ran.
1078
+ */ let photos = (_job_sitePhotos = job.sitePhotos) != null ? _job_sitePhotos : null;
1079
+ let photosReported = false;
1080
+ /**
1081
+ * Whether the publish waits for the late photo pass: a record, or a
1082
+ * picture of a page it puts live, is still on a starter. A page that
1083
+ * failed is not published, so its pictures owe nothing. `pages`: the
1084
+ * pages built so far, the one this pass built among them.
1085
+ */ const latePhotosOwed = (pages)=>Boolean(job.hostId) && aiSitePhotosOwed(photos, pages.map((page)=>page.id));
1086
+ /**
1062
1087
  * A guided start's last pass puts what it built on the site (AGL-3596):
1063
1088
  * only its pages that were built. Once it has, Try again's last pass puts
1064
1089
  * only the pages the retry built live (AGL-3730), never the home address
@@ -1240,7 +1265,40 @@ export function createAiJobSiteStep(deps = {}) {
1240
1265
  unit.screen.id
1241
1266
  ] : [];
1242
1267
  });
1243
- if (!next) return finish(_extends({}, aiUnspentOutcome(model), init), builtPages());
1268
+ if (!next) {
1269
+ // Every part is built: the pictures left on a starter take the job's
1270
+ // own photos before the site goes live (AGL-3988), in a pass of their
1271
+ // own, sized to wait out the image hosts' burst.
1272
+ if (latePhotosOwed(builtPages()) && photos && !await aiJobCancelAsked(context.firestore, job, context.signal)) {
1273
+ var _context_org, _photos_latePasses;
1274
+ const late = await latePhotos(_extends({
1275
+ firestore: context.firestore,
1276
+ job,
1277
+ photos,
1278
+ pageIds: builtPages().map((page)=>page.id),
1279
+ org: (_context_org = context.org) != null ? _context_org : null,
1280
+ now: context.now
1281
+ }, context.signal ? {
1282
+ signal: context.signal
1283
+ } : {}));
1284
+ // A pass whose time ran out while it waited hands what is left to the
1285
+ // next, inside the passes the bound gives it: the last one publishes
1286
+ // whatever the pictures hold, so a site is never held back by them.
1287
+ const latePasses = ((_photos_latePasses = photos.latePasses) != null ? _photos_latePasses : 0) + 1;
1288
+ if (late.photos && latePasses < AI_SITE_LATE_PHOTOS_PASSES - 1) {
1289
+ return _extends({}, aiUnspentOutcome(model), init, {
1290
+ continue: true,
1291
+ sitePhotos: _extends({}, late.photos, {
1292
+ latePasses
1293
+ })
1294
+ });
1295
+ }
1296
+ return finish(_extends({}, aiUnspentOutcome(model), init, {
1297
+ sitePhotos: AI_SITE_PHOTOS_SETTLED
1298
+ }), builtPages());
1299
+ }
1300
+ return finish(_extends({}, aiUnspentOutcome(model), init), builtPages());
1301
+ }
1244
1302
  const unit = units.find((one)=>one.slot === next.slot);
1245
1303
  // The look is the scaffold's own pass (AGL-3660), not the theme job's proposal.
1246
1304
  const catalog = unit.kind === 'products' ? runnerFor('products') : undefined;
@@ -1267,13 +1325,17 @@ export function createAiJobSiteStep(deps = {}) {
1267
1325
  item
1268
1326
  }, item.status === 'running' || othersOpen ? {
1269
1327
  continue: true
1328
+ } : {}, photosReported ? {
1329
+ sitePhotos: photos != null ? photos : AI_SITE_PHOTOS_SETTLED
1270
1330
  } : {});
1271
1331
  /**
1272
1332
  * A unit that failed or was skipped with nothing else open ends the job
1273
1333
  * on this pass (AGL-3676): a guided start whose last unit — its welcome
1274
1334
  * email — failed was never published, because the pass that publishes is
1275
1335
  * the one after. So the end is the publish, whatever the last unit came to.
1276
- */ const closing = async (outcome)=>outcome.continue ? outcome : finish(outcome, builtPages());
1336
+ */ const closing = async (outcome)=>outcome.continue ? outcome : latePhotosOwed(builtPages()) ? _extends({}, outcome, {
1337
+ continue: true
1338
+ }) : finish(outcome, builtPages());
1277
1339
  // A blog's posts and a store's products ask, before their first pass,
1278
1340
  // whether this member may have them here (AGL-3676): a refusal spends
1279
1341
  // nothing, and the row says why, as a build's item does.
@@ -1424,6 +1486,11 @@ export function createAiJobSiteStep(deps = {}) {
1424
1486
  }
1425
1487
  }));
1426
1488
  }
1489
+ // The pictures this unit left on a starter, kept on the job for the late photo pass (AGL-3988).
1490
+ if (outcome.sitePhotos) {
1491
+ photos = aiSiteMergePhotos(photos, outcome.sitePhotos);
1492
+ photosReported = true;
1493
+ }
1427
1494
  const spent = aiUnitSpend(outcome);
1428
1495
  const stopped = aiUnitFailure(unit.slot, outcome);
1429
1496
  if (stopped) {
@@ -1472,7 +1539,12 @@ export function createAiJobSiteStep(deps = {}) {
1472
1539
  rows.set(unit.slot, _extends({}, rows.get(unit.slot), {
1473
1540
  status: degraded ? 'degraded' : 'succeeded'
1474
1541
  }));
1475
- return finish(done, builtPages(unit.kind === 'page' && !templates.has(unit.slot) || unit.kind === 'store' ? outcome.outputs.filter((output)=>output.resource === 'screen') : []));
1542
+ const live = builtPages(unit.kind === 'page' && !templates.has(unit.slot) || unit.kind === 'store' ? outcome.outputs.filter((output)=>output.resource === 'screen') : []);
1543
+ // The publish waits a pass for the late photo pass (AGL-3988).
1544
+ if (latePhotosOwed(live)) return _extends({}, done, {
1545
+ continue: true
1546
+ });
1547
+ return finish(done, live);
1476
1548
  };
1477
1549
  }
1478
1550
  export const runAiJobSiteStep = createAiJobSiteStep();
@@ -1501,7 +1573,9 @@ export const runAiJobSiteStep = createAiJobSiteStep();
1501
1573
  const ledger = ((_job_items1 = job.items) == null ? void 0 : _job_items1.length) ? job.items : aiSiteInitialLedger(units, outputs);
1502
1574
  const next = aiBuildNextUnit(aiSiteLedgerUnits(units), ledger);
1503
1575
  const unit = next ? units.find((one)=>one.slot === next.slot) : undefined;
1504
- if (!unit) return AI_JOB_PAGE_STEP_MINIMUM_MS;
1576
+ // Nothing left to build: the late photo pass, where a picture is still on
1577
+ // a starter (AGL-3988), and the publish in the same pass.
1578
+ if (!unit) return aiSitePhotosOwed(job.sitePhotos) ? Math.max(AI_JOB_PAGE_STEP_MINIMUM_MS, AI_SITE_LATE_PHOTOS_MINIMUM_MS) : AI_JOB_PAGE_STEP_MINIMUM_MS;
1505
1579
  if (unit.kind === 'theme') return AI_SITE_LOOK_BUDGET.minimumMs;
1506
1580
  if (unit.kind === 'posts') return AI_SITE_POST_BUDGET.minimumMs;
1507
1581
  if (unit.kind === 'dataset') return AI_SITE_DATASET_BUDGET.minimumMs;