PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / ViewBuildStartedWatermark.php

ViewBuildStartedWatermark.php in 404 Solution 4.2.0, at includes/ViewBuildStartedWatermark.php

463 lines 23.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Per-build watermark stamp machinery for the staged view-build pipeline.
9 *
10 * Three persistent stamps, three lifetimes (Phase 3a step 3 split of the
11 * legacy single `started_watermark` -- Codex #7):
12 *
13 * - active_build_started_watermark: written at S1 entry on a fresh build,
14 * cleared on S11 success and on every abort (mutation-watermark gate,
15 * prefix-changed gate, force-restart primitive). Read at every stage
16 * boundary by {@see mutationWatermarkAdvancedSinceBuildStart}: a strict
17 * "current() > stamp" comparison drives the abort decision. While this
18 * stamp exists, a build is in flight against the watermark it holds.
19 * - last_build_started_watermark: diagnostic sibling. Written at S1 entry
20 * alongside the active stamp, RETAINED across every settle path. Tells
21 * an operator "what was the most recent stamp we wrote, regardless of
22 * whether that build completed?" -- the question the original single
23 * `started_watermark` could not answer cleanly (a stamp surviving a
24 * completed build was the footgun Codex #7 flagged).
25 * - built_watermark: the cross-build pre-image. Written ONLY at S11
26 * success and ONLY from the active stamp's value, so it records
27 * "what mutation watermark did the LAST SUCCESSFUL view_done snapshot
28 * cover?" Read by future freshness checks (Phase 4) to decide whether
29 * to serve view_done or trigger a rebuild. Survives every fresh-start
30 * and abort cleanup.
31 *
32 * All three stamps live OUTSIDE `$viewBuildProgressOptionNames` because
33 * reads/writes route through {@see readWatermarkOption} / {@see writeWatermarkOption}
34 * to preserve the absent-vs-0 distinction: a fresh install whose mutation
35 * watermark is 0 produces a legitimate stamp of 0, which `readProgressOption`
36 * (clamping to >= 0) cannot distinguish from "never stamped".
37 *
38 * Migration. A pre-Phase-3a-step-3 install upgrading mid-flight may carry
39 * the legacy `started_watermark` option populated by the prior code path.
40 * {@see readActiveBuildStartedWatermark} falls back to the legacy key when
41 * the active key is absent, promotes the value into the active key, and
42 * deletes the legacy key so convergence happens after one tick. The stamp /
43 * clear methods also delete the legacy key whenever they run, so the next
44 * S1 entry, abort, or S11 success on a post-upgrade install converges to
45 * the new naming. Phase 5 cleanup deletes the fallback + the legacy helper
46 * once the rename has been in production for one release cycle.
47 *
48 * Composition. Mixed into `ABJ_404_Solution_DataAccess` alongside
49 * `ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait` (the orchestrator's
50 * abort/fresh-start/gate methods call into this trait via `$this->`).
51 *
52 * @see ABJ_404_Solution_DataAccess_ViewBuildHelpersTrait
53 * @see ABJ_404_Solution_DataAccess_ViewBuildForceRestartTrait
54 * @see ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait
55 *
56 * @property ABJ_404_Solution_DatabaseCore $dbCore
57 * @property ABJ_404_Solution_Functions $f
58 * @property ABJ_404_Solution_Logging $logger
59 * @property ABJ_404_Solution_ViewReadService|null $viewReadService
60 * @property ABJ_404_Solution_LogsRepository|null $logsRepo
61 * @property int $stagedQueryTimeoutSeconds
62 * @property string $lastBatchProgressDetail
63 * @property bool $viewBuildStageOpenForShutdown
64 * @property int $viewBuildShutdownStageNumber
65 * @property string $viewBuildShutdownStageKey
66 * @property bool|null $namedLockSupportedThisRequest
67 * @property bool $fallbackLockLoggedThisRequest
68 * @property bool $usingTransientFallbackLock
69 * @property string $lastNamedLockUnsupportedReason
70 * @property string $lastNamedLockUnsupportedError
71 * @method void abortStagedBuildForMutationWatermarkAdvance(...$arguments)
72 * @method bool acquireTransientFallbackLock(...$arguments)
73 * @method bool acquireViewBuildLock(...$arguments)
74 * @method string activeBuildStartedWatermarkOptionName(...$arguments)
75 * @method bool adminMutationGateBlocks(...$arguments)
76 * @method array<mixed> advanceViewBuildOnce(...$arguments)
77 * @method void assertBuildBufferExistsOrHalt(...$arguments)
78 * @method ?bool attemptRelaxSqlModeForBuildConnection(...$arguments)
79 * @method bool bufferIntegrityPassesForPromote(...$arguments)
80 * @method string buildHaltTransientKey(...$arguments)
81 * @method string buildViewDoneCountQuery(...$arguments)
82 * @method string builtWatermarkOptionName(...$arguments)
83 * @method int bumpMutationWatermark(...$arguments)
84 * @method int bumpStageNoProgressStreak(...$arguments)
85 * @method string capturedPrefixForLog(...$arguments)
86 * @method void capturePrefixAtBuildStart(...$arguments)
87 * @method void claimForegroundViewBuildLease(...$arguments)
88 * @method string classifyAndHandleStageFailure(...$arguments)
89 * @method array<mixed> classifySessionVariableWarnings(...$arguments)
90 * @method string classifyStageFailure(...$arguments)
91 * @method void clearActiveBuildStartedWatermark(...$arguments)
92 * @method void clearAdminMutationGateOptions(...$arguments)
93 * @method void clearAllProgressOptions(...$arguments)
94 * @method void clearPhpEnvironmentProbeCache(...$arguments)
95 * @method void clearPrefixAtStageOne(...$arguments)
96 * @method void clearSessionVariablesProbeCache(...$arguments)
97 * @method void clearSqlModeProbeCache(...$arguments)
98 * @method void clearStagedBuildDegradedState(...$arguments)
99 * @method void clearViewBuildOpenStageForShutdown(...$arguments)
100 * @method void clearViewDoneHardStaleNotice(...$arguments)
101 * @method ABJ_404_Solution_Clock clock(...$arguments)
102 * @method int countLiveRedirects(...$arguments)
103 * @method int countViewBuildRows(...$arguments)
104 * @method string describeBuildProgressForNotice(...$arguments)
105 * @method string describeDegradedNotice(...$arguments)
106 * @method string describeStagedSqlFailure(...$arguments)
107 * @method array<mixed> detectAndAdjustSqlMode(...$arguments)
108 * @method float detectHostStagedQueryLimitSeconds(...$arguments)
109 * @method string doTableNameReplacements(...$arguments)
110 * @method void dropDeletemeTable(...$arguments)
111 * @method void dropTransientBuffersIfPresent(...$arguments)
112 * @method void dropTransientStagedTables(...$arguments)
113 * @method void ensureConnection(...$arguments)
114 * @method void ensureFallbackLockNoticeAndLog(...$arguments)
115 * @method int extendedTimeoutForKilledNonBatchedStage(...$arguments)
116 * @method array<mixed> fetchSessionVariablesRowOrEmpty(...$arguments)
117 * @method string filesystemEnvironmentProbeOptionName(...$arguments)
118 * @method bool forceRestartViewBuild(...$arguments)
119 * @method bool foregroundViewBuildLeaseActive(...$arguments)
120 * @method string formatPhpMemoryBytesHuman(...$arguments)
121 * @method bool gateAbortIfMutationWatermarkAdvanced(...$arguments)
122 * @method string getColumnCollationString(...$arguments)
123 * @method int getCronStuckHours(...$arguments)
124 * @method string getLowercasePrefix(...$arguments)
125 * @method array<string, mixed> getViewBuildProgress(...$arguments)
126 * @method array<mixed> getViewBuildProgressFingerprint(...$arguments)
127 * @method int getViewDoneBuiltAtTimestamp(...$arguments)
128 * @method bool haltIfPrefixChangedSinceStageOne(...$arguments)
129 * @method string humanBatchProgress(...$arguments)
130 * @method float intelligentStagedQueryTimeoutSeconds(...$arguments)
131 * @method void invalidateViewDoneServeableCache(...$arguments)
132 * @method bool isBuildHaltedForHostFailure(...$arguments)
133 * @method bool isCurrentStageOptionName(...$arguments)
134 * @method bool isNamedLockUnsupportedError(...$arguments)
135 * @method bool isResumableStagedKill(...$arguments)
136 * @method bool isStageMarkedSkipped(...$arguments)
137 * @method bool isTransientConnectionError(...$arguments)
138 * @method string lastBuildStartedWatermarkOptionName(...$arguments)
139 * @method string legacyStartedWatermarkOptionName(...$arguments)
140 * @method string localizeOrDefaultViewBuildNotice(...$arguments)
141 * @method bool logsHitsTableExists(...$arguments)
142 * @method void logTimedViewBuildStage(...$arguments)
143 * @method void logViewBuildProgressOptionWrite(...$arguments)
144 * @method void logViewBuildShutdownDiagnostics(...$arguments)
145 * @method void markBuildHaltedForHostFailure(...$arguments)
146 * @method void markBuildStage(...$arguments)
147 * @method void markStageSkippedForHostFailure(...$arguments)
148 * @method void markViewBuildStageCompleted(...$arguments)
149 * @method void markViewBuildStageStarted(...$arguments)
150 * @method void markViewDoneBuildCompleted(...$arguments)
151 * @method void markViewDoneInvalidatedByAdminMutation(...$arguments)
152 * @method int maxBuildBufferId(...$arguments)
153 * @method void maybeRaiseViewDoneHardStaleNotice(...$arguments)
154 * @method bool mutationWatermarkAdvancedSinceBuildStart(...$arguments)
155 * @method int mutationWatermarkObservedByAdminAction(...$arguments)
156 * @method int mutationWatermarkObservedByAdminActionAt(...$arguments)
157 * @method string mutationWatermarkObservedByAdminActionAtOptionName(...$arguments)
158 * @method string mutationWatermarkObservedByAdminActionOptionName(...$arguments)
159 * @method string normalizePathPrefix(...$arguments)
160 * @method bool optionReadBackMatches(...$arguments)
161 * @method int parsePhpMemoryLimitToBytes(...$arguments)
162 * @method bool pathFallsWithinAny(...$arguments)
163 * @method void performFreshStartCleanup(...$arguments)
164 * @method array<mixed> phpDisabledFunctionsList(...$arguments)
165 * @method string phpEnvironmentProbeOptionName(...$arguments)
166 * @method float phpTimeRemainingSeconds(...$arguments)
167 * @method string prefixAtStageOneOptionName(...$arguments)
168 * @method array<mixed> probeFilesystemEnvironmentForBuild(...$arguments)
169 * @method float probeFloatFromValues(...$arguments)
170 * @method int probeIntFromValues(...$arguments)
171 * @method int probeMemoryLimitForS9(...$arguments)
172 * @method array<mixed> probePhpEnvironmentForBuild(...$arguments)
173 * @method array<mixed> probeSessionVariablesAtS1Entry(...$arguments)
174 * @method bool probeSetTimeLimitAvailability(...$arguments)
175 * @method array<mixed> probeSqlModeForBuild(...$arguments)
176 * @method string probeStringFromValues(...$arguments)
177 * @method string progressOptionName(...$arguments)
178 * @method void publishBuiltWatermarkFromActiveBuildStartedWatermark(...$arguments)
179 * @method array<mixed> queryAndGetResults(...$arguments)
180 * @method int readActiveBuildStartedWatermark(...$arguments)
181 * @method array<int, array<string, mixed>> readFromViewDone(...$arguments)
182 * @method int readProgressOption(...$arguments)
183 * @method int readWatermarkOption(...$arguments)
184 * @method void rebuildViewDoneInBackground(...$arguments)
185 * @method bool reconcilePostStageElevenState(...$arguments)
186 * @method string reconcileStagedTablesAtRunnerStartup(...$arguments)
187 * @method int recordStageBatchKilled(...$arguments)
188 * @method void registerViewBuildShutdownDiagnostics(...$arguments)
189 * @method bool releaseAndReacquireBetweenStages(...$arguments)
190 * @method void releaseViewBuildLock(...$arguments)
191 * @method void resetStageNoProgressStreak(...$arguments)
192 * @method string resolveColumnCollationForStagedBuild(...$arguments)
193 * @method void runForceRestartCleanupInsideLock(...$arguments)
194 * @method bool runIdRangeBatchedUpdate(...$arguments)
195 * @method int runInsertBatch(...$arguments)
196 * @method mixed runNonBatchedStageWithKillStreakEscape(...$arguments)
197 * @method array{ran: bool, reason: string, progress: array<string, mixed>} runPageLoadFallbackAdvance(...$arguments)
198 * @method int runRedirectsForViewCountStaged(...$arguments)
199 * @method array<int, array<string, mixed>> runRedirectsForViewStaged(...$arguments)
200 * @method bool runS11SwapWithPreRenameWatermarkRecheck(...$arguments)
201 * @method bool runStagedBuildOnce(...$arguments)
202 * @method bool runStagedBuildStages6Through11(...$arguments)
203 * @method void runStagedSqlFile(...$arguments)
204 * @method void runStagedSqlFileTolerantOfDuplicateKey(...$arguments)
205 * @method mixed runTimedViewBuildStage(...$arguments)
206 * @method int safeCurrentMutationWatermark(...$arguments)
207 * @method string sanitizeUrlBeforeInsert(...$arguments)
208 * @method void scheduleViewDoneRebuild(...$arguments)
209 * @method string sessionVariablesProbeOptionName(...$arguments)
210 * @method void setFilesystemEnvAdminNotice(...$arguments)
211 * @method void setLowMemoryLimitAdminNotice(...$arguments)
212 * @method void setSessionEnvAdminNotice(...$arguments)
213 * @method void setStagedBuildDegradedNotice(...$arguments)
214 * @method void setStagedBuildHaltNotice(...$arguments)
215 * @method void setViewBuildCronStuckNotice(...$arguments)
216 * @method void setViewBuildScheduleFailedNotice(...$arguments)
217 * @method void setViewDoneHardStaleNotice(...$arguments)
218 * @method array<mixed> splitOpenBasedirPaths(...$arguments)
219 * @method string sqlModeProbeOptionName(...$arguments)
220 * @method void stageAddPreJoinIndexes(...$arguments)
221 * @method void stageAddSortIndexes(...$arguments)
222 * @method void stageCreateBuildTable(...$arguments)
223 * @method array<string, mixed> stagedQueryOptions(...$arguments)
224 * @method bool stagedTableExists(...$arguments)
225 * @method bool stageInsertRedirectsBatched(...$arguments)
226 * @method string stageNoProgressStreakOptionName(...$arguments)
227 * @method void stageRenameSwap(...$arguments)
228 * @method string stageSkipOptionName(...$arguments)
229 * @method void stageUpdateExternal(...$arguments)
230 * @method void stageUpdateHits(...$arguments)
231 * @method void stageUpdateHome(...$arguments)
232 * @method bool stageUpdatePostsBatched(...$arguments)
233 * @method void stageUpdateSpecial(...$arguments)
234 * @method bool stageUpdateTermsBatched(...$arguments)
235 * @method void stampStartedWatermarksAtS1Entry(...$arguments)
236 * @method void sweepStaleRebuildTransients(...$arguments)
237 * @method string transientFallbackLockOptionName(...$arguments)
238 * @method bool verifyBuildLockSerializesWriter(...$arguments)
239 * @method bool verifyOptionWriteCoherent(...$arguments)
240 * @method bool verifyPrefixUnchangedSinceStageOne(...$arguments)
241 * @method int viewBuildBatchSize(...$arguments)
242 * @method int viewBuildBatchSizeForStage(...$arguments)
243 * @method array<mixed> viewBuildOnlyTranslations(...$arguments)
244 * @method float viewBuildPerStageBudgetSeconds(...$arguments)
245 * @method string viewBuildTableName(...$arguments)
246 * @method string viewDeletemeTableName(...$arguments)
247 * @method int viewDoneBuiltAt(...$arguments)
248 * @method int viewDoneBuiltWatermark(...$arguments)
249 * @method int viewDoneDataBuiltAt(...$arguments)
250 * @method string viewDoneDataBuiltAtOptionName(...$arguments)
251 * @method string viewDoneFreshnessOptionName(...$arguments)
252 * @method bool viewDoneHasRows(...$arguments)
253 * @method bool viewDoneIsFresh(...$arguments)
254 * @method bool viewDoneIsServeable(...$arguments)
255 * @method int viewDoneMutationInvalidatedAt(...$arguments)
256 * @method string viewDoneMutationInvalidatedAtOptionName(...$arguments)
257 * @method bool viewDoneTableExists(...$arguments)
258 * @method string viewDoneTableName(...$arguments)
259 * @method void writeProgressOption(...$arguments)
260 * @method void writeWatermarkOption(...$arguments)
261 */
262 class ABJ_404_Solution_ViewBuildStartedWatermark extends ABJ_404_Solution_ViewBuildCollaborator {
263
264 /**
265 * Unprefixed option-name suffix for the S11-completion watermark stamp.
266 * Lives on its own (NOT in `$viewBuildProgressOptionNames`) so it
267 * survives across builds: this is the published "what watermark did the
268 * LAST SUCCESSFUL view_done snapshot cover?" pre-image.
269 * clearAllProgressOptions (called on abort and on fresh-start) must NOT
270 * touch it.
271 */
272 public function builtWatermarkOptionName(): string {
273 return $this->getLowercasePrefix() . 'abj404_view_build_built_watermark';
274 }
275
276 /**
277 * Option name for the in-flight build's S1-entry stamp. Cleared on S11
278 * success, on at-stage abort, and by `forceRestartViewBuild()`.
279 */
280 public function activeBuildStartedWatermarkOptionName(): string {
281 return $this->getLowercasePrefix() . 'abj404_view_build_active_started_watermark';
282 }
283
284 /**
285 * Option name for the diagnostic-only "most recent stamp" sibling.
286 * Written at S1 entry alongside the active stamp, RETAINED across S11
287 * success, abort, and force-restart so operators can see the value
288 * regardless of completion.
289 */
290 public function lastBuildStartedWatermarkOptionName(): string {
291 return $this->getLowercasePrefix() . 'abj404_view_build_last_started_watermark';
292 }
293
294 /**
295 * Pre-Phase-3a-step-3 option name. Read via the migration fallback in
296 * {@see readActiveBuildStartedWatermark} and deleted by every stamp /
297 * clear call so post-upgrade installs converge to the new naming after
298 * one build cycle. Phase 5 cleanup deletes this helper.
299 */
300 public function legacyStartedWatermarkOptionName(): string {
301 return $this->getLowercasePrefix() . 'abj404_view_build_started_watermark';
302 }
303
304 /**
305 * Raw watermark-option reader. Returns -1 when the option is absent,
306 * the stored integer otherwise. The absent-vs-0 distinction matters
307 * for the resume-preserves-stamp contract: a fresh install with no
308 * mutations has current()=0, so a stamp of 0 is a legitimate stamped
309 * value, NOT the "no stamp yet" sentinel.
310 *
311 * @param string $fullyPrefixedName Option name as built by
312 * activeBuildStartedWatermarkOptionName() / builtWatermarkOptionName().
313 * @return int -1 when absent; the stored integer otherwise.
314 */
315 public function readWatermarkOption(string $fullyPrefixedName): int {
316 if (!function_exists('get_option')) {
317 return -1;
318 }
319 $value = get_option($fullyPrefixedName, null);
320 if ($value === null || $value === false) {
321 return -1;
322 }
323 if (!is_scalar($value)) {
324 return -1;
325 }
326 return intval($value);
327 }
328
329 /**
330 * Raw watermark-option writer. Autoload=false so the per-build stamps
331 * do not bloat the alloptions cache loaded on every WP page.
332 */
333 public function writeWatermarkOption(string $fullyPrefixedName, int $value): void {
334 if (!function_exists('update_option')) {
335 return;
336 }
337 update_option($fullyPrefixedName, max(0, $value), false);
338 }
339
340 /**
341 * True when the live mutation watermark is strictly greater than the
342 * pre-image stamped at S1 entry of the in-flight build. Returns false
343 * when no stamp exists (no in-flight build) or when current() <= stamp.
344 *
345 * Called at every stage boundary by the orchestrator. A true result
346 * means an external caller bumped the watermark while this build was
347 * running, so the buffer it has assembled does not cover that mutation.
348 * The orchestrator must abort and let the next tick rebuild.
349 */
350 public function mutationWatermarkAdvancedSinceBuildStart(): bool {
351 $started = $this->readActiveBuildStartedWatermark();
352 if ($started < 0) {
353 return false;
354 }
355 if (!class_exists('ABJ_404_Solution_MutationWatermark')) {
356 return false;
357 }
358 $current = ABJ_404_Solution_MutationWatermark::current();
359 return $current > $started;
360 }
361
362 /**
363 * Read the active stamp, falling back to the pre-rename legacy option
364 * name when the new key is absent. The fallback covers the upgrade
365 * window: an install whose previous PHP request stamped
366 * `started_watermark` mid-flight and whose current request is the
367 * first to run with the renamed code must still see the stamp at the
368 * gate-check site, otherwise the stage-boundary abort would miss a
369 * real mutation that landed before the upgrade. On a hit, we promote
370 * the legacy value into the active key (so the next read is direct)
371 * and delete the legacy key so convergence happens after one tick.
372 *
373 * @return int -1 when neither key is set; the stamp value otherwise.
374 */
375 public function readActiveBuildStartedWatermark(): int {
376 $value = $this->readWatermarkOption($this->activeBuildStartedWatermarkOptionName());
377 if ($value >= 0) {
378 return $value;
379 }
380 $legacy = $this->readWatermarkOption($this->legacyStartedWatermarkOptionName());
381 if ($legacy < 0) {
382 return -1;
383 }
384 $this->writeWatermarkOption($this->activeBuildStartedWatermarkOptionName(), $legacy);
385 if (function_exists('delete_option')) {
386 delete_option($this->legacyStartedWatermarkOptionName());
387 }
388 return $legacy;
389 }
390
391 /**
392 * Stamp BOTH started-watermark fields at S1 entry of a fresh build.
393 * Callers route through this from the orchestrator's `if ($stage < 1)`
394 * block, which only fires on fresh starts -- a resuming tick has
395 * `current_stage >= 1` and never re-enters this branch, so the stamps
396 * written here remain the pre-image for every subsequent stage
397 * boundary in the build.
398 *
399 * Always overwrites both. The S11-completion path and the abort
400 * cleanup path clear `active_*` only; the diagnostic `last_*` is
401 * retained for operator visibility and overwritten on the next S1
402 * entry. Also deletes the legacy option name so a post-upgrade
403 * install converges to the new naming.
404 */
405 public function stampStartedWatermarksAtS1Entry(): void {
406 if (!class_exists('ABJ_404_Solution_MutationWatermark')) {
407 return;
408 }
409 $watermark = ABJ_404_Solution_MutationWatermark::current();
410 $this->writeWatermarkOption($this->activeBuildStartedWatermarkOptionName(), $watermark);
411 $this->writeWatermarkOption($this->lastBuildStartedWatermarkOptionName(), $watermark);
412 if (function_exists('delete_option')) {
413 delete_option($this->legacyStartedWatermarkOptionName());
414 }
415 }
416
417 /**
418 * Delete the active stamp. Called from the abort path so the next
419 * tick's fresh-start branch sees no leftover stamp from the aborted
420 * run, and from the S11 success path so the just-completed build's
421 * pre-image does not become the next build's pre-image. (S11 also
422 * publishes built_watermark, which is the cross-build pre-image that
423 * survives the clear.)
424 *
425 * Does NOT clear `last_build_started_watermark`: that stamp's whole
426 * purpose is to survive both abort and success so operators can
427 * observe "what watermark did the most recent build attempt stamp?"
428 * regardless of completion.
429 *
430 * Also deletes the legacy option name so post-upgrade installs
431 * converge to the new naming after the first build settles.
432 */
433 public function clearActiveBuildStartedWatermark(): void {
434 if (!function_exists('delete_option')) {
435 return;
436 }
437 delete_option($this->activeBuildStartedWatermarkOptionName());
438 delete_option($this->legacyStartedWatermarkOptionName());
439 }
440
441 /**
442 * Stamp built_watermark = active stamp at S11 success so future
443 * freshness checks can compare it against the live mutation watermark
444 * to decide if view_done covers the latest data.
445 *
446 * Must be called BEFORE clearActiveBuildStartedWatermark() since the
447 * read source disappears once the active stamp is cleared.
448 * built_watermark itself lives outside the progress registry, so it
449 * survives the post-S11 clears.
450 */
451 public function publishBuiltWatermarkFromActiveBuildStartedWatermark(): void {
452 $started = $this->readActiveBuildStartedWatermark();
453 if ($started < 0) {
454 // No active stamp (and no legacy fallback): pre-Phase-2 install
455 // upgrading mid-build, or a code path that reached S11 without
456 // entering S1. Either way, do not overwrite a prior good
457 // built_watermark with a bogus zero.
458 return;
459 }
460 $this->writeWatermarkOption($this->builtWatermarkOptionName(), $started);
461 }
462 }
463