* feat(3530): STATE.md Document Module via generator (Phase 1 of #3524) Phase 1 of the CJS↔SDK hard-seam migration (parent #3524). Converts the hand-synced state-document.cjs/state-document.ts pair into a generator-driven seam, modeled on the existing command-aliases.generated.* precedent. What landed: - sdk/src/query/state-document.ts is the source of truth. - sdk/scripts/gen-state-document.ts emits get-shit-done/bin/lib/state-document.generated.cjs from the compiled SDK dist via Function.prototype.toString() inspection for the 7 public exports and 3 internal helpers. - sdk/scripts/check-state-document-fresh.mjs is the CI freshness gate; pre-commit hook also runs it when relevant files change. - get-shit-done/bin/lib/state-document.cjs is reduced to a one-line re-export from state-document.generated.cjs so existing callers (state.cjs, workstream-inventory.cjs, init.cjs) need no changes. - New CI step in .github/workflows/test.yml after the existing alias drift check. - sdk/package.json: gen:state-document, check:state-document-fresh scripts. tsx added as devDep. - Root package.json: proxy script for the freshness check. - CONTEXT.md: one-sentence amendment on STATE.md Document Module recording the source-of-truth file path. Tests: - sdk/src/query/state-document.test.ts: 34 vitest fixtures across the 7 public exports (TDD pinning safety net). - tests/state-document-generator.test.cjs: 31 node:test parity assertions comparing SDK source vs generated CJS for every fixture. - Full suite: 9177/9177 pass (baseline was 9146; +31 new tests). One subtle behavior change worth flagging: the old hand-written state-document.cjs used String(str) coercion inside escapeRegex, which the SDK source does not. The generator faithfully matches the SDK (the source of truth per ADR-3524), so the new CJS no longer coerces non-string input to string before regex-escaping. No current caller passes non-string input, so no observable regression in the test suite. Flagged in the PR body for reviewers. Closes #3530. * fix(3530): address state-document review findings
128 lines
4.9 KiB
JavaScript
128 lines
4.9 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* GENERATED FILE — DO NOT EDIT.
|
|
*
|
|
* Source: sdk/src/query/state-document.ts
|
|
* Regenerate: cd sdk && npm run gen:state-document
|
|
*
|
|
* STATE.md Document Module — pure transforms for STATE.md text.
|
|
* This module does not read the filesystem and does not own persistence or locking.
|
|
*/
|
|
|
|
// Internal helpers
|
|
function escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
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 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 shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
|
if (!existingProgress || typeof existingProgress !== 'object')
|
|
return false;
|
|
if (!derivedProgress || typeof derivedProgress !== 'object')
|
|
return false;
|
|
const existing = existingProgress;
|
|
const derived = derivedProgress;
|
|
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
|
|
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
|
|
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
|
|
existingProgressExceedsDerived(existing, derived, '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 = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };
|