gsd-pi 2.52.0 → 2.53.0-dev.07ffe51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -32
- package/dist/headless-query.js +1 -1
- package/dist/resources/extensions/get-secrets-from-user.js +7 -0
- package/dist/resources/extensions/gsd/auto/phases.js +28 -8
- package/dist/resources/extensions/gsd/auto-dispatch.js +5 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +70 -14
- package/dist/resources/extensions/gsd/auto.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -10
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/git-service.js +4 -3
- package/dist/resources/extensions/gsd/guided-flow.js +4 -3
- package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +18 -2
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/state.js +18 -29
- package/dist/resources/extensions/gsd/status-guards.js +12 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +4 -3
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +4 -14
- package/dist/resources/extensions/gsd/tools/plan-slice.js +4 -14
- package/dist/resources/extensions/gsd/tools/plan-task.js +4 -14
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +6 -7
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
- package/dist/resources/extensions/gsd/tools/replan-slice.js +5 -6
- package/dist/resources/extensions/gsd/validation.js +21 -0
- package/dist/resources/extensions/shared/rtk.js +14 -4
- package/dist/rtk.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +4 -4
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-b950e4e384cc62b3.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-024d82be84800e52.js → webpack-bca0e732db0dcec3.js} +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +36 -8
- package/src/resources/extensions/get-secrets-from-user.ts +8 -0
- package/src/resources/extensions/gsd/auto/phases.ts +38 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +73 -14
- package/src/resources/extensions/gsd/auto.ts +21 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -11
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +3 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/git-service.ts +4 -3
- package/src/resources/extensions/gsd/guided-flow.ts +4 -3
- package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -1
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/state.ts +18 -29
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/active-milestone-id-guard.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -30
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-zombie-cleanup.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/rate-limit-model-fallback.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +9 -8
- package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +4 -3
- package/src/resources/extensions/gsd/tools/complete-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +4 -3
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +4 -16
- package/src/resources/extensions/gsd/tools/plan-slice.ts +4 -16
- package/src/resources/extensions/gsd/tools/plan-task.ts +4 -16
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +6 -7
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
- package/src/resources/extensions/gsd/tools/replan-slice.ts +5 -7
- package/src/resources/extensions/gsd/validation.ts +23 -0
- package/src/resources/extensions/shared/rtk.ts +22 -4
- package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-fbecd1237e2d6d1f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → Q5pfrfJIvgUKR3LJLVB0T}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → Q5pfrfJIvgUKR3LJLVB0T}/_ssgManifest.js +0 -0
|
@@ -110,6 +110,16 @@ test("isValidationTerminal returns true for verdict: passed (#1429)", () => {
|
|
|
110
110
|
assert.equal(isValidationTerminal(content), true);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
test("isValidationTerminal returns true for verdict: fail (#2769)", () => {
|
|
114
|
+
const content = "---\nverdict: fail\nremediation_round: 1\n---\n\n# Validation";
|
|
115
|
+
assert.equal(isValidationTerminal(content), true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("isValidationTerminal returns true for any arbitrary verdict string (#2769)", () => {
|
|
119
|
+
const content = "---\nverdict: custom-verdict\nremediation_round: 0\n---\n\n# Validation";
|
|
120
|
+
assert.equal(isValidationTerminal(content), true);
|
|
121
|
+
});
|
|
122
|
+
|
|
113
123
|
test("isValidationTerminal returns false for missing frontmatter", () => {
|
|
114
124
|
const content = "# Validation\nNo frontmatter here.";
|
|
115
125
|
assert.equal(isValidationTerminal(content), false);
|
|
@@ -327,14 +337,14 @@ test("verifyExpectedArtifact rejects VALIDATION with missing verdict field", ()
|
|
|
327
337
|
}
|
|
328
338
|
});
|
|
329
339
|
|
|
330
|
-
test("verifyExpectedArtifact
|
|
340
|
+
test("verifyExpectedArtifact accepts VALIDATION with any extracted verdict", () => {
|
|
331
341
|
const base = makeTmpBase();
|
|
332
342
|
try {
|
|
333
343
|
writeValidation(base, "M001", "---\nverdict: unknown-value\nremediation_round: 0\n---\n\n# Validation");
|
|
334
344
|
clearPathCache();
|
|
335
345
|
clearParseCache();
|
|
336
346
|
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
337
|
-
assert.equal(result,
|
|
347
|
+
assert.equal(result, true, "VALIDATION with any extracted verdict should pass verification");
|
|
338
348
|
} finally {
|
|
339
349
|
cleanup(base);
|
|
340
350
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the milestone completion validation gate pattern matching.
|
|
3
|
+
*
|
|
4
|
+
* The gate in auto-dispatch accepts two evidence formats:
|
|
5
|
+
* 1. Structured template: content contains "Operational" AND ("MET" or "N/A")
|
|
6
|
+
* 2. Prose evidence: matches /[Oo]perational[\s:][^\n]*(?:pass|verified|...)/i
|
|
7
|
+
*
|
|
8
|
+
* These tests exercise the exact same expressions used in auto-dispatch.ts
|
|
9
|
+
* to ensure both formats are correctly recognized, and that content without
|
|
10
|
+
* operational evidence is properly rejected.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
|
|
16
|
+
// ─── Replicate the gate matching logic from auto-dispatch.ts ─────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns true when validation content contains acceptable operational
|
|
20
|
+
* verification evidence (structured or prose). Mirrors the inline checks
|
|
21
|
+
* in the "execute → complete-milestone" dispatch rule.
|
|
22
|
+
*/
|
|
23
|
+
function hasOperationalEvidence(validationContent: string): boolean {
|
|
24
|
+
const structuredMatch =
|
|
25
|
+
validationContent.includes("Operational") &&
|
|
26
|
+
(validationContent.includes("MET") || validationContent.includes("N/A"));
|
|
27
|
+
const proseMatch =
|
|
28
|
+
/[Oo]perational[\s:][^\n]*(?:pass|verified|confirmed|met|complete|true|yes|addressed|covered|n\/a|not\s+applicable)/i.test(
|
|
29
|
+
validationContent,
|
|
30
|
+
);
|
|
31
|
+
return structuredMatch || proseMatch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Structured format ───────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test("structured: Operational + MET passes", () => {
|
|
37
|
+
const content = `| Criteria | Status |
|
|
38
|
+
| Operational | MET |
|
|
39
|
+
| Functional | MET |`;
|
|
40
|
+
assert.ok(hasOperationalEvidence(content));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("structured: Operational + N/A passes", () => {
|
|
44
|
+
const content = `| Criteria | Status |
|
|
45
|
+
| Operational | N/A |
|
|
46
|
+
| Functional | MET |`;
|
|
47
|
+
assert.ok(hasOperationalEvidence(content));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("structured: Operational present with MET on another row still passes (includes is content-wide)", () => {
|
|
51
|
+
// The structured check uses .includes() across the entire content,
|
|
52
|
+
// so "MET" on the Functional row satisfies the condition alongside
|
|
53
|
+
// "Operational" anywhere in the document.
|
|
54
|
+
const content = `| Criteria | Status |
|
|
55
|
+
| Operational | PENDING |
|
|
56
|
+
| Functional | MET |`;
|
|
57
|
+
assert.ok(hasOperationalEvidence(content));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("structured: Operational alone without any MET or N/A anywhere fails", () => {
|
|
61
|
+
const content = `| Criteria | Status |
|
|
62
|
+
| Operational | PENDING |
|
|
63
|
+
| Functional | PENDING |`;
|
|
64
|
+
assert.ok(!hasOperationalEvidence(content));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Prose format ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
test('prose: "Operational: verified" passes', () => {
|
|
70
|
+
const content = `## Validation Report
|
|
71
|
+
Operational: verified — all endpoints responsive.
|
|
72
|
+
Functional: tests pass.`;
|
|
73
|
+
assert.ok(hasOperationalEvidence(content));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('prose: "Operational checks confirmed" passes', () => {
|
|
77
|
+
const content = `## Validation Report
|
|
78
|
+
Operational checks confirmed by smoke test suite.`;
|
|
79
|
+
assert.ok(hasOperationalEvidence(content));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('prose: "Operational — pass" passes', () => {
|
|
83
|
+
const content = `Operational — pass (all services healthy)`;
|
|
84
|
+
assert.ok(hasOperationalEvidence(content));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('prose: "operational: addressed" passes (case-insensitive)', () => {
|
|
88
|
+
const content = `operational: addressed in CI pipeline run #42.`;
|
|
89
|
+
assert.ok(hasOperationalEvidence(content));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('prose: "Operational: not applicable" passes', () => {
|
|
93
|
+
const content = `Operational: not applicable for this library-only change.`;
|
|
94
|
+
assert.ok(hasOperationalEvidence(content));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('prose: "Operational: n/a" passes', () => {
|
|
98
|
+
const content = `Operational: n/a — no runtime components.`;
|
|
99
|
+
assert.ok(hasOperationalEvidence(content));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('prose: "Operational: complete" passes', () => {
|
|
103
|
+
const content = `Operational: complete — all health checks green.`;
|
|
104
|
+
assert.ok(hasOperationalEvidence(content));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── Rejection cases ─────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
test("no operational evidence: unrelated content fails", () => {
|
|
110
|
+
const content = `## Validation Report
|
|
111
|
+
All functional tests pass.
|
|
112
|
+
Code coverage at 92%.`;
|
|
113
|
+
assert.ok(!hasOperationalEvidence(content));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("no operational evidence: word 'operational' buried without qualifying keyword fails", () => {
|
|
117
|
+
const content = `## Validation Report
|
|
118
|
+
The operational aspects were not evaluated in this round.`;
|
|
119
|
+
assert.ok(!hasOperationalEvidence(content));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("no operational evidence: empty content fails", () => {
|
|
123
|
+
assert.ok(!hasOperationalEvidence(""));
|
|
124
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// GSD — validation unit tests
|
|
2
|
+
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
|
|
6
|
+
import { isNonEmptyString, validateStringArray } from '../validation.ts';
|
|
7
|
+
|
|
8
|
+
// ─── isNonEmptyString ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
test('isNonEmptyString: "hello" returns true', () => {
|
|
11
|
+
assert.equal(isNonEmptyString('hello'), true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('isNonEmptyString: " " (whitespace only) returns false', () => {
|
|
15
|
+
assert.equal(isNonEmptyString(' '), false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('isNonEmptyString: "" (empty string) returns false', () => {
|
|
19
|
+
assert.equal(isNonEmptyString(''), false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('isNonEmptyString: null returns false', () => {
|
|
23
|
+
assert.equal(isNonEmptyString(null), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('isNonEmptyString: undefined returns false', () => {
|
|
27
|
+
assert.equal(isNonEmptyString(undefined), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('isNonEmptyString: 42 (number) returns false', () => {
|
|
31
|
+
assert.equal(isNonEmptyString(42), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── validateStringArray ─────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test('validateStringArray: ["a", "b"] returns ["a", "b"]', () => {
|
|
37
|
+
assert.deepEqual(validateStringArray(['a', 'b'], 'items'), ['a', 'b']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('validateStringArray: [] (empty array) returns []', () => {
|
|
41
|
+
assert.deepEqual(validateStringArray([], 'items'), []);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('validateStringArray: "not an array" throws with "must be an array"', () => {
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => validateStringArray('not an array', 'items'),
|
|
47
|
+
(err: Error) => {
|
|
48
|
+
assert.ok(err.message.includes('must be an array'));
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('validateStringArray: ["a", 42] throws with "must contain only non-empty strings"', () => {
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => validateStringArray(['a', 42], 'items'),
|
|
57
|
+
(err: Error) => {
|
|
58
|
+
assert.ok(err.message.includes('must contain only non-empty strings'));
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('validateStringArray: ["a", ""] throws with "must contain only non-empty strings"', () => {
|
|
65
|
+
assert.throws(
|
|
66
|
+
() => validateStringArray(['a', ''], 'items'),
|
|
67
|
+
(err: Error) => {
|
|
68
|
+
assert.ok(err.message.includes('must contain only non-empty strings'));
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
updateMilestoneStatus,
|
|
18
18
|
} from "../gsd-db.js";
|
|
19
19
|
import { resolveMilestonePath, clearPathCache } from "../paths.js";
|
|
20
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
20
21
|
import { saveFile, clearParseCache } from "../files.js";
|
|
21
22
|
import { invalidateStateCache } from "../state.js";
|
|
22
23
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
@@ -134,7 +135,7 @@ export async function handleCompleteMilestone(
|
|
|
134
135
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
|
-
if (milestone.status
|
|
138
|
+
if (isClosedStatus(milestone.status)) {
|
|
138
139
|
guardError = `milestone ${params.milestoneId} is already complete`;
|
|
139
140
|
return;
|
|
140
141
|
}
|
|
@@ -146,7 +147,7 @@ export async function handleCompleteMilestone(
|
|
|
146
147
|
return;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
const incompleteSlices = slices.filter(s => s.status
|
|
150
|
+
const incompleteSlices = slices.filter(s => !isClosedStatus(s.status));
|
|
150
151
|
if (incompleteSlices.length > 0) {
|
|
151
152
|
const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
|
|
152
153
|
guardError = `incomplete slices: ${incompleteIds}`;
|
|
@@ -156,7 +157,7 @@ export async function handleCompleteMilestone(
|
|
|
156
157
|
// Deep check: verify all tasks in all slices are complete
|
|
157
158
|
for (const slice of slices) {
|
|
158
159
|
const tasks = getSliceTasks(params.milestoneId, slice.id);
|
|
159
|
-
const incompleteTasks = tasks.filter(t => t.status
|
|
160
|
+
const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
|
|
160
161
|
if (incompleteTasks.length > 0) {
|
|
161
162
|
const ids = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
162
163
|
guardError = `slice ${slice.id} has incomplete tasks: ${ids}`;
|
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { mkdirSync } from "node:fs";
|
|
12
12
|
|
|
13
13
|
import type { CompleteSliceParams } from "../types.js";
|
|
14
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
15
|
import {
|
|
15
16
|
transaction,
|
|
16
17
|
insertMilestone,
|
|
@@ -225,13 +226,13 @@ export async function handleCompleteSlice(
|
|
|
225
226
|
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
226
227
|
// Only block if they exist and are closed.
|
|
227
228
|
const milestone = getMilestone(params.milestoneId);
|
|
228
|
-
if (milestone && (milestone.status
|
|
229
|
+
if (milestone && isClosedStatus(milestone.status)) {
|
|
229
230
|
guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
230
231
|
return;
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
234
|
-
if (slice && (slice.status
|
|
235
|
+
if (slice && isClosedStatus(slice.status)) {
|
|
235
236
|
guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
@@ -243,7 +244,7 @@ export async function handleCompleteSlice(
|
|
|
243
244
|
return;
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
const incompleteTasks = tasks.filter(t => t.status
|
|
247
|
+
const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
|
|
247
248
|
if (incompleteTasks.length > 0) {
|
|
248
249
|
const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
249
250
|
guardError = `incomplete tasks: ${incompleteIds}`;
|
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { mkdirSync, existsSync } from "node:fs";
|
|
12
12
|
|
|
13
13
|
import type { CompleteTaskParams } from "../types.js";
|
|
14
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
15
|
import {
|
|
15
16
|
transaction,
|
|
16
17
|
insertMilestone,
|
|
@@ -159,19 +160,19 @@ export async function handleCompleteTask(
|
|
|
159
160
|
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
160
161
|
// Only block if they exist and are closed.
|
|
161
162
|
const milestone = getMilestone(params.milestoneId);
|
|
162
|
-
if (milestone && (milestone.status
|
|
163
|
+
if (milestone && isClosedStatus(milestone.status)) {
|
|
163
164
|
guardError = `cannot complete task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
164
165
|
return;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
168
|
-
if (slice && (slice.status
|
|
169
|
+
if (slice && isClosedStatus(slice.status)) {
|
|
169
170
|
guardError = `cannot complete task in a closed slice: ${params.sliceId} (status: ${slice.status})`;
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
174
|
-
if (existingTask && (existingTask.status
|
|
175
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
175
176
|
guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import {
|
|
3
5
|
transaction,
|
|
4
6
|
getMilestone,
|
|
@@ -54,20 +56,6 @@ export interface PlanMilestoneResult {
|
|
|
54
56
|
roadmapPath: string;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
58
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function validateStringArray(value: unknown, field: string): string[] {
|
|
62
|
-
if (!Array.isArray(value)) {
|
|
63
|
-
throw new Error(`${field} must be an array`);
|
|
64
|
-
}
|
|
65
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
66
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
59
|
function validateRiskEntries(value: unknown): Array<{ risk: string; whyItMatters: string }> {
|
|
72
60
|
if (!Array.isArray(value)) {
|
|
73
61
|
throw new Error("keyRisks must be an array");
|
|
@@ -196,7 +184,7 @@ export async function handlePlanMilestone(
|
|
|
196
184
|
try {
|
|
197
185
|
transaction(() => {
|
|
198
186
|
const existingMilestone = getMilestone(params.milestoneId);
|
|
199
|
-
if (existingMilestone && (existingMilestone.status
|
|
187
|
+
if (existingMilestone && isClosedStatus(existingMilestone.status)) {
|
|
200
188
|
guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
|
|
201
189
|
return;
|
|
202
190
|
}
|
|
@@ -209,7 +197,7 @@ export async function handlePlanMilestone(
|
|
|
209
197
|
guardError = `depends_on references unknown milestone: ${depId}`;
|
|
210
198
|
return;
|
|
211
199
|
}
|
|
212
|
-
if (dep.status
|
|
200
|
+
if (!isClosedStatus(dep.status)) {
|
|
213
201
|
guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
|
|
214
202
|
return;
|
|
215
203
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import {
|
|
3
5
|
transaction,
|
|
4
6
|
getMilestone,
|
|
@@ -50,20 +52,6 @@ export interface PlanSliceResult {
|
|
|
50
52
|
taskPlanPaths: string[];
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
54
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function validateStringArray(value: unknown, field: string): string[] {
|
|
58
|
-
if (!Array.isArray(value)) {
|
|
59
|
-
throw new Error(`${field} must be an array`);
|
|
60
|
-
}
|
|
61
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
62
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
55
|
function validateTasks(value: unknown): PlanSliceTaskInput[] {
|
|
68
56
|
if (!Array.isArray(value) || value.length === 0) {
|
|
69
57
|
throw new Error("tasks must be a non-empty array");
|
|
@@ -157,7 +145,7 @@ export async function handlePlanSlice(
|
|
|
157
145
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
158
146
|
return;
|
|
159
147
|
}
|
|
160
|
-
if (parentMilestone.status
|
|
148
|
+
if (isClosedStatus(parentMilestone.status)) {
|
|
161
149
|
guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
|
|
162
150
|
return;
|
|
163
151
|
}
|
|
@@ -167,7 +155,7 @@ export async function handlePlanSlice(
|
|
|
167
155
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
168
156
|
return;
|
|
169
157
|
}
|
|
170
|
-
if (parentSlice.status
|
|
158
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
171
159
|
guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
|
|
172
160
|
return;
|
|
173
161
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderTaskPlanFromDb } from "../markdown-renderer.js";
|
|
@@ -32,20 +34,6 @@ export interface PlanTaskResult {
|
|
|
32
34
|
taskPlanPath: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
36
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function validateStringArray(value: unknown, field: string): string[] {
|
|
40
|
-
if (!Array.isArray(value)) {
|
|
41
|
-
throw new Error(`${field} must be an array`);
|
|
42
|
-
}
|
|
43
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
44
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
45
|
-
}
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
function validateParams(params: PlanTaskParams): PlanTaskParams {
|
|
50
38
|
if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
|
|
51
39
|
if (!isNonEmptyString(params?.sliceId)) throw new Error("sliceId is required");
|
|
@@ -89,13 +77,13 @@ export async function handlePlanTask(
|
|
|
89
77
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
90
78
|
return;
|
|
91
79
|
}
|
|
92
|
-
if (parentSlice.status
|
|
80
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
93
81
|
guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
94
82
|
return;
|
|
95
83
|
}
|
|
96
84
|
|
|
97
85
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
98
|
-
if (existingTask && (existingTask.status
|
|
86
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
99
87
|
guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
|
|
100
88
|
return;
|
|
101
89
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { clearParseCache } from "../files.js";
|
|
3
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
4
|
+
import { isNonEmptyString } from "../validation.js";
|
|
2
5
|
import {
|
|
3
6
|
transaction,
|
|
4
7
|
getMilestone,
|
|
@@ -14,7 +17,6 @@ import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-rendere
|
|
|
14
17
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
18
|
import { writeManifest } from "../workflow-manifest.js";
|
|
16
19
|
import { appendEvent } from "../workflow-events.js";
|
|
17
|
-
import { join } from "node:path";
|
|
18
20
|
|
|
19
21
|
export interface SliceChangeInput {
|
|
20
22
|
sliceId: string;
|
|
@@ -47,9 +49,6 @@ export interface ReassessRoadmapResult {
|
|
|
47
49
|
roadmapPath: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
51
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
52
|
-
}
|
|
53
52
|
|
|
54
53
|
function validateParams(params: ReassessRoadmapParams): ReassessRoadmapParams {
|
|
55
54
|
if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
|
|
@@ -125,7 +124,7 @@ export async function handleReassessRoadmap(
|
|
|
125
124
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
126
125
|
return;
|
|
127
126
|
}
|
|
128
|
-
if (milestone.status
|
|
127
|
+
if (isClosedStatus(milestone.status)) {
|
|
129
128
|
guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
130
129
|
return;
|
|
131
130
|
}
|
|
@@ -136,7 +135,7 @@ export async function handleReassessRoadmap(
|
|
|
136
135
|
guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
|
-
if (completedSlice.status
|
|
138
|
+
if (!isClosedStatus(completedSlice.status)) {
|
|
140
139
|
guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
|
|
141
140
|
return;
|
|
142
141
|
}
|
|
@@ -145,7 +144,7 @@ export async function handleReassessRoadmap(
|
|
|
145
144
|
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
146
145
|
const completedSliceIds = new Set<string>();
|
|
147
146
|
for (const slice of existingSlices) {
|
|
148
|
-
if (slice.status
|
|
147
|
+
if (isClosedStatus(slice.status)) {
|
|
149
148
|
completedSliceIds.add(slice.id);
|
|
150
149
|
}
|
|
151
150
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
transaction,
|
|
21
21
|
} from "../gsd-db.js";
|
|
22
22
|
import { invalidateStateCache } from "../state.js";
|
|
23
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
23
24
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
24
25
|
import { writeManifest } from "../workflow-manifest.js";
|
|
25
26
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -62,8 +63,8 @@ export async function handleReopenSlice(
|
|
|
62
63
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
|
-
if (milestone.status
|
|
66
|
-
guardError = `cannot reopen slice
|
|
66
|
+
if (isClosedStatus(milestone.status)) {
|
|
67
|
+
guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -72,7 +73,7 @@ export async function handleReopenSlice(
|
|
|
72
73
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
|
-
if (slice.status
|
|
76
|
+
if (!isClosedStatus(slice.status)) {
|
|
76
77
|
guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
transaction,
|
|
19
19
|
} from "../gsd-db.js";
|
|
20
20
|
import { invalidateStateCache } from "../state.js";
|
|
21
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
21
22
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
22
23
|
import { writeManifest } from "../workflow-manifest.js";
|
|
23
24
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -63,7 +64,7 @@ export async function handleReopenTask(
|
|
|
63
64
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
if (milestone.status
|
|
67
|
+
if (isClosedStatus(milestone.status)) {
|
|
67
68
|
guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
@@ -73,8 +74,8 @@ export async function handleReopenTask(
|
|
|
73
74
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
if (slice.status
|
|
77
|
-
guardError = `cannot reopen task
|
|
77
|
+
if (isClosedStatus(slice.status)) {
|
|
78
|
+
guardError = `cannot reopen task in a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -83,7 +84,7 @@ export async function handleReopenTask(
|
|
|
83
84
|
guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
|
-
if (task.status
|
|
87
|
+
if (!isClosedStatus(task.status)) {
|
|
87
88
|
guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
deleteTask,
|
|
11
11
|
} from "../gsd-db.js";
|
|
12
12
|
import { invalidateStateCache } from "../state.js";
|
|
13
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
|
+
import { isNonEmptyString } from "../validation.js";
|
|
13
15
|
import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
|
|
14
16
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
17
|
import { writeManifest } from "../workflow-manifest.js";
|
|
@@ -48,10 +50,6 @@ export interface ReplanSliceResult {
|
|
|
48
50
|
planPath: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
52
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
53
|
function validateParams(params: ReplanSliceParams): ReplanSliceParams {
|
|
56
54
|
if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required");
|
|
57
55
|
if (!isNonEmptyString(params?.sliceId)) throw new Error("sliceId is required");
|
|
@@ -104,7 +102,7 @@ export async function handleReplanSlice(
|
|
|
104
102
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
105
103
|
return;
|
|
106
104
|
}
|
|
107
|
-
if (parentSlice.status
|
|
105
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
108
106
|
guardError = `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
109
107
|
return;
|
|
110
108
|
}
|
|
@@ -115,7 +113,7 @@ export async function handleReplanSlice(
|
|
|
115
113
|
guardError = `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}`;
|
|
116
114
|
return;
|
|
117
115
|
}
|
|
118
|
-
if (blockerTask.status
|
|
116
|
+
if (!isClosedStatus(blockerTask.status)) {
|
|
119
117
|
guardError = `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered`;
|
|
120
118
|
return;
|
|
121
119
|
}
|
|
@@ -124,7 +122,7 @@ export async function handleReplanSlice(
|
|
|
124
122
|
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
125
123
|
const completedTaskIds = new Set<string>();
|
|
126
124
|
for (const task of existingTasks) {
|
|
127
|
-
if (task.status
|
|
125
|
+
if (isClosedStatus(task.status)) {
|
|
128
126
|
completedTaskIds.add(task.id);
|
|
129
127
|
}
|
|
130
128
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input-validation primitives for GSD tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Type guard: value is a string with at least one non-whitespace character. */
|
|
6
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate that `value` is an array of non-empty strings.
|
|
12
|
+
* Throws with a message referencing `field` on failure.
|
|
13
|
+
* Returns the validated array (narrowed to string[]).
|
|
14
|
+
*/
|
|
15
|
+
export function validateStringArray(value: unknown, field: string): string[] {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
throw new Error(`${field} must be an array`);
|
|
18
|
+
}
|
|
19
|
+
if (value.some((item) => !isNonEmptyString(item))) {
|
|
20
|
+
throw new Error(`${field} must contain only non-empty strings`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|