getreadystack's picture
Upload index.html with huggingface_hub
695a832 verified
Raw History Blame Contribute Delete
44.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GDPR Consent Proof requirements — check your consent records free</title>
<meta name="description" content="This free browser tool checks stored consent-record JSON against the proof-of-consent fields in GDPR Article 7(1).">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/consent-proof-record-lint"></head>
<body>
<div class="wrap">
<h1>GDPR Consent Proof requirements — check your consent records free</h1>
<p class="lede">Lints stored consent-record JSON against the GDPR Article 7(1) proof-of-consent fields a supervisory authority asks for. Offline, no telemetry. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">{
"export_id": "consent-log-2026-09",
"records": [
{
"subject_id": "u_91ab7c",
"collected_at": "2026-02-11",
"purposes": { "all": true },
"notice_version": "v3",
"consent_method": "banner",
"withdrawal_method": "https://nordwind.example/privacy/preferences",
"controller": "Nordwind Retail GmbH",
"expires_at": "2028-02-11",
"legal_basis": "consent"
},
{
"collected_at": "2025-08-04T10:22:00Z",
"purposes": {
"analytics": { "granted": true, "user_action": false },
"ads": { "granted": true, "user_action": false }
},
"notice_version": "v2",
"notice_text_hash": "sha256:9f2c61ae4b8d370f5c2a98e1740bd3652f8ce09a1db47c3e80f6a25dc9143b77",
"consent_method": "implied_by_continued_browsing",
"withdrawal_method": "write to support",
"controller": "Nordwind Retail GmbH",
"expires_at": "2026-08-04T10:22:00Z",
"legal_basis": "consent"
},
{
"subject_id": "u_77de20",
"collected_at": "2026-05-20T14:03:51Z",
"purposes": { "analytics": true, "ads": false, "personalisation": false },
"notice_version": "privacy-notice-2026-05-18",
"notice_text_hash": "sha256:4f1c9a2be73d5087ac11d6e0f8b34c92a7d05e61bb4f2c8390de77a1c5b02e44",
"consent_method": "banner_save_selection",
"controller": "Nordwind Retail GmbH",
"expires_at": "2027-05-20T14:03:51Z",
"legal_basis": "legitimate_interest",
"tcf": { "tc_string": "CQKm9YAQKm9YAAKA1AENB5CgAP_gAEPgACiQhFBd" }
},
{
"subject_id": "u_0c4188",
"purposes": { "analytics": true, "ads": true },
"consent_method": "banner_accept_all",
"withdrawal_method": "https://nordwind.example/privacy/preferences"
}
]
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Lints one consent-record JSON file open in the editor and names every missing or invalid proof field with its GDPR article. <strong>In the editor:</strong> Runs every consent-record file in the workspace in one pass and writes a dated evidence report you keep and hand to the auditor — that part asks for a licence key.</div>
<p class="getfull"><a href="https://getreadystack.com/api/buy/cl/polar_cl_mSv5XPtyBLW8GXFQGDEDDv9sgkuuOvkN3MOH02P3VUY">Get the full version &mdash; $29</a> &middot; $29 once &middot; one licence key per person or team seat &middot; A GDPR consultant reviewing a consent log bills $150 to $250 an hour.</p>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var CPR_RULES = [
{"id": "subject_ref", "sev": "err", "article": "GDPR Art.7(1)", "msg": "no subject or device reference on this record — you cannot show whose consent it was. Add subject_id."},
{"id": "collected_at", "sev": "err", "article": "GDPR Art.7(1)", "msg": "no collected_at on this record — a proof of consent with no moment attached proves nothing."},
{"id": "timestamp_utc", "sev": "err", "article": "GDPR Art.7(1)", "msg": "collected_at has no UTC offset. Write a full ISO-8601 instant such as 2026-06-01T08:00:00Z, not a bare local date."},
{"id": "purpose_granularity", "sev": "err", "article": "GDPR Art.4(11)", "msg": "consent is stored as one flag for everything. Consent must be specific: store purposes as a map with one entry per purpose."},
{"id": "notice_version", "sev": "err", "article": "EDPB 05/2020 on consent", "msg": "no notice_version on this record, so nobody can say which wording the person actually read."},
{"id": "notice_text_hash", "sev": "warn", "article": "EDPB 05/2020 on consent", "msg": "no notice_text_hash. A version string alone lets a rewritten page keep the old label; store a hash of the exact text shown."},
{"id": "prechecked_default", "sev": "err", "article": "GDPR Recital 32", "msg": "this record was written with a purpose already switched on before the person acted. Pre-ticked boxes and silence are not consent."},
{"id": "withdrawal_method", "sev": "err", "article": "GDPR Art.7(3)", "msg": "no withdrawal_method recorded. Withdrawal must be as easy as giving, and you have to be able to show the route you offered."},
{"id": "controller_identity", "sev": "err", "article": "GDPR Art.13(1)(a)", "msg": "no controller named on this record. A consent log shared between brands or tenants is unusable without it."},
{"id": "consent_expiry", "sev": "err", "article": "GDPR Art.5(1)(e)", "msg": "no expires_at on this record, so the consent is being kept as if it lasted forever."},
{"id": "expiry_too_long", "sev": "warn", "article": "CNIL / EDPB cookie guidance", "msg": "this consent is set to run more than 13 months from the day it was given. Supervisory authorities treat 6 to 13 months as the outer edge."},
{"id": "already_expired", "sev": "warn", "article": "GDPR Art.5(1)(e)", "msg": "this consent expired before today and is still being relied on. Re-ask or stop processing."},
{"id": "legal_basis", "sev": "err", "article": "GDPR Art.6(1)(a)", "msg": "legal_basis on this record is not consent. Legitimate interest cannot be filed inside a consent log and called proof of consent."},
{"id": "tcf_pair", "sev": "warn", "article": "IAB TCF v2.2", "msg": "a tc_string is stored without the cmp_id that produced it, so the string cannot be decoded back to a vendor list later."}
]
;</script>
<script>/* Consent Proof Record Lint — engine
One file, two homes: node (require) and the browser (window). */
(function (root) {
'use strict';
var RULES = (typeof module !== 'undefined' && module.exports)
? require('./rules.json')
: root.CPR_RULES;
var BY_ID = {};
for (var i = 0; i < RULES.length; i++) BY_ID[RULES[i].id] = RULES[i];
var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
/* Line of every object opening, with its nesting depth. Strings are skipped
so a brace inside a value cannot move the count. */
function objectStarts(text) {
var out = [], depth = 0, line = 1, inStr = false, esc = false;
for (var i = 0; i < text.length; i++) {
var c = text.charAt(i);
if (c === '\n') { line++; continue; }
if (inStr) {
if (esc) esc = false;
else if (c === '\\') esc = true;
else if (c === '"') inStr = false;
continue;
}
if (c === '"') { inStr = true; continue; }
if (c === '{') { depth++; out.push({ line: line, depth: depth }); }
else if (c === '[') { depth++; }
else if (c === '}' || c === ']') { depth--; }
}
return out;
}
/* The depth at which exactly `n` objects open is the record level. */
function recordLines(text, n) {
var starts = objectStarts(text), d, k, lines;
for (d = 1; d <= 8; d++) {
lines = [];
for (k = 0; k < starts.length; k++) if (starts[k].depth === d) lines.push(starts[k].line);
if (lines.length === n) return lines;
}
lines = [];
for (k = 0; k < n; k++) lines.push(1);
return lines;
}
function records(data) {
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.records)) return data.records;
if (data && Array.isArray(data.consents)) return data.consents;
if (data && typeof data === 'object') return [data];
return [];
}
function months(fromISO, toISO) {
var a = Date.parse(fromISO), b = Date.parse(toISO);
if (isNaN(a) || isNaN(b)) return null;
return (b - a) / (1000 * 60 * 60 * 24 * 30.4375);
}
function check(text, opts) {
opts = opts || {};
var today = opts.today || '2026-09-18';
var findings = [];
function hit(id, line) {
var r = BY_ID[id];
findings.push({ check: id, sev: r.sev, msg: r.article + ' — ' + r.msg, line: line || 1 });
}
var data;
try {
data = JSON.parse(text);
} catch (e) {
return {
findings: [{
check: 'parse', sev: 'err', line: 1,
msg: 'This file is not valid JSON, so no consent record can be read from it: ' + e.message
}]
};
}
var rows = records(data);
var lines = recordLines(text, rows.length);
for (var i = 0; i < rows.length; i++) {
var r = rows[i] || {};
var ln = lines[i] || 1;
if (!r.subject_id && !r.subject_ref && !r.device_id) hit('subject_ref', ln);
var at = r.collected_at || r.timestamp || r.created_at;
if (!at) hit('collected_at', ln);
else if (!ISO_UTC.test(String(at))) hit('timestamp_utc', ln);
var p = r.purposes;
var keys = (p && typeof p === 'object' && !Array.isArray(p)) ? Object.keys(p) : [];
if (keys.length < 2) hit('purpose_granularity', ln);
if (!r.notice_version) hit('notice_version', ln);
if (!r.notice_text_hash) hit('notice_text_hash', ln);
var pre = r.prechecked_defaults === true || r.default_on === true;
if (!pre && p && typeof p === 'object') {
for (var k = 0; k < keys.length; k++) {
var v = p[keys[k]];
if (v && typeof v === 'object' && v.granted === true && v.user_action === false) pre = true;
}
}
if (pre) hit('prechecked_default', ln);
if (!r.withdrawal_method) hit('withdrawal_method', ln);
if (!r.controller) hit('controller_identity', ln);
var exp = r.expires_at || r.expiry;
if (!exp) hit('consent_expiry', ln);
else {
if (at) {
var m = months(at, exp);
if (m !== null && m > 13) hit('expiry_too_long', ln);
}
if (Date.parse(exp) < Date.parse(today + 'T00:00:00Z')) hit('already_expired', ln);
}
var basis = r.legal_basis || r.basis;
if (basis && String(basis).toLowerCase() !== 'consent') hit('legal_basis', ln);
var tcf = r.tcf || {};
var tcString = tcf.tc_string || r.tc_string;
var cmpId = tcf.cmp_id || r.cmp_id;
if (tcString && !cmpId) hit('tcf_pair', ln);
}
findings.sort(function (a, b) { return a.line - b.line; });
return { findings: findings };
}
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
root.CPRENGINE = api;
if (typeof module !== 'undefined' && module.exports) module.exports = api;
})(typeof window !== 'undefined' ? window : globalThis);
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var CPRREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = CPRREPORT;
else window.CPRREPORT = CPRREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.CPRENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.CPRREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/consent-proof-record-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'consent-proof-record-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/consent-proof-record-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'consent-proof-record-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/consent-proof-record-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'consent-proof-record-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/consent-proof-record-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'consent-proof-record-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/consent-proof-record-lint &lt;file&gt;
npx @readystack/consent-proof-record-lint --mcp
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/consent-proof-record-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 14 rules Consent Proof Record Lint (GDPR Art.7) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"consent-proof-record-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Reads the consent record your code stored and names every proof field GDPR Article 7(1) needs and your log is missing.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A GDPR consultant reviewing a consent log bills $150 to $250 an hour.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_mSv5XPtyBLW8GXFQGDEDDv9sgkuuOvkN3MOH02P3VUY" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-18">Updated 2026-09-18</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-18","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku112765_result_card.jpg" alt="Consent Proof Record Lint (GDPR Art.7)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Four consent records, fifteen findings, and not one of them provable — that is what a Berlin retailer&#x27;s own consent log returned the first time it was read as evidence instead of as a table.</p>
<p>The team had done the visible part right. The banner rendered, reject-all sat next to accept-all, nothing fired before the click. What nobody had read was the row the code wrote afterwards. GDPR Article 7(1) is blunt about which of the two matters: &quot;the controller shall be able to demonstrate that the data subject has consented.&quot; A banner that renders is not a demonstration. The stored record is.</p>
<p>Most consent logs are a user id, a timestamp and a boolean. That is also, almost exactly, what an AI assistant produces when you ask it to store the consent — and it is the shape that passes code review, because as a database row there is nothing wrong with it. It fails as evidence, for four separate reasons, and a complaint surfaces all four at once.</p>
<p>It cannot say which wording the person read. Without a hash of the exact text, a later rewrite quietly inherits the old version label. It cannot say whether anything was already switched on before the person acted — Recital 32 rules out pre-ticked boxes and silence, so a record that does not distinguish granted-by-default from granted-by-click is not recording consent at all. It cannot say how the person could take it back, which Article 7(3) requires to be as easy as giving. And it usually has no end date, so a consent given in 2024 is still being relied on today, against Article 5(1)(e) and against the six-to-thirteen-month refresh window supervisory authorities work to.</p>
<p>Consent Proof Record Lint reads that file directly. The VS Code extension carries 14 rules, each tied to a named source: Articles 4(11), 5(1)(e), 6(1)(a), 7(1), 7(3) and 13(1)(a), Recital 32, EDPB guidelines 05/2020, and IAB TCF v2.2 for the case where a tc_string is stored without the cmp_id that produced it. Open the JSON, run one command, and every finding lands in the Problems panel on the line of the record that caused it.</p>
<p>The numbers in the first sentence are reproducible. Two fixtures ship inside the extension. <code>_fixtures/clean.json</code> holds three well-formed records and returns 0 findings. <code>_fixtures/dirty.json</code> holds four records from a homegrown logger and returns 15 findings — 10 errors and 5 warnings — and trips all 14 rules between them. Run both before you point the linter at anything of your own.</p>
<p>Free cookie scanners do not cover this. A scanner loads the page and reports which trackers fire before consent; that is the front of the house. Article 7(1) asks about the back, and no scanner opens the log you stored.</p>
<p>The clock is the part teams underestimate. Article 12(3) gives one month to answer a data subject&#x27;s request, and Article 83(5) puts consent breaches in the top fine tier — up to 20 million euro or 4% of worldwide annual turnover. A GDPR consultant reviewing a consent log bills 150 to 250 dollars an hour,.</p>
<p>Checking the file you have open is free and complete: all 14 rules, every finding on its line, running entirely on your machine with no network call and no telemetry — which matters, because the file being linted is a log of other people&#x27;s decisions. A free web page runs the same engine with nothing installed. When the audit is not one file but the whole log, the full version runs every consent-record file in the workspace in one pass and writes a dated evidence report you keep and hand to the auditor.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku112765_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku112765_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Consent Proof Record Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a consent-record JSON file in your editor and checks it against 14 rules drawn from GDPR Articles 4(11), 5(1)(e), 6(1)(a), 7(1), 7(3) and 13(1)(a), Recital 32, EDPB guidelines 05/2020 and IAB TCF v2.2. Every finding lands in the Problems panel on the line of the record that caused it, with the article named.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">EU web and platform engineers who own the consent log rather than the banner: the person a DPO forwards a complaint to. If your consent table is a user id, a timestamp and a boolean — the shape an AI assistant writes when asked to store the consent — this reads that table and tells you what a supervisory authority will find absent.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Cookie scanners are free. Why not use those?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A scanner loads your page and reports which trackers fire before consent. That is the front of the house. Article 7(1) asks about the back: the stored record. No scanner opens your consent log, and a chatbot asked to review it will not reliably count pre-checked defaults or a missing notice version record by record. This reads the file you actually stored.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what is paid?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free is complete for one file: all 14 rules on the consent-record JSON open in your editor, every finding on its line with its article. The free web page does the same with no install. The full version, $29 once, runs every consent-record file in the workspace in one pass and writes a dated evidence report you keep and hand to the auditor.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by a person?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A GDPR consultant reviewing a consent log bills $150 to $250 an hour, and the review has to be redone every time the record shape changes. Article 12(3) gives you one month to answer a data subject&#x27;s request, which is not enough time to book that hour. The extension is $29 once,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Consent Proof Record Lint (GDPR Art.7) checks the file you open against 14 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Consent Proof Record Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a consent-record JSON file in your editor and checks it against 14 rules drawn from GDPR Articles 4(11), 5(1)(e), 6(1)(a), 7(1), 7(3) and 13(1)(a), Recital 32, EDPB guidelines 05/2020 and IAB TCF v2.2. Every finding lands in the Problems panel on the line of the record that caused it, with the article named."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "EU web and platform engineers who own the consent log rather than the banner: the person a DPO forwards a complaint to. If your consent table is a user id, a timestamp and a boolean — the shape an AI assistant writes when asked to store the consent — this reads that table and tells you what a supervisory authority will find absent."}}, {"@type": "Question", "name": "Cookie scanners are free. Why not use those?", "acceptedAnswer": {"@type": "Answer", "text": "A scanner loads your page and reports which trackers fire before consent. That is the front of the house. Article 7(1) asks about the back: the stored record. No scanner opens your consent log, and a chatbot asked to review it will not reliably count pre-checked defaults or a missing notice version record by record. This reads the file you actually stored."}}, {"@type": "Question", "name": "What is free and what is paid?", "acceptedAnswer": {"@type": "Answer", "text": "Free is complete for one file: all 14 rules on the consent-record JSON open in your editor, every finding on its line with its article. The free web page does the same with no install. The full version, $29 once, runs every consent-record file in the workspace in one pass and writes a dated evidence report you keep and hand to the auditor."}}, {"@type": "Question", "name": "What would this cost done by a person?", "acceptedAnswer": {"@type": "Answer", "text": "A GDPR consultant reviewing a consent log bills $150 to $250 an hour, and the review has to be redone every time the record shape changes. Article 12(3) gives you one month to answer a data subject's request, which is not enough time to book that hour. The extension is $29 once,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Consent Proof Record Lint (GDPR Art.7) checks the file you open against 14 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:112765,slug:"consent-proof-record-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/gdpr-parental-consent-registration-form"><span class="yj-rel-t">User Registration &amp; Parental Consent for Youth Clubs — GDPR Art. 8 Ages, 30 Co</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/dora-ict-contract-clause-lint"><span class="yj-rel-t">DORA Art. 30 ICT Contract Clause Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/portfolio-ui-kit-gdpr-embeds"><span class="yj-rel-t">Portfolio UI Kit — GDPR Two-Click Embeds &amp; Consent Banner (Figma)</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/saas-ui-kit-gdpr-consent"><span class="yj-rel-t">SaaS UI Kit — GDPR Consent Components for Figma (Cookie Banner, Opt-in, Withdr</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/webflow-restaurant-template"><span class="yj-rel-t">Webflow Restaurant Template — delivered as self-contained HTML you own: 26 pag</span><span class="yj-rel-p">$49</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>