Repository navigation
feat(plugin): appending extract paths to a probe rule keeps detecting on the existing slots; v0.86.0 - #206
Conversation
… on the existing slots; v0.86.0 A stored baseline whose rule fingerprint differs from the current rule's was always re-baselined: observation stored, nothing compared, nothing triggered. Right for a real rule change, but appending one extract path blinded the probe for a full pass on every field the rule already watched. In anchored mode that is the nightly pass right after the origin's daily change. compileRule now also records prefixFingerprints: the fingerprint the rule would have had with its extract list cut to its first k paths, for k < extract.length, built with ruleFingerprint itself so fingerprints already stored match with no migration. A stored fingerprint in that map is compared on its k slots (signatureUnderPrefix: parse, slice, stringify, byte-identical to the shorter rule's signatureOf output; no all-null rule on the prefix; status-signal literals compare whole; a baseline without the shorter rule's shape re-baselines). Changed/unchanged, pageCheck, verification, canary counting and the trigger-then-write ordering are the normal ones; whichever write the outcome takes stores the full observation and current fingerprint. New pass stat `extended` overlays changed/unchanged; no new metric series. Removal, reordering, editing an existing path, or appending together with any other change to the observation still re-baselines. Null (pre-v0.65.0) fingerprints unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for append-only rule edits by allowing extract paths to be appended to the end of a rule without triggering a full re-baseline. It adds prefix fingerprint matching and signature slicing under signatureUnderPrefix to compare and upgrade baselines in place. The feedback suggests a performance optimization to pre-compute and cache the set of status signal signatures on the rule object during compilation, avoiding allocation churn and CPU overhead on hot paths.
| pageCheck, | ||
| }; | ||
| rule.fingerprint = ruleFingerprint(rule); | ||
| rule.prefixFingerprints = prefixFingerprints(rule); |
There was a problem hiding this comment.
To optimize performance during sweep passes, we can pre-compute the set of status signal signatures here and store it on the rule object. This avoids recreating the Set on every row comparison in signatureUnderPrefix, which is a hot path. This aligns with the rule to cache normalized configuration values or computed arrays that are accessed frequently on hot paths to avoid CPU overhead and allocation churn.
| rule.prefixFingerprints = prefixFingerprints(rule); | |
| rule.prefixFingerprints = prefixFingerprints(rule); | |
| rule.statusSignalSignatures = new Set(statusSignals.map((s) => s.signature)); |
References
- Cache normalized configuration values or computed arrays that are accessed frequently on hot paths (e.g., multiple times per second) to avoid CPU overhead and allocation churn, recomputing them only when the configuration is applied.
There was a problem hiding this comment.
Applied in 3dd9097: compileRule now stores rule.statusSignalSignatures beside prefixFingerprints.
| * that rule, and is not comparable. The observation is held to its own shape the same way. | ||
| */ | ||
| export const signatureUnderPrefix = (rule, k, storedSignature, observed) => { | ||
| const literals = new Set((rule.statusSignals ?? []).map((signal) => signal.signature)); |
There was a problem hiding this comment.
Creating a new Set and mapping rule.statusSignals on every invocation of signatureUnderPrefix can introduce unnecessary garbage collection overhead during a large sweep pass. Since rule is compiled once, we can pre-compute this Set of status signal signatures during rule compilation (e.g., in compileRule) and store it on the rule object as rule.statusSignalSignatures. This avoids allocation churn on hot paths.
| const literals = new Set((rule.statusSignals ?? []).map((signal) => signal.signature)); | |
| const literals = rule.statusSignalSignatures ?? new Set((rule.statusSignals ?? []).map((signal) => signal.signature)); |
References
- Cache normalized configuration values or computed arrays that are accessed frequently on hot paths (e.g., multiple times per second) to avoid CPU overhead and allocation churn, recomputing them only when the configuration is applied.
There was a problem hiding this comment.
Applied in 3dd9097: signatureUnderPrefix reads the precomputed rule.statusSignalSignatures, and builds the Set only for a hand-assembled rule (tests).
…t per extended row (review) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # package-lock.json # packages/plugin/package.json
TL;DR
source: requestprobe rule'sextractlist no longer triggers a blind re-baseline pass. Rows whose baseline was taken before the append are compared on the slots they already have, then upgraded to the full observation in the same write.extended, visible in the pass record and the log line. It overlayschanged/unchangedthe same waypageMismatchdoes. No new metric series, so no console release is needed.schema.graphqledit is a comment only) and no data migration.Problem
ruleFingerprinthashes everything a rule observes, including the wholeextractlist. When a stored baseline's fingerprint does not match the current rule's,processOnere-baselines the row: it stores the observation, compares nothing, triggers nothing, and leaves the row out of the canary. It returns before thepageCheckcomparison runs.That is correct for a real rule change. For a one-path append (a per-variant
…[*].availabilityprojection, say) it blinds the probe for one full pass on every field the rule already watched. Inmode: anchoredthat pass is the nightly one scheduled right after the origin's daily change, so that night's changes go undetected until each page's cadence render.Design
rule.prefixFingerprints: Map<fingerprint, k>compileRuleinchangeProbeSpec.jsruleFingerprintitself on{ ...rule, extract: extract.slice(0, k) }, so fingerprints already stored by today's code match with no migration.signatureUnderPrefix(rule, k, stored, observed)changeProbeSpec.jsprocessOnechangeProbe.jsHow an append-only row is handled (verified by the tests below):
pageCheck: runs on the full observation. The indices point intoextractand an append does not shift them, so a check that reads the new path works.changed/unchanged, so they are part of the compared sample.probed = seeded + rebaselined + unchanged + changed + failedstill holds. The console computes compared rows asprobed − seeded − rebaselined − failed, which stays correct.What counts as append-only
pathPattern,invalidateScopeorpageCheckchangedEdge cases
signatureOfoutput. Each path is extracted on its own, and a JSON.stringify → JSON.parse → JSON.stringify round trip is stable. A test builds both strings with the realextractValuesover floats, escaped and non-ASCII strings, a nested object, a missing field and a[*]projection, and asserts they are equal.[null,null,null]), so it reads as changed. It is not passed through the all-null rule and is not treated as a failed probe. The longer rule would report the same change against its own full baseline.Tests
Plugin suite: 1228 pass, 0 fail. Console suite: 277 pass.
npm run lintandnpm run format:checkare clean.New in
changeProbeSpec.test.js:ruleFingerprintoutput: four literal values captured fromorigin/mainbefore this change. If the hashing ever moves, every stored baseline stops matching, so this test is there to catch it.prefixFingerprintsmaps each shorter rule's real fingerprint to its k, holds exactly n-1 entries and never the full fingerprint, and matches nothing for removal, reorder, edit, prepend, insert, or an append combined with an endpoint, method, body, header, signal or source change.signatureUnderPrefix: the byte-identity check above, the all-null prefix, literals, and refusal of badly shaped stored or observed values.New in
changeProbe.test.js(runProbePasswith a stored 3-slot baseline and a 4-slot rule):pageCheckdisagreement (read from the appended slot) triggers on an unchanged row. Verification runs on an agreeing row, alongside the upgrade patch.compared: 10and a trip at a 0.3 threshold.Revert check
Run against
changeProbe.test.jsandchangeProbeSpec.test.js:changeProbe.jsrestored toorigin/mainrunProbePasstest. The non-append and wrong-shape tests fail only on theirextended === 0assertion (the stat does not exist on main); their re-baseline behaviour is the same as main's, so they guard existing behaviour rather than new behaviour.rule.prefixFingerprints = …removed fromcompileRulerunProbePasstests and bothprefixFingerprintstests.changeProbeSpec.jsrestored toorigin/mainsignatureOfThe
ruleFingerprintpin passes before and after by design, since it guards existing output.Rollout
extendedroughly equals the matched count andrebaselinedstays near 0, thenextendeddrops to 0 once the rows are upgraded. A steady non-zeroextendedmeans changes are being deferred (seedeferred).Open items
probe_extendedmetric, on purpose. The console's check that every catalogued probe series is read would force a console release. Operators can see the stat inGET /prerender_admin/change-probe(lastRun.extended) and in the pass log line. If it should be charted, that is a follow-up together with a console change.Version: plugin 0.86.0 (0.85.0 is reserved by #205; 0.87.0 by a sibling PR).
🤖 Generated with Claude Code