openvibers/openvibe.blog Commits
Patch notes: one-off changes get a line, and the full list is not prose (#134)
Changes
@@@ -35,6 +35,8 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? argconst b = out.checks.blocking;console.log(`draft: ${out.post.id} (${out.post.state}) ${out.url}`);console.log(`${b.length} blocking, ${out.checks.warnings.length} warnings${out.usedAi ? ' (AI prose)' : ''}${b.length ? `: ${[...new Set(b.map((x) => x.code))].join(', ')}` : ''}`);++ for (const s of out.stories || []) console.log(`story: ${s.title} (${s.kind}, ${s.sites.length} sites, ${s.entries.length} commits)`);++ for (const c of (out.checks.checklist || [])) console.log(`${c.ok ? 'OK' : 'NO'} ${c.label}${c.detail ? ` — ${c.detail}` : ''}`);console.log('left as a draft: a person reviews it in /write, then approves the revision and publishes.');} catch (err) {console.error(`[patch-notes] ${err.message}`);@@@ -42,4 +44,6 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? arg} finally {await ctx.store.close();}++ // A one-off process: the AI/HTTP keep-alive sockets and the outbox's timers would hold it open.++ process.exit(process.exitCode || 0);})();@@@ -10,7 +10,7 @@* answer replaces the story prose; the tail (list, next, sign-off) stays deterministic. Either way the result is* a DRAFT: this module never publishes.*/--const { siteLabel } = require('./text');++const { siteLabel, normaliseSubject } = require('./text');const { sitesSentence, words } = require('./stories');const AUDIT = { homes: 42, targets: 2386, under: 2348, sitesUnder: 40, shared: 1628, source: 'research-2026-10-29/tap-targets.md' };@@@ -187,6 +187,15 @@ function whatsNext(pulls) {return `## What's next\n\n${lines.join('\n')}\n`;}++/** The user-visible changes outside the top stories: one line each, no commit subjects. */++function alsoShipped(also = []) {++ if (!also.length) return '';++ const lines = [...also]++ .sort((a, b) => siteLabel(a).localeCompare(siteLabel(b)) || String(a.subject).localeCompare(String(b.subject)))++ .map((e) => `- **${siteLabel(e)}:** ${lowerFirst(normaliseSubject(e)).replace(/\.$/, '')}.`);++ return `## Also shipped\n\n${lines.join('\n')}\n`;++}++function fullList(map, stories) {const link = (e) => `- [${sentence(e.subject)}](${e.url || `https://github.com/${e.repo}/commit/${e.sha}`})`;const bySite = (list) => [...list].sort((a, b) => siteLabel(a).localeCompare(siteLabel(b)) || String(a.subject).localeCompare(String(b.subject)));@@@ -220,9 +229,10 @@ function compose({ map, voice, pulls = [] }) {const stories = map.stories;const signoff = (voice && voice.card && voice.card.signoff) || '— the OpenVibe crew';if (!stories.length) {-- const body = [underTheHood(map.chores), fullList(map, []), '', signoff].filter((x) => x != null).join('\n');++ const body = [map.also.length ? alsoShipped(map.also) : underTheHood(map.chores), fullList(map, []), '', signoff].filter((x) => x != null).join('\n');return {-- title: 'The package versions moved', dek: 'Housekeeping only this round: package versions, lockfiles and docs, with no new user-facing change.',++ title: map.also.length ? cap(normaliseSubject(map.also[0])) : 'The package versions moved',++ dek: map.also.length ? 'The smaller changes this round, one line each, with the links below.' : 'Housekeeping only this round: package versions, lockfiles and docs, with no new user-facing change.',body: body.replace(/\n{3,}/g, '\n\n').trim(), tags: ['patch-notes'],};}@@@ -233,6 +243,7 @@ function compose({ map, voice, pulls = [] }) {'## The short version', '',...shortVersionFor(stories).map((b) => `- ${b}`), '',...forPost(stories).flatMap((s) => [`## ${s.title}`, '', ...sectionFor(s, {}), '']),++ alsoShipped(map.also),underTheHood(map.chores),whatsNext(pulls),fullList(map, stories),@@@ -286,4 +297,4 @@ function mergeAi(aiBody, det, voice) {return `${body}\n\n${tail}`.replace(/\n{3,}/g, '\n\n').trim();}--module.exports = { compose, briefFor, mergeAi, titleFor, dekFor, sectionFor, nextItems, AUDIT };++module.exports = { compose, briefFor, mergeAi, titleFor, dekFor, sectionFor, alsoShipped, nextItems, AUDIT };@@@ -19,6 +19,7 @@ const LINK = /\[([^\]]*)\]\(([^)\s]+)\)/g;const TODO = /\[(TODO|FIXME|TBD)[^\]]*\]/i;const countWords = (s) => String(s || '').trim().split(/\s+/).filter(Boolean).length;const storyPart = (body) => String(body || '').split(/<details/i)[0];++const withoutDetails = (body) => String(body || '').replace(/<details[\s\S]*?<\/details>/gi, ' ');function headingsOf(body) {const out = [];@@@ -64,7 +65,8 @@ async function evaluate(draft) {else blocking.push(b);}warnings.push(...slop.warnings);-- const looksText = [title, dek, body, ...tags, ...captions].join('\n');++ // The full list holds exact commit subjects (a "token cap" is not a hat); the story is what describes people.++ const looksText = [title, dek, withoutDetails(body), ...tags, ...captions].join('\n');if (hasLooks(looksText)) add(blocking, 'looks.person', 'The text describes a person (looks, body, clothing, hair, age or a guessed gender). Public AI text never does. Say "the person" or "the streamer", or remove the sentence.');if (isExplicit(title, dek, body)) add(blocking, 'looks.explicit', 'The text describes sexual content. It cannot be published.');checklist.push({ code: 'voice', label: 'The voice card is followed; no banned phrases; no looks language', ok: !blocking.some((b) => /^(slop|looks)\./.test(b.code)), detail: blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length ? `${blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length} blocking` : 'clean' });@@@ -20,6 +20,7 @@const { normaliseSubject, intentOf, siteLabel } = require('./text');const WINDOW_MS = 72 * 3600 * 1000;++const baseKeyOf = (key) => String(key || '').split('#')[0];const NUMBERS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve','thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];const TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty'];@@@ -132,11 +133,14 @@ function scoreOf(group) {function build(entries, { maxStories = 6 } = {}) {const groups = mergeSharedChores(mergeByPackage(cluster(entries)));const stories = [];++ const also = [];const chores = [];const internal = [];for (const g of groups) {if (g.kind === 'chore') { chores.push(g); continue; }-- if (g.kind === 'internal' || g.key === 'outbox') { internal.push(g); continue; }++ if (g.kind === 'internal' || baseKeyOf(g.key) === 'outbox') { internal.push(g); continue; }++ // "Other" is not a workstream: unrelated one-off changes would be merged by site count alone.++ if (baseKeyOf(g.key) === 'other') { also.push(...g.entries); continue; }stories.push(g);}for (const s of stories) {@@@ -146,7 +150,7 @@ function build(entries, { maxStories = 6 } = {}) {}stories.sort((a, b) => b.score - a.score || b.sites.length - a.sites.length || a.key.localeCompare(b.key));const top = stories.slice(0, maxStories);-- const also = stories.slice(maxStories).flatMap((s) => s.entries);++ also.push(...stories.slice(maxStories).flatMap((s) => s.entries));return { stories: top, also, chores: chores.flatMap((g) => g.entries), internal: internal.flatMap((g) => g.entries), groups };}@@@ -31,6 +31,19 @@ const entry = (service, sha, subject, body = null) => ({assert.ok(map.stories.every((s) => s.kind === 'fix' || s.kind === 'feature'));});++ await check('a one-off change is an Also shipped line, never a section shared with unrelated changes', async () => {++ const map = build([++ { ...entry('network', '9'.repeat(40), 'The site switcher follows the account') },++ { ...entry('codes', '8'.repeat(40), 'engine: the agent engine, ported from the portal') },++ ]);++ assert.strictEqual(map.stories.length, 0);++ assert.strictEqual(map.also.length, 2);++ const doc = compose({ map, voice: { card: seedCard() }, pulls: [] });++ assert.ok(!/^## The site switcher/m.test(doc.body));++ assert.ok(doc.body.includes('## Also shipped'));++ assert.ok(/agent engine/i.test(doc.body));++ });++await check('the lockfile round folds into the chores, not into a story', async () => {const map = build(fixture.entries);assert.strictEqual(map.chores.filter((e) => /openvibe-shared v4\./.test(e.subject)).length, 24);@@@ -115,6 +128,8 @@ const entry = (service, sha, subject, body = null) => ({assert.deepStrictEqual(noLooksTags(['red-beanie', 'bearded', 'man', 'tanktop', 'hula hoop', 'laptop']), ['hula hoop', 'laptop']);const r = await evaluate({ title: 'A fix', dek: 'A plain dek about the page and what it does for you.', body: 'A shirtless man sits at a desk.', review: null, authorship: { mode: 'human' } });assert.ok(r.blocking.some((b) => b.code === 'looks.person'));++ const ok = await evaluate({ title: 'A fix', dek: 'A plain dek about the page and what it does for you.', body: '## Story\n\nA change.\n\n<details>\n<summary>All</summary>\n\n- Generation: a token cap per showcase kind\n\n</details>', review: null, authorship: { mode: 'human' } });++ assert.ok(!ok.blocking.some((b) => b.code === 'looks.person'), 'a commit subject in the full list is not a person');});// ── the service: a draft, the gates, a person ───────────
@@ -35,6 +35,8 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? arg
@@ -35,6 +35,8 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? arg
const b = out.checks.blocking;
const b = out.checks.blocking;
console.log(`draft: ${out.post.id} (${out.post.state}) ${out.url}`);
console.log(`draft: ${out.post.id} (${out.post.state}) ${out.url}`);
console.log(`${b.length} blocking, ${out.checks.warnings.length} warnings${out.usedAi ? ' (AI prose)' : ''}${b.length ? `: ${[...new Set(b.map((x) => x.code))].join(', ')}` : ''}`);
console.log(`${b.length} blocking, ${out.checks.warnings.length} warnings${out.usedAi ? ' (AI prose)' : ''}${b.length ? `: ${[...new Set(b.map((x) => x.code))].join(', ')}` : ''}`);
+ for (const s of out.stories || []) console.log(`story: ${s.title} (${s.kind}, ${s.sites.length} sites, ${s.entries.length} commits)`);
+ for (const c of (out.checks.checklist || [])) console.log(`${c.ok ? 'OK' : 'NO'} ${c.label}${c.detail ? ` — ${c.detail}` : ''}`);
console.log('left as a draft: a person reviews it in /write, then approves the revision and publishes.');
console.log('left as a draft: a person reviews it in /write, then approves the revision and publishes.');
} catch (err) {
} catch (err) {
console.error(`[patch-notes] ${err.message}`);
console.error(`[patch-notes] ${err.message}`);
@@ -42,4 +44,6 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? arg
@@ -42,4 +44,6 @@ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? arg
} finally {
} finally {
await ctx.store.close();
await ctx.store.close();
}
}
+ // A one-off process: the AI/HTTP keep-alive sockets and the outbox's timers would hold it open.
+ process.exit(process.exitCode || 0);
})();
})();
@@ -10,7 +10,7 @@
@@ -10,7 +10,7 @@
* answer replaces the story prose; the tail (list, next, sign-off) stays deterministic. Either way the result is
* answer replaces the story prose; the tail (list, next, sign-off) stays deterministic. Either way the result is
* a DRAFT: this module never publishes.
* a DRAFT: this module never publishes.
*/
*/
-const { siteLabel } = require('./text');
+const { siteLabel, normaliseSubject } = require('./text');
const { sitesSentence, words } = require('./stories');
const { sitesSentence, words } = require('./stories');
const AUDIT = { homes: 42, targets: 2386, under: 2348, sitesUnder: 40, shared: 1628, source: 'research-2026-10-29/tap-targets.md' };
const AUDIT = { homes: 42, targets: 2386, under: 2348, sitesUnder: 40, shared: 1628, source: 'research-2026-10-29/tap-targets.md' };
@@ -187,6 +187,15 @@ function whatsNext(pulls) {
@@ -187,6 +187,15 @@ function whatsNext(pulls) {
return `## What's next\n\n${lines.join('\n')}\n`;
return `## What's next\n\n${lines.join('\n')}\n`;
}
}
+/** The user-visible changes outside the top stories: one line each, no commit subjects. */
+function alsoShipped(also = []) {
+ if (!also.length) return '';
+ const lines = [...also]
+ .sort((a, b) => siteLabel(a).localeCompare(siteLabel(b)) || String(a.subject).localeCompare(String(b.subject)))
+ .map((e) => `- **${siteLabel(e)}:** ${lowerFirst(normaliseSubject(e)).replace(/\.$/, '')}.`);
+ return `## Also shipped\n\n${lines.join('\n')}\n`;
+}
+
function fullList(map, stories) {
function fullList(map, stories) {
const link = (e) => `- [${sentence(e.subject)}](${e.url || `https://github.com/${e.repo}/commit/${e.sha}`})`;
const link = (e) => `- [${sentence(e.subject)}](${e.url || `https://github.com/${e.repo}/commit/${e.sha}`})`;
const bySite = (list) => [...list].sort((a, b) => siteLabel(a).localeCompare(siteLabel(b)) || String(a.subject).localeCompare(String(b.subject)));
const bySite = (list) => [...list].sort((a, b) => siteLabel(a).localeCompare(siteLabel(b)) || String(a.subject).localeCompare(String(b.subject)));
@@ -220,9 +229,10 @@ function compose({ map, voice, pulls = [] }) {
@@ -220,9 +229,10 @@ function compose({ map, voice, pulls = [] }) {
const stories = map.stories;
const stories = map.stories;
const signoff = (voice && voice.card && voice.card.signoff) || '— the OpenVibe crew';
const signoff = (voice && voice.card && voice.card.signoff) || '— the OpenVibe crew';
if (!stories.length) {
if (!stories.length) {
- const body = [underTheHood(map.chores), fullList(map, []), '', signoff].filter((x) => x != null).join('\n');
+ const body = [map.also.length ? alsoShipped(map.also) : underTheHood(map.chores), fullList(map, []), '', signoff].filter((x) => x != null).join('\n');
return {
return {
- title: 'The package versions moved', dek: 'Housekeeping only this round: package versions, lockfiles and docs, with no new user-facing change.',
+ title: map.also.length ? cap(normaliseSubject(map.also[0])) : 'The package versions moved',
+ dek: map.also.length ? 'The smaller changes this round, one line each, with the links below.' : 'Housekeeping only this round: package versions, lockfiles and docs, with no new user-facing change.',
body: body.replace(/\n{3,}/g, '\n\n').trim(), tags: ['patch-notes'],
body: body.replace(/\n{3,}/g, '\n\n').trim(), tags: ['patch-notes'],
};
};
}
}
@@ -233,6 +243,7 @@ function compose({ map, voice, pulls = [] }) {
@@ -233,6 +243,7 @@ function compose({ map, voice, pulls = [] }) {
'## The short version', '',
'## The short version', '',
...shortVersionFor(stories).map((b) => `- ${b}`), '',
...shortVersionFor(stories).map((b) => `- ${b}`), '',
...forPost(stories).flatMap((s) => [`## ${s.title}`, '', ...sectionFor(s, {}), '']),
...forPost(stories).flatMap((s) => [`## ${s.title}`, '', ...sectionFor(s, {}), '']),
+ alsoShipped(map.also),
underTheHood(map.chores),
underTheHood(map.chores),
whatsNext(pulls),
whatsNext(pulls),
fullList(map, stories),
fullList(map, stories),
@@ -286,4 +297,4 @@ function mergeAi(aiBody, det, voice) {
@@ -286,4 +297,4 @@ function mergeAi(aiBody, det, voice) {
return `${body}\n\n${tail}`.replace(/\n{3,}/g, '\n\n').trim();
return `${body}\n\n${tail}`.replace(/\n{3,}/g, '\n\n').trim();
}
}
-module.exports = { compose, briefFor, mergeAi, titleFor, dekFor, sectionFor, nextItems, AUDIT };
+module.exports = { compose, briefFor, mergeAi, titleFor, dekFor, sectionFor, alsoShipped, nextItems, AUDIT };
@@ -19,6 +19,7 @@ const LINK = /\[([^\]]*)\]\(([^)\s]+)\)/g;
@@ -19,6 +19,7 @@ const LINK = /\[([^\]]*)\]\(([^)\s]+)\)/g;
const TODO = /\[(TODO|FIXME|TBD)[^\]]*\]/i;
const TODO = /\[(TODO|FIXME|TBD)[^\]]*\]/i;
const countWords = (s) => String(s || '').trim().split(/\s+/).filter(Boolean).length;
const countWords = (s) => String(s || '').trim().split(/\s+/).filter(Boolean).length;
const storyPart = (body) => String(body || '').split(/<details/i)[0];
const storyPart = (body) => String(body || '').split(/<details/i)[0];
+const withoutDetails = (body) => String(body || '').replace(/<details[\s\S]*?<\/details>/gi, ' ');
function headingsOf(body) {
function headingsOf(body) {
const out = [];
const out = [];
@@ -64,7 +65,8 @@ async function evaluate(draft) {
@@ -64,7 +65,8 @@ async function evaluate(draft) {
else blocking.push(b);
else blocking.push(b);
}
}
warnings.push(...slop.warnings);
warnings.push(...slop.warnings);
- const looksText = [title, dek, body, ...tags, ...captions].join('\n');
+ // The full list holds exact commit subjects (a "token cap" is not a hat); the story is what describes people.
+ const looksText = [title, dek, withoutDetails(body), ...tags, ...captions].join('\n');
if (hasLooks(looksText)) add(blocking, 'looks.person', 'The text describes a person (looks, body, clothing, hair, age or a guessed gender). Public AI text never does. Say "the person" or "the streamer", or remove the sentence.');
if (hasLooks(looksText)) add(blocking, 'looks.person', 'The text describes a person (looks, body, clothing, hair, age or a guessed gender). Public AI text never does. Say "the person" or "the streamer", or remove the sentence.');
if (isExplicit(title, dek, body)) add(blocking, 'looks.explicit', 'The text describes sexual content. It cannot be published.');
if (isExplicit(title, dek, body)) add(blocking, 'looks.explicit', 'The text describes sexual content. It cannot be published.');
checklist.push({ code: 'voice', label: 'The voice card is followed; no banned phrases; no looks language', ok: !blocking.some((b) => /^(slop|looks)\./.test(b.code)), detail: blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length ? `${blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length} blocking` : 'clean' });
checklist.push({ code: 'voice', label: 'The voice card is followed; no banned phrases; no looks language', ok: !blocking.some((b) => /^(slop|looks)\./.test(b.code)), detail: blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length ? `${blocking.filter((b) => /^(slop|looks)\./.test(b.code)).length} blocking` : 'clean' });
@@ -20,6 +20,7 @@
@@ -20,6 +20,7 @@
const { normaliseSubject, intentOf, siteLabel } = require('./text');
const { normaliseSubject, intentOf, siteLabel } = require('./text');
const WINDOW_MS = 72 * 3600 * 1000;
const WINDOW_MS = 72 * 3600 * 1000;
+const baseKeyOf = (key) => String(key || '').split('#')[0];
const NUMBERS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
const NUMBERS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
const TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty'];
const TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty'];
@@ -132,11 +133,14 @@ function scoreOf(group) {
@@ -132,11 +133,14 @@ function scoreOf(group) {
function build(entries, { maxStories = 6 } = {}) {
function build(entries, { maxStories = 6 } = {}) {
const groups = mergeSharedChores(mergeByPackage(cluster(entries)));
const groups = mergeSharedChores(mergeByPackage(cluster(entries)));
const stories = [];
const stories = [];
+ const also = [];
const chores = [];
const chores = [];
const internal = [];
const internal = [];
for (const g of groups) {
for (const g of groups) {
if (g.kind === 'chore') { chores.push(g); continue; }
if (g.kind === 'chore') { chores.push(g); continue; }
- if (g.kind === 'internal' || g.key === 'outbox') { internal.push(g); continue; }
+ if (g.kind === 'internal' || baseKeyOf(g.key) === 'outbox') { internal.push(g); continue; }
+ // "Other" is not a workstream: unrelated one-off changes would be merged by site count alone.
+ if (baseKeyOf(g.key) === 'other') { also.push(...g.entries); continue; }
stories.push(g);
stories.push(g);
}
}
for (const s of stories) {
for (const s of stories) {
@@ -146,7 +150,7 @@ function build(entries, { maxStories = 6 } = {}) {
@@ -146,7 +150,7 @@ function build(entries, { maxStories = 6 } = {}) {
}
}
stories.sort((a, b) => b.score - a.score || b.sites.length - a.sites.length || a.key.localeCompare(b.key));
stories.sort((a, b) => b.score - a.score || b.sites.length - a.sites.length || a.key.localeCompare(b.key));
const top = stories.slice(0, maxStories);
const top = stories.slice(0, maxStories);
- const also = stories.slice(maxStories).flatMap((s) => s.entries);
+ also.push(...stories.slice(maxStories).flatMap((s) => s.entries));
return { stories: top, also, chores: chores.flatMap((g) => g.entries), internal: internal.flatMap((g) => g.entries), groups };
return { stories: top, also, chores: chores.flatMap((g) => g.entries), internal: internal.flatMap((g) => g.entries), groups };
}
}
@@ -31,6 +31,19 @@ const entry = (service, sha, subject, body = null) => ({
@@ -31,6 +31,19 @@ const entry = (service, sha, subject, body = null) => ({
assert.ok(map.stories.every((s) => s.kind === 'fix' || s.kind === 'feature'));
assert.ok(map.stories.every((s) => s.kind === 'fix' || s.kind === 'feature'));
});
});
+ await check('a one-off change is an Also shipped line, never a section shared with unrelated changes', async () => {
+ const map = build([
+ { ...entry('network', '9'.repeat(40), 'The site switcher follows the account') },
+ { ...entry('codes', '8'.repeat(40), 'engine: the agent engine, ported from the portal') },
+ ]);
+ assert.strictEqual(map.stories.length, 0);
+ assert.strictEqual(map.also.length, 2);
+ const doc = compose({ map, voice: { card: seedCard() }, pulls: [] });
+ assert.ok(!/^## The site switcher/m.test(doc.body));
+ assert.ok(doc.body.includes('## Also shipped'));
+ assert.ok(/agent engine/i.test(doc.body));
+ });
+
await check('the lockfile round folds into the chores, not into a story', async () => {
await check('the lockfile round folds into the chores, not into a story', async () => {
const map = build(fixture.entries);
const map = build(fixture.entries);
assert.strictEqual(map.chores.filter((e) => /openvibe-shared v4\./.test(e.subject)).length, 24);
assert.strictEqual(map.chores.filter((e) => /openvibe-shared v4\./.test(e.subject)).length, 24);
@@ -115,6 +128,8 @@ const entry = (service, sha, subject, body = null) => ({
@@ -115,6 +128,8 @@ const entry = (service, sha, subject, body = null) => ({
assert.deepStrictEqual(noLooksTags(['red-beanie', 'bearded', 'man', 'tanktop', 'hula hoop', 'laptop']), ['hula hoop', 'laptop']);
assert.deepStrictEqual(noLooksTags(['red-beanie', 'bearded', 'man', 'tanktop', 'hula hoop', 'laptop']), ['hula hoop', 'laptop']);
const r = await evaluate({ title: 'A fix', dek: 'A plain dek about the page and what it does for you.', body: 'A shirtless man sits at a desk.', review: null, authorship: { mode: 'human' } });
const r = await evaluate({ title: 'A fix', dek: 'A plain dek about the page and what it does for you.', body: 'A shirtless man sits at a desk.', review: null, authorship: { mode: 'human' } });
assert.ok(r.blocking.some((b) => b.code === 'looks.person'));
assert.ok(r.blocking.some((b) => b.code === 'looks.person'));
+ const ok = await evaluate({ title: 'A fix', dek: 'A plain dek about the page and what it does for you.', body: '## Story\n\nA change.\n\n<details>\n<summary>All</summary>\n\n- Generation: a token cap per showcase kind\n\n</details>', review: null, authorship: { mode: 'human' } });
+ assert.ok(!ok.blocking.some((b) => b.code === 'looks.person'), 'a commit subject in the full list is not a person');
});
});
// ── the service: a draft, the gates, a person ───────────
// ── the service: a draft, the gates, a person ───────────