Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DataTrust Gate

![DataTrust Gate release-audit visual](public/og.png)

**An evidence-led release auditor for bounded AI dataset candidates.**

I built DataTrust Gate to inspect a local CSV or flat JSON file and return a
Expand Down Expand Up @@ -65,6 +67,14 @@ See [docs/PRIVACY.md](docs/PRIVACY.md) for the lifecycle, tested egress paths, a

## Fixed detector regression suite

![DataTrust Gate fixed detector regression coverage](public/detector-regression.svg)

The figure is generated from the maintained synthetic fixture suite. A filled
bar means the detector matched a separately declared expected finding; it does
not represent measured precision, recall, robustness, or coverage of real
datasets. The clean control and unexpected/missed counts are shown separately
so a matched finding cannot hide an extra or absent result.

The repository includes fixed synthetic detector-regression fixtures with a clean control, isolated single-fault cases, and a compound-fault case. Expected findings are declared separately from scanner inputs. The suite checks whether maintained examples still trigger the intended rules; it is not a measured accuracy benchmark.

Reproduce it:
Expand All @@ -74,6 +84,10 @@ npm run regression
npm run regression -- --json
```

Regenerate the SVG with `npm run evidence:render`. `npm run check` includes
`npm run evidence:check` and fails when the committed figure no longer matches
the fixed suite.

The implementation and expected findings are in [lib/regression-suite.ts](lib/regression-suite.ts); exact assertions are in [tests/audit.test.ts](tests/audit.test.ts).

Current `dtg-fixed-regression-v1` (`1.0.0`) result:
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function generateMetadata(): Promise<Metadata> {
title,
description,
type: "website",
images: [{ url: imageUrl, width: 1664, height: 928, alt: "DataTrust Gate — Evidence before release" }],
images: [{ url: imageUrl, width: 1659, height: 948, alt: "DataTrust Gate — Evidence before release" }],
},
twitter: {
card: "summary_large_image",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"test:unit": "tsx --test tests/audit.test.ts tests/parser.test.ts",
"test:integration": "node --test tests/rendered-html.test.mjs",
"test": "npm run test:unit && npm run build && npm run test:integration",
"check": "npm run lint && npm run typecheck && npm run test",
"evidence:render": "tsx scripts/render-readme-evidence.ts",
"evidence:check": "tsx scripts/render-readme-evidence.ts --check",
"check": "npm run lint && npm run typecheck && npm run evidence:check && npm run test",
"regression": "tsx scripts/print-regression.ts",
"lint": "eslint . --ignore-pattern dist --ignore-pattern .next",
"typecheck": "tsc --noEmit"
Expand Down
82 changes: 82 additions & 0 deletions public/detector-regression.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
130 changes: 130 additions & 0 deletions scripts/render-readme-evidence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { runFixedRegressionSuite } from "../lib/regression-suite";

const outputUrl = new URL(
"../public/detector-regression.svg",
import.meta.url,
);

const categoryLabel: Record<string, string> = {
"pii-email": "Email pattern",
"pii-phone": "Malaysian mobile",
"pii-nric": "NRIC-like pattern",
"pii-ip": "IPv4 pattern",
"exact-duplicate": "Exact duplicate",
"near-duplicate": "Near duplicate",
"split-leakage": "Split leakage",
"conflicting-label": "Conflicting label",
"class-imbalance": "Class imbalance",
"missing-provenance": "Missing provenance",
"missing-license": "Missing licence",
};

function escapeXml(value: string) {
return value.replace(
/[&<>"']/g,
(character) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&apos;",
})[character]!,
);
}

async function renderSvg() {
const report = await runFixedRegressionSuite();
if (
report.expectedFindings !== 15 ||
report.matchedExpectedFindings !== 15 ||
report.unexpectedFindings !== 0 ||
report.missedExpectedFindings !== 0 ||
report.cleanRows !== 10 ||
report.cleanRowsFlagged !== 0
) {
throw new Error(
"The fixed detector regression result changed; review the README claim before rendering.",
);
}

const groups = [report.byCategory.slice(0, 6), report.byCategory.slice(6)];
const panels = groups.map((entries, panelIndex) => {
const originX = panelIndex === 0 ? 80 : 790;
const rows = entries.map((entry, rowIndex) => {
if (entry.unexpected !== 0 || entry.missed !== 0) {
throw new Error(`${entry.category} no longer matches its fixed fixture.`);
}
const y = 204 + rowIndex * 72;
const maxWidth = 250;
const expectedWidth = (entry.expected / 2) * maxWidth;
const matchedWidth = (entry.matched / 2) * maxWidth;
const label = categoryLabel[entry.category] ?? entry.category;
return `<g>
<text x="${originX}" y="${y}" class="label">${escapeXml(label)}</text>
<rect x="${originX + 230}" y="${y - 19}" width="${expectedWidth}" height="22" rx="2" class="expected"/>
<rect x="${originX + 230}" y="${y - 19}" width="${matchedWidth}" height="22" rx="2" class="matched"/>
<text x="${originX + 500}" y="${y}" class="value">${entry.matched}/${entry.expected}</text>
</g>`;
});
return `<g>${rows.join("")}</g>`;
});

const inputHash = report.inputArtifactHash.slice(0, 12);
const goldHash = report.goldArtifactHash.slice(0, 12);

return `<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="790" viewBox="0 0 1500 790" role="img" aria-labelledby="title desc">
<title id="title">DataTrust Gate fixed detector regression coverage</title>
<desc id="desc">Fifteen expected findings across eleven detector categories are matched in ten maintained synthetic fixture cases. There are zero unexpected findings, zero missed expected findings, and zero of ten clean-control rows flagged.</desc>
<defs>
<pattern id="grid" width="34" height="34" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1.5" fill="#29484a"/></pattern>
</defs>
<style>
.title{font:500 38px Inter,Segoe UI,sans-serif;fill:#f2f4ed;letter-spacing:.2px}.kicker{font:700 15px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#c8ef3d;letter-spacing:2px}.subtitle,.scope,.hash{font:400 16px Inter,Segoe UI,sans-serif;fill:#93a7a8}.label{font:500 18px Inter,Segoe UI,sans-serif;fill:#e8eeea}.value{font:700 16px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#f2f4ed}.summary-label{font:500 14px Inter,Segoe UI,sans-serif;fill:#93a7a8;text-transform:uppercase;letter-spacing:1px}.summary-value{font:700 25px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#c8ef3d}.expected{fill:#2a3b3d}.matched{fill:#c8ef3d}.rule{stroke:#365052;stroke-width:1}
</style>
<rect width="1500" height="790" fill="#091314"/>
<rect width="1500" height="790" fill="url(#grid)" opacity=".65"/>
<rect x="44" y="34" width="1412" height="722" fill="#091314" opacity=".94" stroke="#365052"/>
<text x="80" y="78" class="kicker">MAINTAINED SYNTHETIC FIXTURES</text>
<text x="80" y="126" class="title">Fixed detector regression coverage</text>
<text x="80" y="158" class="subtitle">Filled bar = matched expected finding · scale ends at two findings · this is not an accuracy estimate</text>
<path d="M80 178H1420" class="rule"/>
${panels.join("")}
<path d="M80 646H1420" class="rule"/>
<g>
<text x="80" y="682" class="summary-label">Expected matched</text><text x="80" y="718" class="summary-value">${report.matchedExpectedFindings}/${report.expectedFindings}</text>
<text x="350" y="682" class="summary-label">Unexpected</text><text x="350" y="718" class="summary-value">${report.unexpectedFindings}</text>
<text x="570" y="682" class="summary-label">Missed</text><text x="570" y="718" class="summary-value">${report.missedExpectedFindings}</text>
<text x="760" y="682" class="summary-label">Clean rows flagged</text><text x="760" y="718" class="summary-value">${report.cleanRowsFlagged}/${report.cleanRows}</text>
</g>
<text x="1420" y="692" text-anchor="end" class="hash">input ${inputHash} · gold ${goldHash}</text>
<text x="1420" y="720" text-anchor="end" class="scope">${escapeXml(report.id)} v${escapeXml(report.suiteVersion)}</text>
</svg>
`;
}

const expected = await renderSvg();
const check = process.argv.includes("--check");
const normalizeLineEndings = (value: string) => value.replace(/\r\n/g, "\n");

if (check) {
let current = "";
try {
current = readFileSync(outputUrl, "utf8");
} catch {
// A missing file is reported by the stale-evidence message below.
}
if (normalizeLineEndings(current) !== expected) {
process.stderr.write(
`README evidence is stale. Run npm run evidence:render (${fileURLToPath(outputUrl)}).\n`,
);
process.exitCode = 1;
} else {
process.stdout.write("README evidence matches the fixed regression suite.\n");
}
} else {
writeFileSync(outputUrl, expected, "utf8");
process.stdout.write(`Wrote ${fileURLToPath(outputUrl)}\n`);
}
Loading