Repository navigation
feat(plugin+console): rule fingerprint, availability words, [*] extract paths, anchored mode (prerender-v0.65.0 + console v0.13.0) - #159
Conversation
…ored mode; v0.65.0
A change-probe rule edit no longer reads as a mass change: the rule's observation
(endpoint, method, headers, body, extract, status signals) is fingerprinted and stored
with every baseline (ProbeState.ruleFingerprint); a mismatch re-baselines the row
(stored, not compared, not triggered, outside the canary verdict) instead of flipping
100% of matched URLs at once. Pre-fingerprint rows compare as usual and are stamped
once on their next quiet probe.
pageCheck now reads availability words ("In Stock", IN_STOCK, schema.org forms) and
[*] per-variant lists, with per-rule availableValues/unavailableValues on top of the
built-in vocabulary. valueAtPath gains [*] projection so a rule can watch
variants[*].availability without signing whole variant objects.
New scheduler mode "anchored": one pass a day at anchorTime in anchorTimezone, paced
to anchorWindow (0 = ceiling), no boot sweep, canary unchanged; unusable anchors arm
nothing and warn. Adds the rebaselined stat / probe_rebaselined metric.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces an "anchored" mode for the change probe scheduler to run daily sweeps at a scheduled time, adds rule fingerprinting to prevent rule edits from triggering mass re-renders, and enhances availability checks with array projections and custom vocabularies. The review feedback suggests improving exception serialization robustness by falling back to String(e) when logging unusable timezone errors, and clearing stale page claims when a rule is re-baselined due to a fingerprint mismatch.
| } catch (e) { | ||
| at = NaN; | ||
| logger.warn( | ||
| `[prerender] change-probe: anchorTimezone "${config.changeProbe.anchorTimezone}" is not usable (${e?.message ?? e})` |
There was a problem hiding this comment.
When serializing caught exceptions, we should not assume the error is a standard Error object. Safely access the error's properties using optional chaining and fall back to String(error) (e.g., e?.message ?? String(e)) to ensure robust serialization of non-Error throws.
| `[prerender] change-probe: anchorTimezone "${config.changeProbe.anchorTimezone}" is not usable (${e?.message ?? e})` | |
| `[prerender] change-probe: anchorTimezone "${config.changeProbe.anchorTimezone}" is not usable (${e?.message ?? String(e)})` |
References
- When handling caught exceptions, safely access the error message using optional chaining and fall back to String(e) (e.g., e?.message ?? String(e)) to ensure that the error handling or logging block itself does not throw a TypeError if the caught exception is null, undefined, or a non-object.
There was a problem hiding this comment.
Taken in 25b2648 — e?.message ?? String(e).
| if (ruleChanged) { | ||
| stats.rebaselined++; | ||
| await write(row.url, observed, { rowExists: true, fingerprint: rule.fingerprint }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
When a rule is re-baselined due to a fingerprint mismatch, the old page claim (which was based on the old rule's extract list/vocabulary) is no longer valid or comparable to the new rule's observations. We should pass clearClaim: true to write to clear any stale page claims. Ensure that both pageSignature and pageClaimAt are cleared together as a pair to avoid leaving inconsistent half-states.
| if (ruleChanged) { | |
| stats.rebaselined++; | |
| await write(row.url, observed, { rowExists: true, fingerprint: rule.fingerprint }); | |
| return; | |
| } | |
| if (ruleChanged) { | |
| stats.rebaselined++; | |
| await write(row.url, observed, { rowExists: true, fingerprint: rule.fingerprint, clearClaim: true }); | |
| return; | |
| } |
References
- When managing page claims or signatures,
pageSignatureandpageClaimAtmust always be updated or cleared together as a pair to avoid leaving inconsistent half-states.
There was a problem hiding this comment.
Declining this one, and it is worth saying why in case the same suggestion comes back.
pageSignature / pageClaimAt are not derived from the rule at all. They are written by the RENDER path (recordPageClaim, from the browser's structuredOffers), never by the probe — the field comment says "written by the render, never here". So a rule edit does not invalidate them: the claim is a statement about what the cached page says, and the page has not changed just because we now read a different endpoint to check it.
The re-baseline branch also returns before any comparison happens, so there is nothing stale in play — pageDisagrees is only computed further down, guarded on rule.pageCheck && stored?.pageSignature.
Clearing the pair here would actively lose information: the page claim is the only side of the round-trip check that survives a rule edit, and dropping it would blind pageCheck for a full render interval on every matched URL until each page re-renders and re-seeds its claim.
The sibling finding (serializing a non-Error throw in the anchorTimezone warning) is taken in 25b2648.
…, projection order, unreadable variants; console v0.13.0 Five defects from the pre-release review, each with a pinned regression test. - `anchorWindow` was INERT. `sliceSize` was only passed in continuous mode, so the pacer had no denominator, `cycleRatePerSecond` returned Infinity, and an anchored pass burst at the ceiling however the window was set — with `probe_cycle_behind` silent too, since it also needs a slice. Spreading that burst is the entire reason anchored mode exists. - A DST spring-forward anchor armed a run IN THE PAST. An anchor of 02:30 on the day 02:00 jumps to 03:00 names a wall-clock time that never occurs, and the resolver returns an already-passed instant. The pass fired an hour early and then re-armed to the same past instant on every completion: the whole corpus walked back to back at the ceiling rate until the hour was over. A stale anchor is now pushed on by a day, which lands on the next real occurrence because the offset is applied to the resolved instant rather than to the wall clock. - `[*]` projections are SORTED. Left positional, an endpoint that returns its variants in an unstable order reads as 100% changed on every pass — which on the canary cohort is not a wave of per-URL re-renders but a trip and a bulk invalidation of the rule's whole scope. The trade is that two variants swapping prices with each other is invisible; the offers on the page are the same set either way. - A null element in a projected availability list now counts as UNREADABLE, not as absent. `[*]` writes null where the path could not be walked, so a sold-out variant beside one whose availability has not populated used to answer a confident "out of stock" the endpoint never gave — a claim that is compared against the page's own and expires it on disagreement. - METRICS.md failed `format:check`. Console v0.13.0 alongside it: `compared` subtracted seeds and failures but not re-baselined rows, so the pass after any rule edit — which can be almost entirely re-baselined — rendered as "0% changed" over a population nothing had compared, indistinguishable from a static origin. The outcome is now charted and explained in the panel's note. 1008 plugin tests, 258 console, lint and format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g (review) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two packages:
@harperfast/prerender0.65.0 and@harperfast/prerender-console0.13.0 (the probe view has to learn the new outcome or its arithmetic is wrong).Why
Three things the change probe could not do, all hit on a deployment this week while moving a rule to a different endpoint at the site operator's own request:
maxTriggersPerSweep, so most deferred and retried forever), and a certain canary trip invalidating the rule's whole scope. The only safe procedure was a manual dry-run cycle withreprobeAfter: 0, roughly a day without detection."In Stock"/"Out of Stock", or give availability per variant. Those mapped to "no claim", so the page-agreement check silently degraded to price only.What
changeProbeSpec.js ruleFingerprint,ProbeState.ruleFingerprint): the rule's observation (source, URL template, method, headers sorted, body, extract, status signals) is hashed and stored with every baseline. A probe whose rule fingerprint differs from the stored one re-baselines the row: observation stored, nothing compared, nothing triggered, excluded from the canary's verdict (newrebaselinedstat andprobe_rebaselinedmetric). Label, pathPattern, invalidateScope and pageCheck are not part of it. Rows from before the column existed compare as usual and are stamped once on their next quiet probe, so the first edit after this deploy is already free.pageCheck: the endpoint value may be a boolean, an availability word, or a[*]list of per-variant words. Words are matched after dropping case and separators (InStock,IN_STOCK,In Stock,https://schema.org/InStockare one token); the built-in vocabulary is Google's plusAvailable/Unavailable;pageCheck.availableValues/unavailableValuesextend or override it per rule. A per-variant list reads in stock when any variant is, the same reductionpageClaimFromOffersapplies to the page. An unrecognised entry in that list makes the claim indeterminate rather than letting an unreadable value read as "out of stock".[*]in extract paths (valueAtPath): project the rest of the path over an array, unreachable branches as null, and sort the projection so a reordered variant list is not a change (the one case traded away is two variants swapping values with each other — the same multiset, which is what the signature asks about). Lets a rule watchvariants[*].availabilitywithout signing the variant objects whose other fields (inventory counters, store data) move on their own.mode: anchoredwithanchorTime,anchorTimezone,anchorWindow: one full pass a day starting at a local time, paced acrossanchorWindow(0 = theratePerSecondceiling). No boot sweep in this mode; the canary keeps its own cadence. Editing the anchor re-arms via the existingsyncProbeTimersmarker comparison; an unusable timezone arms nothing and warns rather than firing at once; an anchor that has already passed today arms for tomorrow rather than firing immediately (the spring-forward hole).views/probe.js):rebaselinedjoins the outcome list and is subtracted out ofcompared, which otherwise counts re-baselined rows as comparisons that found nothing.Version
@harperfast/prerender0.64.1 → 0.65.0 (prerender-v0.65.0);@harperfast/prerender-console0.12.0 → 0.13.0 (prerender-console-v0.13.0). #154 previously held both of those numbers and has been closed, so they are uncontested; #156 takes plugin 0.66.0 after this.Tests
packages/plugin1008 pass,packages/console258 pass, lint and format clean (this repo has no PR CI). New: fingerprint stability and sensitivity, re-baseline path, legacy stamp, canary verdict exclusion, string and per-variant availability claims, rule vocabulary compile and clash handling,[*]projection and ordering, anchored scheduler arm/re-arm/no-boot-sweep/window pacing, and the console'scomparedarithmetic.Operator notes
ruleFingerprintcolumn toProbeState(node-local,replicate: false). Existing rows need nothing.probe_rebaselinedafter any rule edit; a steady count means a rule that keeps changing.[*]for per-variant availability should pointpageCheck.availableFromat that slot.comparedfor as long as they are apart.Before flipping
mode: anchored, decide the trigger budget and the canary threshold togetherAnchored mode is correct for an origin that reprices on a daily schedule, and it interacts with two existing defaults in a way that is worth settling in advance rather than discovering on a promo night:
maxTriggersPerSweepis 5,000 per node per pass. A deployment runningcontinuousat a 12h cycle gets two passes a day; one anchored pass a day halves that budget. Ordinary drift is a small fraction of it, so this is invisible until it isn't.anchorWindowis the mitigation and it is only functional as of this PR — it was previously accepted and never passed to the pacer. Stagger per node or set a window; and pick an anchor outside 02:00–03:00 local anyway.🤖 Generated with Claude Code