godpowers 1.6.14 → 1.6.15
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/CHANGELOG.md +37 -0
- package/README.md +23 -12
- package/RELEASE.md +60 -52
- package/SKILL.md +11 -1
- package/lib/planning-systems.js +479 -0
- package/lib/reverse-sync.js +7 -1
- package/lib/source-sync.js +220 -0
- package/package.json +3 -3
- package/routing/god-migrate.yaml +61 -0
- package/schema/state.v1.json +57 -0
- package/skills/god-doctor.md +1 -1
- package/skills/god-init.md +10 -0
- package/skills/god-migrate.md +146 -0
- package/skills/god-sync.md +7 -2
- package/skills/god-version.md +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning System Migration
|
|
3
|
+
*
|
|
4
|
+
* Detects GSD, BMAD, and Superpowers project artifacts, converts their useful
|
|
5
|
+
* signals into Godpowers preparation artifacts, and records source-system state
|
|
6
|
+
* so /god-sync can later write progress back through lib/source-sync.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
const state = require('./state');
|
|
14
|
+
|
|
15
|
+
const MAX_FILE_BYTES = 80 * 1024;
|
|
16
|
+
const MAX_SYSTEM_FILES = 80;
|
|
17
|
+
const TEXT_EXTENSIONS = new Set(['.md', '.mdx', '.txt', '.json', '.yaml', '.yml', '.toml']);
|
|
18
|
+
const SKIP_DIRS = new Set([
|
|
19
|
+
'.git',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'dist',
|
|
22
|
+
'build',
|
|
23
|
+
'.next',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'.cache',
|
|
26
|
+
'coverage',
|
|
27
|
+
'vendor'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const GODPOWERS_ARTIFACTS = [
|
|
31
|
+
{
|
|
32
|
+
key: 'prd',
|
|
33
|
+
tier: 'tier-1',
|
|
34
|
+
subStep: 'prd',
|
|
35
|
+
artifact: 'prd/PRD.md',
|
|
36
|
+
title: 'Imported PRD Seed',
|
|
37
|
+
sourceKinds: ['requirements', 'product', 'spec', 'story']
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'arch',
|
|
41
|
+
tier: 'tier-1',
|
|
42
|
+
subStep: 'arch',
|
|
43
|
+
artifact: 'arch/ARCH.md',
|
|
44
|
+
title: 'Imported Architecture Seed',
|
|
45
|
+
sourceKinds: ['architecture', 'technical', 'context']
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'roadmap',
|
|
49
|
+
tier: 'tier-1',
|
|
50
|
+
subStep: 'roadmap',
|
|
51
|
+
artifact: 'roadmap/ROADMAP.md',
|
|
52
|
+
title: 'Imported Roadmap Seed',
|
|
53
|
+
sourceKinds: ['roadmap', 'phase', 'epic', 'story', 'state', 'sprint']
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'stack',
|
|
57
|
+
tier: 'tier-1',
|
|
58
|
+
subStep: 'stack',
|
|
59
|
+
artifact: 'stack/DECISION.md',
|
|
60
|
+
title: 'Imported Stack Seed',
|
|
61
|
+
sourceKinds: ['stack', 'technical', 'config', 'context']
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'build',
|
|
65
|
+
tier: 'tier-2',
|
|
66
|
+
subStep: 'build',
|
|
67
|
+
artifact: 'build/STATE.md',
|
|
68
|
+
title: 'Imported Build State Seed',
|
|
69
|
+
sourceKinds: ['plan', 'summary', 'verification', 'review', 'uat', 'sprint']
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const SYSTEMS = {
|
|
74
|
+
gsd: {
|
|
75
|
+
displayName: 'GSD',
|
|
76
|
+
markerPaths: ['.planning', '.gsd', 'GSD.md'],
|
|
77
|
+
fileRoots: ['.planning', '.gsd'],
|
|
78
|
+
standalonePatterns: [/^gsd.*\.md$/i]
|
|
79
|
+
},
|
|
80
|
+
bmad: {
|
|
81
|
+
displayName: 'BMAD',
|
|
82
|
+
markerPaths: ['_bmad', '_bmad-output', '.bmad-core', 'bmad-core', '.bmad', 'BMAD.md'],
|
|
83
|
+
fileRoots: ['_bmad-output', '.bmad', '.bmad-core', 'bmad-core', 'docs'],
|
|
84
|
+
standalonePatterns: [/^BMAD\.md$/i]
|
|
85
|
+
},
|
|
86
|
+
superpowers: {
|
|
87
|
+
displayName: 'Superpowers',
|
|
88
|
+
markerPaths: ['docs/superpowers', '.superpowers', 'superpowers', 'SUPERPOWERS.md'],
|
|
89
|
+
fileRoots: ['docs/superpowers', '.superpowers', 'superpowers'],
|
|
90
|
+
standalonePatterns: [/^SUPERPOWERS\.md$/i]
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function rel(projectRoot, absPath) {
|
|
95
|
+
return path.relative(projectRoot, absPath).split(path.sep).join('/');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function exists(projectRoot, relPath) {
|
|
99
|
+
return fs.existsSync(path.join(projectRoot, relPath));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureDir(filePath) {
|
|
103
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sha(input) {
|
|
107
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hashFiles(projectRoot, files) {
|
|
111
|
+
const h = crypto.createHash('sha256');
|
|
112
|
+
for (const file of files.map((f) => f.path).sort()) {
|
|
113
|
+
const full = path.join(projectRoot, file);
|
|
114
|
+
h.update(file);
|
|
115
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
116
|
+
h.update(fs.readFileSync(full));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return `sha256:${h.digest('hex')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isTextFile(filePath) {
|
|
123
|
+
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readText(projectRoot, relPath) {
|
|
127
|
+
const full = path.join(projectRoot, relPath);
|
|
128
|
+
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) return '';
|
|
129
|
+
const size = fs.statSync(full).size;
|
|
130
|
+
const buffer = fs.readFileSync(full);
|
|
131
|
+
const raw = buffer.slice(0, Math.min(size, MAX_FILE_BYTES)).toString('utf8');
|
|
132
|
+
return raw.replace(/\r\n/g, '\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function walkFiles(rootDir, projectRoot, out = []) {
|
|
136
|
+
if (!fs.existsSync(rootDir) || out.length >= MAX_SYSTEM_FILES) return out;
|
|
137
|
+
const stat = fs.statSync(rootDir);
|
|
138
|
+
if (stat.isFile()) {
|
|
139
|
+
if (isTextFile(rootDir)) out.push(rel(projectRoot, rootDir));
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
if (!stat.isDirectory()) return out;
|
|
143
|
+
|
|
144
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true })
|
|
145
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (out.length >= MAX_SYSTEM_FILES) break;
|
|
148
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
149
|
+
const full = path.join(rootDir, entry.name);
|
|
150
|
+
if (entry.isDirectory()) walkFiles(full, projectRoot, out);
|
|
151
|
+
else if (entry.isFile() && isTextFile(full)) out.push(rel(projectRoot, full));
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function classifyFile(filePath, content) {
|
|
157
|
+
const lower = filePath.toLowerCase();
|
|
158
|
+
const body = `${lower}\n${content.slice(0, 3000).toLowerCase()}`;
|
|
159
|
+
const kinds = new Set();
|
|
160
|
+
|
|
161
|
+
if (/requirements|requirement|prd|p-must|fr-|nfr|acceptance/.test(body)) kinds.add('requirements');
|
|
162
|
+
if (/project\.md|product|persona|user|problem|outcome|brief|prfaq|spec/.test(body)) kinds.add('product');
|
|
163
|
+
if (/roadmap|milestone|phase|backlog|dependency|depends on/.test(body)) kinds.add('roadmap');
|
|
164
|
+
if (/epic|story|sprint|sprint-status/.test(body)) {
|
|
165
|
+
kinds.add('story');
|
|
166
|
+
kinds.add('sprint');
|
|
167
|
+
}
|
|
168
|
+
if (/architecture|adr|container|service|integration|api|database/.test(body)) kinds.add('architecture');
|
|
169
|
+
if (/stack|framework|runtime|package|dependency|config/.test(body)) kinds.add('stack');
|
|
170
|
+
if (/plan\.md|implementation plan|task|wave/.test(body)) kinds.add('plan');
|
|
171
|
+
if (/summary|verification|review|uat|test/.test(body)) {
|
|
172
|
+
if (/summary/.test(body)) kinds.add('summary');
|
|
173
|
+
if (/verification/.test(body)) kinds.add('verification');
|
|
174
|
+
if (/review/.test(body)) kinds.add('review');
|
|
175
|
+
if (/uat|acceptance/.test(body)) kinds.add('uat');
|
|
176
|
+
}
|
|
177
|
+
if (/context|rules|conventions|preferences/.test(body)) kinds.add('context');
|
|
178
|
+
if (/technical|research|security|risk/.test(body)) kinds.add('technical');
|
|
179
|
+
if (/design|ux|ui|screen|component|interaction/.test(body)) kinds.add('spec');
|
|
180
|
+
|
|
181
|
+
if (kinds.size === 0) kinds.add('context');
|
|
182
|
+
return [...kinds].sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractSignals(content) {
|
|
186
|
+
const lines = content.split('\n');
|
|
187
|
+
const signals = [];
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const trimmed = line.trim();
|
|
190
|
+
if (!trimmed) continue;
|
|
191
|
+
if (/^#{1,3}\s+/.test(trimmed)) signals.push(trimmed.replace(/^#{1,3}\s+/, ''));
|
|
192
|
+
if (/^[-*]\s+\[[ xX]\]/.test(trimmed)) signals.push(trimmed.replace(/^[-*]\s+/, ''));
|
|
193
|
+
if (signals.length >= 8) break;
|
|
194
|
+
}
|
|
195
|
+
return signals;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findStandaloneFiles(projectRoot, system) {
|
|
199
|
+
const rootEntries = fs.existsSync(projectRoot)
|
|
200
|
+
? fs.readdirSync(projectRoot, { withFileTypes: true })
|
|
201
|
+
: [];
|
|
202
|
+
return rootEntries
|
|
203
|
+
.filter((entry) => entry.isFile())
|
|
204
|
+
.map((entry) => entry.name)
|
|
205
|
+
.filter((name) => system.standalonePatterns.some((pattern) => pattern.test(name)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function detectSystem(projectRoot, id, system) {
|
|
209
|
+
const markerHits = [];
|
|
210
|
+
for (const marker of system.markerPaths) {
|
|
211
|
+
if (exists(projectRoot, marker)) markerHits.push(marker);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (id === 'superpowers') {
|
|
215
|
+
for (const skillRoot of ['.claude/skills', '.codex/skills']) {
|
|
216
|
+
const full = path.join(projectRoot, skillRoot);
|
|
217
|
+
if (!fs.existsSync(full)) continue;
|
|
218
|
+
const names = fs.readdirSync(full).join('\n').toLowerCase();
|
|
219
|
+
if (/superpowers|brainstorming|writing-plans|test-driven-development|subagent-driven-development/.test(names)) {
|
|
220
|
+
markerHits.push(skillRoot);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const standalone = findStandaloneFiles(projectRoot, system);
|
|
226
|
+
markerHits.push(...standalone);
|
|
227
|
+
|
|
228
|
+
const files = [];
|
|
229
|
+
for (const root of system.fileRoots) {
|
|
230
|
+
const full = path.join(projectRoot, root);
|
|
231
|
+
if (!fs.existsSync(full)) continue;
|
|
232
|
+
walkFiles(full, projectRoot, files);
|
|
233
|
+
}
|
|
234
|
+
for (const file of standalone) files.push(file);
|
|
235
|
+
|
|
236
|
+
const unique = [...new Set(files)].slice(0, MAX_SYSTEM_FILES);
|
|
237
|
+
if (markerHits.length === 0 && unique.length === 0) return null;
|
|
238
|
+
|
|
239
|
+
const fileRecords = unique.map((file) => {
|
|
240
|
+
const content = readText(projectRoot, file);
|
|
241
|
+
return {
|
|
242
|
+
path: file,
|
|
243
|
+
kinds: classifyFile(file, content),
|
|
244
|
+
signals: extractSignals(content),
|
|
245
|
+
bytes: Buffer.byteLength(content)
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const hasPrimaryRoot = markerHits.some((marker) => ['.planning', '.gsd', '_bmad-output', 'docs/superpowers'].includes(marker));
|
|
250
|
+
const score = markerHits.length * 3 + fileRecords.length;
|
|
251
|
+
const confidence = score >= 10 || (hasPrimaryRoot && fileRecords.length >= 3)
|
|
252
|
+
? 'high'
|
|
253
|
+
: (score >= 4 ? 'medium' : 'low');
|
|
254
|
+
return {
|
|
255
|
+
id,
|
|
256
|
+
name: system.displayName,
|
|
257
|
+
confidence,
|
|
258
|
+
markers: markerHits.sort(),
|
|
259
|
+
files: fileRecords,
|
|
260
|
+
importHash: hashFiles(projectRoot, fileRecords)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function detect(projectRoot) {
|
|
265
|
+
const systems = [];
|
|
266
|
+
for (const [id, system] of Object.entries(SYSTEMS)) {
|
|
267
|
+
const detected = detectSystem(projectRoot, id, system);
|
|
268
|
+
if (detected) systems.push(detected);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
systems,
|
|
272
|
+
detected: systems.length > 0
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function filesForKinds(system, kinds) {
|
|
277
|
+
const wanted = new Set(kinds);
|
|
278
|
+
return system.files.filter((file) => file.kinds.some((kind) => wanted.has(kind)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildSourceLine(system, file) {
|
|
282
|
+
const label = file.signals[0] ? `, signal: ${file.signals[0]}` : '';
|
|
283
|
+
return `- [HYPOTHESIS] ${system.name} source ${file.path} maps to ${file.kinds.join(', ')}${label}.`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildImportedContext(detection) {
|
|
287
|
+
const lines = [];
|
|
288
|
+
lines.push('# Imported Preparation Context');
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push('> [DECISION] This artifact captures non-authoritative context imported from adjacent planning systems.');
|
|
291
|
+
lines.push('> [DECISION] Godpowers artifacts remain the source of truth after they are created.');
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('## Sources Detected');
|
|
294
|
+
lines.push('');
|
|
295
|
+
|
|
296
|
+
if (detection.systems.length === 0) {
|
|
297
|
+
lines.push('- [DECISION] No GSD, BMAD, or Superpowers planning context was detected.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const system of detection.systems) {
|
|
301
|
+
lines.push(`- [DECISION] Source system: ${system.name}.`);
|
|
302
|
+
lines.push(`- [DECISION] Confidence: ${system.confidence}.`);
|
|
303
|
+
lines.push(`- [DECISION] Import hash: ${system.importHash}.`);
|
|
304
|
+
lines.push(`- [HYPOTHESIS] Detection markers: ${system.markers.length ? system.markers.join(', ') : 'content files only'}.`);
|
|
305
|
+
const sampleFiles = system.files.slice(0, 10);
|
|
306
|
+
for (const file of sampleFiles) lines.push(buildSourceLine(system, file));
|
|
307
|
+
if (system.files.length > sampleFiles.length) {
|
|
308
|
+
lines.push(`- [HYPOTHESIS] ${system.name} has ${system.files.length - sampleFiles.length} additional imported files omitted from this summary.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push('## Product Signals For PRD');
|
|
314
|
+
lines.push('');
|
|
315
|
+
appendKindSignals(lines, detection, ['requirements', 'product', 'spec'], 'PRD');
|
|
316
|
+
lines.push('- [OPEN QUESTION] Confirm which imported product signals should become authoritative Godpowers requirements. Owner: user. Due: before /god-prd.');
|
|
317
|
+
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('## Delivery Signals For Roadmap');
|
|
320
|
+
lines.push('');
|
|
321
|
+
appendKindSignals(lines, detection, ['roadmap', 'phase', 'epic', 'story', 'sprint'], 'roadmap');
|
|
322
|
+
lines.push('- [OPEN QUESTION] Confirm whether completed imported phases should be marked done, imported, or pending review. Owner: user. Due: before /god-roadmap.');
|
|
323
|
+
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push('## Technical Signals For Architecture And Stack');
|
|
326
|
+
lines.push('');
|
|
327
|
+
appendKindSignals(lines, detection, ['architecture', 'technical', 'stack', 'config', 'context'], 'architecture and stack');
|
|
328
|
+
lines.push('- [OPEN QUESTION] Confirm which imported technical decisions still apply after migration to Godpowers. Owner: user. Due: before /god-arch.');
|
|
329
|
+
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push('## Sync-Back Policy');
|
|
332
|
+
lines.push('');
|
|
333
|
+
lines.push('- [DECISION] Godpowers may write managed sync-back summaries only inside Godpowers-owned fences or companion files.');
|
|
334
|
+
lines.push('- [DECISION] Godpowers must not rewrite GSD, BMAD, or Superpowers source documents outside managed sync-back sections.');
|
|
335
|
+
lines.push('- [DECISION] Sync-back exists so a project can return to the prior planning system with current Godpowers progress visible.');
|
|
336
|
+
|
|
337
|
+
lines.push('');
|
|
338
|
+
lines.push('## Use Rules');
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push('- [DECISION] Godpowers agents may use this artifact as preparation context only.');
|
|
341
|
+
lines.push('- [DECISION] This artifact must not override `.godpowers/intent.yaml`, `.godpowers/state.json`, `PROGRESS.md`, or any completed Godpowers artifact.');
|
|
342
|
+
lines.push('- [DECISION] This artifact must not override native Pillars files under `agents/*.md`.');
|
|
343
|
+
lines.push('- [DECISION] If imported context conflicts with user intent or a Godpowers artifact, the Godpowers artifact wins and the conflict becomes an open question.');
|
|
344
|
+
lines.push('- [DECISION] PRD, architecture, roadmap, and stack agents should cite imported signals as `[HYPOTHESIS]` until confirmed by Godpowers artifacts or the user.');
|
|
345
|
+
lines.push('');
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function appendKindSignals(lines, detection, kinds, destination) {
|
|
350
|
+
let count = 0;
|
|
351
|
+
for (const system of detection.systems) {
|
|
352
|
+
for (const file of filesForKinds(system, kinds).slice(0, 8)) {
|
|
353
|
+
const signal = file.signals[0] || `${file.path} contains ${file.kinds.join(', ')} context`;
|
|
354
|
+
lines.push(`- [HYPOTHESIS] ${system.name} may inform ${destination}: ${signal}. Source: ${file.path}.`);
|
|
355
|
+
count += 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (count === 0) {
|
|
359
|
+
lines.push(`- [HYPOTHESIS] No imported ${destination} signal was detected.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildSeedArtifact(detection, artifact) {
|
|
364
|
+
const lines = [];
|
|
365
|
+
lines.push(`# ${artifact.title}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push('- [DECISION] This artifact was generated from imported planning-system context.');
|
|
368
|
+
lines.push('- [DECISION] Treat this artifact as a Godpowers seed until a specialist command validates or rewrites it.');
|
|
369
|
+
lines.push('- [DECISION] Imported source files remain historical evidence, not Godpowers source of truth.');
|
|
370
|
+
lines.push('');
|
|
371
|
+
lines.push('## Imported Sources');
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
let count = 0;
|
|
375
|
+
for (const system of detection.systems) {
|
|
376
|
+
for (const file of filesForKinds(system, artifact.sourceKinds).slice(0, 12)) {
|
|
377
|
+
lines.push(`- [HYPOTHESIS] ${system.name} source ${file.path} may inform this artifact.`);
|
|
378
|
+
for (const signal of file.signals.slice(0, 3)) {
|
|
379
|
+
lines.push(`- [HYPOTHESIS] Imported signal from ${file.path}: ${signal}.`);
|
|
380
|
+
}
|
|
381
|
+
count += 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (count === 0) {
|
|
386
|
+
lines.push('- [HYPOTHESIS] No direct source file mapped to this artifact.');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push('## Migration Notes');
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push('- [OPEN QUESTION] Review this seed before treating it as authoritative. Owner: user. Due: before dependent Godpowers work.');
|
|
393
|
+
lines.push('- [DECISION] Run the matching Godpowers command to harden this seed into a full artifact.');
|
|
394
|
+
lines.push('');
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function updateState(projectRoot, detection, writtenArtifacts, opts = {}) {
|
|
399
|
+
let current = state.read(projectRoot);
|
|
400
|
+
if (!current) {
|
|
401
|
+
const projectName = path.basename(projectRoot);
|
|
402
|
+
current = state.init(projectRoot, projectName);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const now = new Date().toISOString();
|
|
406
|
+
current['source-systems'] = detection.systems.map((system) => ({
|
|
407
|
+
id: system.id,
|
|
408
|
+
name: system.name,
|
|
409
|
+
confidence: system.confidence,
|
|
410
|
+
markers: system.markers,
|
|
411
|
+
files: system.files.map((file) => file.path),
|
|
412
|
+
'import-hash': system.importHash,
|
|
413
|
+
'imported-at': now,
|
|
414
|
+
'sync-back-enabled': opts.syncBackEnabled !== false,
|
|
415
|
+
'last-sync-back-hash': null,
|
|
416
|
+
'conflict-count': 0
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
for (const written of writtenArtifacts) {
|
|
420
|
+
if (!current.tiers[written.tier]) current.tiers[written.tier] = {};
|
|
421
|
+
current.tiers[written.tier][written.subStep] = {
|
|
422
|
+
...(current.tiers[written.tier][written.subStep] || {}),
|
|
423
|
+
status: 'imported',
|
|
424
|
+
artifact: written.artifact,
|
|
425
|
+
'artifact-hash': state.hashFile(path.join(projectRoot, '.godpowers', written.artifact)),
|
|
426
|
+
updated: now,
|
|
427
|
+
notes: 'Imported from detected planning-system context.'
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
state.write(projectRoot, current);
|
|
432
|
+
return current;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function importPlanningContext(projectRoot, opts = {}) {
|
|
436
|
+
const detection = opts.detection || detect(projectRoot);
|
|
437
|
+
const prepDir = path.join(projectRoot, '.godpowers', 'prep');
|
|
438
|
+
fs.mkdirSync(prepDir, { recursive: true });
|
|
439
|
+
|
|
440
|
+
const importedContextPath = path.join(prepDir, 'IMPORTED-CONTEXT.md');
|
|
441
|
+
const importedContext = buildImportedContext(detection);
|
|
442
|
+
fs.writeFileSync(importedContextPath, importedContext);
|
|
443
|
+
|
|
444
|
+
const writtenArtifacts = [];
|
|
445
|
+
if (opts.writeSeeds !== false && detection.systems.length > 0) {
|
|
446
|
+
for (const artifact of GODPOWERS_ARTIFACTS) {
|
|
447
|
+
const target = path.join(projectRoot, '.godpowers', artifact.artifact);
|
|
448
|
+
if (fs.existsSync(target) && opts.overwrite !== true) continue;
|
|
449
|
+
const hasSources = detection.systems.some((system) => filesForKinds(system, artifact.sourceKinds).length > 0);
|
|
450
|
+
if (!hasSources && opts.writeEmptySeeds !== true) continue;
|
|
451
|
+
ensureDir(target);
|
|
452
|
+
fs.writeFileSync(target, buildSeedArtifact(detection, artifact));
|
|
453
|
+
writtenArtifacts.push(artifact);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const nextState = updateState(projectRoot, detection, writtenArtifacts, opts);
|
|
458
|
+
return {
|
|
459
|
+
detection,
|
|
460
|
+
importedContextPath: rel(projectRoot, importedContextPath),
|
|
461
|
+
writtenArtifacts: writtenArtifacts.map((artifact) => artifact.artifact),
|
|
462
|
+
state: nextState
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = {
|
|
467
|
+
detect,
|
|
468
|
+
importPlanningContext,
|
|
469
|
+
buildImportedContext,
|
|
470
|
+
buildSeedArtifact,
|
|
471
|
+
SYSTEMS,
|
|
472
|
+
GODPOWERS_ARTIFACTS,
|
|
473
|
+
_private: {
|
|
474
|
+
classifyFile,
|
|
475
|
+
extractSignals,
|
|
476
|
+
filesForKinds,
|
|
477
|
+
sha
|
|
478
|
+
}
|
|
479
|
+
};
|
package/lib/reverse-sync.js
CHANGED
|
@@ -25,6 +25,7 @@ const scanner = require('./code-scanner');
|
|
|
25
25
|
const drift = require('./drift-detector');
|
|
26
26
|
const reviewRequired = require('./review-required');
|
|
27
27
|
const impeccable = require('./impeccable-bridge');
|
|
28
|
+
const sourceSync = require('./source-sync');
|
|
28
29
|
|
|
29
30
|
const FENCE_BEGIN = '<!-- godpowers:linkage:begin -->';
|
|
30
31
|
const FENCE_END = '<!-- godpowers:linkage:end -->';
|
|
@@ -257,7 +258,8 @@ function appendFooters(projectRoot) {
|
|
|
257
258
|
* 3. Run drift detection
|
|
258
259
|
* 4. Run impeccable detect on UI files (when installed)
|
|
259
260
|
* 5. Append fenced footers to artifacts
|
|
260
|
-
* 6.
|
|
261
|
+
* 6. Sync Godpowers progress back to imported planning systems when enabled
|
|
262
|
+
* 7. Surface drift findings to REVIEW-REQUIRED.md
|
|
261
263
|
*
|
|
262
264
|
* Returns aggregate summary.
|
|
263
265
|
*/
|
|
@@ -279,6 +281,9 @@ function run(projectRoot, opts = {}) {
|
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
const footers = appendFooters(projectRoot);
|
|
284
|
+
const sourceSyncResult = opts.runSourceSync === false
|
|
285
|
+
? { skipped: true, reason: 'disabled' }
|
|
286
|
+
: sourceSync.run(projectRoot, opts);
|
|
282
287
|
|
|
283
288
|
// Surface drift + impeccable findings to REVIEW-REQUIRED.md
|
|
284
289
|
const reviewItems = [];
|
|
@@ -312,6 +317,7 @@ function run(projectRoot, opts = {}) {
|
|
|
312
317
|
driftResult,
|
|
313
318
|
impeccableFindings,
|
|
314
319
|
footers,
|
|
320
|
+
sourceSyncResult,
|
|
315
321
|
reviewItems
|
|
316
322
|
};
|
|
317
323
|
}
|