@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.
Files changed (197) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-code-fixer.md +3 -2
  4. package/agents/gsd-debug-session-manager.md +2 -1
  5. package/agents/gsd-debugger.md +4 -3
  6. package/agents/gsd-executor.md +17 -16
  7. package/agents/gsd-intel-updater.md +38 -41
  8. package/agents/gsd-phase-researcher.md +8 -8
  9. package/agents/gsd-plan-checker.md +23 -13
  10. package/agents/gsd-planner.md +32 -188
  11. package/agents/gsd-project-researcher.md +5 -4
  12. package/agents/gsd-research-synthesizer.md +2 -1
  13. package/agents/gsd-ui-researcher.md +2 -1
  14. package/agents/gsd-verifier.md +12 -11
  15. package/bin/install.js +965 -1486
  16. package/commands/gsd/autonomous.md +5 -1
  17. package/commands/gsd/ns-manage.md +8 -1
  18. package/commands/gsd/ns-project.md +5 -0
  19. package/commands/gsd/ns-review.md +4 -1
  20. package/commands/gsd/ns-workflow.md +7 -1
  21. package/commands/gsd/plan-review-convergence.md +5 -4
  22. package/commands/gsd/surface.md +12 -5
  23. package/gemini-extension.json +1 -1
  24. package/gsd-core/bin/gsd-tools.cjs +198 -101
  25. package/gsd-core/bin/gsd_run +20 -0
  26. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  27. package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
  28. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  29. package/gsd-core/bin/lib/check-command-router.cjs +133 -2
  30. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  31. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  32. package/gsd-core/bin/lib/configuration.cjs +4 -34
  33. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  34. package/gsd-core/bin/lib/core.cjs +107 -1817
  35. package/gsd-core/bin/lib/edge-probe.cjs +173 -0
  36. package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
  37. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  38. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  39. package/gsd-core/bin/lib/init.cjs +58 -12
  40. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  41. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  42. package/gsd-core/bin/lib/intel.cjs +3 -3
  43. package/gsd-core/bin/lib/io.cjs +222 -0
  44. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  45. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  46. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  47. package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
  48. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  49. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  50. package/gsd-core/bin/lib/phase.cjs +17 -0
  51. package/gsd-core/bin/lib/probe-core.cjs +257 -0
  52. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  54. package/gsd-core/bin/lib/roadmap.cjs +44 -1
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
  56. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
  57. package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
  58. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
  59. package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
  60. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  61. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  62. package/gsd-core/bin/lib/state-document.cjs +8 -0
  63. package/gsd-core/bin/lib/state.cjs +114 -2
  64. package/gsd-core/bin/lib/surface.cjs +66 -14
  65. package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
  66. package/gsd-core/bin/lib/update-context.cjs +4 -1
  67. package/gsd-core/bin/lib/verify.cjs +104 -1
  68. package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
  69. package/gsd-core/bin/shared/model-catalog.json +11 -6
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
  71. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
  72. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
  73. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
  74. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
  75. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
  76. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
  77. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
  78. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
  79. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
  80. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
  81. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
  82. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
  83. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
  84. package/gsd-core/references/edge-probe.md +261 -0
  85. package/gsd-core/references/planner-antipatterns.md +41 -0
  86. package/gsd-core/references/planner-guidance.md +186 -0
  87. package/gsd-core/references/planner-reviews.md +5 -2
  88. package/gsd-core/templates/phase-prompt.md +7 -7
  89. package/gsd-core/templates/project.md +19 -2
  90. package/gsd-core/templates/spec.md +12 -0
  91. package/gsd-core/templates/summary-complex.md +1 -0
  92. package/gsd-core/templates/summary-minimal.md +1 -0
  93. package/gsd-core/templates/summary-standard.md +1 -0
  94. package/gsd-core/templates/summary.md +1 -0
  95. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  96. package/gsd-core/workflows/add-backlog.md +1 -1
  97. package/gsd-core/workflows/add-phase.md +1 -1
  98. package/gsd-core/workflows/add-tests.md +1 -1
  99. package/gsd-core/workflows/add-todo.md +1 -1
  100. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  101. package/gsd-core/workflows/audit-fix.md +1 -1
  102. package/gsd-core/workflows/audit-milestone.md +1 -1
  103. package/gsd-core/workflows/audit-uat.md +1 -1
  104. package/gsd-core/workflows/autonomous.md +111 -51
  105. package/gsd-core/workflows/check-todos.md +1 -1
  106. package/gsd-core/workflows/cleanup.md +1 -1
  107. package/gsd-core/workflows/code-review-fix.md +6 -4
  108. package/gsd-core/workflows/code-review.md +53 -17
  109. package/gsd-core/workflows/complete-milestone.md +11 -5
  110. package/gsd-core/workflows/debug.md +1 -1
  111. package/gsd-core/workflows/diagnose-issues.md +1 -1
  112. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  113. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  114. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  115. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  116. package/gsd-core/workflows/discuss-phase.md +8 -1
  117. package/gsd-core/workflows/do.md +1 -1
  118. package/gsd-core/workflows/docs-update.md +1 -1
  119. package/gsd-core/workflows/edit-phase.md +1 -1
  120. package/gsd-core/workflows/eval-review.md +4 -1
  121. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  122. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  123. package/gsd-core/workflows/execute-phase.md +8 -1
  124. package/gsd-core/workflows/execute-plan.md +1 -1
  125. package/gsd-core/workflows/explore.md +1 -1
  126. package/gsd-core/workflows/extract-learnings.md +1 -1
  127. package/gsd-core/workflows/forensics.md +1 -1
  128. package/gsd-core/workflows/graduation.md +1 -1
  129. package/gsd-core/workflows/health.md +1 -1
  130. package/gsd-core/workflows/help/modes/full.md +1 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/ingest-docs.md +1 -1
  133. package/gsd-core/workflows/insert-phase.md +1 -1
  134. package/gsd-core/workflows/list-workspaces.md +1 -1
  135. package/gsd-core/workflows/manager.md +1 -1
  136. package/gsd-core/workflows/map-codebase.md +1 -1
  137. package/gsd-core/workflows/milestone-summary.md +1 -1
  138. package/gsd-core/workflows/mvp-phase.md +1 -1
  139. package/gsd-core/workflows/new-milestone.md +9 -1
  140. package/gsd-core/workflows/new-project.md +9 -1
  141. package/gsd-core/workflows/new-workspace.md +1 -1
  142. package/gsd-core/workflows/next.md +1 -1
  143. package/gsd-core/workflows/pause-work.md +1 -1
  144. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  145. package/gsd-core/workflows/plan-phase.md +65 -28
  146. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  147. package/gsd-core/workflows/plant-seed.md +1 -1
  148. package/gsd-core/workflows/profile-user.md +1 -1
  149. package/gsd-core/workflows/progress.md +1 -1
  150. package/gsd-core/workflows/quick.md +2 -2
  151. package/gsd-core/workflows/remove-phase.md +1 -1
  152. package/gsd-core/workflows/remove-workspace.md +1 -1
  153. package/gsd-core/workflows/resume-project.md +1 -1
  154. package/gsd-core/workflows/review.md +1 -1
  155. package/gsd-core/workflows/scan.md +1 -1
  156. package/gsd-core/workflows/secure-phase.md +1 -1
  157. package/gsd-core/workflows/settings-advanced.md +7 -7
  158. package/gsd-core/workflows/settings-integrations.md +1 -1
  159. package/gsd-core/workflows/settings.md +2 -2
  160. package/gsd-core/workflows/ship.md +8 -1
  161. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  162. package/gsd-core/workflows/sketch.md +1 -1
  163. package/gsd-core/workflows/spec-phase.md +130 -1
  164. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  165. package/gsd-core/workflows/spike.md +1 -1
  166. package/gsd-core/workflows/stats.md +1 -1
  167. package/gsd-core/workflows/thread.md +1 -1
  168. package/gsd-core/workflows/transition.md +1 -1
  169. package/gsd-core/workflows/ui-phase.md +1 -1
  170. package/gsd-core/workflows/ui-review.md +1 -1
  171. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  172. package/gsd-core/workflows/update.md +2 -2
  173. package/gsd-core/workflows/validate-phase.md +1 -1
  174. package/gsd-core/workflows/verify-phase.md +1 -1
  175. package/gsd-core/workflows/verify-work.md +8 -1
  176. package/package.json +11 -3
  177. package/scripts/base64-scan.sh +1 -1
  178. package/scripts/changeset/cli.cjs +8 -1
  179. package/scripts/changeset/lint.cjs +38 -2
  180. package/scripts/ci-test-scope.cjs +21 -10
  181. package/scripts/gen-capability-registry.cjs +2293 -0
  182. package/scripts/gen-loop-host-contract.cjs +471 -0
  183. package/scripts/lib/allowlist-ratchet.cjs +101 -1
  184. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  185. package/scripts/lint-regression-test-names.cjs +117 -0
  186. package/scripts/lint-test-file-count.allowlist.json +25 -4
  187. package/scripts/lint-windows-test-portability.cjs +178 -0
  188. package/scripts/prompt-injection-scan.sh +4 -4
  189. package/scripts/research-profiles.cjs +10 -10
  190. package/scripts/run-tests.cjs +133 -29
  191. package/scripts/secret-scan.sh +3 -3
  192. package/scripts/sync-next-version.cjs +133 -0
  193. package/scripts/sync-runtime-launcher.cjs +21 -5
  194. package/scripts/update-size-baseline.cjs +68 -0
  195. package/scripts/workflow-policy.cjs +42 -9
  196. package/scripts/workflow-size.cjs +90 -0
  197. 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 core = require("./core.cjs");
24
- const { output, error, reapStaleTempFiles, ensureGsdTempDir, GSD_TEMP_DIR } = core;
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');