fix(mood): one spelling on the wire, one day-key helper, one written decision

Three small things the audit named together.

The mood reads spread the whole row, so adding the wire keys shipped
every level-A value twice — once as the column name and once as the key
the write path accepts. A client would have picked one and been stuck
with it. The column spelling is stripped now, and the create response's
key set is asserted so it stays stripped. The published contract is
unaffected either way; these routes are not in it.

The dimension series carried its own copy of the day-key subtraction
that the freshness module already exports. One copy now.

And the reason a stale dimension ships with a notice instead of being
dropped the way the status surfaces drop one is written at the builder:
a one-line card has nothing left to say about a value it cannot narrate,
while an open conversation can say when the value is from — and silence
there would leave no way to tell an unanswered dimension from one nobody
has looked at in a week.
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 03:03:38 +02:00
1 parent 4db0f3a62a
commit 3dd79ec734
6 files changed
+83 -34

No files matched your search

+3 -5
View File
@@ -15,7 +15,7 @@ import { Prisma } from "@/generated/prisma/client";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
import { deriveA1, levelAForWire } from "@/lib/mood/level-a";
import { deriveA1, shapeLevelA } from "@/lib/mood/level-a";
import { encryptNote, shapeMoodNote } from "@/lib/crypto/note-cipher";
import { invalidateUserMood } from "@/lib/cache/invalidate";
import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups";
@@ -59,9 +59,8 @@ export const GET = apiHandler(
});
return apiSuccess({
...shapeMoodNote(entry),
// v1.37 — level-A values under the keys the write path takes.
...levelAForWire(entry),
...shapeLevelA(shapeMoodNote(entry)),
tags: parseTags(entry.tags),
});
},
@@ -321,9 +320,8 @@ export const PUT = apiHandler(
}
return apiSuccess({
...entry,
// v1.37 — level-A values under the keys the write path takes.
...levelAForWire(entry),
...shapeLevelA(entry),
tags: parseTags(entry.tags),
// v1.8.5 — surface the persisted structured-tag keys so a client
// hydrating from the update response renders the tag set without a
+5 -6
View File
@@ -24,7 +24,7 @@ import { annotate } from "@/lib/logging/context";
import { withIdempotency } from "@/lib/idempotency";
import { encryptNote, shapeMoodNote } from "@/lib/crypto/note-cipher";
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
import { levelAForWire, levelAForWrite } from "@/lib/mood/level-a";
import { levelAForWrite, shapeLevelA } from "@/lib/mood/level-a";
import { invalidateUserMood } from "@/lib/cache/invalidate";
import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups";
import {
@@ -126,11 +126,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
const entriesWithParsedTags = entries.map(({ tagLinks, ...e }) => ({
// v1.23 — decrypt `noteEncrypted` onto `note`, strip the ciphertext.
...shapeMoodNote(e),
// v1.37 — the five level-A values under the keys the create path takes,
// so the edit form reads back what it wrote instead of mapping column
// names to wire names by hand.
...levelAForWire(e),
// names to wire names by hand. The column spelling is stripped, not
// joined: one number under two names is a choice nobody made.
...shapeLevelA(shapeMoodNote(e)),
tags: parseTags(e.tags),
// v1.8.5 — flat list of binary structured-tag keys attached to the
// entry (rated factors are surfaced separately below).
@@ -402,9 +402,8 @@ async function postMoodEntry(request: NextRequest) {
return apiSuccess(
{
// v1.23 — decrypt `noteEncrypted` onto `note`, strip the ciphertext.
...shapeMoodNote(entry),
// v1.37 — level-A values under their wire keys (see the list read).
...levelAForWire(entry),
...shapeLevelA(shapeMoodNote(entry)),
tags: parseTags(entry.tags),
// v1.8.5 — surface the persisted structured-tag keys so a client
// hydrating from the create response renders the tag set without a