merge: release 6.4.0 (operator decision 53)
This commit is contained in:
commit
fb241d5016
8 changed files with 116 additions and 61 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, pencil-toggle annotation pattern): select text or click any element, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.0",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
},
|
||||
|
|
|
|||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop
|
||||
## v6.4.0 — 2026-10-17 — plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop
|
||||
|
||||
A new plan version, 1.9, in which every path in a step's `Files:` and on its
|
||||
`Test first:` `File:` line stands first, in backticks, so the reader no longer
|
||||
|
|
@ -18,7 +18,7 @@ measured by `npm test` on the third remediation's Step 2 commit
|
|||
The converging critic loop came after that measurement (decisions 43 and
|
||||
44). From round 2 the plan gate counts a critic round by rule A, and both
|
||||
reviewers state rule B. Suite 2602 (2600 pass, 0 fail, 2 skipped), measured
|
||||
by `npm test` on this branch.
|
||||
by `npm test` on `main` at `4e28dc5`, the tree this release review covered.
|
||||
|
||||
Nothing here asks anything of users. Older plans warn, they do not fail: a plan
|
||||
below 1.9 is read as in 6.3.1, apart from the fixes under Fixed, and gets one
|
||||
|
|
@ -99,9 +99,11 @@ warning per step whose `Files:` names a path that is not backticked.
|
|||
### Fixed
|
||||
|
||||
- **The suite no longer hands the session id to the lock CLI.** `npm test`
|
||||
preloads a helper that removes `CLAUDE_CODE_SESSION_ID`, and every test that
|
||||
starts a child process passes an environment without it. A live session's
|
||||
stop guard is no longer pointed at a temp-repo lock.
|
||||
preloads a helper that removes `CLAUDE_CODE_SESSION_ID` for the whole run,
|
||||
and every test that spreads the process environment into a child uses a
|
||||
shared helper without it. A live session's stop guard is no longer pointed
|
||||
at a temp-repo lock. A test file run on its own with `node --test` still
|
||||
passes the id to a child it starts without an `env` (release review).
|
||||
- **The three `Files:` regressions from 6.3.1's Known issues** give 6.3.0's
|
||||
answer again: a source file after a described test is kept, a path that a
|
||||
description only mentions is not read as a file, and a backticked bare name
|
||||
|
|
@ -345,6 +347,24 @@ warning per step whose `Files:` names a path that is not backticked.
|
|||
leaves the count without being marked resolved. This predates the critic
|
||||
loop's diff mode (review `4c340d4a`; deferred by operator decision 51,
|
||||
16.10).
|
||||
- A near-duplicate merge in plan dedup keeps only the survivor's place, so
|
||||
rule A can lower a new major in the changed text to minor when its partner
|
||||
cites an unchanged line, and the round can record PASS. 6.3.1 counted that
|
||||
merge major (review `ef1633b6`; deferred by operator decision 52, 16.10).
|
||||
- Below 1.9, regression A's restored reading also takes a plain slash path
|
||||
that a description only mentions after a comma (`` `tests/a.test.mjs` —
|
||||
covers the parser, lib/parse.mjs stays as is ``). 6.3.1 read only the test
|
||||
file; now the step is no longer tests-only, and the validator warns
|
||||
`PLAN_STEP_PATH_UNTICKED` on the mentioned path (review `38a8ad34`;
|
||||
deferred by operator decision 52, 16.10).
|
||||
- The verdict gate's brief-lowering check compares severities in upper case
|
||||
only, so an engine finding written `"severity": "major"` reads as an
|
||||
illegal lowering and the gate exits 2 (fail-closed) (review `20b626e2`;
|
||||
deferred by operator decision 52, 16.10).
|
||||
- The opt-in workflow review's floor raises an illegally lowered or marked
|
||||
reviewer finding but keeps its `brief_lowered_from` and `contradicts_brief`,
|
||||
so the verdict gate still exits 2 on that review (fail-closed) (review
|
||||
`c614baad`; deferred by operator decision 52, 16.10).
|
||||
|
||||
## v6.3.1 — 2026-10-11 — Fixes from the release review of 6.3.0, and two verdict-gate holes closed
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Contract-driven Claude Code pipeline: brief, research, plan, execute, review. Agent swarms, research triangulation, adversarial review, multi-session resumption.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "voyage",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.0",
|
||||
"private": true,
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, pencil-toggle annotation pattern): select text or click any heading/paragraph/list-item, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
|
||||
"type": "module",
|
||||
|
|
|
|||
30
tests/helpers/changelog-section.mjs
Normal file
30
tests/helpers/changelog-section.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// The CHANGELOG section of the plan_version 1.9 / converging critic loop
|
||||
// delivery, found by its title. Before the release its heading is
|
||||
// `## Unreleased — <title>`; the release commit makes it
|
||||
// `## vX.Y.Z — <date> — <title>` (docs/release-runbook.md § 1.4). The section
|
||||
// is found wherever it stands, so a later delivery's section above it does not
|
||||
// move the pins, and it must stand exactly once.
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
export const SECTION_TITLE = 'plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop';
|
||||
|
||||
const escaped = SECTION_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
export const SECTION_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${escaped}$`);
|
||||
|
||||
// A heading line as written, without a trailing `\r` or trailing spaces.
|
||||
const bare = line => line.replace(/\s+$/, '');
|
||||
|
||||
// { heading, body } of the delivery's section in CHANGELOG text, up to the
|
||||
// next `## ` heading. Fails unless exactly one heading matches.
|
||||
export function deliverySection(text) {
|
||||
const lines = text.split('\n');
|
||||
const at = [];
|
||||
lines.forEach((l, i) => { if (SECTION_HEADING.test(bare(l))) at.push(i); });
|
||||
assert.equal(at.length, 1, `CHANGELOG.md has the "${SECTION_TITLE}" section exactly once (found ${at.length})`);
|
||||
const start = at[0];
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^## /.test(lines[i])) { end = i; break; }
|
||||
}
|
||||
return { heading: bare(lines[start]), body: lines.slice(start + 1, end).join('\n') };
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs';
|
|||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
|
||||
import { SECTION_TITLE, SECTION_HEADING, deliverySection } from '../helpers/changelog-section.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf-8');
|
||||
|
|
@ -285,50 +286,60 @@ test('the template\'s own example steps validate as 1.9 with none of the new pat
|
|||
assert.deepEqual([...soft.errors, ...soft.warnings].map((i) => i.code).filter((c) => NEW_CODES.includes(c)), []);
|
||||
});
|
||||
|
||||
// Step 14: the CHANGELOG's top section is the Unreleased 1.9 delivery, with door
|
||||
// lines the release check accepts. The release check is imported as a module
|
||||
// namespace, so a missing parseDoors export fails an assertion, not the load.
|
||||
// Step 14: the CHANGELOG has the 1.9 delivery's section, with door lines the
|
||||
// release check accepts. The section is found by its title, before and after the
|
||||
// release commit (tests/helpers/changelog-section.mjs). The release check is
|
||||
// imported as a module namespace, so a missing parseDoors export fails an
|
||||
// assertion, not the load.
|
||||
const releaseCheck = await import('../../scripts/release-check.mjs');
|
||||
const UNRELEASED = '## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop';
|
||||
|
||||
function firstChangelogSection() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const start = lines.findIndex((l) => /^## /.test(l));
|
||||
assert.ok(start >= 0, 'CHANGELOG.md has no "## " section');
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^## /.test(lines[i])) { end = i; break; }
|
||||
}
|
||||
return { heading: lines[start], body: lines.slice(start + 1, end).join('\n') };
|
||||
}
|
||||
// The delivery section's body.
|
||||
const unreleasedBody = () => deliverySection(read('CHANGELOG.md')).body;
|
||||
|
||||
// The Unreleased section's body, after checking it is the first section.
|
||||
function unreleasedBody() {
|
||||
const { heading, body } = firstChangelogSection();
|
||||
assert.equal(heading, UNRELEASED);
|
||||
return body;
|
||||
}
|
||||
|
||||
test('CHANGELOG: the first "## " section is the Unreleased plan_version 1.9 section', () => {
|
||||
assert.equal(firstChangelogSection().heading, UNRELEASED);
|
||||
test('CHANGELOG: the plan_version 1.9 delivery section stands exactly once', () => {
|
||||
assert.match(deliverySection(read('CHANGELOG.md')).heading, SECTION_HEADING);
|
||||
});
|
||||
|
||||
test('CHANGELOG: parseDoors reads the Unreleased section\'s door lines, none bad, one naming plan_version 1.9', () => {
|
||||
test('CHANGELOG: the section heading is read before and after the release commit, and nothing else', () => {
|
||||
assert.match(`## Unreleased — ${SECTION_TITLE}`, SECTION_HEADING);
|
||||
assert.match(`## v6.4.0 — 2026-10-17 — ${SECTION_TITLE}`, SECTION_HEADING);
|
||||
for (const [heading, why] of [
|
||||
[`## v6.4.0 — ${SECTION_TITLE}`, 'a release heading without its date'],
|
||||
[`## Unreleased — 2026-10-17 — ${SECTION_TITLE}`, 'an Unreleased heading with a date'],
|
||||
[`## v6.4 — 2026-10-17 — ${SECTION_TITLE}`, 'a two-part version'],
|
||||
[`## v6.4.0 — 2026-10-7 — ${SECTION_TITLE}`, 'a one-digit day'],
|
||||
[`## 6.4.0 — 2026-10-17 — ${SECTION_TITLE}`, 'a version without its v'],
|
||||
[`## Unreleased — ${SECTION_TITLE} and more`, 'another title'],
|
||||
[`## Unreleased — ${SECTION_TITLE.replace('1.9', '1x9')}`, 'the dot in 1.9 is literal'],
|
||||
]) assert.doesNotMatch(heading, SECTION_HEADING, why);
|
||||
});
|
||||
|
||||
test('CHANGELOG: the section is found below a later section, with a trailing \\r, and exactly once', () => {
|
||||
const later = `## Unreleased — the next delivery\n\n### Doors\n\n- two-way: x — y.\n\n`;
|
||||
const ours = `## v6.4.0 — 2026-10-17 — ${SECTION_TITLE}\r\n\nours\n\n## v6.3.1 — 2026-10-11 — older\n\nolder\n`;
|
||||
const s = deliverySection(later + ours);
|
||||
assert.equal(s.heading, `## v6.4.0 — 2026-10-17 — ${SECTION_TITLE}`);
|
||||
assert.equal(s.body.trim(), 'ours');
|
||||
assert.throws(() => deliverySection(ours + ours), /exactly once \(found 2\)/);
|
||||
assert.throws(() => deliverySection(later), /exactly once \(found 0\)/);
|
||||
});
|
||||
|
||||
test('CHANGELOG: parseDoors reads the delivery section\'s door lines, none bad, one naming plan_version 1.9', () => {
|
||||
assert.equal(typeof releaseCheck.parseDoors, 'function', 'scripts/release-check.mjs exports parseDoors');
|
||||
const body = unreleasedBody();
|
||||
const d = releaseCheck.parseDoors(body);
|
||||
assert.equal(d.found, true, 'the Unreleased section has a "### Doors" heading');
|
||||
assert.equal(d.found, true, 'the delivery section has a "### Doors" heading');
|
||||
assert.ok(d.entries.length > 0, 'the Doors list has door lines');
|
||||
assert.deepEqual(d.bad, [], 'every top-level door line reads "- one-way: ..." or "- two-way: ..."');
|
||||
assert.ok(d.entries.some((e) => e.text.includes('plan_version 1.9')),
|
||||
`one door line names plan_version 1.9: ${JSON.stringify(d.entries.map((e) => e.text))}`);
|
||||
});
|
||||
|
||||
// A `### <name>` body of the Unreleased section, up to the next heading.
|
||||
// A `### <name>` body of the delivery section, up to the next heading.
|
||||
const subsection = (body, name) => {
|
||||
const lines = body.split('\n');
|
||||
const start = lines.findIndex((l) => l.replace(/\s+$/, '') === `### ${name}`);
|
||||
assert.ok(start >= 0, `the Unreleased section has a "### ${name}" heading`);
|
||||
assert.ok(start >= 0, `the delivery section has a "### ${name}" heading`);
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^###? /.test(lines[i])) { end = i; break; }
|
||||
|
|
@ -337,7 +348,7 @@ const subsection = (body, name) => {
|
|||
};
|
||||
const knownIssues = (body) => subsection(body, 'Known issues');
|
||||
|
||||
test('CHANGELOG: the Unreleased section\'s Known issues name the session split', () => {
|
||||
test('CHANGELOG: the delivery section\'s Known issues name the session split', () => {
|
||||
const body = unreleasedBody();
|
||||
const known = knownIssues(body);
|
||||
assert.match(known, /session split/, `Known issues name the session split: ${JSON.stringify(known)}`);
|
||||
|
|
@ -361,7 +372,7 @@ test('CHANGELOG: the door lines name operator decisions 20 and 23, and none says
|
|||
`no door line still says "every test file on the Test first File line": ${JSON.stringify(texts)}`);
|
||||
});
|
||||
|
||||
test('CHANGELOG: the Unreleased section no longer says "on a severity tie"', () => {
|
||||
test('CHANGELOG: the delivery section no longer says "on a severity tie"', () => {
|
||||
const body = unreleasedBody();
|
||||
assert.doesNotMatch(body, /on\s+a\s+severity\s+tie/);
|
||||
});
|
||||
|
|
@ -416,9 +427,9 @@ test('CHANGELOG: door lines name operator decisions 29, 30 and 31, none bad, and
|
|||
|
||||
test('CHANGELOG: the section names 08deae68, edf2ec56 and operator decision 27', () => {
|
||||
const body = unreleasedBody();
|
||||
assert.ok(body.includes('08deae68'), 'the Unreleased section names 08deae68');
|
||||
assert.ok(body.includes('edf2ec56'), 'the Unreleased section names edf2ec56');
|
||||
assert.match(body, /\boperator decision 27\b/, 'the Unreleased section names operator decision 27');
|
||||
assert.ok(body.includes('08deae68'), 'the delivery section names 08deae68');
|
||||
assert.ok(body.includes('edf2ec56'), 'the delivery section names edf2ec56');
|
||||
assert.match(body, /\boperator decision 27\b/, 'the delivery section names operator decision 27');
|
||||
});
|
||||
|
||||
test('CHANGELOG: the b4fa1927 bullet follows operator decision 29', () => {
|
||||
|
|
@ -470,11 +481,11 @@ test('CHANGELOG: the cf241526 bullet follows operator decision 30', () => {
|
|||
assert.doesNotMatch(b, /as\s+decision\s+20\s+sets\s+them/, `the cf241526 bullet no longer says "as decision 20 sets them": ${JSON.stringify(b)}`);
|
||||
});
|
||||
|
||||
test('CHANGELOG: the old rerun wording is gone from the Unreleased section', () => {
|
||||
test('CHANGELOG: the old rerun wording is gone from the delivery section', () => {
|
||||
const body = unreleasedBody();
|
||||
const flat = oneSpace(body);
|
||||
for (const phrase of ['reruns red proof once with `plan_tests`', 'reruns the same way', 'the list red proof accepted']) {
|
||||
assert.ok(!flat.includes(phrase), `the Unreleased section no longer says "${phrase}"`);
|
||||
assert.ok(!flat.includes(phrase), `the delivery section no longer says "${phrase}"`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs';
|
|||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { RULE_A, RULE_B, DIFF_FIELDS } from '../../lib/review/critic-diff.mjs';
|
||||
import { deliverySection } from '../helpers/changelog-section.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const read = rel => readFileSync(join(ROOT, rel), 'utf-8');
|
||||
|
|
@ -68,29 +69,22 @@ test('Phase 9 no longer says the loop ends on a round with no BLOCKER', () => {
|
|||
});
|
||||
|
||||
// The narrow round of the A/B review (operator decisions 48 and 50): the
|
||||
// CHANGELOG's open Unreleased section records it, Phase 9 says what a merge
|
||||
// keeps, and Handover 4 states the matcher's overlap rule and the distinct id.
|
||||
// The release check is imported as a module namespace, so a missing parseDoors
|
||||
// export fails an assertion, not the load.
|
||||
// CHANGELOG's delivery section records it, Phase 9 says what a merge keeps, and
|
||||
// Handover 4 states the matcher's overlap rule and the distinct id. The section
|
||||
// is found by its title, before and after the release commit
|
||||
// (tests/helpers/changelog-section.mjs). The release check is imported as a
|
||||
// module namespace, so a missing parseDoors export fails an assertion, not the
|
||||
// load.
|
||||
const releaseCheck = await import('../../scripts/release-check.mjs');
|
||||
|
||||
// The body of the open `## Unreleased` section, up to the next `## ` heading.
|
||||
function unreleasedBody() {
|
||||
const lines = read('CHANGELOG.md').split('\n');
|
||||
const start = lines.findIndex(l => /^## Unreleased\b/.test(l));
|
||||
assert.ok(start >= 0, 'CHANGELOG.md has a "## Unreleased" section');
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^## /.test(lines[i])) { end = i; break; }
|
||||
}
|
||||
return lines.slice(start + 1, end).join('\n');
|
||||
}
|
||||
// The delivery section's body, up to the next `## ` heading.
|
||||
const unreleasedBody = () => deliverySection(read('CHANGELOG.md')).body;
|
||||
|
||||
// A `### <name>` body of the Unreleased section, up to the next heading.
|
||||
// A `### <name>` body of the delivery section, up to the next heading.
|
||||
function subsection(body, name) {
|
||||
const lines = body.split('\n');
|
||||
const start = lines.findIndex(l => l.replace(/\s+$/, '') === `### ${name}`);
|
||||
assert.ok(start >= 0, `the Unreleased section has a "### ${name}" heading`);
|
||||
assert.ok(start >= 0, `the delivery section has a "### ${name}" heading`);
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^###? /.test(lines[i])) { end = i; break; }
|
||||
|
|
@ -113,7 +107,7 @@ test('CHANGELOG: the narrow round of operator decisions 48 and 50', () => {
|
|||
assert.equal(typeof releaseCheck.parseDoors, 'function', 'scripts/release-check.mjs exports parseDoors');
|
||||
const body = unreleasedBody();
|
||||
const d = releaseCheck.parseDoors(body);
|
||||
assert.equal(d.found, true, 'the Unreleased section has a "### Doors" heading');
|
||||
assert.equal(d.found, true, 'the delivery section has a "### Doors" heading');
|
||||
assert.deepEqual(d.bad, [], 'every top-level door line reads "- one-way: ..." or "- two-way: ..."');
|
||||
const doors48 = d.entries.filter(e => /operator\s+decision\s+48\b/.test(flat(e.text)));
|
||||
assert.ok(doors48.length >= 3,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue