@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,173 @@
1
+ "use strict";
2
+ /**
3
+ * Spec-completeness edge-probe — the FIRST adapter of the probe-core resolution model
4
+ * (ADR-457 build model; ADR-550 Decision 7 seam).
5
+ *
6
+ * The generic resolution lifecycle, the status×verification re-cut, `validateResolution`,
7
+ * `validateRequirement`, the `analyzeCoverage` merge/rollup/orphan-reject engine, and the
8
+ * `runProbeCli` scaffold all live in `src/probe-core.cts`. This module keeps ONLY the
9
+ * edge-specific cluster: the five data/behavior shapes, the closed 8-category edge taxonomy,
10
+ * shape classification, edge proposal, and the `{ explicit, backstop }` verification validators.
11
+ *
12
+ * Authored as strict TypeScript (`src/edge-probe.cts`) and compiled by
13
+ * `tsc -p tsconfig.build.json` to the gitignored runtime artifact
14
+ * `gsd-core/bin/lib/edge-probe.cjs`. Do NOT hand-write the `.cjs`; it is emitted. Tests
15
+ * `require()` the built artifact; `pretest` runs `build:lib` first.
16
+ *
17
+ * Pure and dependency-free: it classifies each requirement's data/behavior shape, filters
18
+ * the closed 8-category edge taxonomy to applicable categories, proposes concrete candidate
19
+ * edges, and (via probe-core) merges author resolutions into a coverage report.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.EDGE_VALIDATORS = exports.TAXONOMY = exports.VALID_SHAPES = exports.SHAPE_CUES = void 0;
23
+ exports.classifyShape = classifyShape;
24
+ exports.applicableCategories = applicableCategories;
25
+ exports.validateRequirement = validateRequirement;
26
+ exports.validateResolution = validateResolution;
27
+ exports.proposeEdges = proposeEdges;
28
+ exports.analyzeCoverage = analyzeCoverage;
29
+ const probe_core_cjs_1 = require("./probe-core.cjs");
30
+ /**
31
+ * Word-boundary cues mapping requirement prose -> data/behavior shape.
32
+ * Heuristic and intentionally lossy; an authored `shapes` array overrides it.
33
+ */
34
+ exports.SHAPE_CUES = {
35
+ 'numeric-range': /\b(round(ing|ed)?|threshold|max(imum)?|min(imum)?|limit|bound(ary)?|between|cap|percent|amount|price|count|number|score|rate|decimal)\b/i,
36
+ 'collection': /\b(lists?|arrays?|sets?|items?|collections?|each|every|all|sort(ed|ing)?|merge|dedupe|group|ranges?|intervals?|overlap(ping)?)\b/i,
37
+ 'text': /\b(string|text|names?|labels?|truncate|substring|char(acter)?s?|length|slug|message|unicode)\b/i,
38
+ 'stateful': /\b(save|persist|store|update|toggle|create|delete|remove|submit|retry|apply|register|insert)\b/i,
39
+ 'io': /\b(files?|requests?|fetch|upload|download|network|api|endpoints?|connections?|sockets?)\b/i,
40
+ };
41
+ /** The locked shape vocabulary — exactly the keys of SHAPE_CUES (single source of truth). */
42
+ exports.VALID_SHAPES = new Set(Object.keys(exports.SHAPE_CUES));
43
+ /** Detect which shapes a requirement's prose matches (heuristic). */
44
+ function classifyShape(text) {
45
+ const shapes = [];
46
+ const subject = String(text == null ? '' : text);
47
+ for (const shape of Object.keys(exports.SHAPE_CUES)) {
48
+ if (exports.SHAPE_CUES[shape].test(subject))
49
+ shapes.push(shape);
50
+ }
51
+ return shapes;
52
+ }
53
+ /**
54
+ * Closed taxonomy of 8 domain-boundary edge categories (established QA names).
55
+ * `shapes` lists which requirement shapes make the category relevant.
56
+ */
57
+ exports.TAXONOMY = [
58
+ { id: 'boundary', name: 'Boundary values', shapes: ['numeric-range'], probe: 'What happens exactly at each min/max/threshold — and one step either side?' },
59
+ { id: 'adjacency', name: 'Adjacency / touching', shapes: ['collection'], probe: 'When two things are exactly equal or just touch, do they merge, collide, or separate?' },
60
+ { id: 'empty', name: 'Empty / degenerate', shapes: ['collection', 'text'], probe: 'What is the result for empty, single-element, or null input?' },
61
+ { id: 'encoding', name: 'Encoding / representation', shapes: ['text'], probe: 'Whose definition of length/equality applies — bytes, code points, grapheme clusters, or normalized form?' },
62
+ { id: 'ordering', name: 'Ordering / stability', shapes: ['collection'], probe: 'When elements compare equal, is output order specified and stable?' },
63
+ { id: 'precision', name: 'Precision / overflow', shapes: ['numeric-range'], probe: 'Where can precision loss, overflow, or rounding/tie-breaking occur — and what is the exact contract (e.g. half-up vs half-to-even, ceil/floor/truncate)?' },
64
+ { id: 'idempotency', name: 'Idempotency / repetition', shapes: ['stateful'], probe: 'What happens if this runs twice on the same input?' },
65
+ { id: 'concurrency', name: 'Concurrency / effect ordering', shapes: ['stateful', 'io'], probe: 'If interrupted or run in parallel, what is guaranteed?' },
66
+ ];
67
+ /** Return taxonomy category ids whose applicable shapes intersect the input set. */
68
+ function applicableCategories(shapes) {
69
+ const set = new Set(shapes);
70
+ return exports.TAXONOMY.filter((c) => c.shapes.some((s) => set.has(s))).map((c) => c.id);
71
+ }
72
+ /**
73
+ * The edge adapter's injected runtime validators (ADR-550 #5). `categories` is the closed
74
+ * taxonomy; both verification tiers require a non-empty `resolution` (an explicit AC's text
75
+ * or a backstop note) so plan-phase has a criterion to lift.
76
+ */
77
+ exports.EDGE_VALIDATORS = {
78
+ categories: exports.TAXONOMY.map((c) => c.id),
79
+ verification: ['explicit', 'backstop'],
80
+ requiredFieldsByVerification: { explicit: ['resolution'], backstop: ['resolution'] },
81
+ };
82
+ /**
83
+ * Validate a single requirement — the generic id/text checks (probe-core) plus the edge's
84
+ * `shapes`-must-be-an-array check. A bare string like `shapes:"numeric-range"` would otherwise
85
+ * fall through to prose classification, silently ignoring the authored override.
86
+ *
87
+ * The edge adapter's `text` is REQUIRED (the prose is the classification signal), so reject a
88
+ * missing/empty `text` when no authored `shapes` override is present. Without this, a `{ id }`
89
+ * requirement classifies to zero shapes → zero edges → it is silently DROPPED from coverage
90
+ * with no signal — the exact fail-open this feature exists to eliminate. An explicit `shapes`
91
+ * array (including `[]` for "no applicable categories") is the legitimate way to opt out of
92
+ * prose classification, so `text` is only required when `shapes` is absent.
93
+ */
94
+ function validateRequirement(requirement) {
95
+ (0, probe_core_cjs_1.validateRequirement)(requirement);
96
+ const r = requirement;
97
+ if (r.shapes != null && !Array.isArray(r.shapes)) {
98
+ throw new Error(`requirement ${requirement.id} shapes must be an array when present`);
99
+ }
100
+ if (r.shapes == null && !(typeof r.text === 'string' && r.text.trim())) {
101
+ throw new Error(`requirement ${requirement.id} text must be a non-empty string when no shapes override is provided`);
102
+ }
103
+ }
104
+ /** Validate an edge resolution against the edge verification vocabulary. */
105
+ function validateResolution(resolution) {
106
+ return (0, probe_core_cjs_1.validateResolution)(resolution, exports.EDGE_VALIDATORS);
107
+ }
108
+ /**
109
+ * Propose candidate edges for a requirement. Uses authored `shapes` when present, else
110
+ * classifies from prose. Every proposed edge starts unresolved (verification null).
111
+ */
112
+ function proposeEdges(requirement) {
113
+ validateRequirement(requirement);
114
+ let shapes;
115
+ if (Array.isArray(requirement.shapes)) {
116
+ // Fail closed: an authored array must contain only locked shape values. A non-empty
117
+ // but invalid array (e.g. ['numeric'], a typo for 'numeric-range') would otherwise
118
+ // intersect no category and silently suppress every probe — the gate reads green while
119
+ // nothing was checked. An empty array stays a valid "no applicable categories" override.
120
+ for (const s of requirement.shapes) {
121
+ if (typeof s !== 'string' || !exports.VALID_SHAPES.has(s)) {
122
+ throw new Error(`invalid shape ${JSON.stringify(s)} for requirement ${requirement.id} — must be one of: ${[...exports.VALID_SHAPES].join(', ')}`);
123
+ }
124
+ }
125
+ shapes = requirement.shapes;
126
+ }
127
+ else {
128
+ shapes = classifyShape(requirement.text);
129
+ }
130
+ return applicableCategories(shapes).map((catId) => {
131
+ const cat = exports.TAXONOMY.find((c) => c.id === catId);
132
+ return {
133
+ requirement_id: requirement.id,
134
+ category: catId,
135
+ status: 'unresolved',
136
+ verification: null,
137
+ resolution: null,
138
+ reason: null,
139
+ probe: cat ? cat.probe : '',
140
+ };
141
+ });
142
+ }
143
+ /**
144
+ * Propose edges for every requirement (deterministic propose), then delegate the
145
+ * merge/rollup/orphan-reject to probe-core. Edge-specific pre-checks: requirements must be an
146
+ * array, requirement ids must be unique. Throws on any invalid resolution.
147
+ */
148
+ function analyzeCoverage(requirements, resolutions = []) {
149
+ if (!Array.isArray(requirements)) {
150
+ throw new Error('requirements must be an array');
151
+ }
152
+ const items = [];
153
+ const seenReqIds = new Set();
154
+ for (const req of requirements) {
155
+ validateRequirement(req);
156
+ if (seenReqIds.has(req.id)) {
157
+ throw new Error(`duplicate requirement id ${JSON.stringify(req.id)}`);
158
+ }
159
+ seenReqIds.add(req.id);
160
+ for (const edge of proposeEdges(req))
161
+ items.push(edge);
162
+ }
163
+ return (0, probe_core_cjs_1.analyzeCoverage)(items, resolutions, exports.EDGE_VALIDATORS);
164
+ }
165
+ /*
166
+ * CLI entry (EP-06 invokable surface): `edge-probe.cjs <requirements.json> [resolutions.json]`.
167
+ * The generic I/O plumbing (parse, fail-closed exit 2, pretty-JSON out) lives in probe-core's
168
+ * `runProbeCli`; the edge adapter supplies its `analyzeCoverage`. Guarded by
169
+ * `require.main === module` so it runs only when the compiled `.cjs` is executed directly.
170
+ */
171
+ if (require.main === module) {
172
+ (0, probe_core_cjs_1.runProbeCli)((requirements, resolutions) => analyzeCoverage(requirements, resolutions), { usage: 'edge-probe.cjs <requirements.json> [resolutions.json]' });
173
+ }
@@ -5,6 +5,9 @@
5
5
  * ADR-457 build-at-publish: the hand-written bin/lib/fallow-runner.cjs
6
6
  * collapsed to a TypeScript source of truth. Behaviour is preserved
7
7
  * byte-for-behaviour from the prior hand-written .cjs; only types are added.
8
+ *
9
+ * Parses the real fallow `audit --format json` schema (schema_version 3
10
+ * envelope, nested dead_code/duplication sections). See fallow 2.70.0+.
8
11
  */
9
12
  var __importDefault = (this && this.__importDefault) || function (mod) {
10
13
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -13,6 +16,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.resolveFallowBinary = resolveFallowBinary;
14
17
  exports.requireFallowBinary = requireFallowBinary;
15
18
  exports.normalizeFallowReport = normalizeFallowReport;
19
+ exports.normalizeFallowReportFile = normalizeFallowReportFile;
16
20
  const node_fs_1 = __importDefault(require("node:fs"));
17
21
  const node_path_1 = __importDefault(require("node:path"));
18
22
  function candidateNames() {
@@ -68,48 +72,82 @@ function requireFallowBinary({ cwd, envPath = process.env['PATH'] ?? '' }) {
68
72
  throw new Error('Fallow is enabled but no binary was found. Please install fallow via `npm install -D fallow` or `cargo install fallow`.');
69
73
  }
70
74
  function normalizeFallowReport(report) {
71
- const unused = Array.isArray(report?.unusedExports)
72
- ? report.unusedExports
73
- : [];
74
- const duplicates = Array.isArray(report?.duplicates)
75
- ? report.duplicates
76
- : [];
77
- const circular = Array.isArray(report?.circularDependencies)
78
- ? report.circularDependencies
79
- : [];
75
+ const deadCodeRaw = report?.dead_code;
76
+ const duplicationRaw = report?.duplication;
77
+ const unusedExports = (Array.isArray(deadCodeRaw?.unused_exports)
78
+ ? (deadCodeRaw?.unused_exports ?? [])
79
+ : []).filter((x) => x !== null && typeof x === 'object');
80
+ const unusedFiles = (Array.isArray(deadCodeRaw?.unused_files)
81
+ ? (deadCodeRaw?.unused_files ?? [])
82
+ : []).filter((x) => x !== null && typeof x === 'object');
83
+ const circularDeps = (Array.isArray(deadCodeRaw?.circular_dependencies)
84
+ ? (deadCodeRaw?.circular_dependencies ?? [])
85
+ : []).filter((x) => x !== null && typeof x === 'object');
86
+ const cloneGroups = (Array.isArray(duplicationRaw?.clone_groups)
87
+ ? (duplicationRaw?.clone_groups ?? [])
88
+ : []).filter((x) => x !== null && typeof x === 'object');
80
89
  const findings = [];
81
- for (const item of unused) {
90
+ for (const item of unusedExports) {
91
+ if (!item || typeof item !== 'object')
92
+ continue;
82
93
  findings.push({
83
94
  type: 'unused_export',
84
- message: `Unused export ${item.symbol ?? '<unknown>'}`,
85
- file: item.file ?? '',
95
+ message: `Unused export ${item.export_name ?? '<unknown>'}`,
96
+ file: item.path ?? '',
86
97
  line: item.line ?? null,
87
98
  });
88
99
  }
89
- for (const item of duplicates) {
100
+ for (const item of unusedFiles) {
101
+ if (!item || typeof item !== 'object')
102
+ continue;
90
103
  findings.push({
91
- type: 'duplicate_block',
92
- message: `Duplicate block (${Math.round((item.similarity ?? 0) * 100)}% similarity)`,
93
- file: item.left?.file ?? '',
94
- line: item.left?.start ?? null,
95
- related_file: item.right?.file ?? '',
104
+ type: 'unused_file',
105
+ message: `Unused file ${item.path ?? '<unknown>'}`,
106
+ file: item.path ?? '',
107
+ line: null,
96
108
  });
97
109
  }
98
- for (const item of circular) {
110
+ for (const item of circularDeps) {
111
+ if (!item || typeof item !== 'object')
112
+ continue;
113
+ const files = Array.isArray(item.files) ? item.files : [];
99
114
  findings.push({
100
115
  type: 'circular_dependency',
101
- message: `Circular dependency: ${(item.cycle ?? []).join(' -> ')}`,
102
- file: Array.isArray(item.cycle) && item.cycle.length > 0 ? item.cycle[0] : '',
103
- line: null,
116
+ message: `Circular dependency: ${files.join(' -> ')}`,
117
+ file: files.length > 0 ? files[0] : '',
118
+ line: item.line ?? null,
119
+ });
120
+ }
121
+ for (const group of cloneGroups) {
122
+ if (!group || typeof group !== 'object')
123
+ continue;
124
+ const instances = Array.isArray(group.instances) ? group.instances : [];
125
+ findings.push({
126
+ type: 'duplicate_block',
127
+ message: `Duplicate block (${instances.length} instances)`,
128
+ file: instances[0]?.file ?? '',
129
+ line: instances[0]?.start_line ?? null,
130
+ related_file: instances[1]?.file ?? '',
104
131
  });
105
132
  }
106
133
  return {
107
134
  summary: {
108
- unused_exports: unused.length,
109
- duplicates: duplicates.length,
110
- circular_dependencies: circular.length,
135
+ unused_exports: unusedExports.length,
136
+ unused_files: unusedFiles.length,
137
+ duplicates: cloneGroups.length,
138
+ circular_dependencies: circularDeps.length,
111
139
  total: findings.length,
112
140
  },
113
141
  findings,
114
142
  };
115
143
  }
144
+ function normalizeFallowReportFile(filePath) {
145
+ try {
146
+ const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
147
+ const parsed = JSON.parse(raw);
148
+ return normalizeFallowReport(parsed);
149
+ }
150
+ catch {
151
+ return normalizeFallowReport(null);
152
+ }
153
+ }
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Federated Config — Defensive merge of capability-declared config keys
4
+ *
5
+ * ADR-857 phase 3b: wires the Capability Registry's configSchema into
6
+ * loadConfig as a provably-empty no-op channel until capability keys are
7
+ * migrated out of the central config-schema.
8
+ *
9
+ * Exported function:
10
+ * mergeFederatedConfig({ configSchema, isCentralKey, userConfig })
11
+ * → { values, validKeys, warnings }
12
+ *
13
+ * Design:
14
+ * - For each key in configSchema:
15
+ * If isCentralKey(key) → SKIP; push a pending-migration warning.
16
+ * Else if slice is malformed → SKIP; push a warning. Never throw.
17
+ * Else (valid federated key absent from central):
18
+ * resolvedValue = nested userConfig lookup if present & type-matches; else slice.default.
19
+ * Add key→resolvedValue to values; add key to validKeys.
20
+ * - Guard all object writes with inline literal __proto__/constructor/prototype checks.
21
+ * - Zero external dependencies; no ajv; hand-rolled type checks only.
22
+ *
23
+ * ADR-857 no-op guarantee:
24
+ * With the current registry, every UI key is still present in the central
25
+ * config-schema, so isCentralKey() returns true for all of them and values
26
+ * is always empty. The channel is live but carries no traffic until a key
27
+ * is atomically removed from the central schema (the cutover step).
28
+ *
29
+ * Dependencies: none (zero-dep module).
30
+ */
31
+ // ─── Allowed slice types (mirrors gen-capability-registry.cjs VALID_CONFIG_SLICE_TYPES) ──
32
+ const VALID_SLICE_TYPES = new Set(['boolean', 'string', 'number', 'enum']);
33
+ // ─── Internal helpers ──────────────────────────────────────────────────────────
34
+ /**
35
+ * Returns true if `slice` has a non-empty type, a `default` property, and a
36
+ * non-empty string description. Does NOT throw.
37
+ */
38
+ function _isWellFormedSlice(slice) {
39
+ if (typeof slice !== 'object' || slice === null || Array.isArray(slice))
40
+ return false;
41
+ const s = slice;
42
+ if (typeof s['type'] !== 'string' || s['type'].length === 0)
43
+ return false;
44
+ if (!VALID_SLICE_TYPES.has(s['type']))
45
+ return false;
46
+ if (!Object.prototype.hasOwnProperty.call(s, 'default'))
47
+ return false;
48
+ return true;
49
+ }
50
+ /**
51
+ * Returns true if `value` matches the declared type in the slice.
52
+ * For enum, also validates against slice.values if present.
53
+ */
54
+ function _typeMatches(value, slice) {
55
+ switch (slice.type) {
56
+ case 'boolean': return typeof value === 'boolean';
57
+ case 'string': return typeof value === 'string';
58
+ case 'number': return typeof value === 'number';
59
+ case 'enum':
60
+ // Must be a string AND, if values list is present, must be in it
61
+ if (typeof value !== 'string')
62
+ return false;
63
+ if (Array.isArray(slice.values) && slice.values.length > 0) {
64
+ return slice.values.includes(value);
65
+ }
66
+ return true;
67
+ default: return false;
68
+ }
69
+ }
70
+ /**
71
+ * Traverse a dotted key path through a nested config object.
72
+ * E.g. key="workflow.ui_phase", obj={workflow:{ui_phase:false}} → {found:true, value:false}
73
+ * Returns {found:false} if any segment is missing or not an own property.
74
+ * Handles 1, 2, or N segments generically.
75
+ */
76
+ function _getNestedValue(obj, key) {
77
+ const segments = key.split('.');
78
+ let current = obj;
79
+ for (let i = 0; i < segments.length; i++) {
80
+ const seg = segments[i];
81
+ // Inline literal prototype-pollution guard
82
+ if (seg === '__proto__' || seg === 'constructor' || seg === 'prototype') {
83
+ return { found: false, value: undefined };
84
+ }
85
+ if (typeof current !== 'object' || current === null) {
86
+ return { found: false, value: undefined };
87
+ }
88
+ const cur = current;
89
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
90
+ return { found: false, value: undefined };
91
+ }
92
+ current = cur[seg];
93
+ }
94
+ return { found: true, value: current };
95
+ }
96
+ // ─── Public API ────────────────────────────────────────────────────────────────
97
+ /**
98
+ * Defensive merge of capability-declared config slices into the loadConfig
99
+ * return value.
100
+ *
101
+ * DEFENSIVE contract (never throws, even on bad capability data):
102
+ * - Null/undefined/non-object input → returns empty result.
103
+ * - Null/undefined/non-object userConfig → treated as {} (no overrides).
104
+ * - Central keys are skipped with a pending-migration warning.
105
+ * - Malformed slices are skipped with a warning.
106
+ * - User-supplied values with wrong types (or out-of-enum values) fall back
107
+ * to the slice default (a type-mismatch warning is pushed but the key is
108
+ * still federated with its default; this is best-effort degraded operation).
109
+ */
110
+ function mergeFederatedConfig(input) {
111
+ // FIX 4: Guard null/undefined/non-object input
112
+ if (input === null || input === undefined || typeof input !== 'object') {
113
+ return { values: Object.create(null), validKeys: [], warnings: [] };
114
+ }
115
+ const { configSchema, isCentralKey } = input;
116
+ // FIX 4: Guard null/undefined/non-object userConfig — treat as {}
117
+ const userConfig = (input.userConfig !== null && input.userConfig !== undefined && typeof input.userConfig === 'object' && !Array.isArray(input.userConfig))
118
+ ? input.userConfig
119
+ : {};
120
+ // FIX 6b: Use null-prototype object for all return paths
121
+ const values = Object.create(null);
122
+ const validKeys = [];
123
+ const warnings = [];
124
+ if (typeof configSchema !== 'object' || configSchema === null) {
125
+ return { values: Object.create(null), validKeys: [], warnings: [] };
126
+ }
127
+ for (const key of Object.keys(configSchema)) {
128
+ // S2: inline literal prototype-pollution guard (CodeQL barrier)
129
+ // Guard both the full key AND all dotted-path segments
130
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
131
+ continue;
132
+ const _keySegments = key.split('.');
133
+ if (_keySegments.some((s) => s === '__proto__' || s === 'constructor' || s === 'prototype'))
134
+ continue;
135
+ const slice = configSchema[key];
136
+ // If this key is still in the central schema → pending migration, skip
137
+ try {
138
+ if (isCentralKey(key)) {
139
+ warnings.push('federated-config: key "' + key + '" is still in the central config-schema (pending-migration); ' +
140
+ 'skipping federated resolution until the central schema entry is removed');
141
+ continue;
142
+ }
143
+ }
144
+ catch {
145
+ // isCentralKey threw — treat as unknown, skip defensively
146
+ warnings.push('federated-config: isCentralKey("' + key + '") threw; skipping key');
147
+ continue;
148
+ }
149
+ // Validate slice shape — skip malformed entries
150
+ if (!_isWellFormedSlice(slice)) {
151
+ warnings.push('federated-config: config slice for key "' + key + '" is malformed (missing or invalid type/default); skipping');
152
+ continue;
153
+ }
154
+ const sliceEntry = slice;
155
+ // FIX 1: Resolve value using NESTED dotted-path lookup through userConfig
156
+ let resolvedValue = sliceEntry.default;
157
+ const { found: userHasKey, value: userValue } = _getNestedValue(userConfig, key);
158
+ if (userHasKey && userValue !== undefined) {
159
+ // FIX 5b: For enum, validate against slice.values if present; otherwise check type
160
+ if (_typeMatches(userValue, sliceEntry)) {
161
+ resolvedValue = userValue;
162
+ }
163
+ else {
164
+ const typeDesc = sliceEntry.type === 'enum' && Array.isArray(sliceEntry.values)
165
+ ? 'enum(' + sliceEntry.values.join('|') + ')'
166
+ : sliceEntry.type;
167
+ warnings.push('federated-config: user-supplied value for "' + key + '" has wrong type or invalid enum value ' +
168
+ '(expected ' + typeDesc + ', got ' + typeof userValue +
169
+ (typeof userValue === 'string' ? ' "' + String(userValue) + '"' : '') +
170
+ '); falling back to slice default');
171
+ // resolvedValue stays as slice default
172
+ }
173
+ }
174
+ // S2: inline literal guard before writing to values
175
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
176
+ values[key] = resolvedValue;
177
+ validKeys.push(key);
178
+ }
179
+ }
180
+ return { values, validKeys, warnings };
181
+ }
182
+ module.exports = { mergeFederatedConfig };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+ /**
3
+ * Graphify command router — CLI subcommand dispatcher for `gsd-tools graphify`.
4
+ *
5
+ * ADR-959 (phase 4d-impl-2) pilot: first real capability command cutover.
6
+ * Extracted from the hardcoded `case 'graphify':` arm in gsd-tools.cjs.
7
+ * Behaviour is preserved byte-for-behaviour from the prior inline case;
8
+ * the dispatch path now flows: default → dispatchCapabilityCommand →
9
+ * require(graphify-command-router.cjs) → routeGraphifyCommand.
10
+ *
11
+ * Router signature: { args, cwd, raw, error } — identical to the 12 existing
12
+ * host routers. No new handler/arg convention; the capability registry
13
+ * discovers this router by name.
14
+ *
15
+ * Arg indexing (preserved exactly from the original case):
16
+ * args[0] = 'graphify' (family — matched by dispatchCapabilityCommand)
17
+ * args[1] = subcommand (query | status | diff | build)
18
+ * args[2] = term (query) | 'snapshot' (build snapshot)
19
+ * args.indexOf('--budget') + 1 = budget value
20
+ *
21
+ * Test seam: pass `_graphify` in the options object to inject a recording mock
22
+ * instead of the real graphify module. The `_`-prefix follows the repo's
23
+ * established seam convention (see other routers). Production callers omit it.
24
+ */
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const graphify = require("./graphify.cjs");
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const core = require("./core.cjs");
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const io = require("./io.cjs");
31
+ const { ERROR_REASON } = io;
32
+ // ─── Implementation ───────────────────────────────────────────────────────────
33
+ function routeGraphifyCommand({ args, cwd, raw, error, _graphify }) {
34
+ const subcommand = args[1];
35
+ const g = _graphify ?? graphify;
36
+ if (subcommand === 'query') {
37
+ const term = args[2];
38
+ if (!term) {
39
+ error('Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
40
+ return;
41
+ }
42
+ const budgetIdx = args.indexOf('--budget');
43
+ let budget = null;
44
+ if (budgetIdx !== -1) {
45
+ const rawBudget = args[budgetIdx + 1];
46
+ if (rawBudget === undefined || Number.isNaN(parseInt(rawBudget, 10))) {
47
+ error('Usage: gsd-tools graphify query <term> [--budget <N>]', ERROR_REASON.USAGE);
48
+ return;
49
+ }
50
+ budget = parseInt(rawBudget, 10);
51
+ }
52
+ core.output(g.graphifyQuery(cwd, term, { budget }), raw);
53
+ }
54
+ else if (subcommand === 'status') {
55
+ core.output(g.graphifyStatus(cwd), raw);
56
+ }
57
+ else if (subcommand === 'diff') {
58
+ core.output(g.graphifyDiff(cwd), raw);
59
+ }
60
+ else if (subcommand === 'build') {
61
+ if (args[2] === 'snapshot') {
62
+ core.output(g.writeSnapshot(cwd), raw);
63
+ }
64
+ else {
65
+ core.output(g.graphifyBuild(cwd), raw);
66
+ }
67
+ }
68
+ else {
69
+ error('Unknown graphify subcommand. Available: build, query, status, diff', ERROR_REASON.SDK_UNKNOWN_COMMAND);
70
+ }
71
+ }
72
+ module.exports = {
73
+ routeGraphifyCommand,
74
+ };
@@ -1718,16 +1718,22 @@ function buildSkillManifest(cwd, skillsDir = null) {
1718
1718
  roots.push(rootSummary);
1719
1719
  continue;
1720
1720
  }
1721
- let skillCount = 0;
1722
- for (const entry of entries) {
1723
- if (!entry.isDirectory())
1724
- continue;
1725
- const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md');
1726
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath);
1727
- if (content === null)
1728
- continue;
1721
+ // Track skill names seen within this root to deduplicate dual-routed concretes
1722
+ // (e.g. spec-phase nested under both gsd-ns-workflow and gsd-ns-manage).
1723
+ const seenNamesInRoot = new Set();
1724
+ function pushSkillEntry(
1725
+ // relPath must use forward slashes on all platforms (manifest paths are
1726
+ // posix-style for cross-platform stability; flat entries use template
1727
+ // literals that always produce '/'; nested entries are joined below
1728
+ // with explicit '/' separators rather than path.join).
1729
+ relPath, content) {
1729
1730
  const frontmatter = extractFrontmatter(content);
1730
- const name = frontmatter['name'] || entry.name;
1731
+ const dirPart = relPath.replace(/\/SKILL\.md$/, '');
1732
+ const stem = dirPart.includes('/') ? dirPart.split('/').pop() : dirPart;
1733
+ const name = frontmatter['name'] || stem;
1734
+ if (seenNamesInRoot.has(name))
1735
+ return false; // dedupe dual-routed concretes
1736
+ seenNamesInRoot.add(name);
1731
1737
  const description = frontmatter['description'] || '';
1732
1738
  const triggers = [];
1733
1739
  const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/);
@@ -1746,14 +1752,54 @@ function buildSkillManifest(cwd, skillsDir = null) {
1746
1752
  name,
1747
1753
  description,
1748
1754
  triggers,
1749
- path: entry.name,
1750
- file_path: `${entry.name}/SKILL.md`,
1755
+ path: dirPart,
1756
+ file_path: relPath,
1751
1757
  root: rootInfo.root,
1752
1758
  scope: rootInfo.scope,
1753
1759
  installed: rootInfo.scope !== 'import-only',
1754
1760
  deprecated: !!rootInfo.deprecated,
1755
1761
  });
1756
- skillCount++;
1762
+ return true;
1763
+ }
1764
+ let skillCount = 0;
1765
+ for (const entry of entries) {
1766
+ if (!entry.isDirectory())
1767
+ continue;
1768
+ const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md');
1769
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath);
1770
+ if (content !== null) {
1771
+ if (pushSkillEntry(`${entry.name}/SKILL.md`, content))
1772
+ skillCount++;
1773
+ }
1774
+ // Nested layout: <entry>/skills/<stem>/SKILL.md
1775
+ // Used by cline, qwen, hermes, augment, trae, antigravity (#69 nested=true).
1776
+ // Descend exactly one level into <entry>/skills/ — no deeper recursion.
1777
+ // Scope to gsd-ns-* routers only: never vacuum up an unrelated user skill
1778
+ // that happens to have its own `skills/` subdirectory.
1779
+ if (!entry.name.startsWith('gsd-ns-'))
1780
+ continue;
1781
+ const nestedSkillsDir = node_path_1.default.join(rootPath, entry.name, 'skills');
1782
+ let nestedEntries = [];
1783
+ try {
1784
+ nestedEntries = node_fs_1.default.readdirSync(nestedSkillsDir, { withFileTypes: true });
1785
+ }
1786
+ catch {
1787
+ // No skills/ subdir — flat layout or unreadable; nothing to do.
1788
+ nestedEntries = [];
1789
+ }
1790
+ for (const nested of nestedEntries) {
1791
+ if (!nested.isDirectory())
1792
+ continue;
1793
+ const nestedSkillMd = node_path_1.default.join(nestedSkillsDir, nested.name, 'SKILL.md');
1794
+ const nestedContent = (0, shell_command_projection_cjs_1.platformReadSync)(nestedSkillMd);
1795
+ if (nestedContent === null)
1796
+ continue;
1797
+ // Use forward-slash separator explicitly so manifest paths are posix-style
1798
+ // on all platforms, matching the flat-layout behaviour above.
1799
+ const relPath = `${entry.name}/skills/${nested.name}/SKILL.md`;
1800
+ if (pushSkillEntry(relPath, nestedContent))
1801
+ skillCount++;
1802
+ }
1757
1803
  }
1758
1804
  rootSummary.skill_count = skillCount;
1759
1805
  roots.push(rootSummary);