Completes Block 4 (4b backlog + 4c export/execution). Asymmetric: plan
export is new testable code; execution is pure reuse of the existing
per-repo implement/rollback (no new execution machinery), per the plan's
"reuse existing backup/rollback".
Plan export ("planer følger arbeidsstedet"):
- scanners/lib/campaign-export.mjs (pure, now injected, 8 tests):
planExportPath(repo,sessionId) -> <repo>/docs/config-audit-plan-<sessionId>.md
(sessionId-keyed so same-day re-audits never collide);
buildPlanExportDocument({...,now}) -> provenance header + verbatim plan.
- scanners/campaign-export-cli.mjs (-cli, read-only by default, 10 tests):
--repo resolves the repo's linked session, reads its action-plan.md,
assembles the doc, emits {exportable,problems,targetPath,document}. Two
gates -> exit 1 advisory: no-session-linked / no-action-plan. Writes the
file ONLY under opt-in --write (byte-faithful copy; the LLM never re-types
a 200-line plan). --sessions-dir override for hermetic tests; exit 0/1/3.
Command: commands/campaign.md gains an `export <path>` mode (Step 6:
preview -> approve -> --write), then routes the user to the existing
/config-audit implement (backup + verify) + rollback + set-status
implemented. Nothing auto-written (Verifiseringsplikt).
Byte-stable: lib + -cli + command-doc only -> scanner count stays 15,
agents 7, commands 21 (export is a mode, not a new command), SC-5 +
backcompat suite untouched. suite 1150->1168. Block 4a (migrateLedger)
still deferred to the first breaking schema change.
Docs: CLAUDE.md section + badge 1150->1168/65->67 files; README badge +
campaign row + Testing prose (fixed stale 1055/59 -> true 1168/67).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
3.8 KiB
JavaScript
78 lines
3.8 KiB
JavaScript
/**
|
|
* campaign-export — plan-export transforms (v5.7 Fase 2, Block 4c).
|
|
*
|
|
* The second half of Block 4 ("durable backlog + execution"). Block 4b built the cross-repo
|
|
* prioritized backlog the user picks from; this exports a picked repo's per-repo action plan
|
|
* into the TARGET repo's OWN `docs/` directory, so the plan gets a durable, human-readable
|
|
* home where the work is done ("planer følger arbeidsstedet" — the operator's continuity rule
|
|
* that plans live next to the workplace, in `docs/`).
|
|
*
|
|
* Design mirrors campaign-ledger: PURE, deterministic transforms — `now` is injected as a
|
|
* YYYY-MM-DD string, never read from the clock here, so they are fully unit-testable. The IO
|
|
* (loading the ledger, reading the session's action-plan.md, writing the exported file) lives
|
|
* in the thin `campaign-export-cli` shell. The transforms throw on programmer error
|
|
* (missing/blank required field), consistent with the ledger transforms.
|
|
*
|
|
* NOTE on execution: Block 4c deliberately adds NO new execution machinery. Execution reuses
|
|
* the existing per-repo `/config-audit implement` (which backs up every changed file, applies
|
|
* the plan from the session, and verifies) + `/config-audit rollback`. The exported `docs/`
|
|
* copy is the repo's durable record of the plan, NOT the execution input — `implement` still
|
|
* reads the canonical plan from the session directory. See docs/v5.7-optimization-lens-plan.md
|
|
* §Fase 2 (Block 4).
|
|
*/
|
|
|
|
import { join } from 'node:path';
|
|
|
|
/**
|
|
* The exported plan's destination inside the TARGET repo's own `docs/`. Keyed on the source
|
|
* `sessionId` (timestamp-unique per audit) rather than the calendar date, so two audits of the
|
|
* same repo on the same day produce distinct files (history is preserved, never silently
|
|
* overwritten) and the filename ties the export back to the audit that produced it.
|
|
*
|
|
* @param {string} repoPath - absolute path to the target repo (the ledger stores it resolved)
|
|
* @param {string} sessionId - the config-audit session that produced the plan
|
|
* @returns {string} `<repoPath>/docs/config-audit-plan-<sessionId>.md`
|
|
*/
|
|
export function planExportPath(repoPath, sessionId) {
|
|
if (typeof repoPath !== 'string' || repoPath.trim() === '') {
|
|
throw new TypeError('repoPath is required');
|
|
}
|
|
if (typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
throw new TypeError('sessionId is required');
|
|
}
|
|
return join(repoPath, 'docs', `config-audit-plan-${sessionId}.md`);
|
|
}
|
|
|
|
/**
|
|
* Assemble the exported document: a provenance header (who/when/where this came from + how to
|
|
* execute and undo it) followed by the verbatim session plan body. Pure — given the same inputs
|
|
* it always produces the same bytes, so it is snapshot-testable.
|
|
*
|
|
* @param {{repoName:string, repoPath:string, sessionId:string, planMarkdown:string, now:string}} input
|
|
* @returns {string} the full markdown to write into the repo's docs/
|
|
*/
|
|
export function buildPlanExportDocument({ repoName, repoPath, sessionId, planMarkdown, now } = {}) {
|
|
for (const [k, v] of Object.entries({ repoName, repoPath, sessionId, planMarkdown, now })) {
|
|
if (typeof v !== 'string' || v.trim() === '') {
|
|
throw new TypeError(`${k} is required`);
|
|
}
|
|
}
|
|
|
|
const header = [
|
|
`# Config-Audit Action Plan — ${repoName}`,
|
|
'',
|
|
`> Exported from the config-audit machine-wide campaign on ${now}.`,
|
|
`> **Repo:** \`${repoPath}\``,
|
|
`> **Source session:** \`${sessionId}\``,
|
|
'>',
|
|
'> Generated by `/config-audit plan`. To **execute**: run `/config-audit implement` in this',
|
|
'> repo — it backs up every changed file, applies the plan, then verifies the result. To',
|
|
'> **undo**: `/config-audit rollback`. Record progress back in the campaign with',
|
|
`> \`/config-audit campaign set-status ${repoPath} implemented\`.`,
|
|
'',
|
|
'---',
|
|
'',
|
|
].join('\n');
|
|
|
|
return `${header}${planMarkdown.trimEnd()}\n`;
|
|
}
|