diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fca25d8..b1d1e74 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -114,8 +114,13 @@ jobs: # And its output must match what is committed, so a hand-edit to a # generated file, or a source change that was never regenerated, fails # here rather than shipping. + # Every generated artefact is listed, and the generator is timestamp-free + # by design (T-ENG03) — a run-date in any header would fail this on day two. - name: Assert generated output is current - run: git diff --exit-code -- data/entries docs/data.js docs/incidents.js + run: >- + git diff --exit-code -- + data/entries data/backlinks.json + docs/data.js docs/backlinks.js docs/frameworks-registry.js docs/incidents.js unit-tests: name: Unit tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2311bf..37350d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,28 @@ the corresponding JSON file in `/data/`. The schema is in [`data/schema.json`](data/schema.json). This keeps the machine-readable layer in sync with the markdown. +### Generated files + +`data/entries/*.json`, `data/backlinks.json` and the webapp bundles +(`docs/data.js`, `docs/backlinks.js`, `docs/frameworks-registry.js`, +`docs/incidents.js`) are **written by `scripts/generate.js`, never by hand**. +`data/stats.json` and the README badges come from `npm run stats`. + +The build is deterministic: running `npm run build` twice on the same +sources produces byte-identical output, and the generated files carry no +run date, machine name or other build-time value. CI regenerates everything +and fails on any diff, so after changing a mapping file run: + +```bash +npm run build # generate → validate → stats:check +npm test # includes the determinism and bundle checks +git diff --exit-code # must be clean apart from your intended change +``` + +GitHub Pages serves `docs/` straight from `main`, which is why the bundles +are committed rather than built on deploy. Keep it that way unless the +Pages source is deliberately switched to a workflow-based deploy. + --- ## Code of conduct diff --git a/docs/backlinks.js b/docs/backlinks.js index f323dd8..1c4a46e 100644 --- a/docs/backlinks.js +++ b/docs/backlinks.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Backlinks: 1159 window.CROSSWALK_BACKLINKS = [ { diff --git a/docs/data.js b/docs/data.js index 8442fd5..ab2b5c1 100644 --- a/docs/data.js +++ b/docs/data.js @@ -1,6 +1,5 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Source: OWASP GenAI Crosswalk v1.5.2 -// Generated: 2026-08-28 +// Source: OWASP GenAI Crosswalk v4.0.0 // Entries: 51 window.CROSSWALK_DATA = [ { diff --git a/docs/frameworks-registry.js b/docs/frameworks-registry.js index c6abd6c..efa9a1b 100644 --- a/docs/frameworks-registry.js +++ b/docs/frameworks-registry.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Frameworks: 25 window.CROSSWALK_FRAMEWORKS = [ { diff --git a/docs/incidents.js b/docs/incidents.js index e329a5d..f8884a0 100644 --- a/docs/incidents.js +++ b/docs/incidents.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Incidents: 131 window.CROSSWALK_INCIDENTS = [ { diff --git a/scripts/generate.js b/scripts/generate.js index d1804f1..f73a06a 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -841,15 +841,18 @@ function main() { allEntries.push(entry); } - // Write bundled site data for GitHub Pages query interface + // Write bundled site data for GitHub Pages query interface. + // Bundle headers are deliberately timestamp-free: docs/ is served straight + // from main and CI asserts `git diff --exit-code` on these files, so a + // generated-on date would make every checkout dirty the next day. if (!DRY_RUN && !SINGLE_ID) { + const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; const docsDir = path.join(ROOT, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); const siteDataPath = path.join(docsDir, 'data.js'); const siteData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Source: OWASP GenAI Crosswalk v1.5.2`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, + `// Source: OWASP GenAI Crosswalk v${PKG_VERSION}`, `// Entries: ${allEntries.length}`, `window.CROSSWALK_DATA = ${JSON.stringify(allEntries, null, 2)};`, ].join('\n'); @@ -895,7 +898,6 @@ function main() { const siteBacklinksPath = path.join(docsDir, 'backlinks.js'); const siteBacklinks = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Backlinks: ${backlinksArray.length}`, `window.CROSSWALK_BACKLINKS = ${JSON.stringify(backlinksArray, null, 2)};`, ].join('\n'); @@ -920,7 +922,6 @@ function main() { const fwRegistryPath = path.join(docsDir, 'frameworks-registry.js'); const fwRegistryData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Frameworks: ${fwRegistry.length}`, `window.CROSSWALK_FRAMEWORKS = ${JSON.stringify(fwRegistry, null, 2)};`, ].join('\n'); @@ -934,7 +935,6 @@ function main() { const incPath = path.join(docsDir, 'incidents.js'); const incData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Incidents: ${incDb.incidents.length}`, `window.CROSSWALK_INCIDENTS = ${JSON.stringify(incDb.incidents, null, 2)};`, ].join('\n'); diff --git a/scripts/generate.test.mjs b/scripts/generate.test.mjs index 8c31dce..c531ae4 100644 --- a/scripts/generate.test.mjs +++ b/scripts/generate.test.mjs @@ -117,6 +117,20 @@ test('DRAFT never survives into a stored enum field', () => { assert.deepEqual(leaked.slice(0, 5), [], `${leaked.length} field(s) stored the literal "DRAFT"`); }); +test('webapp bundles carry no build timestamp', () => { + // docs/ is served straight from main and CI diffs these files against a + // fresh generation. A `// Generated: ` header made that diff fail on + // every day but the one the bundle was committed — so the header must + // describe the data, never the run. + for (const f of BUNDLES.filter((b) => fs.existsSync(b))) { + const header = fs.readFileSync(f, 'utf8').split(/\r?\n/).filter((l) => l.startsWith('//')); + for (const line of header) { + assert.doesNotMatch(line, /Generated:/i, `${path.basename(f)} header names a run: ${line}`); + assert.doesNotMatch(line, /\d{4}-\d{2}-\d{2}/, `${path.basename(f)} header carries a date: ${line}`); + } + } +}); + test('webapp bundles stay in step with the entry files', () => { const src = fs.readFileSync(path.join(ROOT, 'docs', 'data.js'), 'utf8'); const start = src.indexOf('[');