godpowers 2.1.1 → 2.2.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/CHANGELOG.md +43 -0
- package/README.md +14 -3
- package/RELEASE.md +44 -48
- package/SKILL.md +3 -3
- package/agents/god-architect.md +3 -0
- package/agents/god-executor.md +17 -0
- package/agents/god-greenfieldifier.md +10 -2
- package/agents/god-launch-strategist.md +3 -0
- package/agents/god-orchestrator.md +27 -1
- package/agents/god-planner.md +9 -1
- package/agents/god-pm.md +23 -1
- package/agents/god-quality-reviewer.md +8 -1
- package/agents/god-reconstructor.md +17 -0
- package/agents/god-roadmap-reconciler.md +7 -0
- package/agents/god-roadmap-updater.md +16 -1
- package/agents/god-roadmapper.md +18 -7
- package/agents/god-spec-reviewer.md +8 -0
- package/agents/god-storyteller.md +5 -0
- package/agents/god-updater.md +6 -0
- package/bin/install.js +1 -1
- package/hooks/session-start.sh +1 -0
- package/lib/dashboard.js +20 -0
- package/lib/feature-awareness.js +6 -0
- package/lib/requirements.js +524 -0
- package/lib/reverse-sync.js +19 -1
- package/lib/route-quality-sync.js +1 -0
- package/lib/state.js +5 -0
- package/package.json +2 -2
- package/routing/god-progress.yaml +27 -0
- package/routing/recipes/whats-done.yaml +28 -0
- package/schema/state.v1.json +25 -0
- package/skills/god-doctor.md +2 -2
- package/skills/god-locate.md +1 -0
- package/skills/god-progress.md +105 -0
- package/skills/god-status.md +11 -0
- package/skills/god-version.md +2 -2
- package/templates/PRD.md +12 -4
- package/templates/PROGRESS.md +2 -0
- package/templates/REQUIREMENTS.md +45 -0
- package/templates/ROADMAP.md +23 -5
|
@@ -50,6 +50,13 @@ Answer each with EVIDENCE from the code:
|
|
|
50
50
|
- Did the executor add future options, broad configurability, or generic
|
|
51
51
|
interfaces that the current slice does not need?
|
|
52
52
|
|
|
53
|
+
6. **Is each planned requirement id annotated in the code?**
|
|
54
|
+
- For each PRD requirement id listed in the slice plan (its `Requirements:`
|
|
55
|
+
field): does the code carry the corresponding `// Implements: P-...`
|
|
56
|
+
annotation that god-executor follows?
|
|
57
|
+
- A planned requirement id with no annotation is a finding, because the
|
|
58
|
+
deliverable ledger derives requirement status from those annotations.
|
|
59
|
+
|
|
53
60
|
## Output
|
|
54
61
|
|
|
55
62
|
Return verdict to orchestrator:
|
|
@@ -75,6 +82,7 @@ Return verdict to orchestrator:
|
|
|
75
82
|
- No scope creep without justification
|
|
76
83
|
- Every touched file has request-trace evidence
|
|
77
84
|
- No speculative flexibility or unrelated cleanup entered the diff
|
|
85
|
+
- Every planned requirement id carries its `// Implements: P-...` annotation
|
|
78
86
|
|
|
79
87
|
If FAIL: orchestrator returns the slice to god-executor with the failures.
|
|
80
88
|
If PASS: orchestrator spawns god-quality-reviewer next.
|
|
@@ -33,6 +33,7 @@ title: "Short noun phrase"
|
|
|
33
33
|
status: pending
|
|
34
34
|
owner: <name>
|
|
35
35
|
deps: []
|
|
36
|
+
requirement: P-MUST-01 # optional; the PRD requirement id this story decomposes
|
|
36
37
|
created: <ISO date>
|
|
37
38
|
---
|
|
38
39
|
|
|
@@ -61,6 +62,10 @@ As a [persona], I want [capability] so that [outcome].
|
|
|
61
62
|
1. Read PRD.md and ARCH.md for context.
|
|
62
63
|
2. If `--with-stories` from /god-feature: decompose the feature spec
|
|
63
64
|
into 3-7 stories (don't exceed 10).
|
|
65
|
+
- When a story decomposes a specific PRD functional requirement, reference
|
|
66
|
+
that requirement id (set `requirement: P-MUST-01` in the frontmatter, or
|
|
67
|
+
mention it in the acceptance criteria) so the story traces back to the PRD
|
|
68
|
+
requirement and the deliverable ledger.
|
|
64
69
|
3. Determine next ID number:
|
|
65
70
|
- List `.godpowers/stories/<feature-slug>/STORY-*.md`
|
|
66
71
|
- Use max + 1, zero-padded to 3 digits
|
package/agents/god-updater.md
CHANGED
|
@@ -113,6 +113,11 @@ After feature work, every artifact that was impacted needs to reflect reality.
|
|
|
113
113
|
- Surfaces drift + impeccable findings to `REVIEW-REQUIRED.md`
|
|
114
114
|
- Update `state.json.linkage` with current `coverage-pct`, `orphan-count`,
|
|
115
115
|
`drift-count`, `review-required-items`
|
|
116
|
+
- Refresh deliverable tracking from the updated linkage map:
|
|
117
|
+
- Call `lib/requirements.writeLedger(projectRoot)` to regenerate
|
|
118
|
+
`.godpowers/REQUIREMENTS.md`
|
|
119
|
+
- Cache the summary into `state.json.deliverables` via
|
|
120
|
+
`lib/requirements.summarizeForState`
|
|
116
121
|
- Emit events: `linkage.snapshot`, `drift.detected` (per finding),
|
|
117
122
|
`review-required.populated`
|
|
118
123
|
- Report counts in the final sync status:
|
|
@@ -121,6 +126,7 @@ After feature work, every artifact that was impacted needs to reflect reality.
|
|
|
121
126
|
- fenced footers updated
|
|
122
127
|
- drift findings
|
|
123
128
|
- REVIEW-REQUIRED.md items created
|
|
129
|
+
- requirement coverage: `<done>/<total> done` and any gaps
|
|
124
130
|
|
|
125
131
|
### Repository documentation sync
|
|
126
132
|
- Call `lib/repo-doc-sync.run(projectRoot, { changedFiles })` when the runtime
|
package/bin/install.js
CHANGED
|
@@ -248,7 +248,7 @@ function runInstall(opts, srcDir) {
|
|
|
248
248
|
log(`\x1b[32mDone!\x1b[0m Installed Godpowers v${VERSION} for ${installed} runtime(s).`);
|
|
249
249
|
log('');
|
|
250
250
|
log(`\x1b[36mInstalled:\x1b[0m`);
|
|
251
|
-
log(` ${surface.skills} slash commands (try: /god-mode, /god-next, /god-status)`);
|
|
251
|
+
log(` ${surface.skills} slash commands (try: /god-mode, /god-next, /god-status, /god-progress)`);
|
|
252
252
|
log(` ${surface.agents} specialist agents`);
|
|
253
253
|
log(' Templates and references for artifact discipline');
|
|
254
254
|
log('');
|
package/hooks/session-start.sh
CHANGED
|
@@ -80,6 +80,7 @@ Next step: run /god-next (it inspects disk state and proposes the next command)
|
|
|
80
80
|
Or: /god-mode for the full autonomous project run
|
|
81
81
|
Or: /god-help to see the catalog
|
|
82
82
|
Or: /god-status for the full project snapshot
|
|
83
|
+
Or: /god-progress for deliverable progress (requirements done / left)
|
|
83
84
|
Or: /god-context refresh after installing a newer Godpowers runtime
|
|
84
85
|
|
|
85
86
|
Disk state is authoritative. Conversation memory is not.
|
package/lib/dashboard.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const cp = require('child_process');
|
|
11
11
|
|
|
12
12
|
const state = require('./state');
|
|
13
|
+
const requirements = require('./requirements');
|
|
13
14
|
const router = require('./router');
|
|
14
15
|
const automationProviders = require('./automation-providers');
|
|
15
16
|
const repoDocSync = require('./repo-doc-sync');
|
|
@@ -239,6 +240,7 @@ function compute(projectRoot, opts = {}) {
|
|
|
239
240
|
proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
|
|
240
241
|
host: hostCapabilities.detect(projectRoot, opts.host || {}),
|
|
241
242
|
next,
|
|
243
|
+
deliverables: { hasRequirements: false },
|
|
242
244
|
openItems: ['No .godpowers/state.json found']
|
|
243
245
|
};
|
|
244
246
|
result.actionBrief = actionBrief(result);
|
|
@@ -251,6 +253,14 @@ function compute(projectRoot, opts = {}) {
|
|
|
251
253
|
const openItems = [];
|
|
252
254
|
const drift = state.detectDrift(projectRoot);
|
|
253
255
|
|
|
256
|
+
const buildSub = s.tiers && s.tiers['tier-2'] && s.tiers['tier-2'].build;
|
|
257
|
+
const deliverables = requirements.derive(projectRoot, {
|
|
258
|
+
buildComplete: Boolean(buildSub && state.isCompleteStatus(buildSub.status))
|
|
259
|
+
});
|
|
260
|
+
if (deliverables.gaps.length > 0) {
|
|
261
|
+
openItems.push(`${deliverables.gaps.length} requirement(s) in a done increment with no linked code, see ${requirements.LEDGER_PATH}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
254
264
|
if (drift.length > 0) openItems.push(`${drift.length} artifact drift item(s), suggest /god-repair`);
|
|
255
265
|
if (next && next.blocker) openItems.push(`${next.blocker} blocks next route`);
|
|
256
266
|
if (reviewCount(projectRoot) > 0) openItems.push('pending review items');
|
|
@@ -269,6 +279,7 @@ function compute(projectRoot, opts = {}) {
|
|
|
269
279
|
proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
|
|
270
280
|
host: hostCapabilities.detect(projectRoot, opts.host || {}),
|
|
271
281
|
next,
|
|
282
|
+
deliverables,
|
|
272
283
|
openItems
|
|
273
284
|
};
|
|
274
285
|
result.actionBrief = actionBrief(result);
|
|
@@ -314,6 +325,11 @@ function render(dashboard, opts = {}) {
|
|
|
314
325
|
const progress = dashboard.progress || {};
|
|
315
326
|
const prd = planning.prd || {};
|
|
316
327
|
const roadmap = planning.roadmap || {};
|
|
328
|
+
const deliverables = dashboard.deliverables || { hasRequirements: false };
|
|
329
|
+
const deliverableLines = requirements.renderProgressLines(deliverables);
|
|
330
|
+
const deliverableBrief = deliverables.hasRequirements
|
|
331
|
+
? ` Requirements: ${requirements.progressBar(deliverables.summary.done, deliverables.summary.total)} done (${deliverables.summary.percent}%)`
|
|
332
|
+
: null;
|
|
317
333
|
const openItems = dashboard.openItems && dashboard.openItems.length > 0
|
|
318
334
|
? dashboard.openItems
|
|
319
335
|
: ['none'];
|
|
@@ -332,6 +348,7 @@ function render(dashboard, opts = {}) {
|
|
|
332
348
|
'Current status:',
|
|
333
349
|
` State: ${dashboard.state}`,
|
|
334
350
|
` Progress: ${progress.percent || 0}% workflow progress (${progress.completed || 0} of ${progress.total || 0} tracked steps complete)`,
|
|
351
|
+
...(deliverableBrief ? [deliverableBrief] : []),
|
|
335
352
|
'',
|
|
336
353
|
'Next:',
|
|
337
354
|
` Recommended: ${next.command || 'describe the next intent'}`,
|
|
@@ -365,6 +382,9 @@ function render(dashboard, opts = {}) {
|
|
|
365
382
|
` Current milestone: ${planning.currentMilestone || 'unknown'}`,
|
|
366
383
|
` Completion basis: ${planning.completionBasis || planning.completion || 'unknown'}`,
|
|
367
384
|
'',
|
|
385
|
+
'Deliverable progress:',
|
|
386
|
+
...deliverableLines,
|
|
387
|
+
'',
|
|
368
388
|
'Proactive checks:',
|
|
369
389
|
` Checkpoint: ${proactive.checkpoint || 'unknown'}`,
|
|
370
390
|
` Reviews: ${proactive.reviews || 'unknown'}`,
|
package/lib/feature-awareness.js
CHANGED
|
@@ -118,6 +118,12 @@ const FEATURES = [
|
|
|
118
118
|
since: '1.6.22',
|
|
119
119
|
commands: ['/god-suite-release'],
|
|
120
120
|
description: 'Plan Mode D suite releases with impacted dependents and planned writes before mutation.'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'deliverable-progress-tracking',
|
|
124
|
+
since: '2.2.0',
|
|
125
|
+
commands: ['/god-progress', '/god-status', '/god-sync'],
|
|
126
|
+
description: 'Track which PRD requirements and roadmap increments are done, in progress, or not started, derived from the linkage map and surfaced in the .godpowers/REQUIREMENTS.md ledger.'
|
|
121
127
|
}
|
|
122
128
|
];
|
|
123
129
|
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements / Deliverable Tracking
|
|
3
|
+
*
|
|
4
|
+
* Answers the question "which requirements are done, in progress, or not
|
|
5
|
+
* started yet?" with a disk-derived, human-readable view. Nothing here is
|
|
6
|
+
* remembered: status is computed every time from
|
|
7
|
+
*
|
|
8
|
+
* .godpowers/prd/PRD.md declared requirements (P-MUST/SHOULD/COULD)
|
|
9
|
+
* .godpowers/roadmap/ROADMAP.md delivery increments and their member reqs
|
|
10
|
+
* .godpowers/links/ linkage forward map (requirement -> code)
|
|
11
|
+
* .godpowers/state.json build/increment completion
|
|
12
|
+
*
|
|
13
|
+
* Status rules (honest, evidence-based):
|
|
14
|
+
* untouched - no code is linked to the requirement yet
|
|
15
|
+
* in-progress - code is linked, but its increment (or the build) is not done
|
|
16
|
+
* done - code is linked AND its increment is done (or build complete)
|
|
17
|
+
*
|
|
18
|
+
* Public API:
|
|
19
|
+
* parsePrdRequirements(projectRoot) -> [{ id, priority, text, acceptance }]
|
|
20
|
+
* parseRoadmapIncrements(projectRoot) -> [{ id, name, horizon, requirements, declaredStatus }]
|
|
21
|
+
* derive(projectRoot, opts) -> { hasRequirements, requirements, increments, summary, gaps, updated }
|
|
22
|
+
* progressBar(done, total, width) -> "[####----] 4/10"
|
|
23
|
+
* renderLedger(derived) -> markdown for .godpowers/REQUIREMENTS.md
|
|
24
|
+
* renderProgressLines(derived) -> string[] compact block for the dashboard
|
|
25
|
+
* writeLedger(projectRoot, derived) -> writes the ledger, returns its path
|
|
26
|
+
* summarizeForState(derived) -> small object cached under state.deliverables
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const linkage = require('./linkage');
|
|
33
|
+
const state = require('./state');
|
|
34
|
+
|
|
35
|
+
const PRD_PATH = '.godpowers/prd/PRD.md';
|
|
36
|
+
const ROADMAP_PATH = '.godpowers/roadmap/ROADMAP.md';
|
|
37
|
+
const LEDGER_PATH = '.godpowers/REQUIREMENTS.md';
|
|
38
|
+
|
|
39
|
+
const PRIORITIES = ['MUST', 'SHOULD', 'COULD'];
|
|
40
|
+
const REQ_ID_RE = /\bP-(MUST|SHOULD|COULD)-(\d+)\b/;
|
|
41
|
+
const REQ_ID_RE_G = /\bP-(MUST|SHOULD|COULD)-\d+\b/g;
|
|
42
|
+
const MILESTONE_ID_RE = /\bM-[\w-]+\b/;
|
|
43
|
+
const LABEL_RE = /\[(?:DECISION|HYPOTHESIS|OPEN QUESTION)\]/g;
|
|
44
|
+
|
|
45
|
+
function readText(projectRoot, relPath) {
|
|
46
|
+
const file = path.join(projectRoot, relPath);
|
|
47
|
+
if (!fs.existsSync(file)) return null;
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(file, 'utf8');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pad2(n) {
|
|
56
|
+
return String(n).padStart(2, '0');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function slugify(text) {
|
|
60
|
+
return String(text)
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
63
|
+
.replace(/^-+|-+$/g, '')
|
|
64
|
+
.slice(0, 40) || 'increment';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// PRD parsing
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse declared requirements from the PRD. Each MUST/SHOULD/COULD bullet
|
|
73
|
+
* becomes a requirement. Explicit `P-MUST-01` ids are honored; bullets without
|
|
74
|
+
* an id are numbered by position within their priority so legacy PRDs still
|
|
75
|
+
* parse.
|
|
76
|
+
*/
|
|
77
|
+
function parsePrdRequirements(projectRoot) {
|
|
78
|
+
const content = readText(projectRoot, PRD_PATH);
|
|
79
|
+
if (!content) return [];
|
|
80
|
+
|
|
81
|
+
const lines = content.split(/\r?\n/);
|
|
82
|
+
const reqs = [];
|
|
83
|
+
const counters = { MUST: 0, SHOULD: 0, COULD: 0 };
|
|
84
|
+
let priority = null;
|
|
85
|
+
|
|
86
|
+
for (const raw of lines) {
|
|
87
|
+
const line = raw.replace(/\s+$/, '');
|
|
88
|
+
const heading = line.match(/^#{2,4}\s+(.*)$/);
|
|
89
|
+
if (heading) {
|
|
90
|
+
const title = heading[1].trim().toUpperCase();
|
|
91
|
+
const hit = PRIORITIES.find(p => title.startsWith(p));
|
|
92
|
+
priority = hit || null;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!priority) continue;
|
|
96
|
+
|
|
97
|
+
// Top-level list item only (requirement bullets are not indented).
|
|
98
|
+
const item = line.match(/^[-*]\s+(.*)$/);
|
|
99
|
+
if (!item) continue;
|
|
100
|
+
let body = item[1].trim();
|
|
101
|
+
if (!body) continue;
|
|
102
|
+
// Drop a leading checkbox if a ledger-style PRD ever feeds back in.
|
|
103
|
+
body = body.replace(/^\[[ xX~]\]\s*/, '');
|
|
104
|
+
|
|
105
|
+
const idMatch = body.match(REQ_ID_RE);
|
|
106
|
+
let id;
|
|
107
|
+
let prio;
|
|
108
|
+
if (idMatch) {
|
|
109
|
+
prio = idMatch[1];
|
|
110
|
+
id = idMatch[0];
|
|
111
|
+
counters[prio] = Math.max(counters[prio], Number(idMatch[2]));
|
|
112
|
+
} else {
|
|
113
|
+
prio = priority;
|
|
114
|
+
counters[prio] += 1;
|
|
115
|
+
id = `P-${prio}-${pad2(counters[prio])}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let text = body
|
|
119
|
+
.replace(REQ_ID_RE, '')
|
|
120
|
+
.replace(LABEL_RE, '')
|
|
121
|
+
.replace(/^[:\-\s]+/, '')
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
let acceptance = '';
|
|
125
|
+
const accSplit = text.split(/\s+--\s+(?:Acceptance|Validation):\s*/i);
|
|
126
|
+
if (accSplit.length > 1) {
|
|
127
|
+
text = accSplit[0].trim();
|
|
128
|
+
acceptance = accSplit.slice(1).join(' / ').trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
reqs.push({ id, priority: prio, text: text || '(unspecified)', acceptance });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// De-duplicate by id, keeping the first occurrence.
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
return reqs.filter(r => (seen.has(r.id) ? false : seen.add(r.id)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// ROADMAP parsing
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse delivery increments and the requirements each one covers. Increments
|
|
145
|
+
* may carry an explicit `M-slug` id and a `Status:` field; both are optional.
|
|
146
|
+
*/
|
|
147
|
+
function parseRoadmapIncrements(projectRoot) {
|
|
148
|
+
const content = readText(projectRoot, ROADMAP_PATH);
|
|
149
|
+
if (!content) return [];
|
|
150
|
+
|
|
151
|
+
const lines = content.split(/\r?\n/);
|
|
152
|
+
const increments = [];
|
|
153
|
+
let horizon = null;
|
|
154
|
+
let current = null;
|
|
155
|
+
let inHaveNots = false;
|
|
156
|
+
|
|
157
|
+
const flush = () => {
|
|
158
|
+
if (current) increments.push(current);
|
|
159
|
+
current = null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const raw of lines) {
|
|
163
|
+
const line = raw.replace(/\s+$/, '');
|
|
164
|
+
|
|
165
|
+
const h2 = line.match(/^##\s+(.*)$/);
|
|
166
|
+
if (h2) {
|
|
167
|
+
flush();
|
|
168
|
+
const title = h2[1].trim().toLowerCase();
|
|
169
|
+
inHaveNots = title.includes('have-not');
|
|
170
|
+
if (title.startsWith('now')) horizon = 'now';
|
|
171
|
+
else if (title.startsWith('next')) horizon = 'next';
|
|
172
|
+
else if (title.startsWith('later')) horizon = 'later';
|
|
173
|
+
else horizon = null;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const h3 = line.match(/^###\s+(.*)$/);
|
|
178
|
+
if (h3 && horizon && !inHaveNots) {
|
|
179
|
+
flush();
|
|
180
|
+
let name = h3[1].trim();
|
|
181
|
+
name = name.replace(/^Delivery Increment\s*\d+\s*:\s*/i, '');
|
|
182
|
+
name = name.replace(/^Theme\s*:\s*/i, '');
|
|
183
|
+
const idMatch = h3[1].match(MILESTONE_ID_RE);
|
|
184
|
+
current = {
|
|
185
|
+
id: idMatch ? idMatch[0] : null,
|
|
186
|
+
name: name || h3[1].trim(),
|
|
187
|
+
horizon,
|
|
188
|
+
requirements: [],
|
|
189
|
+
declaredStatus: null
|
|
190
|
+
};
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!current) continue;
|
|
195
|
+
|
|
196
|
+
const idLine = line.match(/^\s*[-*]\s*\*\*ID\*\*\s*:\s*(.+)$/i);
|
|
197
|
+
if (idLine) {
|
|
198
|
+
const m = idLine[1].match(MILESTONE_ID_RE);
|
|
199
|
+
if (m) current.id = m[0];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const statusLine = line.match(/^\s*[-*]\s*\*\*Status\*\*\s*:\s*(.+)$/i);
|
|
204
|
+
if (statusLine) {
|
|
205
|
+
current.declaredStatus = normalizeStatus(statusLine[1].trim());
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let m;
|
|
210
|
+
while ((m = REQ_ID_RE_G.exec(line)) !== null) {
|
|
211
|
+
if (!current.requirements.includes(m[0])) current.requirements.push(m[0]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
flush();
|
|
215
|
+
|
|
216
|
+
for (const inc of increments) {
|
|
217
|
+
if (!inc.id) inc.id = `M-${slugify(inc.name)}`;
|
|
218
|
+
}
|
|
219
|
+
return increments;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeStatus(value) {
|
|
223
|
+
const v = String(value || '').trim().toLowerCase();
|
|
224
|
+
if (!v) return null;
|
|
225
|
+
if (['done', 'complete', 'completed', 'shipped', 'verified'].some(s => v.startsWith(s))) return 'done';
|
|
226
|
+
if (['building', 'in-progress', 'in progress', 'active', 'wip'].some(s => v.startsWith(s))) return 'building';
|
|
227
|
+
if (['pending', 'not started', 'not-started', 'todo', 'planned', 'queued'].some(s => v.startsWith(s))) return 'pending';
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Status derivation
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
function buildComplete(projectRoot) {
|
|
236
|
+
const s = state.read(projectRoot);
|
|
237
|
+
const build = s && s.tiers && s.tiers['tier-2'] && s.tiers['tier-2'].build;
|
|
238
|
+
return Boolean(build && state.isCompleteStatus(build.status));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compute the full deliverable picture from disk.
|
|
243
|
+
*/
|
|
244
|
+
function derive(projectRoot, opts = {}) {
|
|
245
|
+
const requirements = parsePrdRequirements(projectRoot);
|
|
246
|
+
const increments = parseRoadmapIncrements(projectRoot);
|
|
247
|
+
const forward = linkage.readForward(projectRoot);
|
|
248
|
+
const isBuilt = Object.prototype.hasOwnProperty.call(opts, 'buildComplete')
|
|
249
|
+
? Boolean(opts.buildComplete)
|
|
250
|
+
: buildComplete(projectRoot);
|
|
251
|
+
|
|
252
|
+
const isLinked = id => Array.isArray(forward[id]) && forward[id].length > 0;
|
|
253
|
+
const reqById = new Map(requirements.map(r => [r.id, r]));
|
|
254
|
+
|
|
255
|
+
// Requirement -> increment (first increment that lists it wins).
|
|
256
|
+
const incForReq = new Map();
|
|
257
|
+
for (const inc of increments) {
|
|
258
|
+
for (const id of inc.requirements) {
|
|
259
|
+
if (!incForReq.has(id)) incForReq.set(id, inc);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Derive increment status from member-requirement linkage + declared status.
|
|
264
|
+
for (const inc of increments) {
|
|
265
|
+
const members = inc.requirements.filter(id => reqById.has(id));
|
|
266
|
+
const mustMembers = members.filter(id => reqById.get(id).priority === 'MUST');
|
|
267
|
+
const anyLinked = members.some(isLinked);
|
|
268
|
+
const gateMembers = mustMembers.length > 0 ? mustMembers : members;
|
|
269
|
+
const gateLinked = gateMembers.length > 0 && gateMembers.every(isLinked);
|
|
270
|
+
|
|
271
|
+
let status;
|
|
272
|
+
if (inc.declaredStatus === 'done') status = 'done';
|
|
273
|
+
else if (gateLinked && isBuilt) status = 'done';
|
|
274
|
+
else if (anyLinked || inc.declaredStatus === 'building') status = 'building';
|
|
275
|
+
else status = inc.declaredStatus || 'pending';
|
|
276
|
+
|
|
277
|
+
inc.status = status;
|
|
278
|
+
inc.memberIds = members;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Derive each requirement's status.
|
|
282
|
+
const enriched = requirements.map(r => {
|
|
283
|
+
const inc = incForReq.get(r.id) || null;
|
|
284
|
+
const linked = isLinked(r.id);
|
|
285
|
+
const files = linked ? forward[r.id].slice() : [];
|
|
286
|
+
let status;
|
|
287
|
+
if (!linked) status = 'untouched';
|
|
288
|
+
else if (inc ? inc.status === 'done' : isBuilt) status = 'done';
|
|
289
|
+
else status = 'in-progress';
|
|
290
|
+
const gap = Boolean(inc && inc.status === 'done' && !linked);
|
|
291
|
+
return {
|
|
292
|
+
id: r.id,
|
|
293
|
+
priority: r.priority,
|
|
294
|
+
text: r.text,
|
|
295
|
+
acceptance: r.acceptance,
|
|
296
|
+
status,
|
|
297
|
+
gap,
|
|
298
|
+
files,
|
|
299
|
+
increment: inc ? inc.id : null,
|
|
300
|
+
incrementName: inc ? inc.name : null
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Increment done-counts based on final requirement statuses.
|
|
305
|
+
const statusById = new Map(enriched.map(r => [r.id, r.status]));
|
|
306
|
+
for (const inc of increments) {
|
|
307
|
+
inc.doneCount = inc.memberIds.filter(id => statusById.get(id) === 'done').length;
|
|
308
|
+
inc.totalCount = inc.memberIds.length;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const summary = summarize(enriched, increments);
|
|
312
|
+
const gaps = enriched.filter(r => r.gap);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
hasRequirements: enriched.length > 0,
|
|
316
|
+
requirements: enriched,
|
|
317
|
+
increments,
|
|
318
|
+
summary,
|
|
319
|
+
gaps,
|
|
320
|
+
updated: new Date().toISOString()
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function summarize(requirements, increments) {
|
|
325
|
+
const byPriority = {};
|
|
326
|
+
for (const p of PRIORITIES) {
|
|
327
|
+
byPriority[p] = { done: 0, inProgress: 0, untouched: 0, total: 0 };
|
|
328
|
+
}
|
|
329
|
+
let done = 0;
|
|
330
|
+
let inProgress = 0;
|
|
331
|
+
let untouched = 0;
|
|
332
|
+
for (const r of requirements) {
|
|
333
|
+
const bucket = byPriority[r.priority] || (byPriority[r.priority] = { done: 0, inProgress: 0, untouched: 0, total: 0 });
|
|
334
|
+
bucket.total += 1;
|
|
335
|
+
if (r.status === 'done') { done += 1; bucket.done += 1; }
|
|
336
|
+
else if (r.status === 'in-progress') { inProgress += 1; bucket.inProgress += 1; }
|
|
337
|
+
else { untouched += 1; bucket.untouched += 1; }
|
|
338
|
+
}
|
|
339
|
+
const total = requirements.length;
|
|
340
|
+
const incStatus = { done: 0, building: 0, pending: 0 };
|
|
341
|
+
for (const inc of increments) incStatus[inc.status] = (incStatus[inc.status] || 0) + 1;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
total,
|
|
345
|
+
done,
|
|
346
|
+
inProgress,
|
|
347
|
+
untouched,
|
|
348
|
+
percent: total === 0 ? 0 : Math.round((done / total) * 100),
|
|
349
|
+
byPriority,
|
|
350
|
+
increments: { total: increments.length, ...incStatus }
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Rendering
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
function progressBar(done, total, width = 20) {
|
|
359
|
+
if (!total || total <= 0) return `[${'-'.repeat(width)}] 0/0`;
|
|
360
|
+
const filled = Math.max(0, Math.min(width, Math.round((done / total) * width)));
|
|
361
|
+
return `[${'#'.repeat(filled)}${'-'.repeat(width - filled)}] ${done}/${total}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const MARK = { done: '[x]', 'in-progress': '[~]', untouched: '[ ]' };
|
|
365
|
+
const INC_MARK = { done: '[x]', building: '[~]', pending: '[ ]' };
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Compact lines for the dashboard "Deliverable progress" section.
|
|
369
|
+
*/
|
|
370
|
+
function renderProgressLines(derived) {
|
|
371
|
+
if (!derived || !derived.hasRequirements) {
|
|
372
|
+
return [' Requirements: none declared yet (no PRD requirements found)'];
|
|
373
|
+
}
|
|
374
|
+
const s = derived.summary;
|
|
375
|
+
const byPriority = PRIORITIES
|
|
376
|
+
.filter(p => s.byPriority[p] && s.byPriority[p].total > 0)
|
|
377
|
+
.map(p => `${p} ${s.byPriority[p].done}/${s.byPriority[p].total}`)
|
|
378
|
+
.join(', ');
|
|
379
|
+
const lines = [
|
|
380
|
+
` Requirements: ${progressBar(s.done, s.total)} done (${s.percent}%), ${s.inProgress} in progress, ${s.untouched} not started`
|
|
381
|
+
];
|
|
382
|
+
if (byPriority) lines.push(` By priority: ${byPriority}`);
|
|
383
|
+
if (s.increments.total > 0) {
|
|
384
|
+
lines.push(` Increments: ${s.increments.done} done, ${s.increments.building} building, ${s.increments.pending} pending`);
|
|
385
|
+
}
|
|
386
|
+
if (derived.gaps.length > 0) {
|
|
387
|
+
lines.push(` Gaps: ${derived.gaps.length} requirement(s) in a done increment with no linked code`);
|
|
388
|
+
}
|
|
389
|
+
lines.push(` Ledger: ${LEDGER_PATH}`);
|
|
390
|
+
return lines;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Full human-readable ledger written to .godpowers/REQUIREMENTS.md.
|
|
395
|
+
*/
|
|
396
|
+
function renderLedger(derived) {
|
|
397
|
+
const s = derived.summary;
|
|
398
|
+
const out = [];
|
|
399
|
+
out.push('# Requirements Ledger');
|
|
400
|
+
out.push('');
|
|
401
|
+
out.push('> Disk-derived. Status comes from the linkage map (code that implements');
|
|
402
|
+
out.push('> each requirement) plus build and roadmap-increment state. Regenerate with');
|
|
403
|
+
out.push('> `/god-progress`, `/god-status`, or `/god-sync`. Do not hand-edit statuses;');
|
|
404
|
+
out.push('> they are recomputed from disk.');
|
|
405
|
+
out.push('');
|
|
406
|
+
out.push(`Updated: ${derived.updated}`);
|
|
407
|
+
out.push('Source: PRD + ROADMAP + linkage forward map + build state');
|
|
408
|
+
|
|
409
|
+
if (!derived.hasRequirements) {
|
|
410
|
+
out.push('');
|
|
411
|
+
out.push('No requirements are declared yet. Once the PRD lists MUST/SHOULD/COULD');
|
|
412
|
+
out.push('requirements with stable ids (P-MUST-01, ...), they appear here.');
|
|
413
|
+
out.push('');
|
|
414
|
+
return out.join('\n');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
out.push(`Progress: ${progressBar(s.done, s.total)} done (${s.percent}%) | ${s.inProgress} in progress | ${s.untouched} not started`);
|
|
418
|
+
out.push('');
|
|
419
|
+
out.push('## By priority');
|
|
420
|
+
out.push('');
|
|
421
|
+
out.push('| Priority | Done | In progress | Not started | Total |');
|
|
422
|
+
out.push('|----------|------|-------------|-------------|-------|');
|
|
423
|
+
for (const p of PRIORITIES) {
|
|
424
|
+
const b = s.byPriority[p];
|
|
425
|
+
if (!b || b.total === 0) continue;
|
|
426
|
+
out.push(`| ${p} | ${b.done} | ${b.inProgress} | ${b.untouched} | ${b.total} |`);
|
|
427
|
+
}
|
|
428
|
+
out.push('');
|
|
429
|
+
|
|
430
|
+
const groups = [
|
|
431
|
+
['Done', 'done'],
|
|
432
|
+
['In progress', 'in-progress'],
|
|
433
|
+
['Not started', 'untouched']
|
|
434
|
+
];
|
|
435
|
+
for (const [label, key] of groups) {
|
|
436
|
+
const items = derived.requirements.filter(r => r.status === key);
|
|
437
|
+
out.push(`## ${label} (${items.length})`);
|
|
438
|
+
out.push('');
|
|
439
|
+
if (items.length === 0) {
|
|
440
|
+
out.push('- (none)');
|
|
441
|
+
out.push('');
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const r of items) {
|
|
445
|
+
const inc = r.increment ? ` _(increment: ${r.increment})_` : '';
|
|
446
|
+
const files = r.files.length > 0 ? ` - ${r.files.slice(0, 4).join(', ')}${r.files.length > 4 ? `, +${r.files.length - 4} more` : ''}` : '';
|
|
447
|
+
out.push(`- ${MARK[r.status]} **${r.id}** ${r.text}${inc}${files}`);
|
|
448
|
+
}
|
|
449
|
+
out.push('');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (derived.increments.length > 0) {
|
|
453
|
+
out.push('## Increments');
|
|
454
|
+
out.push('');
|
|
455
|
+
for (const inc of derived.increments) {
|
|
456
|
+
out.push(`- ${INC_MARK[inc.status]} **${inc.id}**: ${inc.name} _[${inc.horizon}]_ - ${inc.status} - ${inc.doneCount}/${inc.totalCount} requirements done`);
|
|
457
|
+
}
|
|
458
|
+
out.push('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (derived.gaps.length > 0) {
|
|
462
|
+
out.push('## Gaps');
|
|
463
|
+
out.push('');
|
|
464
|
+
out.push('Requirements whose increment is marked done but have no implementing code linked:');
|
|
465
|
+
out.push('');
|
|
466
|
+
for (const r of derived.gaps) {
|
|
467
|
+
out.push(`- **${r.id}** ${r.text} _(increment: ${r.increment})_`);
|
|
468
|
+
}
|
|
469
|
+
out.push('');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return out.join('\n');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function writeLedger(projectRoot, derived) {
|
|
476
|
+
const data = derived || derive(projectRoot);
|
|
477
|
+
const file = path.join(projectRoot, LEDGER_PATH);
|
|
478
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
479
|
+
fs.writeFileSync(file, renderLedger(data) + '\n');
|
|
480
|
+
return LEDGER_PATH;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Small cacheable summary for state.json (state.deliverables).
|
|
485
|
+
*/
|
|
486
|
+
function summarizeForState(derived) {
|
|
487
|
+
const s = derived.summary;
|
|
488
|
+
return {
|
|
489
|
+
updated: derived.updated,
|
|
490
|
+
source: 'PRD + ROADMAP + linkage + build state',
|
|
491
|
+
requirements: {
|
|
492
|
+
total: s.total,
|
|
493
|
+
done: s.done,
|
|
494
|
+
'in-progress': s.inProgress,
|
|
495
|
+
untouched: s.untouched,
|
|
496
|
+
percent: s.percent
|
|
497
|
+
},
|
|
498
|
+
increments: derived.increments.map(inc => ({
|
|
499
|
+
id: inc.id,
|
|
500
|
+
name: inc.name,
|
|
501
|
+
horizon: inc.horizon,
|
|
502
|
+
status: inc.status,
|
|
503
|
+
done: inc.doneCount,
|
|
504
|
+
total: inc.totalCount
|
|
505
|
+
})),
|
|
506
|
+
gaps: derived.gaps.length
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = {
|
|
511
|
+
PRD_PATH,
|
|
512
|
+
ROADMAP_PATH,
|
|
513
|
+
LEDGER_PATH,
|
|
514
|
+
parsePrdRequirements,
|
|
515
|
+
parseRoadmapIncrements,
|
|
516
|
+
derive,
|
|
517
|
+
progressBar,
|
|
518
|
+
renderProgressLines,
|
|
519
|
+
renderLedger,
|
|
520
|
+
writeLedger,
|
|
521
|
+
summarizeForState,
|
|
522
|
+
normalizeStatus,
|
|
523
|
+
slugify
|
|
524
|
+
};
|