feat(review): a WARN opens the lock only when every counting MAJOR is answered
This commit is contained in:
parent
ce64851a0d
commit
ce7dbc27a9
8 changed files with 1044 additions and 64 deletions
|
|
@ -241,19 +241,38 @@ const HOLLOW = [
|
|||
'ok', 'okay', 'sure', 'fine', 'kjør på', 'kjor pa',
|
||||
];
|
||||
|
||||
/** @returns {{ approved: boolean, reason: 'explicit'|'empty'|'hollow'|'not-explicit' }} */
|
||||
// AskUserQuestion's own guidance puts «(Recommended)» after the label it
|
||||
// recommends, so the operator's real «Approve (Recommended)» was refused in the
|
||||
// voyage-lekerepo trial (2026-10-06). The suffix is stripped; what is left must
|
||||
// still be an explicit approval on its own.
|
||||
const RECOMMENDED_SUFFIX = /\s*\((?:recommended|anbefalt)\)\s*$/u;
|
||||
|
||||
export function isExplicitApproval(answer) {
|
||||
const t = String(answer ?? '').trim().toLowerCase().replace(/[\s.!]+$/u, '')
|
||||
function normalizeAnswer(answer) {
|
||||
return String(answer ?? '').trim().toLowerCase().replace(/[\s.!]+$/u, '')
|
||||
.replace(RECOMMENDED_SUFFIX, '').replace(/[\s.!]+$/u, '').replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `text` is a hollow answer («sounds good», «whatever you think», …):
|
||||
* the HOLLOW list, read the way isExplicitApproval reads it. The empty answer
|
||||
* is not hollow here; a caller that refuses it says so on its own.
|
||||
* @param {string} text
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isHollowAnswer(text) {
|
||||
return hollowNormalized(normalizeAnswer(text));
|
||||
}
|
||||
|
||||
function hollowNormalized(t) {
|
||||
return t !== '' && HOLLOW.some((h) => t === h || (h.includes(' ') && t.includes(h)));
|
||||
}
|
||||
|
||||
/** @returns {{ approved: boolean, reason: 'explicit'|'empty'|'hollow'|'not-explicit' }} */
|
||||
export function isExplicitApproval(answer) {
|
||||
const t = normalizeAnswer(answer);
|
||||
if (!t) return { approved: false, reason: 'empty' };
|
||||
if (EXPLICIT.has(t)) return { approved: true, reason: 'explicit' };
|
||||
if (HOLLOW.some((h) => t === h || (h.includes(' ') && t.includes(h)))) return { approved: false, reason: 'hollow' };
|
||||
if (hollowNormalized(t)) return { approved: false, reason: 'hollow' };
|
||||
return { approved: false, reason: 'not-explicit' };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const DEFAULT_MESSAGE = 'docs(voyage): record approval';
|
|||
const TRAILER_RE = /^Co-Authored-By: [^\s<>{}](?:[^<>{}\n]*[^\s<>{}])? <[^\s<>@]+@[^\s<>]+>$/;
|
||||
|
||||
/** @returns {string|null} why `trailer` is refused, or null when it is valid */
|
||||
function trailerProblem(trailer) {
|
||||
export function trailerProblem(trailer) {
|
||||
if (typeof trailer !== 'string' || trailer === '') {
|
||||
return '--trailer "Co-Authored-By: <name> <<email>>" is required — nothing committed';
|
||||
}
|
||||
|
|
|
|||
55
lib/util/doors.mjs
Normal file
55
lib/util/doors.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// lib/util/doors.mjs
|
||||
// The `### Doors` list of a CHANGELOG release section: every delivery says
|
||||
// whether it is a one-way door (cannot be walked back without a cost to users)
|
||||
// or a two-way door (can). Moved here from scripts/release-check.mjs, which
|
||||
// re-exports these names, because lib never imports scripts and the review
|
||||
// evidence writer reads the door too. Door types are credited to
|
||||
// mattpocock/skills@c55ee46 (idea only, no text copied).
|
||||
|
||||
export const DOOR_LINE_RE = /^- (one-way|two-way): \S/;
|
||||
|
||||
/**
|
||||
* The CHANGELOG section for `version`, heading line excluded, or null. Matches
|
||||
* `## v1.2.3 …`, `## [1.2.3] …` and `## 1.2.3 …` exactly: 1.1.0 is not 1.10.0
|
||||
* and not 1.1.0-pre.
|
||||
*/
|
||||
export function releaseSection(text, version) {
|
||||
if (typeof text !== 'string' || !version) return null;
|
||||
const lines = text.split('\n');
|
||||
const headingVersion = (line) => {
|
||||
const m = line.match(/^##\s+\[?v?([^\]\s(]+)\]?/);
|
||||
return m ? m[1] : null;
|
||||
};
|
||||
let start = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/^##\s/.test(lines[i]) && headingVersion(lines[i]) === version) { start = i + 1; break; }
|
||||
}
|
||||
if (start === -1) return null;
|
||||
let end = lines.length;
|
||||
for (let i = start; i < lines.length; i++) {
|
||||
if (/^##\s/.test(lines[i])) { end = i; break; }
|
||||
}
|
||||
return lines.slice(start, end).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* The `### Doors` list of a release section. Top-level `- ` lines must read
|
||||
* `- one-way: …` or `- two-way: …`; indented lines continue the entry above.
|
||||
* @returns {{ found: boolean, entries: Array<{door: string, text: string}>, bad: string[] }}
|
||||
*/
|
||||
export function parseDoors(section) {
|
||||
const lines = String(section ?? '').split('\n');
|
||||
const start = lines.findIndex((l) => /^###\s+Doors\s*$/.test(l));
|
||||
if (start === -1) return { found: false, entries: [], bad: [] };
|
||||
const entries = [];
|
||||
const bad = [];
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/^#{2,3}\s/.test(line)) break;
|
||||
if (!line.startsWith('- ')) continue;
|
||||
const m = line.match(DOOR_LINE_RE);
|
||||
if (m) entries.push({ door: m[1], text: line.slice(m[0].length - 1).trim() });
|
||||
else bad.push(line);
|
||||
}
|
||||
return { found: true, entries, bad };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue