@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Preseed Workflow Engine
|
|
3
|
+
*
|
|
4
|
+
* Professional-grade approval workflow for AI-assisted document generation.
|
|
5
|
+
* Handles state machine, versioning, phase dependencies, and quality gates.
|
|
6
|
+
*
|
|
7
|
+
* @package bootspring
|
|
8
|
+
* @module core/preseed-workflow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Document lifecycle states
|
|
18
|
+
*/
|
|
19
|
+
const DOCUMENT_STATUS = {
|
|
20
|
+
EMPTY: 'empty',
|
|
21
|
+
DRAFT: 'draft',
|
|
22
|
+
IN_REVIEW: 'in_review',
|
|
23
|
+
APPROVED: 'approved',
|
|
24
|
+
REJECTED: 'rejected',
|
|
25
|
+
LOCKED: 'locked'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Workflow modes
|
|
30
|
+
*/
|
|
31
|
+
const WORKFLOW_MODE = {
|
|
32
|
+
GENERATION: 'generation',
|
|
33
|
+
REVIEW: 'review',
|
|
34
|
+
APPROVAL: 'approval'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Phase definitions with dependencies
|
|
39
|
+
*/
|
|
40
|
+
const WORKFLOW_PHASES = {
|
|
41
|
+
foundation: {
|
|
42
|
+
name: 'Foundation',
|
|
43
|
+
description: 'Core vision and audience definition',
|
|
44
|
+
documents: ['vision', 'audience'],
|
|
45
|
+
dependencies: [],
|
|
46
|
+
parallel: true
|
|
47
|
+
},
|
|
48
|
+
market: {
|
|
49
|
+
name: 'Market Analysis',
|
|
50
|
+
description: 'Market opportunity and competitive landscape',
|
|
51
|
+
documents: ['market', 'competitors'],
|
|
52
|
+
dependencies: ['foundation'],
|
|
53
|
+
parallel: true
|
|
54
|
+
},
|
|
55
|
+
business: {
|
|
56
|
+
name: 'Business Model',
|
|
57
|
+
description: 'Revenue model and pricing strategy',
|
|
58
|
+
documents: ['business-model'],
|
|
59
|
+
dependencies: ['foundation'],
|
|
60
|
+
parallel: false
|
|
61
|
+
},
|
|
62
|
+
product: {
|
|
63
|
+
name: 'Product Definition',
|
|
64
|
+
description: 'Product requirements and specifications',
|
|
65
|
+
documents: ['prd'],
|
|
66
|
+
dependencies: ['foundation', 'market', 'business'],
|
|
67
|
+
parallel: false
|
|
68
|
+
},
|
|
69
|
+
technical: {
|
|
70
|
+
name: 'Technical Specification',
|
|
71
|
+
description: 'Architecture and implementation details',
|
|
72
|
+
documents: ['technical-spec'],
|
|
73
|
+
dependencies: ['product'],
|
|
74
|
+
parallel: false
|
|
75
|
+
},
|
|
76
|
+
execution: {
|
|
77
|
+
name: 'Execution Planning',
|
|
78
|
+
description: 'Roadmap and milestones',
|
|
79
|
+
documents: ['roadmap'],
|
|
80
|
+
dependencies: ['product'],
|
|
81
|
+
parallel: false
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Quality check criteria per document type
|
|
87
|
+
*/
|
|
88
|
+
const QUALITY_CRITERIA = {
|
|
89
|
+
vision: {
|
|
90
|
+
completeness: [
|
|
91
|
+
{ id: 'problem', label: 'Problem statement defined', weight: 20 },
|
|
92
|
+
{ id: 'solution', label: 'Solution overview provided', weight: 20 },
|
|
93
|
+
{ id: 'value', label: 'Unique value proposition', weight: 15 },
|
|
94
|
+
{ id: 'why_now', label: 'Why now timing explained', weight: 10 },
|
|
95
|
+
{ id: 'features', label: 'Key features listed', weight: 15 }
|
|
96
|
+
],
|
|
97
|
+
clarity: [
|
|
98
|
+
{ id: 'specific', label: 'Specific and measurable', weight: 10 },
|
|
99
|
+
{ id: 'actionable', label: 'Actionable next steps', weight: 10 }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
audience: {
|
|
103
|
+
completeness: [
|
|
104
|
+
{ id: 'primary', label: 'Primary audience defined', weight: 25 },
|
|
105
|
+
{ id: 'segments', label: 'Market segments identified', weight: 20 },
|
|
106
|
+
{ id: 'personas', label: 'User personas created', weight: 25 },
|
|
107
|
+
{ id: 'icp', label: 'Ideal customer profile', weight: 20 }
|
|
108
|
+
],
|
|
109
|
+
clarity: [
|
|
110
|
+
{ id: 'specific', label: 'Specific demographics', weight: 10 }
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
market: {
|
|
114
|
+
completeness: [
|
|
115
|
+
{ id: 'tam', label: 'TAM calculated', weight: 20 },
|
|
116
|
+
{ id: 'sam', label: 'SAM calculated', weight: 20 },
|
|
117
|
+
{ id: 'som', label: 'SOM projected', weight: 20 },
|
|
118
|
+
{ id: 'trends', label: 'Market trends identified', weight: 20 }
|
|
119
|
+
],
|
|
120
|
+
clarity: [
|
|
121
|
+
{ id: 'sources', label: 'Data sources cited', weight: 10 },
|
|
122
|
+
{ id: 'growth', label: 'Growth projections', weight: 10 }
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
competitors: {
|
|
126
|
+
completeness: [
|
|
127
|
+
{ id: 'direct', label: 'Direct competitors listed', weight: 25 },
|
|
128
|
+
{ id: 'indirect', label: 'Indirect competitors noted', weight: 15 },
|
|
129
|
+
{ id: 'positioning', label: 'Positioning defined', weight: 25 },
|
|
130
|
+
{ id: 'differentiation', label: 'Differentiators identified', weight: 25 }
|
|
131
|
+
],
|
|
132
|
+
clarity: [
|
|
133
|
+
{ id: 'strengths', label: 'Strengths/weaknesses analyzed', weight: 10 }
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
'business-model': {
|
|
137
|
+
completeness: [
|
|
138
|
+
{ id: 'model', label: 'Business model type', weight: 20 },
|
|
139
|
+
{ id: 'revenue', label: 'Revenue streams defined', weight: 25 },
|
|
140
|
+
{ id: 'pricing', label: 'Pricing strategy set', weight: 25 },
|
|
141
|
+
{ id: 'economics', label: 'Unit economics calculated', weight: 20 }
|
|
142
|
+
],
|
|
143
|
+
clarity: [
|
|
144
|
+
{ id: 'scalable', label: 'Scalability addressed', weight: 10 }
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
prd: {
|
|
148
|
+
completeness: [
|
|
149
|
+
{ id: 'vision', label: 'Product vision stated', weight: 15 },
|
|
150
|
+
{ id: 'mvp', label: 'MVP features defined', weight: 25 },
|
|
151
|
+
{ id: 'stories', label: 'User stories written', weight: 25 },
|
|
152
|
+
{ id: 'acceptance', label: 'Acceptance criteria', weight: 20 }
|
|
153
|
+
],
|
|
154
|
+
clarity: [
|
|
155
|
+
{ id: 'priority', label: 'Priorities clear', weight: 10 },
|
|
156
|
+
{ id: 'scope', label: 'Scope boundaries set', weight: 5 }
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
'technical-spec': {
|
|
160
|
+
completeness: [
|
|
161
|
+
{ id: 'stack', label: 'Tech stack defined', weight: 25 },
|
|
162
|
+
{ id: 'architecture', label: 'Architecture documented', weight: 25 },
|
|
163
|
+
{ id: 'integrations', label: 'Integrations listed', weight: 20 },
|
|
164
|
+
{ id: 'constraints', label: 'Constraints noted', weight: 15 }
|
|
165
|
+
],
|
|
166
|
+
clarity: [
|
|
167
|
+
{ id: 'diagrams', label: 'Diagrams included', weight: 10 },
|
|
168
|
+
{ id: 'decisions', label: 'Decision rationale', weight: 5 }
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
roadmap: {
|
|
172
|
+
completeness: [
|
|
173
|
+
{ id: 'phases', label: 'Development phases', weight: 30 },
|
|
174
|
+
{ id: 'milestones', label: 'Milestones defined', weight: 25 },
|
|
175
|
+
{ id: 'timeline', label: 'Timeline estimated', weight: 25 }
|
|
176
|
+
],
|
|
177
|
+
clarity: [
|
|
178
|
+
{ id: 'dependencies', label: 'Dependencies mapped', weight: 10 },
|
|
179
|
+
{ id: 'measurable', label: 'Measurable outcomes', weight: 10 }
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Default workflow state
|
|
186
|
+
*/
|
|
187
|
+
const DEFAULT_WORKFLOW_STATE = {
|
|
188
|
+
version: '1.0.0',
|
|
189
|
+
currentPhase: null,
|
|
190
|
+
currentDocument: null,
|
|
191
|
+
mode: WORKFLOW_MODE.GENERATION,
|
|
192
|
+
resumePoint: null,
|
|
193
|
+
startedAt: null,
|
|
194
|
+
lastUpdated: null,
|
|
195
|
+
documents: {},
|
|
196
|
+
phases: {},
|
|
197
|
+
overrides: []
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* PreseedWorkflowEngine - Manages document approval workflow
|
|
202
|
+
*/
|
|
203
|
+
class PreseedWorkflowEngine {
|
|
204
|
+
constructor(projectRoot, options = {}) {
|
|
205
|
+
this.projectRoot = projectRoot;
|
|
206
|
+
this.preseedDir = path.join(projectRoot, '.bootspring', 'preseed');
|
|
207
|
+
this.draftsDir = path.join(this.preseedDir, 'drafts');
|
|
208
|
+
this.approvedDir = path.join(this.preseedDir, 'approved');
|
|
209
|
+
this.historyDir = path.join(this.preseedDir, 'history');
|
|
210
|
+
this.stateFile = path.join(this.preseedDir, 'workflow-state.json');
|
|
211
|
+
this.options = options;
|
|
212
|
+
this.state = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Initialize workflow directories
|
|
217
|
+
*/
|
|
218
|
+
setupDirectories() {
|
|
219
|
+
const dirs = [this.preseedDir, this.draftsDir, this.approvedDir, this.historyDir];
|
|
220
|
+
for (const dir of dirs) {
|
|
221
|
+
if (!fs.existsSync(dir)) {
|
|
222
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Load workflow state from file
|
|
229
|
+
*/
|
|
230
|
+
loadState() {
|
|
231
|
+
if (fs.existsSync(this.stateFile)) {
|
|
232
|
+
try {
|
|
233
|
+
this.state = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
|
|
234
|
+
return true;
|
|
235
|
+
} catch (_e) {
|
|
236
|
+
this.state = { ...DEFAULT_WORKFLOW_STATE };
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.state = { ...DEFAULT_WORKFLOW_STATE };
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Save workflow state to file
|
|
246
|
+
*/
|
|
247
|
+
saveState() {
|
|
248
|
+
this.setupDirectories();
|
|
249
|
+
this.state.lastUpdated = new Date().toISOString();
|
|
250
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Initialize a new workflow
|
|
255
|
+
*/
|
|
256
|
+
initializeWorkflow() {
|
|
257
|
+
this.setupDirectories();
|
|
258
|
+
this.state = {
|
|
259
|
+
...DEFAULT_WORKFLOW_STATE,
|
|
260
|
+
startedAt: new Date().toISOString(),
|
|
261
|
+
lastUpdated: new Date().toISOString()
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Initialize phase states
|
|
265
|
+
for (const [phaseId, phase] of Object.entries(WORKFLOW_PHASES)) {
|
|
266
|
+
this.state.phases[phaseId] = {
|
|
267
|
+
status: 'pending',
|
|
268
|
+
documents: phase.documents.map(docType => docType)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Initialize document states
|
|
273
|
+
for (const phase of Object.values(WORKFLOW_PHASES)) {
|
|
274
|
+
for (const docType of phase.documents) {
|
|
275
|
+
this.state.documents[docType] = {
|
|
276
|
+
status: DOCUMENT_STATUS.EMPTY,
|
|
277
|
+
version: 0,
|
|
278
|
+
qualityScore: null,
|
|
279
|
+
source: null,
|
|
280
|
+
feedback: [],
|
|
281
|
+
history: []
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Start with first phase
|
|
287
|
+
this.state.currentPhase = 'foundation';
|
|
288
|
+
this.state.currentDocument = WORKFLOW_PHASES.foundation.documents[0];
|
|
289
|
+
this.state.mode = WORKFLOW_MODE.GENERATION;
|
|
290
|
+
|
|
291
|
+
this.saveState();
|
|
292
|
+
return this.state;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a phase's dependencies are met
|
|
297
|
+
*/
|
|
298
|
+
arePhaseDependenciesMet(phaseId) {
|
|
299
|
+
const phase = WORKFLOW_PHASES[phaseId];
|
|
300
|
+
if (!phase || !phase.dependencies || phase.dependencies.length === 0) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const depPhaseId of phase.dependencies) {
|
|
305
|
+
const depPhase = this.state.phases[depPhaseId];
|
|
306
|
+
if (!depPhase || depPhase.status !== 'completed') {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if all documents in a phase are approved
|
|
316
|
+
*/
|
|
317
|
+
isPhaseComplete(phaseId) {
|
|
318
|
+
const phase = WORKFLOW_PHASES[phaseId];
|
|
319
|
+
if (!phase) return false;
|
|
320
|
+
|
|
321
|
+
for (const docType of phase.documents) {
|
|
322
|
+
const doc = this.state.documents[docType];
|
|
323
|
+
if (!doc || (doc.status !== DOCUMENT_STATUS.APPROVED && doc.status !== DOCUMENT_STATUS.LOCKED)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get the next available phase
|
|
333
|
+
*/
|
|
334
|
+
getNextPhase(currentPhaseId) {
|
|
335
|
+
const phaseOrder = Object.keys(WORKFLOW_PHASES);
|
|
336
|
+
const currentIndex = phaseOrder.indexOf(currentPhaseId);
|
|
337
|
+
|
|
338
|
+
// Mark current phase as complete if all docs approved
|
|
339
|
+
if (this.isPhaseComplete(currentPhaseId)) {
|
|
340
|
+
this.state.phases[currentPhaseId].status = 'completed';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Find next phase with met dependencies
|
|
344
|
+
for (let i = currentIndex + 1; i < phaseOrder.length; i++) {
|
|
345
|
+
const nextPhaseId = phaseOrder[i];
|
|
346
|
+
if (this.arePhaseDependenciesMet(nextPhaseId)) {
|
|
347
|
+
return nextPhaseId;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get the next document to work on
|
|
356
|
+
*/
|
|
357
|
+
getNextDocument() {
|
|
358
|
+
// First, try to find unapproved docs in current phase
|
|
359
|
+
if (this.state.currentPhase) {
|
|
360
|
+
const phase = WORKFLOW_PHASES[this.state.currentPhase];
|
|
361
|
+
for (const docType of phase.documents) {
|
|
362
|
+
const doc = this.state.documents[docType];
|
|
363
|
+
if (doc.status !== DOCUMENT_STATUS.APPROVED && doc.status !== DOCUMENT_STATUS.LOCKED) {
|
|
364
|
+
return { phase: this.state.currentPhase, document: docType };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Try next phase
|
|
370
|
+
const nextPhase = this.getNextPhase(this.state.currentPhase);
|
|
371
|
+
if (nextPhase) {
|
|
372
|
+
const phase = WORKFLOW_PHASES[nextPhase];
|
|
373
|
+
return { phase: nextPhase, document: phase.documents[0] };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get document filename
|
|
381
|
+
*/
|
|
382
|
+
getDocumentFilename(docType) {
|
|
383
|
+
const names = {
|
|
384
|
+
vision: 'VISION.md',
|
|
385
|
+
audience: 'AUDIENCE.md',
|
|
386
|
+
market: 'MARKET.md',
|
|
387
|
+
competitors: 'COMPETITORS.md',
|
|
388
|
+
'business-model': 'BUSINESS_MODEL.md',
|
|
389
|
+
prd: 'PRD.md',
|
|
390
|
+
'technical-spec': 'TECHNICAL_SPEC.md',
|
|
391
|
+
roadmap: 'ROADMAP.md'
|
|
392
|
+
};
|
|
393
|
+
return names[docType] || `${docType.toUpperCase()}.md`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a new draft version
|
|
398
|
+
*/
|
|
399
|
+
createDraft(docType, content, source = 'generated') {
|
|
400
|
+
const doc = this.state.documents[docType];
|
|
401
|
+
const newVersion = doc.version + 1;
|
|
402
|
+
const filename = this.getDocumentFilename(docType);
|
|
403
|
+
const baseName = filename.replace('.md', '');
|
|
404
|
+
const draftPath = path.join(this.draftsDir, `${baseName}.v${newVersion}.md`);
|
|
405
|
+
|
|
406
|
+
// Write draft
|
|
407
|
+
fs.writeFileSync(draftPath, content);
|
|
408
|
+
|
|
409
|
+
// Update state
|
|
410
|
+
doc.version = newVersion;
|
|
411
|
+
doc.status = DOCUMENT_STATUS.DRAFT;
|
|
412
|
+
doc.source = source;
|
|
413
|
+
doc.history.push({
|
|
414
|
+
version: newVersion,
|
|
415
|
+
action: 'created',
|
|
416
|
+
timestamp: new Date().toISOString(),
|
|
417
|
+
source
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
this.saveState();
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
path: draftPath,
|
|
424
|
+
version: newVersion,
|
|
425
|
+
docType
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get current draft path
|
|
431
|
+
*/
|
|
432
|
+
getCurrentDraftPath(docType) {
|
|
433
|
+
const doc = this.state.documents[docType];
|
|
434
|
+
if (!doc || doc.version === 0) return null;
|
|
435
|
+
|
|
436
|
+
const filename = this.getDocumentFilename(docType);
|
|
437
|
+
const baseName = filename.replace('.md', '');
|
|
438
|
+
return path.join(this.draftsDir, `${baseName}.v${doc.version}.md`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get approved document path
|
|
443
|
+
*/
|
|
444
|
+
getApprovedPath(docType) {
|
|
445
|
+
const filename = this.getDocumentFilename(docType);
|
|
446
|
+
return path.join(this.approvedDir, filename);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Read current draft content
|
|
451
|
+
*/
|
|
452
|
+
readDraft(docType) {
|
|
453
|
+
const draftPath = this.getCurrentDraftPath(docType);
|
|
454
|
+
if (!draftPath || !fs.existsSync(draftPath)) return null;
|
|
455
|
+
return fs.readFileSync(draftPath, 'utf-8');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Calculate quality score for a document
|
|
460
|
+
*/
|
|
461
|
+
calculateQualityScore(docType, content) {
|
|
462
|
+
const criteria = QUALITY_CRITERIA[docType];
|
|
463
|
+
if (!criteria) {
|
|
464
|
+
return { score: 100, breakdown: {} };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const breakdown = {
|
|
468
|
+
completeness: { score: 0, maxScore: 0, checks: [] },
|
|
469
|
+
clarity: { score: 0, maxScore: 0, checks: [] }
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Check completeness criteria
|
|
473
|
+
for (const check of criteria.completeness || []) {
|
|
474
|
+
breakdown.completeness.maxScore += check.weight;
|
|
475
|
+
// Simple heuristic: check if relevant sections exist
|
|
476
|
+
const passed = this.checkCriterion(check.id, docType, content);
|
|
477
|
+
if (passed) {
|
|
478
|
+
breakdown.completeness.score += check.weight;
|
|
479
|
+
}
|
|
480
|
+
breakdown.completeness.checks.push({
|
|
481
|
+
...check,
|
|
482
|
+
passed
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check clarity criteria
|
|
487
|
+
for (const check of criteria.clarity || []) {
|
|
488
|
+
breakdown.clarity.maxScore += check.weight;
|
|
489
|
+
const passed = this.checkCriterion(check.id, docType, content);
|
|
490
|
+
if (passed) {
|
|
491
|
+
breakdown.clarity.score += check.weight;
|
|
492
|
+
}
|
|
493
|
+
breakdown.clarity.checks.push({
|
|
494
|
+
...check,
|
|
495
|
+
passed
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const totalScore = breakdown.completeness.maxScore + breakdown.clarity.maxScore;
|
|
500
|
+
const earnedScore = breakdown.completeness.score + breakdown.clarity.score;
|
|
501
|
+
const score = totalScore > 0 ? Math.round((earnedScore / totalScore) * 100) : 0;
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
score,
|
|
505
|
+
completeness: breakdown.completeness.maxScore > 0
|
|
506
|
+
? Math.round((breakdown.completeness.score / breakdown.completeness.maxScore) * 100)
|
|
507
|
+
: 0,
|
|
508
|
+
clarity: breakdown.clarity.maxScore > 0
|
|
509
|
+
? Math.round((breakdown.clarity.score / breakdown.clarity.maxScore) * 100)
|
|
510
|
+
: 0,
|
|
511
|
+
breakdown
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Check a specific quality criterion
|
|
517
|
+
*/
|
|
518
|
+
checkCriterion(criterionId, docType, content) {
|
|
519
|
+
const contentLower = content.toLowerCase();
|
|
520
|
+
const contentLength = content.length;
|
|
521
|
+
|
|
522
|
+
// Generic checks
|
|
523
|
+
const checks = {
|
|
524
|
+
// Vision checks
|
|
525
|
+
problem: () => contentLower.includes('problem') || contentLower.includes('challenge'),
|
|
526
|
+
solution: () => contentLower.includes('solution') || contentLower.includes('our approach'),
|
|
527
|
+
value: () => contentLower.includes('value') || contentLower.includes('unique'),
|
|
528
|
+
why_now: () => contentLower.includes('why now') || contentLower.includes('timing'),
|
|
529
|
+
features: () => contentLower.includes('feature') || contentLower.includes('capability'),
|
|
530
|
+
|
|
531
|
+
// Audience checks
|
|
532
|
+
primary: () => contentLower.includes('primary') || contentLower.includes('target'),
|
|
533
|
+
segments: () => contentLower.includes('segment'),
|
|
534
|
+
personas: () => contentLower.includes('persona'),
|
|
535
|
+
icp: () => contentLower.includes('ideal customer') || contentLower.includes('icp'),
|
|
536
|
+
|
|
537
|
+
// Market checks
|
|
538
|
+
tam: () => contentLower.includes('tam') || contentLower.includes('total addressable'),
|
|
539
|
+
sam: () => contentLower.includes('sam') || contentLower.includes('serviceable addressable'),
|
|
540
|
+
som: () => contentLower.includes('som') || contentLower.includes('serviceable obtainable'),
|
|
541
|
+
trends: () => contentLower.includes('trend'),
|
|
542
|
+
sources: () => contentLower.includes('source') || contentLower.includes('data'),
|
|
543
|
+
growth: () => contentLower.includes('growth') || contentLower.includes('%'),
|
|
544
|
+
|
|
545
|
+
// Competitor checks
|
|
546
|
+
direct: () => contentLower.includes('direct competitor'),
|
|
547
|
+
indirect: () => contentLower.includes('indirect') || contentLower.includes('alternative'),
|
|
548
|
+
positioning: () => contentLower.includes('position'),
|
|
549
|
+
differentiation: () => contentLower.includes('differenti') || contentLower.includes('unique'),
|
|
550
|
+
strengths: () => contentLower.includes('strength') || contentLower.includes('weakness'),
|
|
551
|
+
|
|
552
|
+
// Business model checks
|
|
553
|
+
model: () => contentLower.includes('model') || contentLower.includes('subscription') || contentLower.includes('freemium'),
|
|
554
|
+
revenue: () => contentLower.includes('revenue'),
|
|
555
|
+
pricing: () => contentLower.includes('pricing') || contentLower.includes('$'),
|
|
556
|
+
economics: () => contentLower.includes('cac') || contentLower.includes('ltv') || contentLower.includes('economics'),
|
|
557
|
+
scalable: () => contentLower.includes('scale') || contentLower.includes('growth'),
|
|
558
|
+
|
|
559
|
+
// PRD checks
|
|
560
|
+
vision: () => contentLower.includes('vision') || contentLower.includes('goal'),
|
|
561
|
+
mvp: () => contentLower.includes('mvp') || contentLower.includes('minimum viable'),
|
|
562
|
+
stories: () => contentLower.includes('user story') || contentLower.includes('as a'),
|
|
563
|
+
acceptance: () => contentLower.includes('acceptance') || contentLower.includes('criteria'),
|
|
564
|
+
priority: () => contentLower.includes('priority') || contentLower.includes('p1') || contentLower.includes('must'),
|
|
565
|
+
scope: () => contentLower.includes('scope') || contentLower.includes('out of scope'),
|
|
566
|
+
|
|
567
|
+
// Technical spec checks
|
|
568
|
+
stack: () => contentLower.includes('stack') || contentLower.includes('technology'),
|
|
569
|
+
architecture: () => contentLower.includes('architecture') || contentLower.includes('design'),
|
|
570
|
+
integrations: () => contentLower.includes('integration') || contentLower.includes('api'),
|
|
571
|
+
constraints: () => contentLower.includes('constraint') || contentLower.includes('requirement'),
|
|
572
|
+
diagrams: () => contentLower.includes('diagram') || contentLower.includes('```') || contentLength > 2000,
|
|
573
|
+
decisions: () => contentLower.includes('decision') || contentLower.includes('chose') || contentLower.includes('rationale'),
|
|
574
|
+
|
|
575
|
+
// Roadmap checks
|
|
576
|
+
phases: () => contentLower.includes('phase') || contentLower.includes('stage'),
|
|
577
|
+
milestones: () => contentLower.includes('milestone'),
|
|
578
|
+
timeline: () => contentLower.includes('timeline') || contentLower.includes('week') || contentLower.includes('month'),
|
|
579
|
+
dependencies: () => contentLower.includes('depend') || contentLower.includes('requires'),
|
|
580
|
+
measurable: () => contentLower.includes('measur') || contentLower.includes('kpi') || contentLower.includes('metric'),
|
|
581
|
+
|
|
582
|
+
// Generic
|
|
583
|
+
specific: () => contentLength > 500,
|
|
584
|
+
actionable: () => contentLower.includes('next step') || contentLower.includes('action') || contentLower.includes('todo')
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const checkFn = checks[criterionId];
|
|
588
|
+
return checkFn ? checkFn() : false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Transition document to review state
|
|
593
|
+
*/
|
|
594
|
+
submitForReview(docType) {
|
|
595
|
+
const doc = this.state.documents[docType];
|
|
596
|
+
if (!doc || doc.status !== DOCUMENT_STATUS.DRAFT) {
|
|
597
|
+
throw new Error(`Document ${docType} is not in draft state`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Calculate quality score
|
|
601
|
+
const content = this.readDraft(docType);
|
|
602
|
+
if (!content) {
|
|
603
|
+
throw new Error(`Cannot read draft for ${docType}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const quality = this.calculateQualityScore(docType, content);
|
|
607
|
+
doc.qualityScore = quality.score;
|
|
608
|
+
doc.status = DOCUMENT_STATUS.IN_REVIEW;
|
|
609
|
+
doc.history.push({
|
|
610
|
+
version: doc.version,
|
|
611
|
+
action: 'submitted_for_review',
|
|
612
|
+
timestamp: new Date().toISOString(),
|
|
613
|
+
qualityScore: quality.score
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
this.state.mode = WORKFLOW_MODE.REVIEW;
|
|
617
|
+
this.saveState();
|
|
618
|
+
|
|
619
|
+
return quality;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Approve a document
|
|
624
|
+
*/
|
|
625
|
+
approveDocument(docType) {
|
|
626
|
+
const doc = this.state.documents[docType];
|
|
627
|
+
if (!doc || (doc.status !== DOCUMENT_STATUS.IN_REVIEW && doc.status !== DOCUMENT_STATUS.DRAFT)) {
|
|
628
|
+
throw new Error(`Document ${docType} cannot be approved from status: ${doc?.status}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Copy draft to approved
|
|
632
|
+
const draftPath = this.getCurrentDraftPath(docType);
|
|
633
|
+
const approvedPath = this.getApprovedPath(docType);
|
|
634
|
+
|
|
635
|
+
if (draftPath && fs.existsSync(draftPath)) {
|
|
636
|
+
// Ensure approved directory exists
|
|
637
|
+
if (!fs.existsSync(this.approvedDir)) {
|
|
638
|
+
fs.mkdirSync(this.approvedDir, { recursive: true });
|
|
639
|
+
}
|
|
640
|
+
fs.copyFileSync(draftPath, approvedPath);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Archive the draft to history
|
|
644
|
+
const historyPath = path.join(
|
|
645
|
+
this.historyDir,
|
|
646
|
+
`${this.getDocumentFilename(docType).replace('.md', '')}.v${doc.version}.approved.md`
|
|
647
|
+
);
|
|
648
|
+
if (draftPath && fs.existsSync(draftPath)) {
|
|
649
|
+
fs.copyFileSync(draftPath, historyPath);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Update state
|
|
653
|
+
doc.status = DOCUMENT_STATUS.APPROVED;
|
|
654
|
+
doc.approvedAt = new Date().toISOString();
|
|
655
|
+
doc.history.push({
|
|
656
|
+
version: doc.version,
|
|
657
|
+
action: 'approved',
|
|
658
|
+
timestamp: new Date().toISOString()
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Check if phase is complete
|
|
662
|
+
const phaseId = this.findPhaseForDocument(docType);
|
|
663
|
+
if (phaseId && this.isPhaseComplete(phaseId)) {
|
|
664
|
+
this.state.phases[phaseId].status = 'completed';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Move to next document
|
|
668
|
+
const next = this.getNextDocument();
|
|
669
|
+
if (next) {
|
|
670
|
+
this.state.currentPhase = next.phase;
|
|
671
|
+
this.state.currentDocument = next.document;
|
|
672
|
+
this.state.mode = WORKFLOW_MODE.GENERATION;
|
|
673
|
+
} else {
|
|
674
|
+
// Workflow complete
|
|
675
|
+
this.state.currentPhase = null;
|
|
676
|
+
this.state.currentDocument = null;
|
|
677
|
+
this.state.mode = null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.saveState();
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
approved: true,
|
|
684
|
+
next,
|
|
685
|
+
workflowComplete: !next
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Reject a document with feedback
|
|
691
|
+
*/
|
|
692
|
+
rejectDocument(docType, feedback = '') {
|
|
693
|
+
const doc = this.state.documents[docType];
|
|
694
|
+
if (!doc || doc.status !== DOCUMENT_STATUS.IN_REVIEW) {
|
|
695
|
+
throw new Error(`Document ${docType} is not in review state`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
doc.status = DOCUMENT_STATUS.REJECTED;
|
|
699
|
+
doc.feedback.push({
|
|
700
|
+
timestamp: new Date().toISOString(),
|
|
701
|
+
message: feedback
|
|
702
|
+
});
|
|
703
|
+
doc.history.push({
|
|
704
|
+
version: doc.version,
|
|
705
|
+
action: 'rejected',
|
|
706
|
+
timestamp: new Date().toISOString(),
|
|
707
|
+
feedback
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
this.state.mode = WORKFLOW_MODE.GENERATION;
|
|
711
|
+
this.saveState();
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
rejected: true,
|
|
715
|
+
feedback
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Find which phase a document belongs to
|
|
721
|
+
*/
|
|
722
|
+
findPhaseForDocument(docType) {
|
|
723
|
+
for (const [phaseId, phase] of Object.entries(WORKFLOW_PHASES)) {
|
|
724
|
+
if (phase.documents.includes(docType)) {
|
|
725
|
+
return phaseId;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Open document in external editor
|
|
733
|
+
*/
|
|
734
|
+
openInEditor(docType) {
|
|
735
|
+
const draftPath = this.getCurrentDraftPath(docType);
|
|
736
|
+
if (!draftPath || !fs.existsSync(draftPath)) {
|
|
737
|
+
throw new Error(`No draft found for ${docType}`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Get file hash before editing
|
|
741
|
+
const beforeContent = fs.readFileSync(draftPath, 'utf-8');
|
|
742
|
+
const beforeHash = crypto.createHash('md5').update(beforeContent).digest('hex');
|
|
743
|
+
|
|
744
|
+
// Get editor from environment
|
|
745
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vim';
|
|
746
|
+
|
|
747
|
+
return new Promise((resolve, reject) => {
|
|
748
|
+
const child = spawn(editor, [draftPath], {
|
|
749
|
+
stdio: 'inherit',
|
|
750
|
+
shell: true
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
child.on('close', (code) => {
|
|
754
|
+
if (code !== 0) {
|
|
755
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Check if file changed
|
|
760
|
+
const afterContent = fs.readFileSync(draftPath, 'utf-8');
|
|
761
|
+
const afterHash = crypto.createHash('md5').update(afterContent).digest('hex');
|
|
762
|
+
|
|
763
|
+
const changed = beforeHash !== afterHash;
|
|
764
|
+
|
|
765
|
+
if (changed) {
|
|
766
|
+
// Update document state
|
|
767
|
+
const doc = this.state.documents[docType];
|
|
768
|
+
doc.history.push({
|
|
769
|
+
version: doc.version,
|
|
770
|
+
action: 'edited',
|
|
771
|
+
timestamp: new Date().toISOString()
|
|
772
|
+
});
|
|
773
|
+
this.saveState();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
resolve({
|
|
777
|
+
changed,
|
|
778
|
+
path: draftPath
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
child.on('error', (err) => {
|
|
783
|
+
reject(new Error(`Failed to open editor: ${err.message}`));
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Import/upload an existing document
|
|
790
|
+
*/
|
|
791
|
+
importDocument(docType, sourcePath) {
|
|
792
|
+
if (!fs.existsSync(sourcePath)) {
|
|
793
|
+
throw new Error(`Source file not found: ${sourcePath}`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
797
|
+
return this.createDraft(docType, content, 'uploaded');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Get workflow progress summary
|
|
802
|
+
*/
|
|
803
|
+
getProgress() {
|
|
804
|
+
if (!this.state) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const phases = Object.entries(WORKFLOW_PHASES).map(([phaseId, phase]) => {
|
|
809
|
+
const phaseState = this.state.phases[phaseId] || { status: 'pending' };
|
|
810
|
+
const documents = phase.documents.map(docType => {
|
|
811
|
+
const doc = this.state.documents[docType] || { status: DOCUMENT_STATUS.EMPTY };
|
|
812
|
+
return {
|
|
813
|
+
type: docType,
|
|
814
|
+
name: this.getDocumentFilename(docType),
|
|
815
|
+
status: doc.status,
|
|
816
|
+
version: doc.version,
|
|
817
|
+
qualityScore: doc.qualityScore
|
|
818
|
+
};
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const approvedCount = documents.filter(
|
|
822
|
+
d => d.status === DOCUMENT_STATUS.APPROVED || d.status === DOCUMENT_STATUS.LOCKED
|
|
823
|
+
).length;
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
id: phaseId,
|
|
827
|
+
name: phase.name,
|
|
828
|
+
description: phase.description,
|
|
829
|
+
status: phaseState.status,
|
|
830
|
+
dependenciesMet: this.arePhaseDependenciesMet(phaseId),
|
|
831
|
+
documents,
|
|
832
|
+
progress: {
|
|
833
|
+
approved: approvedCount,
|
|
834
|
+
total: documents.length,
|
|
835
|
+
percentage: Math.round((approvedCount / documents.length) * 100)
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const totalDocs = Object.keys(this.state.documents).length;
|
|
841
|
+
const approvedDocs = Object.values(this.state.documents).filter(
|
|
842
|
+
d => d.status === DOCUMENT_STATUS.APPROVED || d.status === DOCUMENT_STATUS.LOCKED
|
|
843
|
+
).length;
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
currentPhase: this.state.currentPhase,
|
|
847
|
+
currentDocument: this.state.currentDocument,
|
|
848
|
+
mode: this.state.mode,
|
|
849
|
+
startedAt: this.state.startedAt,
|
|
850
|
+
lastUpdated: this.state.lastUpdated,
|
|
851
|
+
phases,
|
|
852
|
+
overall: {
|
|
853
|
+
approved: approvedDocs,
|
|
854
|
+
total: totalDocs,
|
|
855
|
+
percentage: Math.round((approvedDocs / totalDocs) * 100)
|
|
856
|
+
},
|
|
857
|
+
isComplete: approvedDocs === totalDocs
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Get resume point for continuing workflow
|
|
863
|
+
*/
|
|
864
|
+
getResumePoint() {
|
|
865
|
+
if (!this.state || !this.state.currentPhase) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const doc = this.state.documents[this.state.currentDocument];
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
phase: this.state.currentPhase,
|
|
873
|
+
phaseName: WORKFLOW_PHASES[this.state.currentPhase]?.name,
|
|
874
|
+
document: this.state.currentDocument,
|
|
875
|
+
documentName: this.getDocumentFilename(this.state.currentDocument),
|
|
876
|
+
documentStatus: doc?.status,
|
|
877
|
+
mode: this.state.mode,
|
|
878
|
+
lastUpdated: this.state.lastUpdated
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Override a phase gate (with justification)
|
|
884
|
+
*/
|
|
885
|
+
overridePhaseGate(phaseId, justification) {
|
|
886
|
+
if (!justification || justification.trim().length < 10) {
|
|
887
|
+
throw new Error('A meaningful justification (10+ chars) is required to override phase gates');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
this.state.overrides.push({
|
|
891
|
+
phaseId,
|
|
892
|
+
justification,
|
|
893
|
+
timestamp: new Date().toISOString()
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Mark dependencies as overridden
|
|
897
|
+
this.state.phases[phaseId].overridden = true;
|
|
898
|
+
|
|
899
|
+
this.saveState();
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
overridden: true,
|
|
903
|
+
phaseId,
|
|
904
|
+
justification
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Check if workflow exists
|
|
910
|
+
*/
|
|
911
|
+
hasWorkflow() {
|
|
912
|
+
return fs.existsSync(this.stateFile);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Reset workflow (dangerous!)
|
|
917
|
+
*/
|
|
918
|
+
resetWorkflow() {
|
|
919
|
+
if (fs.existsSync(this.stateFile)) {
|
|
920
|
+
fs.unlinkSync(this.stateFile);
|
|
921
|
+
}
|
|
922
|
+
this.state = null;
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
module.exports = {
|
|
928
|
+
PreseedWorkflowEngine,
|
|
929
|
+
DOCUMENT_STATUS,
|
|
930
|
+
WORKFLOW_MODE,
|
|
931
|
+
WORKFLOW_PHASES,
|
|
932
|
+
QUALITY_CRITERIA,
|
|
933
|
+
DEFAULT_WORKFLOW_STATE
|
|
934
|
+
};
|