@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,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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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.
|
|
85
|
-
file: item.
|
|
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
|
|
100
|
+
for (const item of unusedFiles) {
|
|
101
|
+
if (!item || typeof item !== 'object')
|
|
102
|
+
continue;
|
|
90
103
|
findings.push({
|
|
91
|
-
type: '
|
|
92
|
-
message: `
|
|
93
|
-
file: item.
|
|
94
|
-
line:
|
|
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
|
|
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: ${
|
|
102
|
-
file:
|
|
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:
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
|
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:
|
|
1750
|
-
file_path:
|
|
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
|
-
|
|
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);
|