From 94378ac1b79830670ccb29bf26b9115fe6544372 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 06:20:47 +0000 Subject: [PATCH] Shared prose-name gate: read the meta/ sidecars too Copy of abap2UI5's `.github/shared/check-prose-names.mjs`, which is the source. The gate's scope was eight markdown files. In a repository whose ports each carry a `meta/.json`, that misses where class names are written most often: `deviations[].what`, `audit.note` and `checked.note` are long-form prose - samples-controls' own AGENTS.md calls a deviation "a log entry about a process" and puts it there BECAUSE it is prose - and those sentences cite sibling ports constantly. Not hypothetical. A 2026-08-21 sweep removed the retired `z2ui5_cl_demo_app_*` citations and recorded itself finished: "36 class names across 8 prose files, every one of them existing". Two days later `meta/z2ui5_cl_smpc_app_038.json` still named `z2ui5_cl_demo_app_038`, a class that exists nowhere. It survived because it was never looked at, and it was found by a human reading that one port against its original. It travels further than a sidecar suggests: generate-overview.mjs bakes those texts into ABAP that gets pulled into a customer system. A second SIDECARS scope beside PROSE rather than more entries in it, since the files are JSON and the reader differs; both then reduce to a labelled piece of prose, so the regex, ABSENT and the whole ecosystem-wide resolution (foreign SAMPLES.md lookup included) run over sidecar text unchanged. The sidecar's own `class` field is not read - it resolves trivially and would double every port into the count. A repository with no `meta/` contributes no files, so this is inert where there are no sidecars. Measured: samples 25 names / no sidecars, samples-stack 14 / none, samples-controls 182 names across 8 files and 622 sidecars, all resolving. With the 038 defect put back, the run fails naming the deviation index rather than the 4 KB file. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E8iuqE8QFQXpb1LzHQ3Ym5 --- scripts/check-prose-names.mjs | 53 +++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/scripts/check-prose-names.mjs b/scripts/check-prose-names.mjs index 003c8e8..fb81a30 100644 --- a/scripts/check-prose-names.mjs +++ b/scripts/check-prose-names.mjs @@ -67,6 +67,37 @@ const PROSE = ['README.md', 'CONTRIBUTING.md', 'AGENTS.md', 'CLAUDE.md', 'TRAINI * is history, not drift. Same reasoning as abap2UI5's changelog cut-off. */ const HISTORY = /STATUS-history\.md$/; +/* The other half of the prose, and in a sample repository the bigger half. + * Every port carries a `meta/.json` whose `deviations[].what`, + * `audit.note` and `checked.note` are long-form sentences — samples-controls' + * own AGENTS.md calls a deviation "a log entry about a process" and puts it + * there BECAUSE it is prose — and those sentences cite sibling ports + * constantly. They were out of scope until 2026-08-24, which is how + * `meta/z2ui5_cl_smpc_app_038.json` kept citing the retired + * `z2ui5_cl_demo_app_038` through a corpus-wide sweep that reported itself + * finished. It travels further than a sidecar suggests: generate-overview.mjs + * bakes these texts into ABAP that gets pulled into a customer system. + * + * Read as a second scope rather than as more entries in PROSE, because the + * files are JSON and the reader differs. NOT the sidecar's own `class` field, + * which names the port the file belongs to and would double every port into + * the count. A repository with no `meta/` contributes nothing, so this is + * inert in the consumers that do not use sidecars. */ +const SIDECARS = 'meta'; +const SIDECAR_PROSE = (file) => { + const out = []; + let doc; + try { doc = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return out; } + const at = path.relative(ROOT, file); + (doc.deviations ?? []).forEach((d, i) => { + if (typeof d?.what === 'string') out.push({ label: `${at} deviations[${i}].what`, text: d.what }); + }); + for (const [key, holder] of [['audit', doc.audit], ['checked', doc.checked]]) { + if (typeof holder?.note === 'string') out.push({ label: `${at} ${key}.note`, text: holder.note }); + } + return out; +}; + /* Names that are meant to be absent, from `scripts/prose-absent.json` — one * per repository, because what a repository deliberately names in the past * tense is its own business. Each needs its reason, so an entry cannot become @@ -166,10 +197,27 @@ async function catalogueOf(repo) { const problems = []; let checked = 0; +/* Both scopes reduce to the same thing: a labelled piece of prose. Everything + * below — the regex, ABSENT, the ecosystem-wide resolution including the + * foreign SAMPLES.md lookup — then runs over sidecar text unchanged, which is + * the whole reason to do this in the shared script rather than beside it. */ +const sources = []; for (const file of PROSE) { const at = path.join(ROOT, file); if (!fs.existsSync(at) || HISTORY.test(at)) continue; - const text = fs.readFileSync(at, 'utf8'); + sources.push({ label: file, text: fs.readFileSync(at, 'utf8') }); +} +const sidecarDir = path.join(ROOT, SIDECARS); +let sidecarFiles = 0; +if (fs.existsSync(sidecarDir)) { + for (const name of fs.readdirSync(sidecarDir).sort()) { + if (!name.endsWith('.json')) continue; + sidecarFiles += 1; + sources.push(...SIDECAR_PROSE(path.join(sidecarDir, name))); + } +} + +for (const { label: file, text } of sources) { const seen = new Set(); /* Two shapes look like a name and are not one; both are a PREFIX standing for @@ -228,7 +276,8 @@ for (const file of PROSE) { } } -console.log(`prose-names: ${checked} class name(s) checked in ${PROSE.length} prose file(s)`); +console.log(`prose-names: ${checked} class name(s) checked in ${PROSE.length} prose file(s)` + + (sidecarFiles ? ` and ${sidecarFiles} ${SIDECARS}/ sidecar(s)` : '')); for (const n of notes) console.log(` ${n}`); if (problems.length) {