NW3 (CC-26 §6 PoC): delegate trekplan Phase 7 synthesis to a synthesis-agent, adopt only if Δ main-context ≥30% with no quality loss. Operator chose the deterministic-proof path (live ≥3-run bake-off was env-blocked: no API key; installed plugin is a cache copy so a new agent is invisible to `claude -p`). Decisive structural finding: trekplan Phase 5 runs the swarm FOREGROUND, so its outputs are already resident in main before Phase 7. Delegating only Phase 7 evicts nothing → Δ_faithful = 0% (BASE-independent). The ≥30% saving needs an out-of-scope Phase-5 redesign (swarm-writes-to-disk / nested orchestrator). VERDICT: DECLINED per measurement. - agents/synthesis-agent.md — dormant, schema-conformant deliverable (NOT wired) - lib/plan/synthesis-digest-schema.mjs — digest output contract (+ tests) - scripts/synthesis-measure.mjs — deterministic Δ-accounting core (+ tests) - tests/fixtures/synthesis/ — 7 exploration outputs + representative digest - docs/T1-synthesis-poc-results.md — measurement + verdict (reproducible) - CLAUDE.md — agent table row (doc-consistency: 24 agents) Tests 670 → 695 (693 pass / 2 skip / 0 fail). `claude plugin validate` clean (only the pre-existing root-CLAUDE.md warning). commands/trekplan.md untouched. [skip-docs] rationale: no user-facing feature ships (NW3 declined; agent dormant and unwired). The substantive doc is docs/T1-synthesis-poc-results.md; the README/CHANGELOG roll-up for NW1–NW3 is the S13 coordinated release per docs/W1-narrow-wins-plan.md §S13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
161 lines
6.3 KiB
JavaScript
161 lines
6.3 KiB
JavaScript
// tests/lib/synthesis-digest-schema.test.mjs
|
|
// NW3 (S12) — digest-schema contract for the synthesis-agent.
|
|
//
|
|
// The synthesis-agent (agents/synthesis-agent.md) ingests the trekplan Phase-5/7
|
|
// exploration outputs and emits a trailing fenced ```json block: the findings
|
|
// DIGEST main currently writes inline in Phase 7. This pins that digest's shape
|
|
// so a delegated path could VALIDATE it (not merely parse it) and so the
|
|
// measurement harness has a fixed quality contract to compare against.
|
|
//
|
|
// Load-bearing fields (what Phase 8 deep-planning consumes): task,
|
|
// architecture_model, and the five synthesis arrays (reusable_code,
|
|
// contradictions, risks, gaps, sources). Each source must be origin-tagged
|
|
// codebase|research (Phase 7 rule 7: "track whether it came from codebase
|
|
// analysis or external research"). Mirrors lib/review/findings-schema.mjs.
|
|
//
|
|
// When this test fails, fix the schema or the producer — do NOT relax the
|
|
// assertion to hide drift.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import {
|
|
validateDigest,
|
|
validateAgentOutput,
|
|
extractDigestBlock,
|
|
ORIGIN_VALUES,
|
|
DIGEST_REQUIRED_FIELDS,
|
|
} from '../../lib/plan/synthesis-digest-schema.mjs';
|
|
|
|
function wellFormed() {
|
|
return {
|
|
agent: 'synthesis-agent',
|
|
task: 'Add a per-wave rate-limit guard to trekexecute headless launches',
|
|
architecture_model:
|
|
'trekexecute spawns parallel headless waves via the Bash launcher template; ' +
|
|
'concurrency is currently unbounded per wave.',
|
|
reusable_code: [
|
|
{ ref: 'lib/util/result.mjs:33', note: 'issue() for structured errors' },
|
|
{ ref: 'templates/headless-launch-template.md:10', note: 'wave dispatch site' },
|
|
],
|
|
contradictions: [
|
|
'architecture-mapper says waves are sequential; dependency-tracer shows a parallel fan-out',
|
|
],
|
|
risks: [
|
|
{ risk: 'A rate-limit that blocks too aggressively starves long waves', severity: 'medium' },
|
|
],
|
|
gaps: ['No measured per-wave token ceiling exists yet — becomes an assumption'],
|
|
sources: [
|
|
{ finding: 'wave launcher lives in the template', origin: 'codebase' },
|
|
{ finding: 'CC headless --max-budget-usd semantics', origin: 'research' },
|
|
],
|
|
};
|
|
}
|
|
|
|
test('digest schema: a well-formed digest validates', () => {
|
|
const r = validateDigest(wellFormed());
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.deepEqual(r.errors, []);
|
|
});
|
|
|
|
test('digest schema: exposes the required-field + origin contracts', () => {
|
|
assert.ok(Array.isArray(DIGEST_REQUIRED_FIELDS) && DIGEST_REQUIRED_FIELDS.length > 0);
|
|
for (const f of ['task', 'architecture_model', 'reusable_code', 'contradictions', 'risks', 'gaps', 'sources']) {
|
|
assert.ok(DIGEST_REQUIRED_FIELDS.includes(f), `required fields must include ${f}`);
|
|
}
|
|
assert.deepEqual([...ORIGIN_VALUES].sort(), ['codebase', 'research']);
|
|
});
|
|
|
|
test('digest schema: rejects a non-object payload', () => {
|
|
for (const bad of [null, 42, 'x', ['a']]) {
|
|
const r = validateDigest(bad);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_NOT_OBJECT'), `expected DIGEST_NOT_OBJECT for ${JSON.stringify(bad)}`);
|
|
}
|
|
});
|
|
|
|
test('digest schema: rejects missing/empty task', () => {
|
|
for (const t of [undefined, '', 123]) {
|
|
const d = wellFormed();
|
|
d.task = t;
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_MISSING_TASK'));
|
|
}
|
|
});
|
|
|
|
test('digest schema: rejects missing/empty architecture_model', () => {
|
|
const d = wellFormed();
|
|
delete d.architecture_model;
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_MISSING_ARCHITECTURE'));
|
|
});
|
|
|
|
test('digest schema: each synthesis array must be an array', () => {
|
|
const cases = [
|
|
['reusable_code', 'DIGEST_REUSABLE_NOT_ARRAY'],
|
|
['contradictions', 'DIGEST_CONTRADICTIONS_NOT_ARRAY'],
|
|
['risks', 'DIGEST_RISKS_NOT_ARRAY'],
|
|
['gaps', 'DIGEST_GAPS_NOT_ARRAY'],
|
|
['sources', 'DIGEST_SOURCES_NOT_ARRAY'],
|
|
];
|
|
for (const [field, code] of cases) {
|
|
const d = wellFormed();
|
|
d[field] = { not: 'an array' };
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, false, `${field} as object should fail`);
|
|
assert.ok(r.errors.some((e) => e.code === code), `expected ${code}`);
|
|
}
|
|
});
|
|
|
|
test('digest schema: empty synthesis arrays are valid (a clean digest can have no contradictions/gaps)', () => {
|
|
const d = wellFormed();
|
|
d.contradictions = [];
|
|
d.gaps = [];
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
});
|
|
|
|
test('digest schema: a source with a non-enum origin is rejected', () => {
|
|
const d = wellFormed();
|
|
d.sources = [{ finding: 'x', origin: 'guess' }];
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_SOURCE_BAD_ORIGIN'));
|
|
});
|
|
|
|
test('digest schema: missing agent name is a warning, not an error', () => {
|
|
const d = wellFormed();
|
|
delete d.agent;
|
|
const r = validateDigest(d);
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.ok(r.warnings.some((w) => w.code === 'DIGEST_MISSING_AGENT'));
|
|
});
|
|
|
|
test('extractDigestBlock: pulls the LAST fenced json block from agent prose', () => {
|
|
const text =
|
|
'Here is my synthesis.\n\n' +
|
|
'```json\n{"stale": true}\n```\n\n' +
|
|
'Actually, the final digest:\n\n' +
|
|
'```json\n' + JSON.stringify(wellFormed()) + '\n```\n';
|
|
const block = extractDigestBlock(text);
|
|
assert.ok(block && JSON.parse(block).task, 'should extract the last json block');
|
|
assert.equal(JSON.parse(block).agent, 'synthesis-agent');
|
|
});
|
|
|
|
test('validateAgentOutput: no json fence → stable NO_JSON code', () => {
|
|
const r = validateAgentOutput('just prose, no fence');
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_NO_JSON_BLOCK'));
|
|
});
|
|
|
|
test('validateAgentOutput: malformed json → stable PARSE code', () => {
|
|
const r = validateAgentOutput('```json\n{ not valid }\n```');
|
|
assert.equal(r.valid, false);
|
|
assert.ok(r.errors.some((e) => e.code === 'DIGEST_PARSE_ERROR'));
|
|
});
|
|
|
|
test('validateAgentOutput: well-formed fenced digest validates end-to-end', () => {
|
|
const r = validateAgentOutput('Synthesis complete.\n```json\n' + JSON.stringify(wellFormed()) + '\n```');
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
});
|