* refactor: tighten sdk-first architecture seams Refs #3312 * refactor: finish state document seam cleanup Refs #3312 * test: harden minimal install cleanup assertion * ci: support sdk-scoped package lock * fix(3316): restore root package-lock.json and align changeset pr ref Reverts dec57a83 ("ci: support sdk-scoped package lock") and restores the root package-lock.json that c249d34d deleted. The deletion was the wrong direction: - The root package.json declares its own runtime and dev deps (@anthropic-ai/claude-agent-sdk, ws, c8). Without a root lockfile, `npm install --no-package-lock` resolves whatever satisfies semver at install time — CI today and CI in six months can install different transitive trees, defeating reproducibility. - The lockfile has been part of every release on this repo (long history on main); removing it loses the npm audit / Dependabot target without compensating benefit. - The CI workaround pattern (cache-dependency-path: sdk/package-lock.json + `npm install --no-package-lock`) papered over the symptom rather than fix the cause. Also fix the changeset pr: from 3312 (issue) to 3316 (PR). CONTEXT.md flags this exact failure mode as a recurring CodeRabbit finding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address coderabbit review findings * fix: close remaining coderabbit threads --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
4.5 KiB
JavaScript
123 lines
4.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* STATE.md Document Module
|
|
*
|
|
* Pure transforms for STATE.md text. This module does not read the filesystem
|
|
* and does not own persistence or locking.
|
|
*/
|
|
|
|
function escapeRegex(str) {
|
|
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function stateExtractField(content, fieldName) {
|
|
const escaped = escapeRegex(fieldName);
|
|
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
|
|
const boldMatch = content.match(boldPattern);
|
|
if (boldMatch) return boldMatch[1].trim();
|
|
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
|
|
const plainMatch = content.match(plainPattern);
|
|
return plainMatch ? plainMatch[1].trim() : null;
|
|
}
|
|
|
|
function stateReplaceField(content, fieldName, newValue) {
|
|
const escaped = escapeRegex(fieldName);
|
|
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
if (boldPattern.test(content)) {
|
|
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
|
|
}
|
|
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
|
if (plainPattern.test(content)) {
|
|
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
let result = stateReplaceField(content, primary, value);
|
|
if (result) return result;
|
|
if (fallback) {
|
|
result = stateReplaceField(content, fallback, value);
|
|
if (result) return result;
|
|
}
|
|
return content;
|
|
}
|
|
|
|
function normalizeStateStatus(status, pausedAt) {
|
|
let normalizedStatus = status || 'unknown';
|
|
const statusLower = (status || '').toLowerCase();
|
|
if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
|
|
normalizedStatus = 'paused';
|
|
} else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
|
|
normalizedStatus = 'executing';
|
|
} else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
|
|
normalizedStatus = 'planning';
|
|
} else if (statusLower.includes('discussing')) {
|
|
normalizedStatus = 'discussing';
|
|
} else if (statusLower.includes('verif')) {
|
|
normalizedStatus = 'verifying';
|
|
} else if (statusLower.includes('complete') || statusLower.includes('done')) {
|
|
normalizedStatus = 'completed';
|
|
} else if (statusLower.includes('ready to execute')) {
|
|
normalizedStatus = 'executing';
|
|
}
|
|
return normalizedStatus;
|
|
}
|
|
|
|
function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
|
|
const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
|
|
const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
|
|
|
|
if (!hasPlanData && !hasPhaseData) return null;
|
|
|
|
const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
|
|
const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
|
|
|
|
return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
|
|
}
|
|
|
|
function toFiniteNumber(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
|
|
const existing = toFiniteNumber(existingProgress[key]);
|
|
const derived = toFiniteNumber(derivedProgress[key]);
|
|
return existing !== null && derived !== null && existing > derived;
|
|
}
|
|
|
|
function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
|
if (!existingProgress || typeof existingProgress !== 'object') return false;
|
|
if (!derivedProgress || typeof derivedProgress !== 'object') return false;
|
|
|
|
return (
|
|
existingProgressExceedsDerived(existingProgress, derivedProgress, 'total_phases') ||
|
|
existingProgressExceedsDerived(existingProgress, derivedProgress, 'completed_phases') ||
|
|
existingProgressExceedsDerived(existingProgress, derivedProgress, 'total_plans') ||
|
|
existingProgressExceedsDerived(existingProgress, derivedProgress, 'completed_plans')
|
|
);
|
|
}
|
|
|
|
function normalizeProgressNumbers(progress) {
|
|
if (!progress || typeof progress !== 'object') return progress;
|
|
|
|
const normalized = { ...progress };
|
|
for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
|
|
const number = toFiniteNumber(normalized[key]);
|
|
if (number !== null) normalized[key] = number;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
module.exports = {
|
|
computeProgressPercent,
|
|
normalizeProgressNumbers,
|
|
normalizeStateStatus,
|
|
shouldPreserveExistingProgress,
|
|
stateExtractField,
|
|
stateReplaceField,
|
|
stateReplaceFieldWithFallback,
|
|
};
|