* 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
This commit is contained in:
@@ -4,3 +4,7 @@ set -euo pipefail
|
||||
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.generated\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
|
||||
npm run check:alias-drift
|
||||
fi
|
||||
|
||||
if git diff --cached --name-only | grep -Eq "^sdk/src/query/state-document\.|^get-shit-done/bin/lib/state-document\.generated\.cjs$|^sdk/scripts/gen-state-document\.ts$|^sdk/scripts/check-state-document-fresh\.mjs$"; then
|
||||
npm run check:state-document-fresh
|
||||
fi
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -103,6 +103,11 @@ jobs:
|
||||
shell: bash
|
||||
run: node sdk/scripts/check-command-aliases-fresh.mjs
|
||||
|
||||
- name: SDK generated state-document artifact drift check
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
|
||||
shell: bash
|
||||
run: node sdk/scripts/check-state-document-fresh.mjs
|
||||
|
||||
- name: Run tests with coverage
|
||||
shell: bash
|
||||
run: npm run test:coverage
|
||||
|
||||
@@ -26,7 +26,7 @@ Adapter Module that satisfies native query dispatch at the Dispatch Policy seam,
|
||||
Module owning projection from dispatch results/errors to CLI `{ exitCode, stdoutChunks, stderrLines }` output contract.
|
||||
|
||||
### STATE.md Document Module
|
||||
Shared CJS/SDK pure transform Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan `.planning/phases` and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and CJS/SDK read-modify-write paths remain Adapters.
|
||||
Shared CJS/SDK pure transform Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan `.planning/phases` and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and CJS/SDK read-modify-write paths remain Adapters. Source of truth: `sdk/src/query/state-document.ts`; CJS callers consume the generator-emitted `get-shit-done/bin/lib/state-document.generated.cjs` via the thin re-export at `get-shit-done/bin/lib/state-document.cjs`.
|
||||
|
||||
### Query Execution Policy Module
|
||||
Module owning query transport routing policy projection (`preferNative`, fallback policy, workstream subprocess forcing) at execution seam.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-05-13",
|
||||
"generated": "2026-05-14",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
@@ -306,6 +306,7 @@
|
||||
"shell-command-projection.cjs",
|
||||
"state-command-router.cjs",
|
||||
"state-document.cjs",
|
||||
"state-document.generated.cjs",
|
||||
"state.cjs",
|
||||
"surface.cjs",
|
||||
"template.cjs",
|
||||
|
||||
@@ -360,7 +360,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
|
||||
---
|
||||
|
||||
## CLI Modules (59 shipped)
|
||||
## CLI Modules (60 shipped)
|
||||
|
||||
Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
|
||||
@@ -415,6 +415,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
|
||||
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
|
||||
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
|
||||
| `state-document.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/state-document.ts` via `sdk/scripts/gen-state-document.ts`; do not edit directly |
|
||||
| `surface.cjs` | Runtime surface module — manages the runtime enable/disable surface state independently of the install-time profile marker (ADR-0011 Phase 2) |
|
||||
| `template.cjs` | Template selection and filling with variable substitution |
|
||||
| `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support |
|
||||
|
||||
@@ -1,122 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* STATE.md Document Module
|
||||
* STATE.md Document Module — CJS adapter.
|
||||
*
|
||||
* Pure transforms for STATE.md text. This module does not read the filesystem
|
||||
* and does not own persistence or locking.
|
||||
* The implementation is generated from sdk/src/query/state-document.ts and
|
||||
* lives in state-document.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
|
||||
* and tests) can continue to require('./state-document') unchanged.
|
||||
*/
|
||||
|
||||
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,
|
||||
};
|
||||
module.exports = require('./state-document.generated.cjs');
|
||||
|
||||
127
get-shit-done/bin/lib/state-document.generated.cjs
Normal file
127
get-shit-done/bin/lib/state-document.generated.cjs
Normal file
@@ -0,0 +1,127 @@
|
||||
'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 };
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -86,9 +86,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -102,9 +99,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -118,9 +112,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -134,9 +125,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"build:sdk": "cd sdk && npm ci && npm run build",
|
||||
"check:alias-drift": "cd sdk && npm run check:alias-drift",
|
||||
"check:state-document-fresh": "cd sdk && npm run check:state-document-fresh",
|
||||
"prepublishOnly": "npm run build:hooks && npm run build:sdk",
|
||||
"pretest": "npm run build:sdk && npm run lint:skill-deps",
|
||||
"pretest:coverage": "npm run build:sdk",
|
||||
|
||||
508
sdk/package-lock.json
generated
508
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@gsd-build/sdk",
|
||||
"version": "1.39.0-rc.4",
|
||||
"version": "1.50.0-canary.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@gsd-build/sdk",
|
||||
"version": "1.39.0-rc.4",
|
||||
"version": "1.50.0-canary.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
|
||||
@@ -18,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.22.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
@@ -1754,6 +1755,509 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
|
||||
"integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"check:alias-drift": "npm run build && node scripts/check-command-aliases-fresh.mjs",
|
||||
"gen:state-document": "npm run build && npx tsx scripts/gen-state-document.ts",
|
||||
"check:state-document-fresh": "npm run build && node scripts/check-state-document-fresh.mjs",
|
||||
"prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --project unit",
|
||||
@@ -48,6 +50,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.22.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
|
||||
114
sdk/scripts/check-state-document-fresh.mjs
Normal file
114
sdk/scripts/check-state-document-fresh.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Freshness check for state-document.generated.cjs.
|
||||
*
|
||||
* Regenerates the expected CJS content in-memory (without writing to disk) and
|
||||
* compares it to the committed file. Exits 0 if they match, 1 if stale.
|
||||
*
|
||||
* Run: node sdk/scripts/check-state-document-fresh.mjs
|
||||
* (Requires sdk/dist to be built first — `npm run build` in sdk/.)
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const BANNER = `'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.
|
||||
*/`;
|
||||
|
||||
/**
|
||||
* Extract a top-level function declaration (non-exported) from a JS source
|
||||
* string by scanning for `function <name>(` and capturing the entire body
|
||||
* including balanced braces.
|
||||
*/
|
||||
function extractFunctionFromSource(source, name) {
|
||||
const marker = `function ${name}(`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start === -1) throw new Error(`Could not find function ${name} in compiled source`);
|
||||
const braceOpen = source.indexOf('{', start);
|
||||
if (braceOpen === -1) throw new Error(`Could not find opening brace for function ${name}`);
|
||||
let depth = 0;
|
||||
let i = braceOpen;
|
||||
for (; i < source.length; i++) {
|
||||
if (source[i] === '{') depth++;
|
||||
else if (source[i] === '}') { depth--; if (depth === 0) break; }
|
||||
}
|
||||
if (depth !== 0) {
|
||||
throw new Error(`Could not find closing brace for function ${name}`);
|
||||
}
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
|
||||
const distUrl = new URL('../dist/query/state-document.js', import.meta.url);
|
||||
const {
|
||||
stateExtractField,
|
||||
stateReplaceField,
|
||||
stateReplaceFieldWithFallback,
|
||||
normalizeStateStatus,
|
||||
computeProgressPercent,
|
||||
shouldPreserveExistingProgress,
|
||||
normalizeProgressNumbers,
|
||||
} = await import(distUrl.href);
|
||||
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
|
||||
|
||||
const escapeRegexBody = extractFunctionFromSource(compiledSource, 'escapeRegex');
|
||||
const toFiniteNumberBody = extractFunctionFromSource(compiledSource, 'toFiniteNumber');
|
||||
const existingProgressExceedsDerivedBody = extractFunctionFromSource(compiledSource, 'existingProgressExceedsDerived');
|
||||
const stateExtractFieldBody = stateExtractField.toString();
|
||||
const stateReplaceFieldBody = stateReplaceField.toString();
|
||||
const stateReplaceFieldWithFallbackBody = stateReplaceFieldWithFallback.toString();
|
||||
const normalizeStateStatusBody = normalizeStateStatus.toString();
|
||||
const computeProgressPercentBody = computeProgressPercent.toString();
|
||||
const shouldPreserveExistingProgressBody = shouldPreserveExistingProgress.toString();
|
||||
const normalizeProgressNumbersBody = normalizeProgressNumbers.toString();
|
||||
|
||||
const expected = [
|
||||
BANNER,
|
||||
'',
|
||||
'// Internal helpers',
|
||||
escapeRegexBody,
|
||||
'',
|
||||
toFiniteNumberBody,
|
||||
'',
|
||||
existingProgressExceedsDerivedBody,
|
||||
'',
|
||||
stateExtractFieldBody,
|
||||
'',
|
||||
stateReplaceFieldBody,
|
||||
'',
|
||||
stateReplaceFieldWithFallbackBody,
|
||||
'',
|
||||
normalizeStateStatusBody,
|
||||
'',
|
||||
computeProgressPercentBody,
|
||||
'',
|
||||
shouldPreserveExistingProgressBody,
|
||||
'',
|
||||
normalizeProgressNumbersBody,
|
||||
'',
|
||||
'module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const committedPath = resolve(here, '..', '..', 'get-shit-done', 'bin', 'lib', 'state-document.generated.cjs');
|
||||
const committed = await readFile(committedPath, 'utf-8');
|
||||
|
||||
if (expected === committed) {
|
||||
console.log('state-document.generated.cjs is fresh');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('state-document.generated.cjs is STALE.');
|
||||
console.error('Regenerate: cd sdk && npm run gen:state-document');
|
||||
process.exit(1);
|
||||
}
|
||||
142
sdk/scripts/gen-state-document.ts
Normal file
142
sdk/scripts/gen-state-document.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generator for the STATE.md Document Module CJS artifact.
|
||||
*
|
||||
* Reads the compiled ESM output from sdk/dist/query/state-document.js,
|
||||
* extracts function source via Function.prototype.toString() for exports
|
||||
* and via source-text extraction for internal helpers, then emits
|
||||
* get-shit-done/bin/lib/state-document.generated.cjs.
|
||||
*
|
||||
* Run: cd sdk && npx tsx scripts/gen-state-document.ts
|
||||
* Freshness check: node sdk/scripts/check-state-document-fresh.mjs
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const BANNER = `'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.
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
/**
|
||||
* Extract a top-level function declaration (non-exported) from a JS source
|
||||
* string by scanning for `function <name>(` and capturing the entire body
|
||||
* including balanced braces.
|
||||
*/
|
||||
function extractFunctionFromSource(source: string, name: string): string {
|
||||
const marker = `function ${name}(`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start === -1) {
|
||||
throw new Error(`Could not find function ${name} in compiled source`);
|
||||
}
|
||||
// Find the opening brace
|
||||
const braceOpen = source.indexOf('{', start);
|
||||
if (braceOpen === -1) {
|
||||
throw new Error(`Could not find opening brace for function ${name}`);
|
||||
}
|
||||
// Walk forward counting braces until balanced
|
||||
let depth = 0;
|
||||
let i = braceOpen;
|
||||
for (; i < source.length; i++) {
|
||||
if (source[i] === '{') depth++;
|
||||
else if (source[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
if (depth !== 0) {
|
||||
throw new Error(`Could not find closing brace for function ${name}`);
|
||||
}
|
||||
// Return from `function name(` through the closing `}`
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
|
||||
export async function buildStateDocumentCjs(): Promise<string> {
|
||||
// Load the compiled ESM module to get exports via Function.prototype.toString()
|
||||
const distUrl = new URL('../dist/query/state-document.js', import.meta.url);
|
||||
const {
|
||||
stateExtractField,
|
||||
stateReplaceField,
|
||||
stateReplaceFieldWithFallback,
|
||||
normalizeStateStatus,
|
||||
computeProgressPercent,
|
||||
shouldPreserveExistingProgress,
|
||||
normalizeProgressNumbers,
|
||||
} = await import(distUrl.href);
|
||||
|
||||
// Also read the compiled JS as text to extract non-exported helpers
|
||||
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
|
||||
|
||||
// Extract non-exported helpers from source text
|
||||
const escapeRegexBody = extractFunctionFromSource(compiledSource, 'escapeRegex');
|
||||
const toFiniteNumberBody = extractFunctionFromSource(compiledSource, 'toFiniteNumber');
|
||||
const existingProgressExceedsDerivedBody = extractFunctionFromSource(compiledSource, 'existingProgressExceedsDerived');
|
||||
|
||||
// Get exported function bodies via Function.prototype.toString()
|
||||
const stateExtractFieldBody = stateExtractField.toString();
|
||||
const stateReplaceFieldBody = stateReplaceField.toString();
|
||||
const stateReplaceFieldWithFallbackBody = stateReplaceFieldWithFallback.toString();
|
||||
const normalizeStateStatusBody = normalizeStateStatus.toString();
|
||||
const computeProgressPercentBody = computeProgressPercent.toString();
|
||||
const shouldPreserveExistingProgressBody = shouldPreserveExistingProgress.toString();
|
||||
const normalizeProgressNumbersBody = normalizeProgressNumbers.toString();
|
||||
|
||||
const parts: string[] = [
|
||||
BANNER.trimEnd(),
|
||||
'',
|
||||
'// Internal helpers',
|
||||
escapeRegexBody,
|
||||
'',
|
||||
toFiniteNumberBody,
|
||||
'',
|
||||
existingProgressExceedsDerivedBody,
|
||||
'',
|
||||
stateExtractFieldBody,
|
||||
'',
|
||||
stateReplaceFieldBody,
|
||||
'',
|
||||
stateReplaceFieldWithFallbackBody,
|
||||
'',
|
||||
normalizeStateStatusBody,
|
||||
'',
|
||||
computeProgressPercentBody,
|
||||
'',
|
||||
shouldPreserveExistingProgressBody,
|
||||
'',
|
||||
normalizeProgressNumbersBody,
|
||||
'',
|
||||
'module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };',
|
||||
'',
|
||||
];
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const content = await buildStateDocumentCjs();
|
||||
const outPath = fileURLToPath(
|
||||
new URL('../../get-shit-done/bin/lib/state-document.generated.cjs', import.meta.url),
|
||||
);
|
||||
await writeFile(outPath, content, 'utf-8');
|
||||
console.log(`Written: ${outPath}`);
|
||||
}
|
||||
|
||||
// Only run main() when this file is the entry point, not when imported.
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const entryPath = process.argv[1] ? new URL(process.argv[1], 'file://').pathname : '';
|
||||
if (scriptPath === entryPath || process.argv[1] === scriptPath) {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
197
sdk/src/query/state-document.test.ts
Normal file
197
sdk/src/query/state-document.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Unit tests for STATE.md Document Module — stateExtractField.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
stateExtractField,
|
||||
stateReplaceField,
|
||||
stateReplaceFieldWithFallback,
|
||||
normalizeStateStatus,
|
||||
computeProgressPercent,
|
||||
shouldPreserveExistingProgress,
|
||||
normalizeProgressNumbers,
|
||||
} from './state-document.js';
|
||||
|
||||
describe('stateExtractField', () => {
|
||||
it('extracts value from bold pattern', () => {
|
||||
const content = 'Some content\n**FieldName:** the value\nMore content';
|
||||
expect(stateExtractField(content, 'FieldName')).toBe('the value');
|
||||
});
|
||||
|
||||
it('extracts value from plain pattern', () => {
|
||||
const content = 'Some content\nFieldName: the value\nMore content';
|
||||
expect(stateExtractField(content, 'FieldName')).toBe('the value');
|
||||
});
|
||||
|
||||
it('returns null when field is missing', () => {
|
||||
const content = 'Some content\nOtherField: something\nMore content';
|
||||
expect(stateExtractField(content, 'FieldName')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateReplaceField', () => {
|
||||
it('replaces value in bold pattern', () => {
|
||||
const content = 'Some content\n**Status:** old value\nMore content';
|
||||
const result = stateReplaceField(content, 'Status', 'new value');
|
||||
expect(result).toBe('Some content\n**Status:** new value\nMore content');
|
||||
});
|
||||
|
||||
it('replaces value in plain pattern', () => {
|
||||
const content = 'Some content\nStatus: old value\nMore content';
|
||||
const result = stateReplaceField(content, 'Status', 'new value');
|
||||
expect(result).toBe('Some content\nStatus: new value\nMore content');
|
||||
});
|
||||
|
||||
it('returns null when field is missing', () => {
|
||||
const content = 'Some content\nOtherField: something\nMore content';
|
||||
const result = stateReplaceField(content, 'Status', 'new value');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateReplaceFieldWithFallback', () => {
|
||||
it('replaces primary field when it exists', () => {
|
||||
const content = 'Status: old\nState: backup';
|
||||
const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
|
||||
expect(result).toBe('Status: new\nState: backup');
|
||||
});
|
||||
|
||||
it('replaces fallback field when primary is missing', () => {
|
||||
const content = 'Other: something\nState: backup';
|
||||
const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
|
||||
expect(result).toBe('Other: something\nState: new');
|
||||
});
|
||||
|
||||
it('returns content unchanged when neither field exists', () => {
|
||||
const content = 'Other: something\nAnother: value';
|
||||
const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeStateStatus', () => {
|
||||
it('returns paused for status containing "paused"', () => {
|
||||
expect(normalizeStateStatus('paused')).toBe('paused');
|
||||
});
|
||||
|
||||
it('returns paused for status containing "stopped"', () => {
|
||||
expect(normalizeStateStatus('stopped')).toBe('paused');
|
||||
});
|
||||
|
||||
it('returns paused when non-null pausedAt is provided', () => {
|
||||
expect(normalizeStateStatus('active', '2024-01-01')).toBe('paused');
|
||||
});
|
||||
|
||||
it('returns executing for status containing "executing"', () => {
|
||||
expect(normalizeStateStatus('executing')).toBe('executing');
|
||||
});
|
||||
|
||||
it('returns executing for status "in progress"', () => {
|
||||
expect(normalizeStateStatus('in progress')).toBe('executing');
|
||||
});
|
||||
|
||||
it('returns executing for status "ready to execute"', () => {
|
||||
expect(normalizeStateStatus('ready to execute')).toBe('executing');
|
||||
});
|
||||
|
||||
it('returns planning for status containing "planning"', () => {
|
||||
expect(normalizeStateStatus('planning')).toBe('planning');
|
||||
});
|
||||
|
||||
it('returns discussing for status containing "discussing"', () => {
|
||||
expect(normalizeStateStatus('discussing')).toBe('discussing');
|
||||
});
|
||||
|
||||
it('returns verifying for status containing "verif"', () => {
|
||||
expect(normalizeStateStatus('verifying')).toBe('verifying');
|
||||
});
|
||||
|
||||
it('returns completed for status containing "complete"', () => {
|
||||
expect(normalizeStateStatus('completed')).toBe('completed');
|
||||
});
|
||||
|
||||
it('returns completed for status containing "done"', () => {
|
||||
expect(normalizeStateStatus('done')).toBe('completed');
|
||||
});
|
||||
|
||||
it('returns unknown for unrecognized status', () => {
|
||||
expect(normalizeStateStatus('something-else')).toBe('something-else');
|
||||
});
|
||||
|
||||
it('returns unknown for null status', () => {
|
||||
expect(normalizeStateStatus(null)).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeProgressPercent', () => {
|
||||
it('uses only plans data when phases data is absent', () => {
|
||||
expect(computeProgressPercent(3, 10, null, null)).toBe(30);
|
||||
});
|
||||
|
||||
it('uses only phases data when plans data is absent', () => {
|
||||
expect(computeProgressPercent(null, null, 2, 4)).toBe(50);
|
||||
});
|
||||
|
||||
it('uses minimum fraction when both plans and phases data are present', () => {
|
||||
// plans: 8/10 = 80%, phases: 3/10 = 30% → min = 30%
|
||||
expect(computeProgressPercent(8, 10, 3, 10)).toBe(30);
|
||||
});
|
||||
|
||||
it('returns null when neither plans nor phases data is present', () => {
|
||||
expect(computeProgressPercent(null, null, null, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when total is 0 (treated as no data)', () => {
|
||||
expect(computeProgressPercent(0, 0, null, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldPreserveExistingProgress', () => {
|
||||
it('returns true when existing total_phases exceeds derived', () => {
|
||||
expect(shouldPreserveExistingProgress({ total_phases: 10 }, { total_phases: 5 })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when derived exceeds existing', () => {
|
||||
expect(shouldPreserveExistingProgress({ total_phases: 5 }, { total_phases: 10 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when existingProgress is not an object', () => {
|
||||
expect(shouldPreserveExistingProgress(null, { total_phases: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when both are null', () => {
|
||||
expect(shouldPreserveExistingProgress(null, null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeProgressNumbers', () => {
|
||||
it('coerces all five tracked keys to numbers', () => {
|
||||
const input = {
|
||||
total_phases: '10',
|
||||
completed_phases: '3',
|
||||
total_plans: '5',
|
||||
completed_plans: '2',
|
||||
percent: '60',
|
||||
};
|
||||
expect(normalizeProgressNumbers(input)).toEqual({
|
||||
total_phases: 10,
|
||||
completed_phases: 3,
|
||||
total_plans: 5,
|
||||
completed_plans: 2,
|
||||
percent: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns non-object input unchanged', () => {
|
||||
expect(normalizeProgressNumbers(null)).toBeNull();
|
||||
expect(normalizeProgressNumbers('string')).toBe('string');
|
||||
});
|
||||
|
||||
it('preserves extra keys untouched', () => {
|
||||
const input = { total_phases: '4', extra_key: 'hello' };
|
||||
const result = normalizeProgressNumbers(input) as Record<string, unknown>;
|
||||
expect(result.total_phases).toBe(4);
|
||||
expect(result.extra_key).toBe('hello');
|
||||
});
|
||||
});
|
||||
261
tests/state-document-generator.test.cjs
Normal file
261
tests/state-document-generator.test.cjs
Normal file
@@ -0,0 +1,261 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Parity test — verifies that state-document.generated.cjs produces identical
|
||||
* results to the compiled SDK ESM output for all exported functions.
|
||||
*
|
||||
* SDK side: require('../sdk/dist/query/state-document.js') via createRequire
|
||||
* CJS side: require('../get-shit-done/bin/lib/state-document.generated.cjs')
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { createRequire } = require('node:module');
|
||||
|
||||
// The SDK dist is ESM; wrap with createRequire targeting the project root so
|
||||
// Node resolves the path correctly from this CJS context.
|
||||
const requireFromRoot = createRequire(__filename);
|
||||
|
||||
// CJS side — direct require works fine
|
||||
const cjs = requireFromRoot('../get-shit-done/bin/lib/state-document.generated.cjs');
|
||||
|
||||
describe('state-document-generator parity: stateReplaceFieldWithFallback', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{
|
||||
label: 'primary hit',
|
||||
content: 'Status: old\nState: backup',
|
||||
primary: 'Status',
|
||||
fallback: 'State',
|
||||
value: 'new',
|
||||
expected: 'Status: new\nState: backup',
|
||||
},
|
||||
{
|
||||
label: 'fallback hit',
|
||||
content: 'Other: something\nState: backup',
|
||||
primary: 'Status',
|
||||
fallback: 'State',
|
||||
value: 'new',
|
||||
expected: 'Other: something\nState: new',
|
||||
},
|
||||
{
|
||||
label: 'neither hit returns unchanged content',
|
||||
content: 'Other: something\nAnother: value',
|
||||
primary: 'Status',
|
||||
fallback: 'State',
|
||||
value: 'new',
|
||||
expected: 'Other: something\nAnother: value',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, content, primary, fallback, value, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.stateReplaceFieldWithFallback(content, primary, fallback, value);
|
||||
const cjsResult = cjs.stateReplaceFieldWithFallback(content, primary, fallback, value);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('state-document-generator parity: normalizeStateStatus', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{ label: 'paused via "paused"', status: 'paused', expected: 'paused' },
|
||||
{ label: 'paused via "stopped"', status: 'stopped', expected: 'paused' },
|
||||
{ label: 'paused via non-null pausedAt', status: 'active', pausedAt: '2024-01-01', expected: 'paused' },
|
||||
{ label: 'executing via "executing"', status: 'executing', expected: 'executing' },
|
||||
{ label: 'executing via "in progress"', status: 'in progress', expected: 'executing' },
|
||||
{ label: 'executing via "ready to execute"', status: 'ready to execute', expected: 'executing' },
|
||||
{ label: 'planning via "planning"', status: 'planning', expected: 'planning' },
|
||||
{ label: 'discussing via "discussing"', status: 'discussing', expected: 'discussing' },
|
||||
{ label: 'verifying via "verif"', status: 'verifying', expected: 'verifying' },
|
||||
{ label: 'completed via "complete"', status: 'completed', expected: 'completed' },
|
||||
{ label: 'completed via "done"', status: 'done', expected: 'completed' },
|
||||
{ label: 'unknown fallback', status: 'something-else', expected: 'something-else' },
|
||||
{ label: 'null status', status: null, expected: 'unknown' },
|
||||
];
|
||||
|
||||
for (const { label, status, pausedAt, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.normalizeStateStatus(status, pausedAt);
|
||||
const cjsResult = cjs.normalizeStateStatus(status, pausedAt);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('state-document-generator parity: computeProgressPercent', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{ label: 'only plans data', cp: 3, tp: 10, cf: null, tf: null, expected: 30 },
|
||||
{ label: 'only phases data', cp: null, tp: null, cf: 2, tf: 4, expected: 50 },
|
||||
{ label: 'both present uses min', cp: 8, tp: 10, cf: 3, tf: 10, expected: 30 },
|
||||
{ label: 'neither returns null', cp: null, tp: null, cf: null, tf: null, expected: null },
|
||||
{ label: 'total of 0 treated as no data', cp: 0, tp: 0, cf: null, tf: null, expected: null },
|
||||
];
|
||||
|
||||
for (const { label, cp, tp, cf, tf, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.computeProgressPercent(cp, tp, cf, tf);
|
||||
const cjsResult = cjs.computeProgressPercent(cp, tp, cf, tf);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('state-document-generator parity: shouldPreserveExistingProgress', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{
|
||||
label: 'existing exceeds derived on total_phases → true',
|
||||
existing: { total_phases: 10 },
|
||||
derived: { total_phases: 5 },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
label: 'derived exceeds existing → false',
|
||||
existing: { total_phases: 5 },
|
||||
derived: { total_phases: 10 },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'malformed input (non-object) → false',
|
||||
existing: null,
|
||||
derived: { total_phases: 5 },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
label: 'both null → false',
|
||||
existing: null,
|
||||
derived: null,
|
||||
expected: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, existing, derived, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.shouldPreserveExistingProgress(existing, derived);
|
||||
const cjsResult = cjs.shouldPreserveExistingProgress(existing, derived);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('state-document-generator parity: normalizeProgressNumbers', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{
|
||||
label: 'coerces all five tracked keys to numbers',
|
||||
input: { total_phases: '10', completed_phases: '3', total_plans: '5', completed_plans: '2', percent: '60' },
|
||||
expected: { total_phases: 10, completed_phases: 3, total_plans: 5, completed_plans: 2, percent: 60 },
|
||||
},
|
||||
{
|
||||
label: 'non-object null returned unchanged',
|
||||
input: null,
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
label: 'extra keys preserved untouched',
|
||||
input: { total_phases: '4', extra_key: 'hello' },
|
||||
expected: { total_phases: 4, extra_key: 'hello' },
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, input, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.normalizeProgressNumbers(input);
|
||||
const cjsResult = cjs.normalizeProgressNumbers(input);
|
||||
assert.deepStrictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.deepStrictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.deepStrictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// SDK ESM side — dynamically import so we can test both; wrap in a top-level
|
||||
// async test suite.
|
||||
describe('state-document-generator parity: stateExtractField', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{
|
||||
label: 'bold pattern',
|
||||
content: 'Some content\n**FieldName:** the value\nMore content',
|
||||
fieldName: 'FieldName',
|
||||
expected: 'the value',
|
||||
},
|
||||
{
|
||||
label: 'plain pattern',
|
||||
content: 'Some content\nFieldName: the value\nMore content',
|
||||
fieldName: 'FieldName',
|
||||
expected: 'the value',
|
||||
},
|
||||
{
|
||||
label: 'missing field returns null',
|
||||
content: 'Some content\nOtherField: something\nMore content',
|
||||
fieldName: 'FieldName',
|
||||
expected: null,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, content, fieldName, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.stateExtractField(content, fieldName);
|
||||
const cjsResult = cjs.stateExtractField(content, fieldName);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('state-document-generator parity: stateReplaceField', async () => {
|
||||
const sdk = await import('../sdk/dist/query/state-document.js');
|
||||
|
||||
const fixtures = [
|
||||
{
|
||||
label: 'bold replace',
|
||||
content: 'Some content\n**Status:** old value\nMore content',
|
||||
fieldName: 'Status',
|
||||
newValue: 'new value',
|
||||
expected: 'Some content\n**Status:** new value\nMore content',
|
||||
},
|
||||
{
|
||||
label: 'plain replace',
|
||||
content: 'Some content\nStatus: old value\nMore content',
|
||||
fieldName: 'Status',
|
||||
newValue: 'new value',
|
||||
expected: 'Some content\nStatus: new value\nMore content',
|
||||
},
|
||||
{
|
||||
label: 'missing field returns null',
|
||||
content: 'Some content\nOtherField: something\nMore content',
|
||||
fieldName: 'Status',
|
||||
newValue: 'new value',
|
||||
expected: null,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { label, content, fieldName, newValue, expected } of fixtures) {
|
||||
test(label, () => {
|
||||
const sdkResult = sdk.stateReplaceField(content, fieldName, newValue);
|
||||
const cjsResult = cjs.stateReplaceField(content, fieldName, newValue);
|
||||
assert.strictEqual(sdkResult, expected, `SDK: ${label}`);
|
||||
assert.strictEqual(cjsResult, expected, `CJS: ${label}`);
|
||||
assert.strictEqual(sdkResult, cjsResult, `SDK/CJS parity: ${label}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user