@artymclabin/qa-review 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/client/QAReviewOverlay.js +40 -8
- package/dist/client/fingerprint.d.ts +18 -3
- package/dist/client/fingerprint.js +19 -4
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/journey.d.ts +18 -0
- package/dist/client/journey.js +34 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.13 - 2026-10-07
|
|
4
|
+
|
|
5
|
+
- JOURNEY-WIDE TOTALS: in a cross-page journey the footer and the finish card
|
|
6
|
+
now show approve/reject totals across EVERY page (`whole journey: N approved
|
|
7
|
+
· M rejected · T items`), next to the page line (now labelled `this page,
|
|
8
|
+
all runs`). Previously the only ledger count was page-scoped and unlabelled,
|
|
9
|
+
so at the end of a ten-page journey with 12 verdicts it read "1 approved".
|
|
10
|
+
New exports: `fetchJourneyVerdicts`, `tallyJourney`, `VerdictTally`.
|
|
11
|
+
- RUN SNAPSHOT SAVES ITSELF: when `submitUrl` is set, the whole-run snapshot
|
|
12
|
+
is POSTed automatically once per finished round. The "Save session
|
|
13
|
+
snapshot (optional)" button is gone - it asked the reviewer for a decision
|
|
14
|
+
with no stake in it. A failed save shows as a warning line.
|
|
15
|
+
|
|
3
16
|
## 0.3.10 - 2026-09-19
|
|
4
17
|
|
|
5
18
|
- OUTSIDE TAP MINIMIZES THE PANEL, ON MOBILE: the only way to collapse the
|
|
@@ -34,7 +34,7 @@ import { createPortal } from "react-dom";
|
|
|
34
34
|
import { Check, X, ChevronLeft, ChevronRight, ClipboardCopy, PartyPopper, Database, Undo2, Crosshair, Locate, ExternalLink, Minimize2, Monitor, Smartphone, ClipboardCheck, } from "lucide-react";
|
|
35
35
|
import { isTaskItem } from "./types.js";
|
|
36
36
|
import { QAStore } from "./store.js";
|
|
37
|
-
import { buildJourneyNavUrl, ensurePrefetchLink, fetchPendingCounts, isPrefetchFresh, journeyFinishView, journeyIndex, nextPendingPage, resolveFinishAction, shouldPrefetch, } from "./journey.js";
|
|
37
|
+
import { buildJourneyNavUrl, ensurePrefetchLink, fetchJourneyVerdicts, fetchPendingCounts, isPrefetchFresh, journeyFinishView, journeyIndex, nextPendingPage, resolveFinishAction, shouldPrefetch, tallyJourney, } from "./journey.js";
|
|
38
38
|
import { fingerprintStatus, itemFingerprint } from "./fingerprint.js";
|
|
39
39
|
import { DEVICE_LABEL, DEVICE_TOOLTIP, approvedDevicesOf, detectDevice, isFullyApproved, requiredDevices, toggleDevice, } from "./device.js";
|
|
40
40
|
import { applySubHighlights } from "./highlight.js";
|
|
@@ -211,6 +211,8 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
211
211
|
// Journey: pending counts (for the journey-complete panel only - page-level
|
|
212
212
|
// completion navigates IMMEDIATELY with no interstitial).
|
|
213
213
|
const [journeyCounts, setJourneyCounts] = React.useState(null);
|
|
214
|
+
// Ledger maps of every journey page, for journey-wide approve/reject totals.
|
|
215
|
+
const [journeyMaps, setJourneyMaps] = React.useState(null);
|
|
214
216
|
const [journeyComplete, setJourneyComplete] = React.useState(false);
|
|
215
217
|
// The journey page a full-page navigation is in flight to (loading label).
|
|
216
218
|
const [navigatingTo, setNavigatingTo] = React.useState(null);
|
|
@@ -725,6 +727,21 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
725
727
|
};
|
|
726
728
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
727
729
|
}, [active, finished, inJourney, index, roundTotal]);
|
|
730
|
+
// Journey-wide totals: load every page's ledger once on activation and
|
|
731
|
+
// again when the round finishes (other tabs/sessions may have written).
|
|
732
|
+
React.useEffect(() => {
|
|
733
|
+
if (!active || !inJourney)
|
|
734
|
+
return;
|
|
735
|
+
let cancelled = false;
|
|
736
|
+
void fetchJourneyVerdicts(stateUrl ?? DEFAULT_STATE_URL, journey.pages).then((maps) => {
|
|
737
|
+
if (!cancelled)
|
|
738
|
+
setJourneyMaps(maps);
|
|
739
|
+
});
|
|
740
|
+
return () => {
|
|
741
|
+
cancelled = true;
|
|
742
|
+
};
|
|
743
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
744
|
+
}, [active, inJourney, finished]);
|
|
728
745
|
// Journey: the moment this page's round is done, resolve the next step and
|
|
729
746
|
// navigate IMMEDIATELY (no interstitial, no success popup). Uses the
|
|
730
747
|
// PREFETCHED counts when fresh (instant hop); falls back to an on-demand
|
|
@@ -851,6 +868,23 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
851
868
|
setSave({ status: "error", message: "Network error - use Copy JSON instead." });
|
|
852
869
|
}
|
|
853
870
|
}, [submitUrl, buildPayload]);
|
|
871
|
+
// 🚨 The run snapshot saves ITSELF when a round finishes (0.3.13). It used to
|
|
872
|
+
// be a "Save session snapshot (optional)" button, which asked the reviewer to
|
|
873
|
+
// make a decision with no stake in it: Arty 2026-10-07, "Either save it or
|
|
874
|
+
// don't save it. If it's useful, save it... Why did you ask me to click that
|
|
875
|
+
// button instead?" Once per finish; a failure surfaces as a warning only.
|
|
876
|
+
const snapshotDoneRef = React.useRef(false);
|
|
877
|
+
React.useEffect(() => {
|
|
878
|
+
if (!finished) {
|
|
879
|
+
snapshotDoneRef.current = false;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
// Nothing decided this round = nothing to snapshot.
|
|
883
|
+
if (!submitUrl || snapshotDoneRef.current || Object.keys(sessionVerdicts).length === 0)
|
|
884
|
+
return;
|
|
885
|
+
snapshotDoneRef.current = true;
|
|
886
|
+
void saveToDb();
|
|
887
|
+
}, [finished, submitUrl, saveToDb, sessionVerdicts]);
|
|
854
888
|
if (!active || !hydrated || typeof document === "undefined")
|
|
855
889
|
return null;
|
|
856
890
|
// Inline CSS-variable overrides for the injected default theme.
|
|
@@ -863,6 +897,8 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
863
897
|
// LEDGER (all-time on this page) vs THIS ROUND (acted on in this run).
|
|
864
898
|
const approved = Object.values(results).filter((r) => r.verdict === "approve").length;
|
|
865
899
|
const rejected = Object.values(results).filter((r) => r.verdict === "reject").length;
|
|
900
|
+
const liveMap = Object.fromEntries(Object.values(results).map((r) => [r.id, { verdict: r.verdict }]));
|
|
901
|
+
const journeyTally = inJourney && journeyMaps ? tallyJourney(journey.pages, journeyMaps, { [target]: liveMap }) : null;
|
|
866
902
|
const sessionApproved = Object.values(sessionVerdicts).filter((v) => v === "approve").length;
|
|
867
903
|
const sessionRejected = Object.values(sessionVerdicts).filter((v) => v === "reject").length;
|
|
868
904
|
/* ------------------------------ finish panel ------------------------------ */
|
|
@@ -876,14 +912,10 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
876
912
|
? `Loading ${navigatingTo.label ?? navigatingTo.path}…`
|
|
877
913
|
: "Checking remaining pages…", _jsx("span", { className: "qar-muted", style: { fontSize: 11, fontWeight: 400 }, children: "The review continues on the next page - do not close this tab." })] }) }), document.body);
|
|
878
914
|
}
|
|
879
|
-
return createPortal(_jsx("div", { className: "qar-theme qar-finish-backdrop", style: themeStyle, children: _jsxs("div", { className: "qar-finish-card", children: [_jsx(PartyPopper, { className: "qar-finish-icon", size: 40, "aria-hidden": true }), _jsx("h2", { className: "qar-finish-title", children: inJourney ? "Journey complete" : "QA review complete" }), _jsxs("p", { className: "qar-finish-stats", "data-qatip": "Verdicts you recorded in THIS review run", children: [_jsxs("span", { className: "qar-green", children: [sessionApproved, " approved"] }), " \u00B7", " ", _jsxs("span", { className: "qar-red", children: [sessionRejected, " rejected"] }), " \u00B7 this round"] }), _jsxs("p", { className: "qar-finish-alltime", "data-qatip": "Ledger totals for this page across all runs (durable review database)", children: ["all-
|
|
915
|
+
return createPortal(_jsx("div", { className: "qar-theme qar-finish-backdrop", style: themeStyle, children: _jsxs("div", { className: "qar-finish-card", children: [_jsx(PartyPopper, { className: "qar-finish-icon", size: 40, "aria-hidden": true }), _jsx("h2", { className: "qar-finish-title", children: inJourney ? "Journey complete" : "QA review complete" }), _jsxs("p", { className: "qar-finish-stats", "data-qatip": "Verdicts you recorded in THIS review run", children: [_jsxs("span", { className: "qar-green", children: [sessionApproved, " approved"] }), " \u00B7", " ", _jsxs("span", { className: "qar-red", children: [sessionRejected, " rejected"] }), " \u00B7 this round"] }), _jsxs("p", { className: "qar-finish-alltime", "data-qatip": "Ledger totals for this page across all runs (durable review database)", children: ["this page, all runs: ", approved, " approved \u00B7 ", rejected, " rejected \u00B7 ", total, " items"] }), journeyTally && (_jsxs("p", { className: "qar-finish-alltime", "data-qatip": "Ledger totals across every page of this journey", children: ["whole journey: ", journeyTally.approved, " approved \u00B7 ", journeyTally.rejected, " rejected \u00B7", " ", journeyTally.total, " items"] })), _jsxs("p", { className: "qar-finish-autosaved", "data-qatip": "Every verdict was written to the server ledger the moment you decided it", children: [_jsx(Database, { size: 14, "aria-hidden": true }), " Auto-saved to the review database as you go."] }), inJourney && journeyCounts && (_jsx("div", { className: "qar-journey", children: journey.pages.map((p, i) => {
|
|
880
916
|
const left = journeyCounts[p.target] ?? 0;
|
|
881
917
|
return (_jsxs("div", { className: `qar-journey-row${i === journeyIdx ? " qar-current" : ""}`, children: [_jsxs("span", { children: [p.label ?? p.path, i === journeyIdx ? " (here)" : ""] }), _jsx("span", { className: left === 0 ? "qar-journey-done" : "qar-journey-left", children: left === 0 ? "✓ done" : `${left} left` })] }, p.target));
|
|
882
|
-
}) })), _jsxs("div", { className: "qar-finish-actions", children: [
|
|
883
|
-
? "Saving…"
|
|
884
|
-
: save.status === "ok"
|
|
885
|
-
? "Snapshot saved ✓"
|
|
886
|
-
: "Save session snapshot (optional)"] }), save.status !== "ok" && (_jsx("p", { className: "qar-finish-autosaved", style: { marginTop: 4 }, children: "Your verdicts above are already saved - this only logs a snapshot of the whole run." })), save.status === "error" && _jsx("p", { className: "qar-warn", children: save.message })] })), _jsxs("button", { type: "button", onClick: copyResults, className: "qar-btn-outline-accent", "data-qatip": "Copy all verdicts of this page as JSON", children: [_jsx(ClipboardCopy, { size: 16, "aria-hidden": true }), copied ? "Copied!" : "Copy JSON"] }), roundTotal > 0 && (_jsx("button", { type: "button", onClick: () => {
|
|
918
|
+
}) })), _jsxs("div", { className: "qar-finish-actions", children: [save.status === "error" && (_jsxs("p", { className: "qar-warn", children: ["Run snapshot not saved: ", save.message] })), _jsxs("button", { type: "button", onClick: copyResults, className: "qar-btn-outline-accent", "data-qatip": "Copy all verdicts of this page as JSON", children: [_jsx(ClipboardCopy, { size: 16, "aria-hidden": true }), copied ? "Copied!" : "Copy JSON"] }), roundTotal > 0 && (_jsx("button", { type: "button", onClick: () => {
|
|
887
919
|
setFinished(false);
|
|
888
920
|
// 🚨 THE LAST ITEM, NOT THE FIRST (Arty 2026-08-09: "Clicking
|
|
889
921
|
// on 'Back to review' in the QA interactive panel should go to
|
|
@@ -988,5 +1020,5 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
|
|
|
988
1020
|
return (_jsxs("button", { type: "button", onClick: () => approveDevice(d), className: cls, "data-qatip": `${DEVICE_TOOLTIP[d]}${isHint ? " (A)" : ""}${done ? " - already approved" : ""}`, children: [multiDevice ? (d === "pc" ? (_jsx(Monitor, { size: 16, "aria-hidden": true })) : (_jsx(Smartphone, { size: 16, "aria-hidden": true }))) : (_jsx(Check, { size: 16, "aria-hidden": true })), multiDevice ? `${done ? "✓ " : ""}${DEVICE_LABEL[d]}` : "Approve"] }, d));
|
|
989
1021
|
}), _jsx("button", { type: "button", onClick: next, disabled: !hasNext, "aria-label": "Next item", "data-qatip": index >= roundTotal - 1 && inJourney
|
|
990
1022
|
? "Finish this page and continue the journey (→)"
|
|
991
|
-
: "Next item (→)", className: "qar-nav-btn", children: _jsx(ChevronRight, { size: 16 }) })] }), _jsxs("div", { className: "qar-footer", children: [_jsx("span", { "data-qatip": "Keyboard shortcuts", children: "keys: A approve \u00B7 R reject \u00B7 \u2190/\u2192 \u00B7 U undo \u00B7 M minimize \u00B7 Esc" }), _jsxs("span", { children: [_jsxs("span", { className: "qar-footer-green", "data-qatip": "Items decided in this round", children: [decidedInRound, "/", roundTotal, " this round"] }), " ", _jsxs("span", { "data-qatip": "
|
|
1023
|
+
: "Next item (→)", className: "qar-nav-btn", children: _jsx(ChevronRight, { size: 16 }) })] }), _jsxs("div", { className: "qar-footer", children: [_jsx("span", { "data-qatip": "Keyboard shortcuts", children: "keys: A approve \u00B7 R reject \u00B7 \u2190/\u2192 \u00B7 U undo \u00B7 M minimize \u00B7 Esc" }), _jsxs("span", { children: [_jsxs("span", { className: "qar-footer-green", "data-qatip": "Items decided in this round", children: [decidedInRound, "/", roundTotal, " this round"] }), " ", journeyTally ? (_jsxs("span", { "data-qatip": "Ledger totals across every page of this journey", children: ["\u00B7 journey: ", journeyTally.approved, " approved \u00B7 ", journeyTally.rejected, " rejected"] })) : (_jsxs("span", { "data-qatip": "Items on this page across all runs (ledger)", children: ["\u00B7 this page: ", approvedCount, " approved \u00B7 ", rejected, " rejected"] }))] })] })] })] }), document.body);
|
|
992
1024
|
}
|
|
@@ -5,9 +5,24 @@ export declare function normalizeText(text: string): string;
|
|
|
5
5
|
export declare function fingerprintText(text: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* Fingerprint the CURRENT content of an item: anchored items hash the
|
|
8
|
-
* element's
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* element's textContent; off-DOM task items hash their question text (sub,
|
|
9
|
+
* falling back to title). Returns null when the anchor is not present (e.g.
|
|
10
|
+
* other-viewport element) - no judgement is made then.
|
|
11
|
+
*
|
|
12
|
+
* 🚨 textContent, NEVER innerText (Arty 2026-09-24, GameReady fundraise FAQ).
|
|
13
|
+
* innerText is spec'd to approximate what a user currently SEES, so it
|
|
14
|
+
* excludes anything not laid out - which includes the body of a closed
|
|
15
|
+
* native `<details>`. Every accordion-style FAQ defaults to collapsed, so an
|
|
16
|
+
* anchor on the answer paragraph inside it fingerprinted ONLY the always-
|
|
17
|
+
* visible summary/question text forever, regardless of how the answer was
|
|
18
|
+
* edited. "NOT ALTERED since your rejection" then showed on an item that
|
|
19
|
+
* had, in fact, been fixed the same day it was rejected - the exact
|
|
20
|
+
* regression this poka-yoke exists to prevent, silently defeated by the one
|
|
21
|
+
* DOM shape (collapsed accordion) FAQ content overwhelmingly uses. textContent
|
|
22
|
+
* does not care about layout/visibility, which is what "did the words
|
|
23
|
+
* change" actually needs - this was invisible in tests because jsdom does
|
|
24
|
+
* not implement `<details>` collapse or innerText's visibility semantics at
|
|
25
|
+
* all, so the bug could only ever surface against a real rendered page.
|
|
11
26
|
*/
|
|
12
27
|
export declare function itemFingerprint(item: QAReviewItem, doc: Document): string | null;
|
|
13
28
|
export type FingerprintStatus = "unchanged" | "changed";
|
|
@@ -16,9 +16,24 @@ export function fingerprintText(text) {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Fingerprint the CURRENT content of an item: anchored items hash the
|
|
19
|
-
* element's
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* element's textContent; off-DOM task items hash their question text (sub,
|
|
20
|
+
* falling back to title). Returns null when the anchor is not present (e.g.
|
|
21
|
+
* other-viewport element) - no judgement is made then.
|
|
22
|
+
*
|
|
23
|
+
* 🚨 textContent, NEVER innerText (Arty 2026-09-24, GameReady fundraise FAQ).
|
|
24
|
+
* innerText is spec'd to approximate what a user currently SEES, so it
|
|
25
|
+
* excludes anything not laid out - which includes the body of a closed
|
|
26
|
+
* native `<details>`. Every accordion-style FAQ defaults to collapsed, so an
|
|
27
|
+
* anchor on the answer paragraph inside it fingerprinted ONLY the always-
|
|
28
|
+
* visible summary/question text forever, regardless of how the answer was
|
|
29
|
+
* edited. "NOT ALTERED since your rejection" then showed on an item that
|
|
30
|
+
* had, in fact, been fixed the same day it was rejected - the exact
|
|
31
|
+
* regression this poka-yoke exists to prevent, silently defeated by the one
|
|
32
|
+
* DOM shape (collapsed accordion) FAQ content overwhelmingly uses. textContent
|
|
33
|
+
* does not care about layout/visibility, which is what "did the words
|
|
34
|
+
* change" actually needs - this was invisible in tests because jsdom does
|
|
35
|
+
* not implement `<details>` collapse or innerText's visibility semantics at
|
|
36
|
+
* all, so the bug could only ever surface against a real rendered page.
|
|
22
37
|
*/
|
|
23
38
|
export function itemFingerprint(item, doc) {
|
|
24
39
|
if (!item.selector)
|
|
@@ -26,7 +41,7 @@ export function itemFingerprint(item, doc) {
|
|
|
26
41
|
const el = doc.querySelector(item.selector);
|
|
27
42
|
if (!el)
|
|
28
43
|
return null;
|
|
29
|
-
return fingerprintText(el.
|
|
44
|
+
return fingerprintText(el.textContent ?? "");
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
47
|
* Compare the fingerprint STORED with the last verdict against the current
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { QAReviewOverlay, isClickGesture, nextMinimized, AUTO_BUBBLE_MAX_WIDTH_PX, type BubbleEvent, type QAReviewOverlayProps, } from "./QAReviewOverlay.js";
|
|
2
2
|
export { QAStore, createQAStore, targetFromLocation, type QAStoreOptions, type PersistedVerdict, type PendingOp, type VerdictMap, } from "./store.js";
|
|
3
|
-
export { journeyIndex, countPending, countUnverdicted, nextPendingPage, buildJourneyNavUrl, fetchPendingCounts, resolveFinishAction, journeyFinishView, shouldPrefetch, isPrefetchFresh, ensurePrefetchLink, PREFETCH_WINDOW_ITEMS, PREFETCH_MAX_AGE_MS, type FinishAction, type QAJourneyConfig, type QAJourneyPage, } from "./journey.js";
|
|
3
|
+
export { journeyIndex, countPending, countUnverdicted, nextPendingPage, buildJourneyNavUrl, fetchPendingCounts, fetchJourneyVerdicts, tallyJourney, resolveFinishAction, journeyFinishView, shouldPrefetch, isPrefetchFresh, ensurePrefetchLink, PREFETCH_WINDOW_ITEMS, PREFETCH_MAX_AGE_MS, type FinishAction, type QAJourneyConfig, type QAJourneyPage, type VerdictTally, } from "./journey.js";
|
|
4
4
|
export { normalizeText, fingerprintText, itemFingerprint, fingerprintStatus, type FingerprintStatus, } from "./fingerprint.js";
|
|
5
5
|
export { requiredDevices, approvedDevicesOf, isFullyApproved, nextApprovedDevices, toggleDevice, detectDevice, DEVICE_LABEL, DEVICE_TOOLTIP, type QADevice, } from "./device.js";
|
|
6
6
|
export { findMatchRanges, applySubHighlights, type MatchRange } from "./highlight.js";
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { QAReviewOverlay, isClickGesture, nextMinimized, AUTO_BUBBLE_MAX_WIDTH_PX, } from "./QAReviewOverlay.js";
|
|
2
2
|
export { QAStore, createQAStore, targetFromLocation, } from "./store.js";
|
|
3
|
-
export { journeyIndex, countPending, countUnverdicted, nextPendingPage, buildJourneyNavUrl, fetchPendingCounts, resolveFinishAction, journeyFinishView, shouldPrefetch, isPrefetchFresh, ensurePrefetchLink, PREFETCH_WINDOW_ITEMS, PREFETCH_MAX_AGE_MS, } from "./journey.js";
|
|
3
|
+
export { journeyIndex, countPending, countUnverdicted, nextPendingPage, buildJourneyNavUrl, fetchPendingCounts, fetchJourneyVerdicts, tallyJourney, resolveFinishAction, journeyFinishView, shouldPrefetch, isPrefetchFresh, ensurePrefetchLink, PREFETCH_WINDOW_ITEMS, PREFETCH_MAX_AGE_MS, } from "./journey.js";
|
|
4
4
|
export { normalizeText, fingerprintText, itemFingerprint, fingerprintStatus, } from "./fingerprint.js";
|
|
5
5
|
export { requiredDevices, approvedDevicesOf, isFullyApproved, nextApprovedDevices, toggleDevice, detectDevice, DEVICE_LABEL, DEVICE_TOOLTIP, } from "./device.js";
|
|
6
6
|
export { findMatchRanges, applySubHighlights } from "./highlight.js";
|
package/dist/client/journey.d.ts
CHANGED
|
@@ -87,3 +87,21 @@ export declare function ensurePrefetchLink(doc: Document, href: string): void;
|
|
|
87
87
|
* a page silently).
|
|
88
88
|
*/
|
|
89
89
|
export declare function fetchPendingCounts(stateUrl: string, pages: readonly QAJourneyPage[]): Promise<Record<string, number>>;
|
|
90
|
+
/** Ledger verdict map per journey page target (null = fetch failed). */
|
|
91
|
+
export declare function fetchJourneyVerdicts(stateUrl: string, pages: readonly QAJourneyPage[]): Promise<Record<string, VerdictMap | null>>;
|
|
92
|
+
export interface VerdictTally {
|
|
93
|
+
approved: number;
|
|
94
|
+
rejected: number;
|
|
95
|
+
total: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Approve / reject / item totals over a whole journey (0.3.13). Counts only the
|
|
99
|
+
* page's declared `itemIds`, so stale ledger rows for retired items never
|
|
100
|
+
* inflate the numbers. `overrides` replaces a page's ledger map with live state
|
|
101
|
+
* (the current page's in-memory verdicts are fresher than any fetch).
|
|
102
|
+
*
|
|
103
|
+
* 🚨 Arty 2026-10-07: the panel said "1 approved, 0 rejected" at the end of a
|
|
104
|
+
* ten-page journey he had just walked through with 12 verdicts - the count was
|
|
105
|
+
* page-scoped and unlabeled, so it read as the whole review.
|
|
106
|
+
*/
|
|
107
|
+
export declare function tallyJourney(pages: readonly QAJourneyPage[], maps: Readonly<Record<string, VerdictMap | null | undefined>>, overrides?: Readonly<Record<string, VerdictMap>>): VerdictTally;
|
package/dist/client/journey.js
CHANGED
|
@@ -112,6 +112,14 @@ export function ensurePrefetchLink(doc, href) {
|
|
|
112
112
|
* a page silently).
|
|
113
113
|
*/
|
|
114
114
|
export async function fetchPendingCounts(stateUrl, pages) {
|
|
115
|
+
const maps = await fetchJourneyVerdicts(stateUrl, pages);
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const p of pages)
|
|
118
|
+
out[p.target] = countUnverdicted(p.itemIds, maps[p.target]);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Ledger verdict map per journey page target (null = fetch failed). */
|
|
122
|
+
export async function fetchJourneyVerdicts(stateUrl, pages) {
|
|
115
123
|
const out = {};
|
|
116
124
|
await Promise.all(pages.map(async (p) => {
|
|
117
125
|
let map = null;
|
|
@@ -128,7 +136,32 @@ export async function fetchPendingCounts(stateUrl, pages) {
|
|
|
128
136
|
catch {
|
|
129
137
|
map = null;
|
|
130
138
|
}
|
|
131
|
-
out[p.target] =
|
|
139
|
+
out[p.target] = map;
|
|
132
140
|
}));
|
|
133
141
|
return out;
|
|
134
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Approve / reject / item totals over a whole journey (0.3.13). Counts only the
|
|
145
|
+
* page's declared `itemIds`, so stale ledger rows for retired items never
|
|
146
|
+
* inflate the numbers. `overrides` replaces a page's ledger map with live state
|
|
147
|
+
* (the current page's in-memory verdicts are fresher than any fetch).
|
|
148
|
+
*
|
|
149
|
+
* 🚨 Arty 2026-10-07: the panel said "1 approved, 0 rejected" at the end of a
|
|
150
|
+
* ten-page journey he had just walked through with 12 verdicts - the count was
|
|
151
|
+
* page-scoped and unlabeled, so it read as the whole review.
|
|
152
|
+
*/
|
|
153
|
+
export function tallyJourney(pages, maps, overrides = {}) {
|
|
154
|
+
const t = { approved: 0, rejected: 0, total: 0 };
|
|
155
|
+
for (const p of pages) {
|
|
156
|
+
const map = overrides[p.target] ?? maps[p.target];
|
|
157
|
+
t.total += p.itemIds.length;
|
|
158
|
+
for (const id of p.itemIds) {
|
|
159
|
+
const v = map?.[id]?.verdict;
|
|
160
|
+
if (v === "approve")
|
|
161
|
+
t.approved++;
|
|
162
|
+
else if (v === "reject")
|
|
163
|
+
t.rejected++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return t;
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artymclabin/qa-review",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Interactive on-page QA review overlay (spotlight walkthrough, approve/reject verdict ledger) with a framework-agnostic server handler factory and a self-provisioning Postgres store.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|