mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
refactor(OPS85): seed + pós-condição do gerador na lib com pin unitário; rest[0] no guard
This commit is contained in:
parent
cbe30604c0
commit
ef01b15467
4 changed files with 91 additions and 11 deletions
|
|
@ -11,7 +11,9 @@
|
|||
* Usage: node scripts/build-changelog.mjs [--stdout] [--check]
|
||||
* --stdout print the aggregate to stdout instead of writing the file
|
||||
* --check verify the local aggregate (when present) is up to date —
|
||||
* local sanity only, not a CI/gate requirement since OPS85
|
||||
* local sanity only, not a CI/gate requirement since OPS85.
|
||||
* With the local file absent it is a no-op (nothing to compare).
|
||||
* --stdout and --check together: --check wins (verification only).
|
||||
*/
|
||||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
|
@ -20,10 +22,10 @@ import {
|
|||
buildChangelog,
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_DIR,
|
||||
CHANGELOG_HEADER,
|
||||
CHANGELOG_HISTORY,
|
||||
listChangelogEntries,
|
||||
splitChangelogBody,
|
||||
missingAggregateEntries,
|
||||
seedAggregateFromHistory,
|
||||
} from './lib/changelog.mjs'
|
||||
|
||||
const repoRoot = process.cwd()
|
||||
|
|
@ -53,8 +55,7 @@ if (aggregateContent === null) {
|
|||
} catch {
|
||||
die(`não consegui ler ${CHANGELOG_AGGREGATE} nem o seed ${CHANGELOG_HISTORY}`)
|
||||
}
|
||||
const { blocks } = splitChangelogBody(historyContent)
|
||||
aggregateContent = [CHANGELOG_HEADER, ...blocks].join('\n\n') + '\n'
|
||||
aggregateContent = seedAggregateFromHistory(historyContent)
|
||||
}
|
||||
|
||||
let files
|
||||
|
|
@ -84,10 +85,9 @@ const next = buildChangelog({
|
|||
|
||||
// Post-condition (OPS85): no parsed entry may silently vanish from the
|
||||
// generated aggregate — replaces the old committed-aggregate multiset.
|
||||
for (const { path, content } of withContent) {
|
||||
if (!next.includes(content.trimEnd())) {
|
||||
die(`entrada ausente do agregado gerado (perda silenciosa?): ${path}`)
|
||||
}
|
||||
const missing = missingAggregateEntries(withContent, next)
|
||||
if (missing.length > 0) {
|
||||
die(`entrada(s) ausente(s) do agregado gerado (perda silenciosa?): ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ if (diffLines.length === 0) {
|
|||
|
||||
// --no-renames guarantees the `STATUS<TAB>path` shape (no R entries).
|
||||
const changed = diffLines.map((line) => {
|
||||
const [status, ...rest] = line.split('\t')
|
||||
return { status, path: rest.join('\t') }
|
||||
const [status, path] = line.split('\t')
|
||||
return { status, path }
|
||||
})
|
||||
|
||||
const body = readPrBody()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,35 @@ const ENTRY_FILE_RE = /^(\d{4})-(\d{2})-(\d{2})-([a-z0-9-]+)\.md$/
|
|||
/** Leading line of a "Recently resolved" entry block. */
|
||||
const ENTRY_BLOCK_RE = /^\*\*Recently resolved \((\d{4}-\d{2}-\d{2})\)/
|
||||
|
||||
/**
|
||||
* Seeds the regenerable aggregate from the frozen HISTORY snapshot (OPS85):
|
||||
* the snapshot's entry blocks become the seed blocks under the fresh
|
||||
* CHANGELOG_HEADER. Used when the local aggregate file is absent.
|
||||
*
|
||||
* @param {string} historyContent - content of docs/CHANGELOG-AGENTS-HISTORY.md
|
||||
* @returns {string} - aggregate-shaped content with CHANGELOG_HEADER + blocks
|
||||
*/
|
||||
export function seedAggregateFromHistory(historyContent) {
|
||||
const { blocks } = splitChangelogBody(historyContent)
|
||||
return [CHANGELOG_HEADER, ...blocks].join('\n\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-condition of the generator (OPS85): every parsed entry must appear
|
||||
* in the generated aggregate, or the entry was silently lost — the role the
|
||||
* old committed-aggregate multiset played. Substring check is honest for
|
||||
* "no silent loss" (a vanishing block fails; see build-changelog.mjs).
|
||||
*
|
||||
* @param {{ path: string, content: string }[]} entries
|
||||
* @param {string} aggregateContent
|
||||
* @returns {string[]} - paths of entries missing from the aggregate
|
||||
*/
|
||||
export function missingAggregateEntries(entries, aggregateContent) {
|
||||
return entries
|
||||
.filter(({ content }) => !aggregateContent.includes(content.trimEnd()))
|
||||
.map(({ path }) => path)
|
||||
}
|
||||
|
||||
/**
|
||||
* `changelog-rewrite: <motivo>` — standalone-line escape in the PR body for
|
||||
* legitimate aggregate loss (header rewrites, D8-style restorations). Kept
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ import {
|
|||
assertChangelogAppendOnly,
|
||||
buildChangelog,
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_HEADER,
|
||||
CHANGELOG_HISTORY,
|
||||
CHANGELOG_REWRITE_ESCAPE_RE,
|
||||
entryDateOf,
|
||||
listChangelogEntries,
|
||||
missingAggregateEntries,
|
||||
parseChangelogEntryName,
|
||||
seedAggregateFromHistory,
|
||||
splitChangelogBody,
|
||||
} from '../../scripts/lib/changelog.mjs'
|
||||
|
||||
|
|
@ -207,6 +210,54 @@ describe('CHANGELOG_REWRITE_ESCAPE_RE', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('seedAggregateFromHistory', () => {
|
||||
const history = `${HEADER}\n\n${ENTRY_2026_08_12}\n`
|
||||
|
||||
it('uses the fresh CHANGELOG_HEADER with the HISTORY blocks as seed', () => {
|
||||
const seeded = seedAggregateFromHistory(history)
|
||||
expect(seeded.startsWith(CHANGELOG_HEADER)).toBe(true)
|
||||
expect(seeded).toContain(ENTRY_2026_08_12)
|
||||
})
|
||||
|
||||
it('round-trips: build over the seed is idempotent and complete', () => {
|
||||
const seeded = seedAggregateFromHistory(history)
|
||||
const next = buildChangelog({
|
||||
entries: [{ content: ENTRY_2026_08_12 }, { content: ENTRY_2026_08_13 }],
|
||||
aggregateContent: seeded,
|
||||
})
|
||||
expect(next).toBe(`${CHANGELOG_HEADER}\n\n${ENTRY_2026_08_13}\n\n${ENTRY_2026_08_12}\n`)
|
||||
expect(
|
||||
missingAggregateEntries(
|
||||
[
|
||||
{ path: 'docs/changelog/2026-08-12-x.md', content: ENTRY_2026_08_12 },
|
||||
{ path: 'docs/changelog/2026-08-13-y.md', content: ENTRY_2026_08_13 },
|
||||
],
|
||||
next,
|
||||
),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('missingAggregateEntries', () => {
|
||||
it('flags an entry absent from the generated aggregate', () => {
|
||||
expect(
|
||||
missingAggregateEntries(
|
||||
[{ path: 'docs/changelog/2026-08-12-x.md', content: ENTRY_2026_08_12 }],
|
||||
'outro conteúdo',
|
||||
),
|
||||
).toEqual(['docs/changelog/2026-08-12-x.md'])
|
||||
})
|
||||
|
||||
it('returns [] when every entry text appears', () => {
|
||||
expect(
|
||||
missingAggregateEntries(
|
||||
[{ path: 'docs/changelog/2026-08-12-x.md', content: ENTRY_2026_08_12 }],
|
||||
`prefácio\n\n${ENTRY_2026_08_12}\n`,
|
||||
),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertChangelogAppendOnly', () => {
|
||||
it('passes on a pure addition in docs/changelog/ with no other changelog diff', () => {
|
||||
expect(
|
||||
|
|
|
|||
Loading…
Reference in a new issue