heyspex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict / drift analysis for hs-ground — same engine as console On Deck
|
|
3
|
+
* ([[HS-EPIC-6RKN-spec-conflict-detection-pre-flight-check]], GET /api/specs/:id/conflicts).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join, basename as pathBasename } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
detectSpecConflicts,
|
|
10
|
+
extractBehavioralClaims,
|
|
11
|
+
findConflicts,
|
|
12
|
+
resolveWikilink,
|
|
13
|
+
extractWikilinks,
|
|
14
|
+
} from './plan-push.mjs';
|
|
15
|
+
|
|
16
|
+
const SOURCE_OF_TRUTH_RULES = [
|
|
17
|
+
{
|
|
18
|
+
layer: 'L1',
|
|
19
|
+
spec: 'heyspex-north-star',
|
|
20
|
+
rule: 'North-star wins on vision and product intent conflicts.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
layer: 'L2',
|
|
24
|
+
spec: 'architecture-pillars',
|
|
25
|
+
rule: 'Pillars win on milestone scope and system-shape conflicts.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
layer: 'memory-decision',
|
|
29
|
+
rule: 'Memory decisions/lessons are authoritative for documented implementation choices.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
layer: 'protocol',
|
|
33
|
+
rule: 'Default: **protocol specs** are source of truth for stable product behavior.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
layer: 'epic',
|
|
37
|
+
rule: 'Epics may supersede protocols only with explicit override ([[hs-resolve]] Option A/C).',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
layer: 'console',
|
|
41
|
+
rule: 'Focused epic conflicts use the same detector as Console **On Deck** and **Specs** (`/api/specs/:id/conflicts`).',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function readSpecContent(vaultRoot, specBasename) {
|
|
46
|
+
const path = resolveWikilink(vaultRoot, specBasename);
|
|
47
|
+
if (!path) return null;
|
|
48
|
+
try {
|
|
49
|
+
return readFileSync(path, 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseFrontmatter(content) {
|
|
56
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
57
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
58
|
+
const body = content.slice(match[0].length).trim();
|
|
59
|
+
const frontmatter = {};
|
|
60
|
+
const lines = match[1].split(/\r?\n/);
|
|
61
|
+
let currentArray = null;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (!line.trim()) continue;
|
|
64
|
+
const arrayItem = line.match(/^\s+-\s*(.*)$/);
|
|
65
|
+
if (arrayItem && currentArray) {
|
|
66
|
+
currentArray.push(arrayItem[1].trim().replace(/^["']|["']$/g, ''));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const keyMatch = line.match(/^(\w[\w_-]*):\s*(.*)$/);
|
|
70
|
+
if (keyMatch) {
|
|
71
|
+
const key = keyMatch[1];
|
|
72
|
+
const value = keyMatch[2].trim();
|
|
73
|
+
currentArray = null;
|
|
74
|
+
if (value === '') {
|
|
75
|
+
frontmatter[key] = [];
|
|
76
|
+
currentArray = frontmatter[key];
|
|
77
|
+
} else {
|
|
78
|
+
frontmatter[key] = value.replace(/^["']|["']$/g, '');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { frontmatter, body };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeConflict(raw, meta) {
|
|
86
|
+
return {
|
|
87
|
+
...raw,
|
|
88
|
+
source: meta.source,
|
|
89
|
+
severity: meta.severity || 'blocker',
|
|
90
|
+
spec_a: meta.spec_a || raw.epic_source || null,
|
|
91
|
+
spec_b: meta.spec_b || raw.spec,
|
|
92
|
+
recommended_source_of_truth: meta.recommended_source_of_truth || 'protocol',
|
|
93
|
+
rationale: meta.rationale || SOURCE_OF_TRUTH_RULES[3].rule,
|
|
94
|
+
resolve_before_context: meta.resolve_before_context !== false,
|
|
95
|
+
console_ui_note:
|
|
96
|
+
meta.source === 'focused_epic'
|
|
97
|
+
? 'Also visible in Console On Deck / Specs editor for the focused epic.'
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function detectPairwiseDrift(vaultRoot, basenameA, basenameB) {
|
|
103
|
+
const contentA = readSpecContent(vaultRoot, basenameA);
|
|
104
|
+
const contentB = readSpecContent(vaultRoot, basenameB);
|
|
105
|
+
if (!contentA || !contentB) return [];
|
|
106
|
+
|
|
107
|
+
const claimsA = extractBehavioralClaims(contentA, basenameA);
|
|
108
|
+
const claimsB = extractBehavioralClaims(contentB, basenameB);
|
|
109
|
+
if (claimsA.length === 0 || claimsB.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
const aVsB = findConflicts(claimsA, claimsB);
|
|
112
|
+
const bVsA = findConflicts(claimsB, claimsA);
|
|
113
|
+
|
|
114
|
+
const merged = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
|
|
117
|
+
for (const c of [...aVsB, ...bVsA]) {
|
|
118
|
+
const key = [basenameA, basenameB, c.action, c.spec_claim].sort().join('|');
|
|
119
|
+
if (seen.has(key)) continue;
|
|
120
|
+
seen.add(key);
|
|
121
|
+
merged.push(
|
|
122
|
+
normalizeConflict(c, {
|
|
123
|
+
source: 'cross_spec_drift',
|
|
124
|
+
spec_a: basenameA,
|
|
125
|
+
spec_b: basenameB,
|
|
126
|
+
epic_source: basenameA,
|
|
127
|
+
recommended_source_of_truth: 'resolve_interactively',
|
|
128
|
+
rationale: `Drift between [[${basenameA}]] and [[${basenameB}]] — pick one authority (usually protocol for behavior, epic only if superseding).`,
|
|
129
|
+
resolve_before_context: true,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return merged;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} vaultRoot
|
|
138
|
+
* @param {object} options
|
|
139
|
+
* @param {string} [options.topic]
|
|
140
|
+
* @param {string} [options.summary] - draft plan / chat description
|
|
141
|
+
* @param {string[]} [options.protocolBasenames]
|
|
142
|
+
* @param {Array<{basename: string, relevance: number, type?: string}>} [options.topSpecs]
|
|
143
|
+
* @param {boolean} [options.check_focus_epic]
|
|
144
|
+
* @param {string} [options.focusSpecPath] - Neon layer-2 focus selection spec_path,
|
|
145
|
+
* injected at the async handler boundary ([[HS-TASK-FSEL]]); takes precedence
|
|
146
|
+
* over the dormant `.heyspex/focus.yaml` on a Neon-up machine.
|
|
147
|
+
*/
|
|
148
|
+
export function analyzeGroundingConflicts(vaultRoot, options = {}) {
|
|
149
|
+
const {
|
|
150
|
+
topic = '',
|
|
151
|
+
summary = '',
|
|
152
|
+
protocolBasenames = [],
|
|
153
|
+
topSpecs = [],
|
|
154
|
+
check_focus_epic = true,
|
|
155
|
+
focusSpecPath = null,
|
|
156
|
+
} = options;
|
|
157
|
+
|
|
158
|
+
const allConflicts = [];
|
|
159
|
+
const linkedSpecsChecked = new Set();
|
|
160
|
+
let focusedEpicId = null;
|
|
161
|
+
let focusedEpicPath = null;
|
|
162
|
+
|
|
163
|
+
// 1) Focused epic (same as hs-resolve / console On Deck). The Neon layer-2
|
|
164
|
+
// selection is injected as `focusSpecPath` at the async handler boundary
|
|
165
|
+
// ([[HS-TASK-FSEL]]). There is no `.heyspex/focus.yaml` fallback
|
|
166
|
+
// ([[no-fallbacks-by-default]]) — when the console is unreachable / no focus
|
|
167
|
+
// is set, the focused-epic conflict check is simply skipped.
|
|
168
|
+
if (check_focus_epic) {
|
|
169
|
+
const focusPath = focusSpecPath;
|
|
170
|
+
if (focusPath && existsSync(join(vaultRoot, focusPath))) {
|
|
171
|
+
try {
|
|
172
|
+
const epicContent = readFileSync(join(vaultRoot, focusPath), 'utf8');
|
|
173
|
+
const { frontmatter } = parseFrontmatter(epicContent);
|
|
174
|
+
if (frontmatter.type === 'epic') {
|
|
175
|
+
focusedEpicId = frontmatter.id || pathBasename(focusPath, '.md');
|
|
176
|
+
focusedEpicPath = focusPath;
|
|
177
|
+
const related = extractWikilinks(frontmatter.related || []);
|
|
178
|
+
const { conflicts, linked_specs_checked } = detectSpecConflicts(
|
|
179
|
+
vaultRoot,
|
|
180
|
+
epicContent,
|
|
181
|
+
related,
|
|
182
|
+
);
|
|
183
|
+
linked_specs_checked.forEach((s) => linkedSpecsChecked.add(s));
|
|
184
|
+
for (const c of conflicts) {
|
|
185
|
+
allConflicts.push(
|
|
186
|
+
normalizeConflict(c, {
|
|
187
|
+
source: 'focused_epic',
|
|
188
|
+
spec_a: focusedEpicId,
|
|
189
|
+
spec_b: c.spec,
|
|
190
|
+
recommended_source_of_truth: 'protocol',
|
|
191
|
+
rationale:
|
|
192
|
+
'On-deck epic vs linked protocol — resolve via /hs-resolve before grounding a new plan on stale claims.',
|
|
193
|
+
resolve_before_context: true,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
/* skip */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2) Draft plan (topic + summary) vs governing protocols
|
|
205
|
+
const draftBody = [
|
|
206
|
+
`# Draft plan: ${topic}`,
|
|
207
|
+
'',
|
|
208
|
+
summary || topic,
|
|
209
|
+
'',
|
|
210
|
+
'## Proposed behavior',
|
|
211
|
+
'',
|
|
212
|
+
summary,
|
|
213
|
+
].join('\n');
|
|
214
|
+
|
|
215
|
+
const protocolSet = [
|
|
216
|
+
...new Set([
|
|
217
|
+
...protocolBasenames,
|
|
218
|
+
...topSpecs.filter((s) => s.type === 'protocol' || String(s.basename).includes('protocol')).map((s) => s.basename),
|
|
219
|
+
]),
|
|
220
|
+
].slice(0, 12);
|
|
221
|
+
|
|
222
|
+
if (draftBody.trim().length > 20 && protocolSet.length > 0) {
|
|
223
|
+
const { conflicts, linked_specs_checked } = detectSpecConflicts(
|
|
224
|
+
vaultRoot,
|
|
225
|
+
draftBody,
|
|
226
|
+
protocolSet,
|
|
227
|
+
);
|
|
228
|
+
linked_specs_checked.forEach((s) => linkedSpecsChecked.add(s));
|
|
229
|
+
for (const c of conflicts) {
|
|
230
|
+
allConflicts.push(
|
|
231
|
+
normalizeConflict(c, {
|
|
232
|
+
source: 'draft_plan',
|
|
233
|
+
spec_a: '(draft plan from hs-ground)',
|
|
234
|
+
spec_b: c.spec,
|
|
235
|
+
recommended_source_of_truth: 'protocol',
|
|
236
|
+
rationale:
|
|
237
|
+
'Draft plan contradicts a governing protocol — align draft or update protocol before using as plan context.',
|
|
238
|
+
resolve_before_context: true,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 3) Cross-spec drift among high-relevance discoveries (pairwise)
|
|
245
|
+
const driftCandidates = topSpecs
|
|
246
|
+
.filter((s) => s.relevance >= 3)
|
|
247
|
+
.slice(0, 6)
|
|
248
|
+
.map((s) => s.basename);
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < driftCandidates.length; i++) {
|
|
251
|
+
for (let j = i + 1; j < driftCandidates.length; j++) {
|
|
252
|
+
const pairDrift = detectPairwiseDrift(vaultRoot, driftCandidates[i], driftCandidates[j]);
|
|
253
|
+
allConflicts.push(...pairDrift);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const conflictedBasenames = new Set();
|
|
258
|
+
for (const c of allConflicts) {
|
|
259
|
+
if (c.spec_b) conflictedBasenames.add(c.spec_b);
|
|
260
|
+
if (c.spec_a && !String(c.spec_a).includes('draft')) conflictedBasenames.add(c.spec_a);
|
|
261
|
+
if (c.spec_a && c.spec_b) {
|
|
262
|
+
conflictedBasenames.add(c.spec_a);
|
|
263
|
+
conflictedBasenames.add(c.spec_b);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const hasBlocking = allConflicts.some((c) => c.resolve_before_context);
|
|
268
|
+
|
|
269
|
+
const recommendations = [];
|
|
270
|
+
if (allConflicts.length > 0) {
|
|
271
|
+
recommendations.push({
|
|
272
|
+
priority: 'blocker',
|
|
273
|
+
action: 'Resolve conflicts before using conflicting specs as authoritative plan context.',
|
|
274
|
+
steps: [
|
|
275
|
+
focusedEpicId
|
|
276
|
+
? `Run \`/hs-resolve ${focusedEpicId}\` or open Console **On Deck** for the focused epic.`
|
|
277
|
+
: 'Run `/hs-resolve` when an epic is on deck, or fix draft/protocol alignment in chat.',
|
|
278
|
+
'Pick source of truth per row in `conflicts` (default: protocol for behavior; north-star for vision).',
|
|
279
|
+
'Re-run `/hs-ground` after resolution so `safe_context_specs` is current.',
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (focusedEpicId && allConflicts.some((c) => c.source === 'focused_epic')) {
|
|
285
|
+
recommendations.push({
|
|
286
|
+
priority: 'info',
|
|
287
|
+
action: 'Console parity',
|
|
288
|
+
steps: [
|
|
289
|
+
`Epic ${focusedEpicId} conflicts match GET /api/specs/${focusedEpicId}/conflicts — same as On Deck UI.`,
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
has_blocking_conflicts: hasBlocking,
|
|
296
|
+
conflict_count: allConflicts.length,
|
|
297
|
+
conflicts: allConflicts,
|
|
298
|
+
linked_specs_checked: [...linkedSpecsChecked],
|
|
299
|
+
focused_epic: focusedEpicId
|
|
300
|
+
? { id: focusedEpicId, path: focusedEpicPath, conflicts_in_ui: true }
|
|
301
|
+
: null,
|
|
302
|
+
source_of_truth_rules: SOURCE_OF_TRUTH_RULES,
|
|
303
|
+
conflicted_spec_basenames: [...conflictedBasenames],
|
|
304
|
+
recommendations,
|
|
305
|
+
resolve_commands: [
|
|
306
|
+
focusedEpicId ? `/hs-resolve ${focusedEpicId}` : '/hs-resolve',
|
|
307
|
+
'Console: On Deck surface → Spec Conflicts section',
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {string[]} suggestedRelated
|
|
314
|
+
* @param {Set<string>|string[]} conflictedBasenames
|
|
315
|
+
*/
|
|
316
|
+
export function computeSafeContextSpecs(suggestedRelated, conflictedBasenames) {
|
|
317
|
+
const blocked = conflictedBasenames instanceof Set
|
|
318
|
+
? conflictedBasenames
|
|
319
|
+
: new Set(conflictedBasenames);
|
|
320
|
+
return suggestedRelated.filter((b) => !blocked.has(b));
|
|
321
|
+
}
|