Hash what the score actually depends on, and nothing that cannot move

The fingerprint carried the composition twice: once as its own key and once
as the ordered pillar ids inside the per-pillar triples. No input could make
those two disagree, so the term could not be broken and the test that
claimed to cover it passed with the term removed. Dropped the duplicate and
replaced the test with one that pins what the ids are for: a different set
of pillars producing the same numbers in the same order is a different
standing, and now reads as one.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 15:07:35 +02:00
1 parent 860a1fa299
commit d89b90da4b
2 files changed
+39 -7

No files matched your search

@@ -152,7 +152,7 @@ describe("input fingerprint", () => {
);
});
it("moves when the composition moves", () => {
it("moves when the composition narrows", () => {
const narrowed = {
...base,
composition: ["BLOOD_PRESSURE", "SLEEP"] as ScorePillarId[],
@@ -162,6 +162,33 @@ describe("input fingerprint", () => {
);
});
it("moves when a different pillar set produces the same numbers", () => {
// Same count, same scores, same order, same scoring modes — only WHICH
// pillars counted differs. Without the pillar id in the hash this is the
// case that reads as an unchanged standing, and it is exactly the change
// a configuration edit makes.
const swapped = {
...base,
composition: ["GLYCAEMIA", "SLEEP", "ADIPOSITY"] as ScorePillarId[],
pillars: [
pillar("GLYCAEMIA", 88, { deltaIdentity: "shared" }),
pillar("SLEEP", 74, { deltaIdentity: "shared" }),
pillar("ADIPOSITY", 66, { deltaIdentity: "shared" }),
],
};
const original = {
...base,
pillars: [
pillar("BLOOD_PRESSURE", 88, { deltaIdentity: "shared" }),
pillar("SLEEP", 74, { deltaIdentity: "shared" }),
pillar("ADIPOSITY", 66, { deltaIdentity: "shared" }),
],
};
expect(healthScoreInputFingerprint(swapped)).not.toBe(
healthScoreInputFingerprint(original),
);
});
it("moves when a counted pillar's score moves", () => {
const shifted = {
...base,
+11 -6
View File
@@ -132,11 +132,17 @@ function countedPillars(
* SHA-256 over exactly what the stored composite and band depend on.
*
* The covered set is the whole point, so it is spelled out rather than
* "hash the report": the algorithm version, the composition, and each counted
* pillar's score together with its scoring-mode identity. Drop any one of
* them and two genuinely different standings collapse onto one fingerprint —
* a key missing a dimension its value depends on, which is a defect class
* this codebase has shipped before.
* "hash the report": the algorithm version, and then one ordered triple per
* counted pillar — its id, its score, and the scoring mode that produced that
* score. Drop any one of those and two genuinely different standings collapse
* onto one fingerprint, which is a key missing a dimension its value depends
* on, a defect class this codebase has shipped before.
*
* The composition is not hashed separately. It IS the ordered list of ids in
* those triples, and an earlier draft carried it twice: a key that no input
* could ever make disagree with its neighbour, and therefore a key no test
* could make fail. A hash term nothing can move is not defence in depth, it
* is decoration on a guard.
*
* What is deliberately NOT covered: the account's configuration version. A
* configuration decides the composition, and the composition is already here.
@@ -153,7 +159,6 @@ export function healthScoreInputFingerprint(input: {
const canonical = JSON.stringify({
v: FINGERPRINT_VERSION,
scoreVersion: input.scoreVersion,
composition: [...input.composition],
pillars: counted.map((pillar) => [
pillar.id,
pillar.score,