Files
pbakaus_impeccable/skill/scripts/lib/impeccable-paths.mjs
T
Paul BakausandClaude 4e381305e1 Fix source-safety, detector, and lock defects in Live polling work
Addresses the review findings on #371, plus several the bots did not catch.
All fixes have regression coverage that fails on the prior code.

Source corruption:
- Vue accept dropped valueless root attrs (disabled, v-cloak) and, worse,
  rewrote @click="x" as a literal click="x" DOM attribute, because the attr
  parser was name-anchored and skipped the sigil. Tokenize the whole Vue attr
  grammar and normalize shorthands so accept round-trips directives.
- --variant was interpolated unescaped into a RegExp, so --variant '.*' matched
  the original block first and reported a successful accept while silently
  restoring the original. Validate against the digits pattern the browser and
  the /events schema already enforce.
- --id reached path.join unvalidated, so --id ../../../../etc/evil wrote and
  read receipts outside the project. Hoist the existing safeSessionId check
  into impeccable-paths and apply it at every id-to-path sink.

Accept/lock correctness:
- Plain HTML/JSX accept and discard did not catch SOURCE_LOCKED, so contention
  exited non-zero with empty stdout and the agent got no JSON to retry on.
- Lock staleness was mtime-only and never read the pid it records: a holder
  whose critical section outran 60s had its live lock swept, admitting a second
  writer to the same file, while a crashed holder blocked accepts for a full
  60s. Decide staleness by owner liveness, and release only our own lock.

Detector:
- isNeutralColor only parses computed color forms, so routing authored CSS
  through it reported inset 4px 0 0 #000 / black / #e5e7eb as chromatic
  side-tab stripes. Add an authored-color neutrality test covering hex and
  named neutrals; the fixture had no literal-color cases at all.
- Rule line numbers were off by one for every rule after the first, and
  commented-out CSS was scanned as live rules.

Server:
- An error reply carries no sourceEventType, and inferSourceEventType returned
  undefined, which acknowledgePendingEvent treats as a wildcard: a stale
  generate worker's failure consumed the user's queued Accept, which then
  reached no agent and left the browser in SAVING forever.
- The generate preflight spawned live-wrap.mjs synchronously inside the request
  handler, freezing the single-threaded server for the whole scaffold (~7.6s
  measured on this repo, 15s ceiling) and stalling Accept/Discard/SSE. Make it
  async, claiming the lease before the first await so no event double-delivers.
- Every browser checkpoint was echoed back as variant_progress, so a Tune
  slider drag remounted the preview under the user's cursor and latched the
  *_reviewable phases from the wrong trigger. Gate on the reason.

Cleanup:
- Collapse four divergent benchmark argv parsers into scripts/lib/cli-args.mjs.
  Three silently misread flags: --iterations 20 benchmarked 5, --agent llm ran
  the fake agent, --median-target=0.4 used the default threshold.
- Drop a snapshot cache this branch made write-only (it grew per session for
  the server's lifetime and was never read), a dead exported reconcile helper,
  and the unused deferReply branch.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 13:48:44 -07:00

144 lines
5.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { resolveProjectRoot } from '../context.mjs';
export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs';
export const IMPECCABLE_DIR = '.impeccable';
export const LIVE_DIR = 'live';
export const CRITIQUE_DIR = 'critique';
export function getImpeccableDir(cwd = process.cwd(), options = {}) {
return path.join(resolveProjectRoot(cwd, options), IMPECCABLE_DIR);
}
export function getDesignSidecarPath(cwd = process.cwd(), options = {}) {
return path.join(getImpeccableDir(cwd, options), 'design.json');
}
export function getDesignSidecarCandidates(cwd = process.cwd(), contextDir = cwd, options = {}) {
const projectRoot = resolveProjectRoot(cwd, options);
const candidates = [
getDesignSidecarPath(cwd, options),
path.join(projectRoot, 'DESIGN.json'),
];
const contextLegacy = path.join(contextDir, 'DESIGN.json');
if (!candidates.includes(contextLegacy)) candidates.push(contextLegacy);
return candidates;
}
export function resolveDesignSidecarPath(cwd = process.cwd(), contextDir = cwd, options = {}) {
return firstExisting(getDesignSidecarCandidates(cwd, contextDir, options));
}
export function getLiveDir(cwd = process.cwd(), options = {}) {
return path.join(getImpeccableDir(cwd, options), LIVE_DIR);
}
export function getLiveConfigPath(cwd = process.cwd(), options = {}) {
return path.join(getLiveDir(cwd, options), 'config.json');
}
export function getLegacyLiveConfigPath(scriptsDir) {
return path.join(scriptsDir, 'config.json');
}
export function resolveLiveConfigPath({ cwd = process.cwd(), scriptsDir, env = process.env, targetPath } = {}) {
if (env.IMPECCABLE_LIVE_CONFIG && env.IMPECCABLE_LIVE_CONFIG.trim()) {
const configured = env.IMPECCABLE_LIVE_CONFIG.trim();
return path.isAbsolute(configured) ? configured : path.resolve(cwd, configured);
}
const primary = getLiveConfigPath(cwd, { targetPath });
if (fs.existsSync(primary)) return primary;
if (scriptsDir) {
const legacy = getLegacyLiveConfigPath(scriptsDir);
if (fs.existsSync(legacy)) return legacy;
}
return primary;
}
export function getLiveServerPath(cwd = process.cwd(), options = {}) {
return path.join(getLiveDir(cwd, options), 'server.json');
}
export function getLegacyLiveServerPath(cwd = process.cwd(), options = {}) {
return path.join(resolveProjectRoot(cwd, options), '.impeccable-live.json');
}
export function readLiveServerInfo(cwd = process.cwd(), options = {}) {
for (const filePath of [getLiveServerPath(cwd, options), getLegacyLiveServerPath(cwd, options)]) {
try {
const info = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (info && typeof info.pid === 'number' && !isLiveServerPidReachable(info.pid)) {
try { fs.unlinkSync(filePath); } catch {}
continue;
}
return { info, path: filePath };
} catch {
/* try next */
}
}
return null;
}
export function isLiveServerPidReachable(pid) {
try {
process.kill(pid, 0);
return true;
} catch (err) {
// ESRCH means "no such process". EPERM means the process exists but this
// user cannot signal it, so the live server info is still valid.
return err?.code !== 'ESRCH';
}
}
export function writeLiveServerInfo(cwd = process.cwd(), info, options = {}) {
const filePath = getLiveServerPath(cwd, options);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(info));
return filePath;
}
export function removeLiveServerInfo(cwd = process.cwd(), options = {}) {
for (const filePath of [getLiveServerPath(cwd, options), getLegacyLiveServerPath(cwd, options)]) {
try { fs.unlinkSync(filePath); } catch {}
}
}
/**
* Session IDs become path segments (journals, snapshots, accept receipts,
* preview manifests, generated component dirs). They arrive from CLI `--id`
* arguments and HTTP payloads, so anything containing a separator or `..` must
* be rejected before it reaches path.join, which would happily escape
* `.impeccable/live/`. Real IDs are 8 hex chars; the tests use short slugs.
*/
export function safeSessionId(id) {
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id)) {
throw new Error('invalid session id: ' + id);
}
return id;
}
export function getLiveSessionsDir(cwd = process.cwd(), options = {}) {
return path.join(getLiveDir(cwd, options), 'sessions');
}
export function getLegacyLiveSessionsDir(cwd = process.cwd(), options = {}) {
return path.join(resolveProjectRoot(cwd, options), '.impeccable-live', 'sessions');
}
export function getLiveAnnotationsDir(cwd = process.cwd(), options = {}) {
return path.join(getLiveDir(cwd, options), 'annotations');
}
export function getCritiqueDir(cwd = process.cwd(), options = {}) {
return path.join(getImpeccableDir(cwd, options), CRITIQUE_DIR);
}
export function getLegacyLiveAnnotationsDir(cwd = process.cwd(), options = {}) {
return path.join(resolveProjectRoot(cwd, options), '.impeccable-live', 'annotations');
}
function firstExisting(paths) {
return paths.find((filePath) => fs.existsSync(filePath)) || null;
}