diff --git a/.gitignore b/.gitignore
index d4d9883ff..bc6e40f98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,4 @@ yarn-error.log*
.idea
venv/*
+bun.lock
diff --git a/src/__tests__/layout/VersionBadge.test.js b/src/__tests__/layout/VersionBadge.test.js
new file mode 100644
index 000000000..85557c171
--- /dev/null
+++ b/src/__tests__/layout/VersionBadge.test.js
@@ -0,0 +1,59 @@
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import VersionBadge from "../../layout/VersionBadge";
+
+describe("VersionBadge", () => {
+ test("renders a compact strip with the country package version and dataset", () => {
+ render(
+ ,
+ );
+ expect(
+ screen.getByLabelText(/versions used for this result/i),
+ ).toHaveTextContent("policyengine-us@1.653.3 · enhanced_cps");
+ });
+
+ test("renders nothing when modelVersion is missing", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ test("maps country ids to package names", () => {
+ render(
+ ,
+ );
+ expect(screen.getByLabelText(/versions used/i)).toHaveTextContent(
+ "policyengine-uk@2.88.0",
+ );
+ });
+
+ test("falls back to 'default' when dataset is not set", () => {
+ render();
+ expect(screen.getByLabelText(/versions used/i)).toHaveTextContent(
+ "policyengine-us@1.653.3 · default",
+ );
+ });
+
+ test("includes data version and truncated h5 sha when provided", () => {
+ render(
+ ,
+ );
+ const badge = screen.getByLabelText(/versions used/i);
+ expect(badge).toHaveTextContent("enhanced_cps@1.85.2");
+ expect(badge).toHaveTextContent("sha256:abc12345");
+ expect(badge).not.toHaveTextContent(
+ "fedcba1234567890fedcba1234567890fedcba1234567890abc",
+ );
+ });
+});
diff --git a/src/layout/VersionBadge.jsx b/src/layout/VersionBadge.jsx
new file mode 100644
index 000000000..e829d62ea
--- /dev/null
+++ b/src/layout/VersionBadge.jsx
@@ -0,0 +1,139 @@
+import { useState } from "react";
+import { Popover } from "antd";
+import style from "../style";
+
+/**
+ * Small always-visible strip that identifies which model version and
+ * dataset produced a simulation result.
+ *
+ * See PolicyEngine/policyengine-app#2831 for motivation. This is the
+ * plain version-identification surface — the TRACE-bound "Cite this
+ * result" download is scoped separately in #2830 and blocked on the
+ * api work in PolicyEngine/policyengine-api#3485.
+ *
+ * Today the api metadata endpoint only exposes the country-package
+ * version and the selectable dataset name. Once the api migrates to
+ * policyengine.py v4 (api#3486) and begins returning
+ * policyengine-{country}-data version and h5 content hash on every
+ * simulation response, this component should read those additional
+ * fields and expand the visible badge.
+ */
+export default function VersionBadge(props) {
+ const {
+ countryId,
+ modelVersion,
+ dataset,
+ dataVersion,
+ h5Sha,
+ compact = false,
+ } = props;
+
+ const [popoverOpen, setPopoverOpen] = useState(false);
+
+ if (!modelVersion) {
+ return null;
+ }
+
+ const modelPackageName =
+ countryId === "us"
+ ? "policyengine-us"
+ : countryId === "uk"
+ ? "policyengine-uk"
+ : countryId === "canada"
+ ? "policyengine-canada"
+ : `policyengine-${countryId}`;
+
+ // Human-readable dataset label. The api exposes canonical names like
+ // "enhanced_cps" or "cps" — surface them as-is so a reader who
+ // knows the pipeline can tell exactly what ran.
+ const datasetLabel = dataset || "default";
+
+ const compactStrip = (
+
+ {`${modelPackageName}@${modelVersion} · ${datasetLabel}`}
+ {dataVersion && `@${dataVersion}`}
+ {h5Sha && ` · h5 sha256:${h5Sha.substring(0, 8)}…`}
+
+ );
+
+ const detailContent = (
+
+
+
Model package
+
+ {modelPackageName}=={modelVersion}
+
+
+
+
Dataset
+
+ {datasetLabel}
+ {dataVersion ? `@${dataVersion}` : ""}
+
+
+ {h5Sha && (
+
+
h5 content hash
+
+ sha256:{h5Sha}
+
+
+ )}
+
+ These identify the pinned software and microdata that produced this
+ result. A citable TRO that binds them under a SHA-256 composition
+ fingerprint is coming — see issue{" "}
+
+ policyengine-app#2830
+
+ .
+
+
+ );
+
+ return (
+
+ {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ setPopoverOpen(!popoverOpen);
+ }
+ }}
+ style={{ cursor: "pointer" }}
+ >
+ {compactStrip}
+
+
+ );
+}
diff --git a/src/pages/household/output/HouseholdOutput.jsx b/src/pages/household/output/HouseholdOutput.jsx
index 8f9956edc..f6b4b6d48 100644
--- a/src/pages/household/output/HouseholdOutput.jsx
+++ b/src/pages/household/output/HouseholdOutput.jsx
@@ -10,6 +10,7 @@ import PoliciesModelledPopup from "../../../modals/PoliciesModelledPopup";
import React from "react";
import { message } from "antd";
import ResultActions from "layout/ResultActions";
+import VersionBadge from "../../../layout/VersionBadge";
export default function HouseholdOutput(props) {
const [searchParams] = useSearchParams();
@@ -131,6 +132,9 @@ export default function HouseholdOutput(props) {
const facebookLink = `https://www.facebook.com/sharer/sharer.php?u=${url}`;
const linkedInLink = `https://www.linkedin.com/sharing/share-offsite/?url=${url}`;
+ const selectedVersion = urlParams.get("version") || metadata.version;
+ const dataset = urlParams.get("dataset");
+
return (
{pane}
+ {householdBaseline && !loading && (
+
+
+
+ )}
);
}
diff --git a/src/pages/policy/output/Display.jsx b/src/pages/policy/output/Display.jsx
index e5e278ded..deaeb834b 100644
--- a/src/pages/policy/output/Display.jsx
+++ b/src/pages/policy/output/Display.jsx
@@ -18,6 +18,7 @@ import PolicyBreakdown from "./PolicyBreakdown";
import { Helmet } from "react-helmet";
import useCountryId from "../../../hooks/useCountryId";
import BottomImpactDescription from "../../../layout/BottomImpactDescription";
+import VersionBadge from "../../../layout/VersionBadge";
import { Link } from "react-router-dom";
import MultiYearBudgetaryImpact from "./budget/MultiYearBudgetaryImpact";
@@ -352,19 +353,34 @@ export function LowLevelDisplay(props) {
//eslint-disable-next-line
const bottomElements =
mobile & !embed ? null : (
-
- {bottomText}
- {bottomLink && (
-
- Learn more
-
- )}
-
+
+ {bottomText}
+ {bottomLink && (
+
+ Learn more
+
+ )}
+
+
+
);
// If ?embed=True, just show `pane`, full screen.