mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Fix: critique snapshot close path (#660)
Preserve critique trend history while closing the exact processed snapshot. Track target identity and content freshness for local files and URLs, isolate colliding streams, and prevent legacy snapshots from resurfacing after a modern close. Validated with 50 focused tests, a clean 17-provider build, 834 core tests, passing GitHub CI and Cursor Bugbot, Greptile 5/5, and zero unresolved review threads. AI-assisted maintainer repair: implemented and reviewed with Codex under @pbakaus direction.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
### Purpose
|
||||
|
||||
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
|
||||
Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive of that run.
|
||||
|
||||
### Hard Invariants
|
||||
|
||||
@@ -103,7 +103,7 @@ Codex failure accounting: final Run Notes must include target slug, ignore list,
|
||||
|
||||
Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
|
||||
|
||||
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
|
||||
The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is an archive of that run.
|
||||
|
||||
<codex>
|
||||
Codex final-answer note: `$impeccable critique` produces a report artifact, so the final chat response should intentionally exceed the usual concise close-out style. Do not title the final response "Critique Summary" unless the user explicitly asked for a summary.
|
||||
@@ -231,7 +231,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
|
||||
node {{scripts_path}}/critique-storage.mjs write "<resolved target>" <body-file>
|
||||
```
|
||||
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
|
||||
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not.
|
||||
|
||||
3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
|
||||
If a prior critique exists, use it as one input:
|
||||
|
||||
```bash
|
||||
node {{scripts_path}}/critique-storage.mjs latest "<resolved target>"
|
||||
node {{scripts_path}}/critique-storage.mjs latest "<resolved target>" --json
|
||||
```
|
||||
|
||||
Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
|
||||
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way.
|
||||
|
||||
## 3. Triage
|
||||
|
||||
@@ -95,3 +95,11 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
|
||||
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
|
||||
|
||||
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
|
||||
|
||||
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
|
||||
|
||||
```bash
|
||||
node {{scripts_path}}/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
|
||||
```
|
||||
|
||||
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
|
||||
|
||||
@@ -8,7 +8,7 @@ Reason over the signals; there is no score to obey:
|
||||
|
||||
- `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system).
|
||||
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
|
||||
- `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale.
|
||||
- `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
|
||||
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
|
||||
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
|
||||
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs latest <slug> [--json]
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
* node critique-storage.mjs close <resolved-target> <snapshot-file>
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
@@ -27,6 +28,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { slugFromTarget } from './lib/target-slug.mjs';
|
||||
@@ -50,6 +52,45 @@ export function nowFilenameStamp(date = new Date()) {
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an exact content fingerprint for a local file target. URLs and
|
||||
* non-files return null because their content is not available here.
|
||||
*
|
||||
* The fingerprint deliberately describes bytes, not Git state or mtimes:
|
||||
* critique often assesses an uncommitted file, and a later polish run should
|
||||
* inherit that backlog when the bytes are unchanged regardless of staging.
|
||||
*/
|
||||
function resolveLocalTargetPath(target, { cwd = process.cwd() } = {}) {
|
||||
if (!target || /^https?:\/\//i.test(target)) return null;
|
||||
return path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target);
|
||||
}
|
||||
|
||||
function resolveTargetIdentity(target, { cwd = process.cwd() } = {}) {
|
||||
if (!target || typeof target !== 'string') return null;
|
||||
if (/^https?:\/\//i.test(target)) {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
const pathname = url.pathname.replace(/\/+$/, '') || '/';
|
||||
return `url:${url.origin}${pathname}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const filePath = resolveLocalTargetPath(target, { cwd });
|
||||
return filePath ? `file:${filePath}` : null;
|
||||
}
|
||||
|
||||
export function fingerprintTarget(target, { cwd = process.cwd() } = {}) {
|
||||
const filePath = resolveLocalTargetPath(target, { cwd });
|
||||
if (!filePath) return null;
|
||||
try {
|
||||
if (!fs.statSync(filePath).isFile()) return null;
|
||||
return `sha256:${createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
@@ -62,14 +103,27 @@ export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
const contents = `${front}\n${body.trim()}\n`;
|
||||
|
||||
// A second critique can finish in the same UTC second. Use exclusive
|
||||
// creation and a fixed-width suffix so concurrent writers cannot replace
|
||||
// history and lexical ordering still keeps collision entries newest.
|
||||
for (let collision = 0; collision <= 9999; collision += 1) {
|
||||
const suffix = collision === 0 ? '' : `~${String(collision).padStart(4, '0')}`;
|
||||
const filePath = path.join(dir, `${timestamp}${suffix}__${slug}.md`);
|
||||
try {
|
||||
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', flag: 'wx' });
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
if (error?.code !== 'EEXIST') throw error;
|
||||
}
|
||||
}
|
||||
throw new Error(`Too many critique snapshots for ${slug} at ${timestamp}`);
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
@@ -98,6 +152,8 @@ function parseFrontmatter(text) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
value = value === 'true';
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
@@ -107,7 +163,7 @@ function parseFrontmatter(text) {
|
||||
/**
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(?:~\d{4})?__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
@@ -118,24 +174,105 @@ function listSnapshots(suffix, cwd) {
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
function readSnapshot(filePath) {
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
function snapshotTargetIdentity(snapshot) {
|
||||
const targetPath = snapshot?.meta.target_path;
|
||||
return snapshot?.meta.target_identity
|
||||
|| (targetPath ? `file:${targetPath}` : null);
|
||||
}
|
||||
|
||||
function readNewestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
return readSnapshot(listSnapshots(`__${slug}.md`, cwd).at(-1));
|
||||
}
|
||||
|
||||
function readNewestSnapshotForIdentity(
|
||||
slug,
|
||||
targetIdentity,
|
||||
{ cwd = process.cwd() } = {},
|
||||
) {
|
||||
const matches = listSnapshots(`__${slug}.md`, cwd)
|
||||
.map(readSnapshot)
|
||||
.filter((snapshot) => snapshotTargetIdentity(snapshot) === targetIdentity);
|
||||
return matches.at(-1) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
const latest = readNewestSnapshot(slug, { cwd });
|
||||
return latest?.meta.closed === true ? null : latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark one exact snapshot closed without deleting the score history consumed
|
||||
* by `trend`. Exact identity matters: a newer critique may land after polish
|
||||
* reads its backlog, and that newer snapshot must remain live. `snapshotFile`
|
||||
* may be the absolute path returned by readLatestSnapshot() or the basename
|
||||
* emitted by `latest --json`. Returns the path marked closed, or null.
|
||||
*/
|
||||
export function closeSnapshot(snapshotFile, { cwd = process.cwd() } = {}) {
|
||||
if (!snapshotFile || typeof snapshotFile !== 'string') return null;
|
||||
const dir = path.resolve(getCritiqueDir(cwd));
|
||||
const snapshotPath = path.isAbsolute(snapshotFile)
|
||||
? path.resolve(snapshotFile)
|
||||
: path.resolve(dir, snapshotFile);
|
||||
const filename = path.basename(snapshotPath);
|
||||
if (
|
||||
path.dirname(snapshotPath) !== dir
|
||||
|| !SNAPSHOT_FILENAME.test(filename)
|
||||
) return null;
|
||||
|
||||
let snapshot;
|
||||
try {
|
||||
if (!fs.lstatSync(snapshotPath).isFile()) return null;
|
||||
snapshot = readSnapshot(snapshotPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!snapshot || snapshot.meta.closed === true) return null;
|
||||
const closedBody = snapshot.body.replace(
|
||||
/^(---\r?\n[\s\S]*?)(\r?\n---)/,
|
||||
'$1\nclosed: true$2',
|
||||
);
|
||||
if (closedBody === snapshot.body) {
|
||||
throw new Error(`Cannot close snapshot without frontmatter: ${snapshot.path}`);
|
||||
}
|
||||
fs.writeFileSync(snapshot.path, closedBody, 'utf-8');
|
||||
return snapshot.path;
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
const snapshots = listSnapshots('.md', cwd).map(readSnapshot);
|
||||
const identifiedSlugs = new Set(
|
||||
snapshots
|
||||
.filter((snapshot) => snapshotTargetIdentity(snapshot))
|
||||
.map((snapshot) => snapshot.meta.slug),
|
||||
);
|
||||
const latestByTarget = new Map();
|
||||
for (const snapshot of snapshots) {
|
||||
if (!snapshot?.meta.slug) continue;
|
||||
// Slugs are lossy: distinct targets such as foo/bar and foo-bar can share
|
||||
// one. Keep each known identity's latest open/closed state independent so
|
||||
// closing one target cannot hide another target's live backlog. Once a
|
||||
// slug has any identity-aware snapshot, its older legacy records are no
|
||||
// longer independently routable and must not resurface as zombie work.
|
||||
const targetIdentity = snapshotTargetIdentity(snapshot);
|
||||
if (!targetIdentity && identifiedSlugs.has(snapshot.meta.slug)) continue;
|
||||
const streamKey = targetIdentity || `slug:${snapshot.meta.slug}`;
|
||||
latestByTarget.set(streamKey, snapshot);
|
||||
}
|
||||
return [...latestByTarget.values()]
|
||||
.filter((snapshot) => snapshot.meta.closed !== true)
|
||||
.sort((a, b) => a.path.localeCompare(b.path))
|
||||
.at(-1) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,9 +290,13 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
// Accept either a ready slug or a concrete target (path/URL) everywhere, so
|
||||
// callers never have to run the slug step separately. Anything containing a
|
||||
// path or URL marker is resolved through slugFromTarget.
|
||||
function isReadySlug(value) {
|
||||
return /^[a-z0-9-]+$/.test(value || '') && !value.includes('/');
|
||||
}
|
||||
|
||||
function coerceSlug(value) {
|
||||
if (!value) return null;
|
||||
if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
|
||||
if (isReadySlug(value)) return value;
|
||||
return slugFromTarget(value);
|
||||
}
|
||||
|
||||
@@ -181,14 +322,124 @@ function main(argv) {
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
// The helper, not caller-provided metadata, owns the target fingerprint.
|
||||
// This makes the snapshot describe the exact file bytes critique saw.
|
||||
delete meta.target_fingerprint;
|
||||
delete meta.target_path;
|
||||
delete meta.target_identity;
|
||||
const targetIdentity = resolveTargetIdentity(slugArg);
|
||||
if (targetIdentity) meta.target_identity = targetIdentity;
|
||||
const targetFingerprint = fingerprintTarget(slugArg);
|
||||
if (targetFingerprint) {
|
||||
meta.target_fingerprint = targetFingerprint;
|
||||
meta.target_path = resolveLocalTargetPath(slugArg);
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(coerceSlug(args[0]));
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
const target = args[0];
|
||||
const format = args[1];
|
||||
const slug = coerceSlug(target);
|
||||
if (!slug || (format && format !== '--json')) {
|
||||
process.stderr.write('usage: latest <slug-or-target> [--json]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const targetFingerprint = fingerprintTarget(target);
|
||||
const targetPath = resolveLocalTargetPath(target);
|
||||
const targetIdentity = resolveTargetIdentity(target);
|
||||
const readySlug = isReadySlug(target);
|
||||
const newestForSlug = readNewestSnapshot(slug);
|
||||
if (!newestForSlug) { process.exit(2); }
|
||||
|
||||
// Concrete targets select the newest snapshot for their exact identity,
|
||||
// not merely the newest filename for a lossy slug. This keeps distinct
|
||||
// targets such as foo/bar and foo-bar from hiding each other's backlog.
|
||||
const exactSnapshot = readNewestSnapshotForIdentity(slug, targetIdentity);
|
||||
let latest = exactSnapshot;
|
||||
if (!latest && !readySlug) {
|
||||
// Legacy snapshots have no identity. Preserve their old explicit
|
||||
// path/URL behavior only when no known target identity was selected.
|
||||
latest = readNewestSnapshotForIdentity(slug, null);
|
||||
}
|
||||
if (!latest) latest = newestForSlug;
|
||||
if (latest.meta.closed === true) { process.exit(2); }
|
||||
|
||||
const recordedTargetPath = latest.meta.target_path;
|
||||
const recordedTargetIdentity = snapshotTargetIdentity(latest);
|
||||
const matchingIdentity = recordedTargetIdentity === targetIdentity;
|
||||
|
||||
// Bare slugs remain a supported lookup mode, including for URL
|
||||
// snapshots. But when a same-named local file exists, the request is
|
||||
// ambiguous unless that exact file owns the snapshot identity.
|
||||
if (readySlug && !recordedTargetIdentity) {
|
||||
process.stderr.write(
|
||||
'ambiguous legacy snapshot target; use an explicit ./path or full URL\n',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
if (readySlug && targetPath && fs.existsSync(targetPath) && !matchingIdentity) {
|
||||
process.stderr.write(
|
||||
'ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const concreteTarget = !readySlug || matchingIdentity;
|
||||
if (concreteTarget && recordedTargetIdentity && !matchingIdentity) {
|
||||
process.exit(2);
|
||||
}
|
||||
const concreteLocalTarget = concreteTarget && targetPath;
|
||||
if (concreteLocalTarget && latest.meta.target_fingerprint !== targetFingerprint) {
|
||||
closeSnapshot(latest.path);
|
||||
process.exit(2);
|
||||
}
|
||||
if (format === '--json') {
|
||||
process.stdout.write(JSON.stringify({
|
||||
snapshot_file: path.basename(latest.path),
|
||||
body: latest.body,
|
||||
}, null, 2) + '\n');
|
||||
} else {
|
||||
process.stdout.write(latest.body);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'close': {
|
||||
const [slugArg, snapshotFile, ...extra] = args;
|
||||
const slug = coerceSlug(slugArg);
|
||||
if (!slug || !snapshotFile || extra.length > 0) {
|
||||
process.stderr.write('usage: close <resolved-target> <snapshot-file>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
path.basename(snapshotFile) !== snapshotFile
|
||||
|| !SNAPSHOT_FILENAME.test(snapshotFile)
|
||||
|| !snapshotFile.endsWith(`__${slug}.md`)
|
||||
) process.exit(2);
|
||||
|
||||
// A slug and filename are not enough to prove ownership because two
|
||||
// distinct targets can normalize to the same slug. Modern snapshots
|
||||
// carry a canonical identity, so require the supplied resolved target
|
||||
// to match it before allowing the exact snapshot to be closed. Legacy
|
||||
// snapshots without identity retain their historical close behavior.
|
||||
const snapshotPath = path.join(getCritiqueDir(process.cwd()), snapshotFile);
|
||||
let snapshot;
|
||||
try {
|
||||
if (!fs.lstatSync(snapshotPath).isFile()) process.exit(2);
|
||||
snapshot = readSnapshot(snapshotPath);
|
||||
} catch {
|
||||
process.exit(2);
|
||||
}
|
||||
const recordedTargetIdentity = snapshotTargetIdentity(snapshot);
|
||||
if (
|
||||
recordedTargetIdentity
|
||||
&& recordedTargetIdentity !== resolveTargetIdentity(slugArg)
|
||||
) process.exit(2);
|
||||
|
||||
const closed = closeSnapshot(snapshotFile);
|
||||
if (!closed) { process.exit(2); }
|
||||
process.stdout.write(`${closed}\n`);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
@@ -197,7 +448,7 @@ function main(argv) {
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|close> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -14,11 +14,13 @@ import { fileURLToPath } from 'node:url';
|
||||
const SCRIPT = fileURLToPath(new URL('../skill/scripts/critique-storage.mjs', import.meta.url));
|
||||
|
||||
import {
|
||||
fingerprintTarget,
|
||||
slugFromTarget,
|
||||
writeSnapshot,
|
||||
readLatestSnapshot,
|
||||
readLatestSnapshotAcrossTargets,
|
||||
readTrend,
|
||||
closeSnapshot,
|
||||
nowFilenameStamp,
|
||||
} from '../skill/scripts/critique-storage.mjs';
|
||||
|
||||
@@ -87,6 +89,25 @@ describe('nowFilenameStamp', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fingerprintTarget', () => {
|
||||
it('fingerprints exact local file bytes independent of Git state', () => {
|
||||
const target = join(cwd, 'index.html');
|
||||
writeFileSync(target, '<main>hello</main>');
|
||||
const first = fingerprintTarget(target, { cwd });
|
||||
assert.match(first, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(fingerprintTarget('index.html', { cwd }), first);
|
||||
|
||||
writeFileSync(target, '<main>changed</main>');
|
||||
assert.notEqual(fingerprintTarget(target, { cwd }), first);
|
||||
});
|
||||
|
||||
it('returns null for URLs, directories, and missing files', () => {
|
||||
assert.equal(fingerprintTarget('https://example.com/page', { cwd }), null);
|
||||
assert.equal(fingerprintTarget('.', { cwd }), null);
|
||||
assert.equal(fingerprintTarget('missing.html', { cwd }), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeSnapshot + readLatestSnapshot', () => {
|
||||
it('round-trips body and frontmatter', () => {
|
||||
const out = writeSnapshot({
|
||||
@@ -115,6 +136,33 @@ describe('writeSnapshot + readLatestSnapshot', () => {
|
||||
assert.match(latest.body, /new/);
|
||||
});
|
||||
|
||||
it('preserves same-second snapshots with a sortable collision suffix', () => {
|
||||
const now = new Date('2026-05-12T18:30:00Z');
|
||||
const first = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 20 },
|
||||
body: 'first',
|
||||
cwd,
|
||||
now,
|
||||
});
|
||||
const second = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 30 },
|
||||
body: 'second',
|
||||
cwd,
|
||||
now,
|
||||
});
|
||||
|
||||
assert.notEqual(second, first);
|
||||
assert.ok(first.endsWith('2026-05-12T18-30-00Z__index-astro.md'));
|
||||
assert.ok(second.endsWith('2026-05-12T18-30-00Z~0001__index-astro.md'));
|
||||
assert.match(readLatestSnapshot('index-astro', { cwd }).body, /second/);
|
||||
assert.deepEqual(
|
||||
readTrend('index-astro', { cwd }).map((entry) => entry.total_score),
|
||||
[20, 30],
|
||||
);
|
||||
});
|
||||
|
||||
it('picks the newest snapshot across target slugs', () => {
|
||||
writeSnapshot({ slug: 'home', meta: {}, body: 'old', cwd, now: new Date('2026-05-01T00:00:00Z') });
|
||||
writeSnapshot({ slug: 'pricing', meta: {}, body: 'new', cwd, now: new Date('2026-05-12T00:00:00Z') });
|
||||
@@ -161,6 +209,129 @@ describe('writeSnapshot + readLatestSnapshot', () => {
|
||||
const latest = readLatestSnapshot('x', { cwd });
|
||||
assert.equal(latest.meta.target, 'docs: critique # main');
|
||||
});
|
||||
|
||||
it('closeSnapshot returns the path and leaves readLatestSnapshot null', () => {
|
||||
const out = writeSnapshot({ slug: 'index-astro', meta: { total_score: 20 }, body: 'open', cwd });
|
||||
const closed = closeSnapshot(out, { cwd });
|
||||
assert.equal(closed, out);
|
||||
assert.ok(closed.endsWith('__index-astro.md'));
|
||||
assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
|
||||
});
|
||||
|
||||
it('closeSnapshot closes the backlog without deleting its trend history', () => {
|
||||
writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 21, p0_count: 7 },
|
||||
body: 'old leftover',
|
||||
cwd,
|
||||
now: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
const newest = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 30 },
|
||||
body: 'newer',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
const closed = closeSnapshot(newest, { cwd });
|
||||
assert.equal(closed, newest);
|
||||
assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
|
||||
const trend = readTrend('index-astro', { cwd });
|
||||
assert.equal(trend.length, 2);
|
||||
assert.equal(trend[0].total_score, 21);
|
||||
assert.equal(trend[1].total_score, 30);
|
||||
assert.equal(trend[1].closed, true);
|
||||
});
|
||||
|
||||
it('a new snapshot reopens a previously closed slug', () => {
|
||||
const resolved = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 20 },
|
||||
body: 'resolved',
|
||||
cwd,
|
||||
now: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
closeSnapshot(resolved, { cwd });
|
||||
const reopened = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 15 },
|
||||
body: 'new findings',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
|
||||
assert.equal(readLatestSnapshot('index-astro', { cwd }).path, reopened);
|
||||
assert.equal(readTrend('index-astro', { cwd }).length, 2);
|
||||
});
|
||||
|
||||
it('latest across targets skips a closed slug without hiding other backlogs', () => {
|
||||
const pricing = writeSnapshot({
|
||||
slug: 'pricing',
|
||||
meta: { total_score: 25 },
|
||||
body: 'pricing backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
const home = writeSnapshot({
|
||||
slug: 'home',
|
||||
meta: { total_score: 30 },
|
||||
body: 'home backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
closeSnapshot(home, { cwd });
|
||||
|
||||
assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, pricing);
|
||||
});
|
||||
|
||||
it('latest across targets keeps colliding target identities independent', () => {
|
||||
const original = writeSnapshot({
|
||||
slug: 'foo-bar',
|
||||
meta: {
|
||||
target_identity: `file:${join(cwd, 'foo', 'bar')}`,
|
||||
total_score: 20,
|
||||
},
|
||||
body: 'older original backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
const colliding = writeSnapshot({
|
||||
slug: 'foo-bar',
|
||||
meta: {
|
||||
target_identity: `file:${join(cwd, 'foo-bar')}`,
|
||||
total_score: 30,
|
||||
},
|
||||
body: 'newer colliding backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
closeSnapshot(colliding, { cwd });
|
||||
|
||||
assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, original);
|
||||
});
|
||||
|
||||
it('latest across targets does not resurrect legacy work after identity migration', () => {
|
||||
writeSnapshot({
|
||||
slug: 'index-html',
|
||||
meta: { total_score: 20 },
|
||||
body: 'legacy backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
const modern = writeSnapshot({
|
||||
slug: 'index-html',
|
||||
meta: {
|
||||
target_identity: `file:${join(cwd, 'index.html')}`,
|
||||
total_score: 30,
|
||||
},
|
||||
body: 'modern backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
closeSnapshot(modern, { cwd });
|
||||
|
||||
assert.equal(readLatestSnapshotAcrossTargets({ cwd }), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLI entry point', () => {
|
||||
@@ -204,6 +375,486 @@ describe('CLI entry point', () => {
|
||||
});
|
||||
assert.equal(r.status, 2);
|
||||
});
|
||||
|
||||
it('inherits an unchanged untracked file snapshot and closes it after any byte change', () => {
|
||||
const target = join(cwd, 'index.html');
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
writeFileSync(target, '<main>assessed worktree</main>');
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
|
||||
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
const written = readLatestSnapshot('index-html', { cwd });
|
||||
assert.equal(written.meta.target_path, target);
|
||||
assert.equal(written.meta.target_identity, `file:${target}`);
|
||||
assert.match(written.meta.target_fingerprint, /^sha256:[a-f0-9]{64}$/);
|
||||
|
||||
const unchanged = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(unchanged.status, 0, `stderr: ${unchanged.stderr}`);
|
||||
assert.match(unchanged.stdout, /improve hierarchy/);
|
||||
|
||||
// The edit can happen in the same clock second as the snapshot; exact
|
||||
// bytes, rather than timestamp precision, determine freshness.
|
||||
writeFileSync(target, '<main>newer worktree</main>');
|
||||
const changed = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(changed.status, 2, `stderr: ${changed.stderr}`);
|
||||
assert.equal(readLatestSnapshot('index-html', { cwd }), null);
|
||||
assert.equal(readTrend('index-html', { cwd })[0].closed, true);
|
||||
});
|
||||
|
||||
it('fingerprints extensionless local targets instead of mistaking them for slugs', () => {
|
||||
const target = join(cwd, 'main');
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
writeFileSync(target, '<main>assessed</main>');
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
|
||||
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
|
||||
writeFileSync(target, '<main>changed</main>');
|
||||
const changed = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(changed.status, 2, `stderr: ${changed.stderr}`);
|
||||
assert.equal(readLatestSnapshot('main', { cwd }), null);
|
||||
|
||||
const deletedTarget = join(cwd, 'shell');
|
||||
writeFileSync(deletedTarget, '#!/bin/sh\n');
|
||||
const deletedWrite = spawnSync(process.execPath, [SCRIPT, 'write', deletedTarget, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(deletedWrite.status, 0, `stderr: ${deletedWrite.stderr}`);
|
||||
rmSync(deletedTarget);
|
||||
const deleted = spawnSync(process.execPath, [SCRIPT, 'latest', deletedTarget], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(deleted.status, 2, `stderr: ${deleted.stderr}`);
|
||||
assert.equal(readLatestSnapshot('shell', { cwd }), null);
|
||||
});
|
||||
|
||||
it('rejects a concrete target that collides with another target slug', () => {
|
||||
const originalDir = join(cwd, 'foo');
|
||||
const originalTarget = join('foo', 'bar');
|
||||
const originalPath = join(cwd, originalTarget);
|
||||
const ambiguousTarget = join(cwd, 'foo-bar');
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
mkdirSync(originalDir);
|
||||
writeFileSync(originalPath, '<main>assessed original</main>');
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: preserve this backlog');
|
||||
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', originalTarget, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
|
||||
// This distinct extensionless file shares the original target's slug.
|
||||
// The bare value is ambiguous while that file exists, and an explicit
|
||||
// local path is a known identity mismatch. Neither may inherit or close
|
||||
// the original snapshot.
|
||||
writeFileSync(ambiguousTarget, '<main>different target</main>');
|
||||
const ambiguous = spawnSync(process.execPath, [SCRIPT, 'latest', 'foo-bar'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(ambiguous.status, 2, `stderr: ${ambiguous.stderr}`);
|
||||
assert.match(ambiguous.stderr, /ambiguous snapshot slug/);
|
||||
const explicitOther = spawnSync(process.execPath, [SCRIPT, 'latest', './foo-bar'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(explicitOther.status, 2, `stderr: ${explicitOther.stderr}`);
|
||||
assert.notEqual(readLatestSnapshot('foo-bar', { cwd }), null);
|
||||
|
||||
// Once the local name collision is gone, the same bare value is an
|
||||
// intentional slug lookup and can return the original backlog.
|
||||
rmSync(ambiguousTarget);
|
||||
const bySlug = spawnSync(process.execPath, [SCRIPT, 'latest', 'foo-bar'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(bySlug.status, 0, `stderr: ${bySlug.stderr}`);
|
||||
assert.match(bySlug.stdout, /preserve this backlog/);
|
||||
|
||||
// The recorded original path still owns freshness invalidation.
|
||||
writeFileSync(originalPath, '<main>changed original</main>');
|
||||
const changedOriginal = spawnSync(process.execPath, [SCRIPT, 'latest', originalTarget], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(changedOriginal.status, 2, `stderr: ${changedOriginal.stderr}`);
|
||||
assert.equal(readLatestSnapshot('foo-bar', { cwd }), null);
|
||||
});
|
||||
|
||||
it('finds the exact target backlog when two live snapshots share a slug', () => {
|
||||
const originalDir = join(cwd, 'foo');
|
||||
const originalTarget = join('foo', 'bar');
|
||||
const otherTarget = join(cwd, 'foo-bar');
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
mkdirSync(originalDir);
|
||||
writeFileSync(join(cwd, originalTarget), '<main>original</main>');
|
||||
writeFileSync(otherTarget, '<main>other</main>');
|
||||
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: original backlog');
|
||||
const originalWrite = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'write', originalTarget, bodyFile],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(originalWrite.status, 0, `stderr: ${originalWrite.stderr}`);
|
||||
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: newer other backlog');
|
||||
const otherWrite = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'write', './foo-bar', bodyFile],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(otherWrite.status, 0, `stderr: ${otherWrite.stderr}`);
|
||||
|
||||
const originalLatest = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', originalTarget, '--json'],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(originalLatest.status, 0, `stderr: ${originalLatest.stderr}`);
|
||||
const originalResult = JSON.parse(originalLatest.stdout);
|
||||
assert.match(originalResult.body, /original backlog/);
|
||||
assert.doesNotMatch(originalResult.body, /newer other backlog/);
|
||||
|
||||
const otherLatest = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', './foo-bar', '--json'],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(otherLatest.status, 0, `stderr: ${otherLatest.stderr}`);
|
||||
const otherResult = JSON.parse(otherLatest.stdout);
|
||||
assert.match(otherResult.body, /newer other backlog/);
|
||||
assert.notEqual(otherResult.snapshot_file, originalResult.snapshot_file);
|
||||
|
||||
const closeOriginal = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
originalTarget,
|
||||
originalResult.snapshot_file,
|
||||
], { cwd, encoding: 'utf-8' });
|
||||
assert.equal(closeOriginal.status, 0, `stderr: ${closeOriginal.stderr}`);
|
||||
|
||||
const closedOriginal = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', originalTarget],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(closedOriginal.status, 2, `stderr: ${closedOriginal.stderr}`);
|
||||
const stillOpenOther = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', './foo-bar'],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(stillOpenOther.status, 0, `stderr: ${stillOpenOther.stderr}`);
|
||||
assert.match(stillOpenOther.stdout, /newer other backlog/);
|
||||
});
|
||||
|
||||
it('closes a local snapshot when its target is deleted or replaced by a directory', () => {
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
|
||||
|
||||
for (const replacement of ['missing', 'directory']) {
|
||||
const target = join(cwd, `${replacement}.html`);
|
||||
writeFileSync(target, '<main>assessed</main>');
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
|
||||
rmSync(target);
|
||||
if (replacement === 'directory') mkdirSync(target);
|
||||
|
||||
const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(latest.status, 2, `stderr: ${latest.stderr}`);
|
||||
const slug = `${replacement}-html`;
|
||||
assert.equal(readLatestSnapshot(slug, { cwd }), null);
|
||||
assert.equal(readTrend(slug, { cwd })[0].closed, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('treats a legacy local-file snapshot without a fingerprint as stale', () => {
|
||||
const target = join(cwd, 'index.html');
|
||||
writeFileSync(target, '<main>current</main>');
|
||||
writeSnapshot({ slug: 'index-html', meta: { total_score: 20 }, body: 'legacy', cwd });
|
||||
|
||||
const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(latest.status, 2, `stderr: ${latest.stderr}`);
|
||||
assert.equal(readTrend('index-html', { cwd })[0].closed, true);
|
||||
});
|
||||
|
||||
it('rejects an ambiguous legacy extensionless lookup until the path is explicit', () => {
|
||||
const target = join(cwd, 'main');
|
||||
writeFileSync(target, '<main>changed since legacy critique</main>');
|
||||
writeSnapshot({ slug: 'main', meta: { total_score: 20 }, body: 'legacy stale', cwd });
|
||||
|
||||
const ambiguous = spawnSync(process.execPath, [SCRIPT, 'latest', 'main'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(ambiguous.status, 2, `stderr: ${ambiguous.stderr}`);
|
||||
assert.match(ambiguous.stderr, /ambiguous legacy snapshot target/);
|
||||
assert.notEqual(readLatestSnapshot('main', { cwd }), null);
|
||||
|
||||
const explicit = spawnSync(process.execPath, [SCRIPT, 'latest', './main'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(explicit.status, 2, `stderr: ${explicit.stderr}`);
|
||||
assert.equal(readLatestSnapshot('main', { cwd }), null);
|
||||
assert.equal(readTrend('main', { cwd })[0].closed, true);
|
||||
});
|
||||
|
||||
it('keeps URL snapshots current without a local fingerprint', () => {
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
|
||||
const target = 'https://example.com/page';
|
||||
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
assert.equal(
|
||||
readLatestSnapshot('example-com-page', { cwd }).meta.target_identity,
|
||||
'url:https://example.com/page',
|
||||
);
|
||||
|
||||
const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
|
||||
assert.match(latest.stdout, /improve hierarchy/);
|
||||
|
||||
const bySlug = spawnSync(process.execPath, [SCRIPT, 'latest', 'example-com-page'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(bySlug.status, 0, `stderr: ${bySlug.stderr}`);
|
||||
assert.match(bySlug.stdout, /improve hierarchy/);
|
||||
});
|
||||
|
||||
it('keeps URL schemes and non-default ports in separate identity streams', () => {
|
||||
const bodyFile = join(cwd, 'critique.md');
|
||||
const targets = [
|
||||
['http://example.test/review', 'http backlog'],
|
||||
['https://example.test/review', 'https backlog'],
|
||||
['https://example.test:8443/review', 'port backlog'],
|
||||
];
|
||||
|
||||
for (const [target, backlog] of targets) {
|
||||
writeFileSync(bodyFile, `# Critique\n\nP1: ${backlog}`);
|
||||
const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
}
|
||||
|
||||
const results = targets.map(([target, backlog]) => {
|
||||
const latest = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', target, '--json'],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
|
||||
const result = JSON.parse(latest.stdout);
|
||||
assert.match(result.body, new RegExp(backlog));
|
||||
return result;
|
||||
});
|
||||
assert.equal(new Set(results.map((result) => result.snapshot_file)).size, 3);
|
||||
|
||||
const wrongClose = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
targets[1][0],
|
||||
results[0].snapshot_file,
|
||||
], { cwd, encoding: 'utf-8' });
|
||||
assert.equal(wrongClose.status, 2, `stderr: ${wrongClose.stderr}`);
|
||||
const httpStillOpen = spawnSync(
|
||||
process.execPath,
|
||||
[SCRIPT, 'latest', targets[0][0]],
|
||||
{ cwd, encoding: 'utf-8' },
|
||||
);
|
||||
assert.equal(httpStillOpen.status, 0, `stderr: ${httpStillOpen.stderr}`);
|
||||
assert.match(httpStillOpen.stdout, /http backlog/);
|
||||
|
||||
const closeHttp = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
targets[0][0],
|
||||
results[0].snapshot_file,
|
||||
], { cwd, encoding: 'utf-8' });
|
||||
assert.equal(closeHttp.status, 0, `stderr: ${closeHttp.stderr}`);
|
||||
|
||||
for (const [target, backlog] of targets.slice(1)) {
|
||||
const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
|
||||
assert.match(latest.stdout, new RegExp(backlog));
|
||||
}
|
||||
});
|
||||
|
||||
it('latest --json returns the exact snapshot identity and body', () => {
|
||||
const target = 'https://example.com/exact';
|
||||
const snapshot = writeSnapshot({
|
||||
slug: 'example-com-exact',
|
||||
meta: { total_score: 20 },
|
||||
body: 'exact backlog',
|
||||
cwd,
|
||||
});
|
||||
const r = spawnSync(process.execPath, [SCRIPT, 'latest', target, '--json'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
||||
const result = JSON.parse(r.stdout);
|
||||
assert.equal(result.snapshot_file, basename(snapshot));
|
||||
assert.match(result.body, /exact backlog/);
|
||||
});
|
||||
|
||||
it('close subcommand closes the identified snapshot and preserves its trend', () => {
|
||||
const snapshot = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 20 },
|
||||
body: 'open',
|
||||
cwd,
|
||||
});
|
||||
const r = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
'index-astro',
|
||||
basename(snapshot),
|
||||
], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
||||
assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
|
||||
assert.equal(readTrend('index-astro', { cwd }).length, 1);
|
||||
assert.equal(readTrend('index-astro', { cwd })[0].closed, true);
|
||||
});
|
||||
|
||||
it('close subcommand leaves a newer critique backlog active', () => {
|
||||
const target = 'https://example.com/index';
|
||||
const first = writeSnapshot({
|
||||
slug: 'example-com-index',
|
||||
meta: { total_score: 20 },
|
||||
body: 'first backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:00Z'),
|
||||
});
|
||||
const read = spawnSync(process.execPath, [SCRIPT, 'latest', target, '--json'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(read.status, 0, `stderr: ${read.stderr}`);
|
||||
assert.equal(JSON.parse(read.stdout).snapshot_file, basename(first));
|
||||
|
||||
const newer = writeSnapshot({
|
||||
slug: 'example-com-index',
|
||||
meta: { total_score: 30 },
|
||||
body: 'newer unprocessed backlog',
|
||||
cwd,
|
||||
now: new Date('2026-05-12T00:00:01Z'),
|
||||
});
|
||||
const close = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
target,
|
||||
basename(first),
|
||||
], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(close.status, 0, `stderr: ${close.stderr}`);
|
||||
assert.equal(readLatestSnapshot('example-com-index', { cwd }).path, newer);
|
||||
assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, newer);
|
||||
const trend = readTrend('example-com-index', { cwd });
|
||||
assert.equal(trend[0].closed, true);
|
||||
assert.equal(trend[1].closed, undefined);
|
||||
});
|
||||
|
||||
it('close subcommand exits 2 when the identified snapshot is already closed', () => {
|
||||
const snapshot = writeSnapshot({
|
||||
slug: 'index-astro',
|
||||
meta: { total_score: 20 },
|
||||
body: 'open',
|
||||
cwd,
|
||||
});
|
||||
closeSnapshot(snapshot, { cwd });
|
||||
const r = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
'index-astro',
|
||||
basename(snapshot),
|
||||
], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 2);
|
||||
});
|
||||
|
||||
it('close subcommand exits 2 when no snapshot exists', () => {
|
||||
const r = spawnSync(process.execPath, [
|
||||
SCRIPT,
|
||||
'close',
|
||||
'never-written',
|
||||
'2026-05-12T00-00-00Z__never-written.md',
|
||||
], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 2);
|
||||
});
|
||||
|
||||
it('close subcommand requires the identity returned by latest --json', () => {
|
||||
const r = spawnSync(process.execPath, [SCRIPT, 'close', 'index-astro'], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /snapshot-file/);
|
||||
});
|
||||
|
||||
it('close subcommand rejects a snapshot identity from another slug', () => {
|
||||
const home = writeSnapshot({ slug: 'home', meta: { total_score: 20 }, body: 'home', cwd });
|
||||
const r = spawnSync(process.execPath, [SCRIPT, 'close', 'pricing', basename(home)], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(r.status, 2);
|
||||
assert.notEqual(readLatestSnapshot('home', { cwd }), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTrend', () => {
|
||||
|
||||
@@ -52,4 +52,19 @@ describe('skill reference authoring contracts', () => {
|
||||
assert.match(accessibility, /not disabling all motion/);
|
||||
assert.match(verify, /reduced[- ]motion/i);
|
||||
});
|
||||
|
||||
it('uses an exact content fingerprint before inheriting a critique snapshot', () => {
|
||||
const critique = readFileSync(join(ROOT, 'skill/reference/critique.md'), 'utf-8').replace(/\r\n?/g, '\n');
|
||||
const polish = readFileSync(join(ROOT, 'skill/reference/polish.md'), 'utf-8').replace(/\r\n?/g, '\n');
|
||||
|
||||
assert.match(critique, /records an exact content fingerprint/);
|
||||
assert.match(polish, /compares the file's exact current content fingerprint/);
|
||||
assert.match(polish, /Unchanged staged, unstaged, or untracked content remains current/);
|
||||
assert.match(polish, /any byte change, deletion, or replacement with a non-file closes the backlog/);
|
||||
assert.match(polish, /latest "<resolved target>" --json/);
|
||||
assert.match(polish, /exact `snapshot_file` identity/);
|
||||
assert.match(polish, /close "<resolved target>" "<snapshot_file returned by latest>"/);
|
||||
assert.match(polish, /if a newer critique landed meanwhile, its backlog stays live/);
|
||||
assert.doesNotMatch(polish, /git status|git log/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user