@opengsd/gsd-core 1.4.4 → 1.5.0-rc.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase Locator — Phase-directory search and location
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 rollout phase 2d: extracted from core.cts (issue #881).
|
|
6
|
+
* Owns active-phase discovery against the `.planning/phases/` tree
|
|
7
|
+
* (`searchPhaseInDir`, `findPhaseInternal`) and archived-phase-dir
|
|
8
|
+
* enumeration (`getArchivedPhaseDirs`), matching phase ids/tokens against
|
|
9
|
+
* the filesystem. Behaviour is preserved byte-for-behaviour from the prior
|
|
10
|
+
* location; only the module boundary moved. core.cjs re-exports
|
|
11
|
+
* `searchPhaseInDir`, `findPhaseInternal`, and `getArchivedPhaseDirs` for back-compat.
|
|
12
|
+
*
|
|
13
|
+
* New imports should pull phase-locator helpers from phase-locator.cjs
|
|
14
|
+
* directly.
|
|
15
|
+
*
|
|
16
|
+
* Dependencies (leaf modules only — no core.cjs, no loadConfig):
|
|
17
|
+
* - node:fs / node:path (stdlib)
|
|
18
|
+
* - ./phase-id.cjs (normalizePhaseName, phaseTokenMatches, extractPhaseToken)
|
|
19
|
+
* - ./core-utils.cjs (readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath)
|
|
20
|
+
* - ./planning-workspace.cjs (planningDir)
|
|
21
|
+
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const phaseIdModule = require("./phase-id.cjs");
|
|
29
|
+
const { normalizePhaseName, phaseTokenMatches, extractPhaseToken } = phaseIdModule;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const coreUtilsModule = require("./core-utils.cjs");
|
|
32
|
+
const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath } = coreUtilsModule;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
35
|
+
const { planningDir } = planningWorkspace;
|
|
36
|
+
// ─── Phase search helpers ─────────────────────────────────────────────────────
|
|
37
|
+
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
38
|
+
try {
|
|
39
|
+
const dirs = readSubdirectories(baseDir, true);
|
|
40
|
+
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
41
|
+
if (!match)
|
|
42
|
+
return null;
|
|
43
|
+
const phaseToken = extractPhaseToken(match);
|
|
44
|
+
const phaseNumber = phaseToken || normalized;
|
|
45
|
+
const afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|
|
46
|
+
const phaseName = afterToken || null;
|
|
47
|
+
const phaseDir = node_path_1.default.join(baseDir, match);
|
|
48
|
+
const { plans: unsortedPlans, summaries: unsortedSummaries, hasResearch, hasContext, hasVerification, hasReviews } = getPhaseFileStats(phaseDir);
|
|
49
|
+
const plans = unsortedPlans.sort();
|
|
50
|
+
const summaries = unsortedSummaries.sort();
|
|
51
|
+
const completedPlanIds = new Set(summaries.flatMap(s => {
|
|
52
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
53
|
+
const canonical = extractCanonicalPlanId(s);
|
|
54
|
+
return canonical === exact ? [exact] : [exact, canonical];
|
|
55
|
+
}));
|
|
56
|
+
const incompletePlans = plans.filter(p => {
|
|
57
|
+
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
58
|
+
const canonical = extractCanonicalPlanId(p);
|
|
59
|
+
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
found: true,
|
|
63
|
+
directory: toPosixPath(node_path_1.default.join(relBase, match)),
|
|
64
|
+
phase_number: phaseNumber,
|
|
65
|
+
phase_name: phaseName,
|
|
66
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
67
|
+
plans,
|
|
68
|
+
summaries,
|
|
69
|
+
incomplete_plans: incompletePlans,
|
|
70
|
+
has_research: hasResearch,
|
|
71
|
+
has_context: hasContext,
|
|
72
|
+
has_verification: hasVerification,
|
|
73
|
+
has_reviews: hasReviews,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function findPhaseInternal(cwd, phase) {
|
|
81
|
+
if (!phase)
|
|
82
|
+
return null;
|
|
83
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
84
|
+
const normalized = normalizePhaseName(phase);
|
|
85
|
+
const relPhasesDir = toPosixPath(node_path_1.default.relative(cwd, phasesDir));
|
|
86
|
+
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
|
|
87
|
+
if (current)
|
|
88
|
+
return current;
|
|
89
|
+
const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
|
|
90
|
+
if (!node_fs_1.default.existsSync(milestonesDir))
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
|
|
94
|
+
const archiveDirs = milestoneEntries
|
|
95
|
+
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
96
|
+
.map(e => e.name)
|
|
97
|
+
.sort()
|
|
98
|
+
.reverse();
|
|
99
|
+
for (const archiveName of archiveDirs) {
|
|
100
|
+
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
101
|
+
const version = versionMatch[1];
|
|
102
|
+
const archivePath = node_path_1.default.join(milestonesDir, archiveName);
|
|
103
|
+
const relBase = '.planning/milestones/' + archiveName;
|
|
104
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
105
|
+
if (result) {
|
|
106
|
+
result.archived = version;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* intentionally empty */ }
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function getArchivedPhaseDirs(cwd) {
|
|
115
|
+
const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
|
|
116
|
+
const results = [];
|
|
117
|
+
if (!node_fs_1.default.existsSync(milestonesDir))
|
|
118
|
+
return results;
|
|
119
|
+
try {
|
|
120
|
+
const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
|
|
121
|
+
const phaseDirs = milestoneEntries
|
|
122
|
+
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
123
|
+
.map(e => e.name)
|
|
124
|
+
.sort()
|
|
125
|
+
.reverse();
|
|
126
|
+
for (const archiveName of phaseDirs) {
|
|
127
|
+
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
128
|
+
const version = versionMatch[1];
|
|
129
|
+
const archivePath = node_path_1.default.join(milestonesDir, archiveName);
|
|
130
|
+
const dirs = readSubdirectories(archivePath, true);
|
|
131
|
+
for (const dir of dirs) {
|
|
132
|
+
results.push({
|
|
133
|
+
name: dir,
|
|
134
|
+
milestone: version,
|
|
135
|
+
basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
|
|
136
|
+
fullPath: node_path_1.default.join(archivePath, dir),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { /* intentionally empty */ }
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
module.exports = {
|
|
145
|
+
searchPhaseInDir,
|
|
146
|
+
findPhaseInternal,
|
|
147
|
+
getArchivedPhaseDirs,
|
|
148
|
+
};
|
|
@@ -32,6 +32,9 @@ const stateMod = require("./state.cjs");
|
|
|
32
32
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
33
33
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
34
34
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
36
|
+
const uatPredicate = require("./uat-predicate.cjs");
|
|
37
|
+
const { evaluateUatPassed } = uatPredicate;
|
|
35
38
|
const { escapeRegex, loadConfig, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error, readSubdirectories, phaseTokenMatches, ERROR_REASON, } = core;
|
|
36
39
|
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
37
40
|
const { extractFrontmatter } = frontmatterMod;
|
|
@@ -1292,6 +1295,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1292
1295
|
};
|
|
1293
1296
|
output(result, raw);
|
|
1294
1297
|
}
|
|
1298
|
+
function cmdPhaseUatPassed(cwd, phaseNum, raw, opts = {}) {
|
|
1299
|
+
if (!phaseNum) {
|
|
1300
|
+
error('phase number required for phase uat-passed');
|
|
1301
|
+
}
|
|
1302
|
+
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum);
|
|
1303
|
+
if (!phaseInfoRaw) {
|
|
1304
|
+
error(`Phase ${phaseNum} not found`);
|
|
1305
|
+
}
|
|
1306
|
+
const phaseInfo = phaseInfoRaw;
|
|
1307
|
+
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1308
|
+
const report = evaluateUatPassed(phaseFullDir, { policy: opts.policy });
|
|
1309
|
+
output({ phase: phaseNum, ...report }, raw);
|
|
1310
|
+
}
|
|
1295
1311
|
module.exports = {
|
|
1296
1312
|
cmdPhasesList,
|
|
1297
1313
|
cmdPhaseNextDecimal,
|
|
@@ -1303,5 +1319,6 @@ module.exports = {
|
|
|
1303
1319
|
cmdPhaseInsert,
|
|
1304
1320
|
cmdPhaseRemove,
|
|
1305
1321
|
cmdPhaseComplete,
|
|
1322
|
+
cmdPhaseUatPassed,
|
|
1306
1323
|
computeDependencyLevels,
|
|
1307
1324
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* probe-core — generic spec-phase probe resolution model (ADR-550 Decision 7).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the edge-probe (the first adapter) once the prohibition probe (#644)
|
|
6
|
+
* proved it the *second* adapter of the same model: one adapter is a hypothetical seam,
|
|
7
|
+
* two is a real one. This module owns everything generic — the resolution lifecycle,
|
|
8
|
+
* the status×verification re-cut, `validateResolution`/`validateRequirement`, the
|
|
9
|
+
* `analyzeCoverage(items, resolutions?, validators)` merge/rollup/orphan-reject engine,
|
|
10
|
+
* the `byVerification` rollup, and the `runProbeCli` I/O scaffold. Each probe is a thin
|
|
11
|
+
* adapter: it supplies the proposal logic (deterministic for edge, LLM-recall for
|
|
12
|
+
* prohibition) and its closed vocabularies via injected validators.
|
|
13
|
+
*
|
|
14
|
+
* Authored as strict TypeScript (`src/probe-core.cts`) and compiled by
|
|
15
|
+
* `tsc -p tsconfig.build.json` to the gitignored runtime artifact
|
|
16
|
+
* `gsd-core/bin/lib/probe-core.cjs`. Do NOT hand-write the `.cjs`; it is emitted.
|
|
17
|
+
*
|
|
18
|
+
* Two orthogonal axes (the re-cut):
|
|
19
|
+
* - status: resolved | dismissed | unresolved — the resolution LIFECYCLE (shared)
|
|
20
|
+
* - verification: <probe-defined> | null — HOW a resolved item is verified
|
|
21
|
+
* The edge adapter declares `verification: explicit | backstop`; the prohibition adapter
|
|
22
|
+
* (#644) will declare `test | judgment`. Splitting the axes keeps the lifecycle enum free
|
|
23
|
+
* of a verification fact and lets a sibling probe add its own tiers without a parallel enum.
|
|
24
|
+
*
|
|
25
|
+
* Typing is hybrid (ADR-550 #5): generic type params for adapter DX, but enforcement runs
|
|
26
|
+
* through injected runtime validators, because the CLI executes over JSON where TS types are
|
|
27
|
+
* erased. The contract test pins the validators, not the types.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.VALID_STATUS = void 0;
|
|
34
|
+
exports.validateRequirement = validateRequirement;
|
|
35
|
+
exports.validateResolution = validateResolution;
|
|
36
|
+
exports.analyzeCoverage = analyzeCoverage;
|
|
37
|
+
exports.runProbeCli = runProbeCli;
|
|
38
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
40
|
+
exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
|
|
41
|
+
function errMessage(e) {
|
|
42
|
+
return e instanceof Error ? e.message : String(e);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
|
|
46
|
+
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
|
|
47
|
+
* that forgets to validate inside its closure could hand back a malformed object. Rather than
|
|
48
|
+
* stringify garbage as green output, `runProbeCli` checks the report shape and fails closed.
|
|
49
|
+
*/
|
|
50
|
+
function isValidReport(report) {
|
|
51
|
+
if (report == null || typeof report !== 'object')
|
|
52
|
+
return false;
|
|
53
|
+
const r = report;
|
|
54
|
+
if (!Array.isArray(r.items))
|
|
55
|
+
return false;
|
|
56
|
+
const c = r.coverage;
|
|
57
|
+
if (c == null || typeof c !== 'object')
|
|
58
|
+
return false;
|
|
59
|
+
if (typeof c.applicable !== 'number' || typeof c.resolved !== 'number' || typeof c.unresolved !== 'number') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (c.byVerification == null || typeof c.byVerification !== 'object')
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate a requirement's generic structural fields — fail closed on malformed input rather
|
|
68
|
+
* than coercing it. Probe-specific fields (e.g. the edge adapter's `shapes`) are validated by
|
|
69
|
+
* the adapter. Typed loosely because the CLI casts arbitrary parsed JSON to `Requirement`.
|
|
70
|
+
*/
|
|
71
|
+
function validateRequirement(requirement) {
|
|
72
|
+
const r = requirement;
|
|
73
|
+
if (typeof r.id !== 'string' || !r.id.trim()) {
|
|
74
|
+
throw new Error(`requirement id must be a non-empty string (got ${JSON.stringify(r.id)})`);
|
|
75
|
+
}
|
|
76
|
+
if (r.text != null && typeof r.text !== 'string') {
|
|
77
|
+
throw new Error(`requirement ${r.id} text must be a string when present`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate a resolution against the probe's injected validators. Rejects an unknown status,
|
|
82
|
+
* a dismissal without a non-empty reason, a `resolved` item with a missing/unknown
|
|
83
|
+
* verification tier, and a `resolved` item missing any field its tier requires (per
|
|
84
|
+
* `requiredFieldsByVerification`). Returns true on success.
|
|
85
|
+
*/
|
|
86
|
+
function validateResolution(r, validators) {
|
|
87
|
+
const key = `${r.requirement_id}::${r.category}`;
|
|
88
|
+
if (!exports.VALID_STATUS.includes(r.status)) {
|
|
89
|
+
throw new Error(`invalid status "${r.status}" for ${key}`);
|
|
90
|
+
}
|
|
91
|
+
// Invariant (this module's header): `verification` is null unless `status` is `resolved`.
|
|
92
|
+
// Enforce it for EVERY status — a dismissed/unresolved resolution carrying a verification
|
|
93
|
+
// tier would otherwise merge verbatim (`analyzeCoverage` below) and silently break the
|
|
94
|
+
// model for the second adapter (#644) that inherits this seam. Fail closed across the full
|
|
95
|
+
// status×verification space, not just `resolved`.
|
|
96
|
+
if (r.status !== 'resolved' && r.verification != null) {
|
|
97
|
+
throw new Error(`verification must be null unless status is "resolved" (got "${r.verification}") for ${key}`);
|
|
98
|
+
}
|
|
99
|
+
// An `unresolved` resolution is an UNACTED item: it must carry no resolution/reason payload.
|
|
100
|
+
// A populated payload is an authoring mistake (the author meant resolved/dismissed) that
|
|
101
|
+
// would otherwise be silently dropped into the unresolved count with no error pointing at
|
|
102
|
+
// it. Reject it so the mistake surfaces.
|
|
103
|
+
if (r.status === 'unresolved') {
|
|
104
|
+
if (r.resolution != null && String(r.resolution).trim()) {
|
|
105
|
+
throw new Error(`unresolved must not carry a resolution (${key})`);
|
|
106
|
+
}
|
|
107
|
+
if (r.reason != null && String(r.reason).trim()) {
|
|
108
|
+
throw new Error(`unresolved must not carry a reason (${key})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (r.status === 'dismissed' && !(r.reason && String(r.reason).trim())) {
|
|
112
|
+
throw new Error(`dismissed requires a reason (${key})`);
|
|
113
|
+
}
|
|
114
|
+
if (r.status === 'resolved') {
|
|
115
|
+
const tier = r.verification;
|
|
116
|
+
if (tier == null) {
|
|
117
|
+
throw new Error(`resolved requires a verification tier (one of: ${validators.verification.join(', ')}) for ${key}`);
|
|
118
|
+
}
|
|
119
|
+
if (!validators.verification.includes(tier)) {
|
|
120
|
+
throw new Error(`invalid verification "${tier}" for ${key} — must be one of: ${validators.verification.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
const required = validators.requiredFieldsByVerification[tier] ?? [];
|
|
123
|
+
for (const field of required) {
|
|
124
|
+
// field is 'resolution' | 'reason'; both are `string | null | undefined` on Resolution,
|
|
125
|
+
// so the indexed access is string-typed (no unknown-to-string coercion).
|
|
126
|
+
const value = r[field];
|
|
127
|
+
if (!(value != null && String(value).trim())) {
|
|
128
|
+
throw new Error(`${tier} requires a ${field} (${key})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Merge author resolutions onto ALREADY-PROPOSED items and roll up coverage counts.
|
|
136
|
+
*
|
|
137
|
+
* Core operates on `items[]`, never a `proposeFn`: probes have different deterministic
|
|
138
|
+
* surfaces (edge = deterministic propose + LLM resolve; prohibition = LLM propose + deterministic
|
|
139
|
+
* validate/merge), so proposal stays in each adapter and core must not assume it is deterministic.
|
|
140
|
+
*
|
|
141
|
+
* `coverage.resolved` is the COUNT of CLOSED items (`resolved` + `dismissed` status) =
|
|
142
|
+
* `applicable - unresolved` — the pre-re-cut "covered + dismissed + backstop" set,
|
|
143
|
+
* count-preserved. `byVerification` breaks the `resolved`-status items down by tier (each tier
|
|
144
|
+
* initialized to 0). Throws on any invalid resolution, a duplicate, an orphan (a resolution
|
|
145
|
+
* matching no proposed item), or a proposed item whose category is outside `validators.categories`.
|
|
146
|
+
*/
|
|
147
|
+
function analyzeCoverage(items, resolutions = [], validators) {
|
|
148
|
+
if (!Array.isArray(items)) {
|
|
149
|
+
throw new Error('items must be an array');
|
|
150
|
+
}
|
|
151
|
+
const key = (r) => `${r.requirement_id}::${r.category}`;
|
|
152
|
+
const resMap = new Map();
|
|
153
|
+
for (const r of resolutions) {
|
|
154
|
+
validateResolution(r, validators);
|
|
155
|
+
if (resMap.has(key(r))) {
|
|
156
|
+
throw new Error(`duplicate resolution for ${key(r)}`);
|
|
157
|
+
}
|
|
158
|
+
resMap.set(key(r), r);
|
|
159
|
+
}
|
|
160
|
+
const validCategories = new Set(validators.categories);
|
|
161
|
+
const merged = [];
|
|
162
|
+
const itemKeys = new Set();
|
|
163
|
+
for (const item of items) {
|
|
164
|
+
if (!validCategories.has(item.category)) {
|
|
165
|
+
throw new Error(`item ${key(item)} has unknown category "${item.category}" — not one of: ${validators.categories.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
itemKeys.add(key(item));
|
|
168
|
+
const o = resMap.get(key(item));
|
|
169
|
+
if (o) {
|
|
170
|
+
merged.push({ ...item, status: o.status, verification: o.verification ?? null, resolution: o.resolution ?? null, reason: o.reason ?? null });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// No author resolution: the item is rolled up VERBATIM, so its own status/fields must be
|
|
174
|
+
// valid too. The edge adapter only proposes `unresolved` items, but the prohibition adapter
|
|
175
|
+
// (#644) proposes LLM-generated items that arrive already populated — one carrying an
|
|
176
|
+
// out-of-enum status (e.g. the dropped "covered") or `dismissed` with no reason would
|
|
177
|
+
// otherwise be counted closed without validation. An Item is structurally a superset of a
|
|
178
|
+
// Resolution, so the same fail-closed check guards both. (ADR-550 Decision 5 hardens this
|
|
179
|
+
// shared seam for the second adapter; m1.)
|
|
180
|
+
validateResolution(item, validators);
|
|
181
|
+
merged.push(item);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Reject orphan resolutions — a resolution whose (requirement_id, category) matches no
|
|
185
|
+
// proposed item (typo'd category or a non-applicable one) would otherwise be silently
|
|
186
|
+
// dropped, leaving the author believing an item is resolved while the report shows it
|
|
187
|
+
// unresolved (adversarial-review HIGH; preserved from the edge-probe's original engine).
|
|
188
|
+
for (const k of resMap.keys()) {
|
|
189
|
+
if (!itemKeys.has(k)) {
|
|
190
|
+
throw new Error(`unknown resolution for ${k} — no matching proposed item (typo'd category or non-applicable shape?)`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const unresolved = merged.filter((i) => i.status === 'unresolved').length;
|
|
194
|
+
const applicable = merged.length;
|
|
195
|
+
const resolved = applicable - unresolved; // closed set: resolved-status + dismissed
|
|
196
|
+
const byVerification = {};
|
|
197
|
+
for (const tier of validators.verification)
|
|
198
|
+
byVerification[tier] = 0;
|
|
199
|
+
for (const i of merged) {
|
|
200
|
+
if (i.status === 'resolved' && i.verification != null) {
|
|
201
|
+
byVerification[i.verification] = (byVerification[i.verification] ?? 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { items: merged, coverage: { applicable, resolved, unresolved, byVerification } };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
208
|
+
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|
|
209
|
+
* line to stderr and exits 2. A JSON-parse failure or any `analyze` throw is a handled error:
|
|
210
|
+
* stderr + exit 2, never an uncaught stack trace — so the engine's fail-closed validation
|
|
211
|
+
* surfaces at the workflow boundary rather than failing open.
|
|
212
|
+
*/
|
|
213
|
+
function runProbeCli(analyze, options) {
|
|
214
|
+
const argv = options.argv ?? process.argv;
|
|
215
|
+
const readFile = options.readFile ?? ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
216
|
+
const write = options.write ?? ((s) => { process.stdout.write(s); });
|
|
217
|
+
const writeErr = options.writeErr ?? ((s) => { process.stderr.write(s); });
|
|
218
|
+
const exit = options.exit ?? ((code) => { process.exit(code); });
|
|
219
|
+
const reqPath = argv[2];
|
|
220
|
+
const resPath = argv[3];
|
|
221
|
+
if (!reqPath) {
|
|
222
|
+
writeErr(`usage: ${options.usage}\n`);
|
|
223
|
+
exit(2);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let requirements;
|
|
227
|
+
try {
|
|
228
|
+
requirements = JSON.parse(readFile(reqPath));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
writeErr(`error: cannot parse JSON from ${reqPath}: ${errMessage(e)}\n`);
|
|
232
|
+
exit(2);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let resolutions = [];
|
|
236
|
+
if (resPath) {
|
|
237
|
+
try {
|
|
238
|
+
resolutions = JSON.parse(readFile(resPath));
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
writeErr(`error: cannot parse JSON from ${resPath}: ${errMessage(e)}\n`);
|
|
242
|
+
exit(2);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const report = analyze(requirements, resolutions);
|
|
248
|
+
if (!isValidReport(report)) {
|
|
249
|
+
throw new Error('adapter returned a structurally-invalid coverage report (expected { items[], coverage{ applicable, resolved, unresolved, byVerification } })');
|
|
250
|
+
}
|
|
251
|
+
write(`${JSON.stringify(report, null, 2)}\n`);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
writeErr(`error: ${errMessage(e)}\n`);
|
|
255
|
+
exit(2);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -20,8 +20,8 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
20
20
|
const node_os_1 = __importDefault(require("node:os"));
|
|
21
21
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
-
const
|
|
24
|
-
const { output, error, reapStaleTempFiles, ensureGsdTempDir, GSD_TEMP_DIR } =
|
|
23
|
+
const ioModule = require("./io.cjs");
|
|
24
|
+
const { output, error, reapStaleTempFiles, ensureGsdTempDir, GSD_TEMP_DIR } = ioModule;
|
|
25
25
|
// ─── Session I/O Helpers ──────────────────────────────────────────────────────
|
|
26
26
|
function getSessionsDir(overridePath) {
|
|
27
27
|
const dir = overridePath || node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'projects');
|