popeye-cli 2.1.0 → 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/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +4 -1
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +23 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts.map +1 -1
- package/dist/pipeline/artifact-manager.js +3 -0
- package/dist/pipeline/artifact-manager.js.map +1 -1
- package/dist/pipeline/gate-engine.js +1 -1
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/migration.d.ts.map +1 -1
- package/dist/pipeline/migration.js +3 -26
- package/dist/pipeline/migration.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +5 -0
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts +1 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +49 -10
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +2 -3
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
- package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
- package/dist/pipeline/skills/constitution-generator.js +210 -0
- package/dist/pipeline/skills/constitution-generator.js.map +1 -0
- package/dist/pipeline/skills/generator.d.ts +65 -0
- package/dist/pipeline/skills/generator.d.ts.map +1 -0
- package/dist/pipeline/skills/generator.js +221 -0
- package/dist/pipeline/skills/generator.js.map +1 -0
- package/dist/pipeline/skills/role-map.d.ts +38 -0
- package/dist/pipeline/skills/role-map.d.ts.map +1 -0
- package/dist/pipeline/skills/role-map.js +234 -0
- package/dist/pipeline/skills/role-map.js.map +1 -0
- package/dist/pipeline/skills/types.d.ts +47 -0
- package/dist/pipeline/skills/types.d.ts.map +1 -0
- package/dist/pipeline/skills/types.js +5 -0
- package/dist/pipeline/skills/types.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +5 -0
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +1 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +3 -0
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +1 -0
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +15 -0
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +5 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/interactive.ts +4 -1
- package/src/generators/all.ts +23 -1
- package/src/pipeline/artifact-manager.ts +3 -0
- package/src/pipeline/gate-engine.ts +1 -1
- package/src/pipeline/migration.ts +5 -30
- package/src/pipeline/orchestrator.ts +6 -0
- package/src/pipeline/phases/intake.ts +60 -11
- package/src/pipeline/phases/role-planning.ts +2 -3
- package/src/pipeline/skills/constitution-generator.ts +236 -0
- package/src/pipeline/skills/generator.ts +287 -0
- package/src/pipeline/skills/role-map.ts +248 -0
- package/src/pipeline/skills/types.ts +53 -0
- package/src/pipeline/type-defs/artifacts.ts +1 -0
- package/tests/pipeline/migration.test.ts +4 -3
- package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
- package/tests/pipeline/skills/generator.test.ts +213 -0
- package/tests/pipeline/skills/role-map.test.ts +198 -0
package/src/generators/all.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type { GenerationResult } from './python.js';
|
|
|
10
10
|
import { generateFullstackProject } from './fullstack.js';
|
|
11
11
|
import { generateWebsiteProject } from './website.js';
|
|
12
12
|
import type { WebsiteContentContext } from './website-context.js';
|
|
13
|
-
import { buildWebsiteContext, validateWebsiteContext } from './website-context.js';
|
|
13
|
+
import { buildWebsiteContext, validateWebsiteContext, resolveBrandAssets } from './website-context.js';
|
|
14
|
+
import { loadWebsiteStrategy } from '../workflow/website-strategy.js';
|
|
14
15
|
import {
|
|
15
16
|
generateDesignTokensPackage as generateDesignTokensPackageImpl,
|
|
16
17
|
generateUiPackage as generateUiPackageImpl,
|
|
@@ -403,6 +404,25 @@ export async function generateAllProject(
|
|
|
403
404
|
console.warn(`[website-context] Warning: ${contextWarning}`);
|
|
404
405
|
}
|
|
405
406
|
|
|
407
|
+
// Resolve brand assets (logo output path) — must happen after buildWebsiteContext
|
|
408
|
+
if (contentContext?.brand) {
|
|
409
|
+
try {
|
|
410
|
+
contentContext.brandAssets = await resolveBrandAssets(projectDir, contentContext.brand);
|
|
411
|
+
} catch {
|
|
412
|
+
// Non-fatal: brand assets are optional
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Load website strategy if previously generated (e.g. re-scaffold)
|
|
417
|
+
try {
|
|
418
|
+
const strategyData = await loadWebsiteStrategy(projectDir);
|
|
419
|
+
if (strategyData && contentContext) {
|
|
420
|
+
contentContext.strategy = strategyData.strategy;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// Strategy won't exist on first scaffold — generated later by runPlanMode
|
|
424
|
+
}
|
|
425
|
+
|
|
406
426
|
// Soft validation: log quality issues without blocking monorepo generation
|
|
407
427
|
if (contentContext) {
|
|
408
428
|
const validation = validateWebsiteContext(contentContext, projectName);
|
|
@@ -419,12 +439,14 @@ export async function generateAllProject(
|
|
|
419
439
|
filesCreated.push(...fullstackResult.filesCreated);
|
|
420
440
|
|
|
421
441
|
// Generate website app
|
|
442
|
+
// skipValidation: strategy is generated later by runPlanMode, not at scaffold time
|
|
422
443
|
const websiteResult = await generateWebsiteProject(spec, projectDir, {
|
|
423
444
|
baseDir: path.join(projectDir, 'apps', 'website'),
|
|
424
445
|
workspaceMode: true,
|
|
425
446
|
skipDocker: true, // Website runs outside Docker (npm run dev / npm start)
|
|
426
447
|
skipReadme: false,
|
|
427
448
|
contentContext: contentContext,
|
|
449
|
+
skipValidation: true,
|
|
428
450
|
});
|
|
429
451
|
if (!websiteResult.success) {
|
|
430
452
|
return {
|
|
@@ -45,6 +45,8 @@ const ARTIFACT_DIRS: Record<string, string> = {
|
|
|
45
45
|
resolved_commands: 'checks',
|
|
46
46
|
constitution: 'governance',
|
|
47
47
|
change_request: 'governance',
|
|
48
|
+
additional_context: 'context',
|
|
49
|
+
skill_generation_log: 'context',
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
/** All required subdirectories under /docs/ */
|
|
@@ -62,6 +64,7 @@ const DOCS_SUBDIRS = [
|
|
|
62
64
|
'checks',
|
|
63
65
|
'journal',
|
|
64
66
|
'governance',
|
|
67
|
+
'context',
|
|
65
68
|
];
|
|
66
69
|
|
|
67
70
|
// ─── Helper Functions ────────────────────────────────────
|
|
@@ -40,7 +40,7 @@ export interface GateResult {
|
|
|
40
40
|
const GATE_DEFINITIONS: Record<PipelinePhase, GateDefinition> = {
|
|
41
41
|
INTAKE: {
|
|
42
42
|
phase: 'INTAKE',
|
|
43
|
-
requiredArtifacts: ['master_plan', 'repo_snapshot'
|
|
43
|
+
requiredArtifacts: ['master_plan', 'repo_snapshot'],
|
|
44
44
|
requiredChecks: [],
|
|
45
45
|
allowedTransitions: ['CONSENSUS_MASTER_PLAN'],
|
|
46
46
|
failTransition: 'RECOVERY_LOOP',
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Auto-triggered on load when pipelinePhase is missing from state.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { PipelinePhase, PipelineState
|
|
6
|
+
import type { PipelinePhase, PipelineState } from './types.js';
|
|
7
7
|
import { createDefaultPipelineState } from './types.js';
|
|
8
8
|
import type { ProjectState, WorkflowPhase } from '../types/workflow.js';
|
|
9
|
+
import { getActiveRoles } from './skills/role-map.js';
|
|
10
|
+
import type { OutputLanguage } from '../types/project.js';
|
|
9
11
|
|
|
10
12
|
// ─── Phase Mapping ───────────────────────────────────────
|
|
11
13
|
|
|
@@ -52,8 +54,8 @@ export function migrateToPipelineState(state: ProjectState): PipelineState {
|
|
|
52
54
|
// Map legacy phase
|
|
53
55
|
pipeline.pipelinePhase = toPipelinePhase(state.phase);
|
|
54
56
|
|
|
55
|
-
// Derive active roles from language
|
|
56
|
-
pipeline.activeRoles =
|
|
57
|
+
// Derive active roles from language using shared role-map
|
|
58
|
+
pipeline.activeRoles = getActiveRoles(state.language as OutputLanguage);
|
|
57
59
|
|
|
58
60
|
return pipeline;
|
|
59
61
|
}
|
|
@@ -62,30 +64,3 @@ export function migrateToPipelineState(state: ProjectState): PipelineState {
|
|
|
62
64
|
export function needsPipelineMigration(state: unknown): boolean {
|
|
63
65
|
return !(state as Record<string, unknown>).pipeline;
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
// ─── Role Derivation ─────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function deriveActiveRoles(language: string): PipelineRole[] {
|
|
69
|
-
const baseRoles: PipelineRole[] = [
|
|
70
|
-
'DISPATCHER', 'ARCHITECT', 'REVIEWER', 'ARBITRATOR',
|
|
71
|
-
'DEBUGGER', 'AUDITOR', 'JOURNALIST', 'RELEASE_MANAGER',
|
|
72
|
-
'QA_TESTER',
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
switch (language) {
|
|
76
|
-
case 'fullstack':
|
|
77
|
-
case 'all':
|
|
78
|
-
return [
|
|
79
|
-
...baseRoles,
|
|
80
|
-
'DB_EXPERT', 'BACKEND_PROGRAMMER', 'FRONTEND_PROGRAMMER',
|
|
81
|
-
'WEBSITE_PROGRAMMER', 'UI_UX_SPECIALIST',
|
|
82
|
-
];
|
|
83
|
-
case 'python':
|
|
84
|
-
case 'typescript':
|
|
85
|
-
return [...baseRoles, 'BACKEND_PROGRAMMER'];
|
|
86
|
-
case 'website':
|
|
87
|
-
return [...baseRoles, 'WEBSITE_PROGRAMMER', 'MARKETING_EXPERT', 'SOCIAL_EXPERT'];
|
|
88
|
-
default:
|
|
89
|
-
return [...baseRoles, 'BACKEND_PROGRAMMER'];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -159,6 +159,11 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
|
|
|
159
159
|
|
|
160
160
|
onPhaseComplete?.(phase, result);
|
|
161
161
|
|
|
162
|
+
// Log phase outcome — critical for diagnosing pipeline loops
|
|
163
|
+
if (!result.success) {
|
|
164
|
+
onProgress?.(`Phase ${phase} FAILED: ${result.message}${result.error ? ` — ${result.error}` : ''}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
// v1.1: Verify constitution integrity before evaluating gate
|
|
163
168
|
const constitutionCheck = verifyConstitution(pipeline, projectDir);
|
|
164
169
|
|
|
@@ -201,6 +206,7 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
|
|
|
201
206
|
}
|
|
202
207
|
} else {
|
|
203
208
|
// ─── FAIL ────────────────────────────────────────
|
|
209
|
+
onProgress?.(`Gate FAILED for ${phase}: ${gateResult.blockers.join('; ')}`);
|
|
204
210
|
if (pipeline.recoveryCount >= pipeline.maxRecoveryIterations) {
|
|
205
211
|
phase = 'STUCK';
|
|
206
212
|
} else {
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* INTAKE phase — normalize user prompt into structured Master Plan v1.
|
|
3
3
|
* Reuses expandIdea() and createPlan() from workflow.
|
|
4
4
|
* v1.1: Creates constitution artifact and stores hash.
|
|
5
|
+
* v1.2: Generates project-specific skills and constitution.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
7
10
|
import type { PhaseContext, PhaseResult } from './phase-context.js';
|
|
8
11
|
import { successResult, failureResult } from './phase-context.js';
|
|
9
12
|
import { generateRepoSnapshot, createSnapshotArtifact } from '../repo-snapshot.js';
|
|
10
13
|
import { createConstitutionArtifact, computeConstitutionHash } from '../constitution.js';
|
|
14
|
+
import { getActiveRoles, inferTechStack } from '../skills/role-map.js';
|
|
15
|
+
import { generateProjectSkills } from '../skills/generator.js';
|
|
16
|
+
import { generateConstitution } from '../skills/constitution-generator.js';
|
|
17
|
+
import type { OutputLanguage } from '../../types/project.js';
|
|
11
18
|
|
|
12
19
|
export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
|
|
13
20
|
const { projectDir, pipeline, artifactManager } = context;
|
|
@@ -20,14 +27,7 @@ export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
|
|
|
20
27
|
artifacts.push(snapshotEntry);
|
|
21
28
|
pipeline.latestRepoSnapshot = artifactManager.toArtifactRef(snapshotEntry);
|
|
22
29
|
|
|
23
|
-
// 2.
|
|
24
|
-
const constitutionEntry = createConstitutionArtifact(projectDir, artifactManager);
|
|
25
|
-
if (constitutionEntry) {
|
|
26
|
-
artifacts.push(constitutionEntry);
|
|
27
|
-
}
|
|
28
|
-
pipeline.constitutionHash = computeConstitutionHash(projectDir);
|
|
29
|
-
|
|
30
|
-
// 3. Store additional_context artifact if session guidance provided
|
|
30
|
+
// 2. Store additional_context artifact if session guidance provided
|
|
31
31
|
const guidance = pipeline.sessionGuidance ?? '';
|
|
32
32
|
if (guidance) {
|
|
33
33
|
const ctxEntry = artifactManager.createAndStoreText(
|
|
@@ -38,6 +38,9 @@ export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
|
|
|
38
38
|
artifacts.push(ctxEntry);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// 3. Push pre-AI artifacts to pipeline state now (survives if AI calls fail below)
|
|
42
|
+
pipeline.artifacts.push(...artifacts);
|
|
43
|
+
|
|
41
44
|
// 4. Expand idea using existing workflow
|
|
42
45
|
const { expandIdea, createPlan } = await import('../../workflow/plan-mode.js');
|
|
43
46
|
const expandedIdea = await expandIdea(
|
|
@@ -45,20 +48,66 @@ export async function runIntake(context: PhaseContext): Promise<PhaseResult> {
|
|
|
45
48
|
context.state.language,
|
|
46
49
|
);
|
|
47
50
|
|
|
48
|
-
// 5.
|
|
51
|
+
// 5. Determine active roles
|
|
52
|
+
const language = context.state.language as OutputLanguage;
|
|
53
|
+
pipeline.activeRoles = getActiveRoles(language);
|
|
54
|
+
|
|
55
|
+
// 6-8. Generate project-specific skills and constitution (non-fatal)
|
|
56
|
+
const skillsDir = join(projectDir, 'skills');
|
|
57
|
+
try {
|
|
58
|
+
const projectName = context.state.name ?? 'Project';
|
|
59
|
+
|
|
60
|
+
await generateProjectSkills(
|
|
61
|
+
{
|
|
62
|
+
language,
|
|
63
|
+
expandedSpec: expandedIdea,
|
|
64
|
+
snapshot,
|
|
65
|
+
sessionGuidance: guidance || undefined,
|
|
66
|
+
activeRoles: pipeline.activeRoles,
|
|
67
|
+
skillsDir,
|
|
68
|
+
projectName,
|
|
69
|
+
},
|
|
70
|
+
artifactManager,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const techStack = inferTechStack(language, snapshot, expandedIdea);
|
|
74
|
+
generateConstitution({
|
|
75
|
+
language,
|
|
76
|
+
projectName,
|
|
77
|
+
techStack,
|
|
78
|
+
expandedSpec: expandedIdea,
|
|
79
|
+
sessionGuidance: guidance || undefined,
|
|
80
|
+
skillsDir,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Clear skill loader cache so it picks up new .md files
|
|
84
|
+
context.skillLoader.clearCache();
|
|
85
|
+
} catch {
|
|
86
|
+
// Skill/constitution generation is non-fatal — pipeline continues with defaults
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 9. Create constitution artifact and store hash (AFTER generation)
|
|
90
|
+
const constitutionEntry = createConstitutionArtifact(projectDir, artifactManager);
|
|
91
|
+
if (constitutionEntry) {
|
|
92
|
+
artifacts.push(constitutionEntry);
|
|
93
|
+
pipeline.artifacts.push(constitutionEntry);
|
|
94
|
+
}
|
|
95
|
+
pipeline.constitutionHash = computeConstitutionHash(projectDir);
|
|
96
|
+
|
|
97
|
+
// 10. Create master plan — prepend guidance so planner sees constraints first
|
|
49
98
|
const planInput = guidance
|
|
50
99
|
? `${guidance}\n\n---\n\n${expandedIdea}`
|
|
51
100
|
: expandedIdea;
|
|
52
101
|
const plan = await createPlan(planInput, '', context.state.language);
|
|
53
102
|
|
|
54
|
-
//
|
|
103
|
+
// 11. Store master plan as artifact
|
|
55
104
|
const planEntry = artifactManager.createAndStoreText(
|
|
56
105
|
'master_plan',
|
|
57
106
|
plan,
|
|
58
107
|
'INTAKE',
|
|
59
108
|
);
|
|
60
109
|
artifacts.push(planEntry);
|
|
61
|
-
pipeline.artifacts.push(
|
|
110
|
+
pipeline.artifacts.push(planEntry);
|
|
62
111
|
|
|
63
112
|
return successResult('INTAKE', artifacts, 'Master Plan v1 created');
|
|
64
113
|
} catch (err) {
|
|
@@ -46,10 +46,9 @@ export async function runRolePlanning(context: PhaseContext): Promise<PhaseResul
|
|
|
46
46
|
|
|
47
47
|
const { executePrompt } = await import('../../adapters/claude.js');
|
|
48
48
|
|
|
49
|
-
// Generate plan for each role
|
|
49
|
+
// Generate plan for each role (skip roles not in activeRoles)
|
|
50
50
|
for (const role of PLANNING_ROLES) {
|
|
51
|
-
|
|
52
|
-
if (role === 'WEBSITE_PROGRAMMER' && !pipeline.activeRoles.includes('WEBSITE_PROGRAMMER')) {
|
|
51
|
+
if (!pipeline.activeRoles.includes(role)) {
|
|
53
52
|
continue;
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic constitution generation — no AI call required.
|
|
3
|
+
* Produces skills/POPEYE_CONSTITUTION.md from templates + inferred tech stack.
|
|
4
|
+
* Includes pipeline governance invariants that never change.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import type { OutputLanguage } from '../../types/project.js';
|
|
11
|
+
import type { ConstitutionContext, TechStack } from './types.js';
|
|
12
|
+
|
|
13
|
+
// ─── Constants ──────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const CONSTITUTION_FILENAME = 'POPEYE_CONSTITUTION.md';
|
|
16
|
+
const PIPELINE_VERSION = '1.0';
|
|
17
|
+
|
|
18
|
+
// ─── Public API ─────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate the project constitution file if it doesn't already exist.
|
|
22
|
+
* Entirely deterministic — built from templates and tech stack data.
|
|
23
|
+
*
|
|
24
|
+
* @param context - Constitution generation context
|
|
25
|
+
*/
|
|
26
|
+
export function generateConstitution(context: ConstitutionContext): void {
|
|
27
|
+
const { skillsDir } = context;
|
|
28
|
+
|
|
29
|
+
if (shouldSkipConstitution(skillsDir)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!existsSync(skillsDir)) {
|
|
34
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const content = buildConstitutionContent(context);
|
|
38
|
+
const constitutionPath = join(skillsDir, CONSTITUTION_FILENAME);
|
|
39
|
+
writeFileSync(constitutionPath, content, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if constitution generation should be skipped.
|
|
44
|
+
* Returns true if the file already exists (hand-written or prior run).
|
|
45
|
+
*
|
|
46
|
+
* @param skillsDir - Path to the skills directory
|
|
47
|
+
* @returns true if generation should be skipped
|
|
48
|
+
*/
|
|
49
|
+
export function shouldSkipConstitution(skillsDir: string): boolean {
|
|
50
|
+
const constitutionPath = join(skillsDir, CONSTITUTION_FILENAME);
|
|
51
|
+
return existsSync(constitutionPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Content Assembly ───────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the full constitution markdown content.
|
|
58
|
+
*
|
|
59
|
+
* @param context - Constitution generation context
|
|
60
|
+
* @returns Complete markdown string
|
|
61
|
+
*/
|
|
62
|
+
function buildConstitutionContent(context: ConstitutionContext): string {
|
|
63
|
+
const { projectName, language, techStack, sessionGuidance } = context;
|
|
64
|
+
const date = new Date().toISOString().split('T')[0];
|
|
65
|
+
|
|
66
|
+
const sections = [
|
|
67
|
+
`# Project Constitution: ${projectName}`,
|
|
68
|
+
'',
|
|
69
|
+
`Generated: ${date} | Language: ${language} | Pipeline: v${PIPELINE_VERSION}`,
|
|
70
|
+
'',
|
|
71
|
+
getTechStackSection(techStack),
|
|
72
|
+
getArchitectureRules(techStack),
|
|
73
|
+
getCodeQualityRules(),
|
|
74
|
+
getGovernanceRules(),
|
|
75
|
+
getConstraintsSection(language, sessionGuidance),
|
|
76
|
+
getImmutabilitySection(),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return sections.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Template Sections ──────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate the tech stack section from inferred stack data.
|
|
86
|
+
*
|
|
87
|
+
* @param techStack - Inferred tech stack
|
|
88
|
+
* @returns Markdown section
|
|
89
|
+
*/
|
|
90
|
+
export function getTechStackSection(techStack: TechStack): string {
|
|
91
|
+
const lines = ['## Tech Stack'];
|
|
92
|
+
if (techStack.language) lines.push(`- Language: ${techStack.language}`);
|
|
93
|
+
if (techStack.backend) lines.push(`- Framework: ${techStack.backend}`);
|
|
94
|
+
if (techStack.frontend) lines.push(`- Frontend: ${techStack.frontend}`);
|
|
95
|
+
if (techStack.database) lines.push(`- Database: ${techStack.database}`);
|
|
96
|
+
if (techStack.orm) lines.push(`- ORM: ${techStack.orm}`);
|
|
97
|
+
if (techStack.testing) lines.push(`- Testing: ${techStack.testing}`);
|
|
98
|
+
lines.push('');
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate architecture rules based on the tech stack.
|
|
104
|
+
*
|
|
105
|
+
* @param techStack - Inferred tech stack
|
|
106
|
+
* @returns Markdown section
|
|
107
|
+
*/
|
|
108
|
+
export function getArchitectureRules(techStack: TechStack): string {
|
|
109
|
+
const rules: string[] = [];
|
|
110
|
+
let ruleNum = 1;
|
|
111
|
+
|
|
112
|
+
if (techStack.backend?.includes('FastAPI')) {
|
|
113
|
+
rules.push(`${ruleNum++}. All API endpoints MUST use async/await`);
|
|
114
|
+
}
|
|
115
|
+
if (techStack.backend?.includes('Express')) {
|
|
116
|
+
rules.push(`${ruleNum++}. Use Express middleware pattern for cross-cutting concerns`);
|
|
117
|
+
}
|
|
118
|
+
if (techStack.backend?.includes('Django')) {
|
|
119
|
+
rules.push(`${ruleNum++}. Follow Django app structure conventions`);
|
|
120
|
+
}
|
|
121
|
+
if (techStack.orm?.includes('SQLAlchemy')) {
|
|
122
|
+
rules.push(`${ruleNum++}. Database access exclusively via SQLAlchemy ORM`);
|
|
123
|
+
}
|
|
124
|
+
if (techStack.orm?.includes('Prisma')) {
|
|
125
|
+
rules.push(`${ruleNum++}. Database access exclusively via Prisma client`);
|
|
126
|
+
}
|
|
127
|
+
if (techStack.language?.includes('Python')) {
|
|
128
|
+
rules.push(`${ruleNum++}. Environment variables via python-dotenv, never hardcoded`);
|
|
129
|
+
rules.push(`${ruleNum++}. PEP8 style with type hints on all functions`);
|
|
130
|
+
}
|
|
131
|
+
if (techStack.language?.includes('TypeScript')) {
|
|
132
|
+
rules.push(`${ruleNum++}. TypeScript strict mode, no implicit any`);
|
|
133
|
+
rules.push(`${ruleNum++}. Environment variables via dotenv, never hardcoded`);
|
|
134
|
+
}
|
|
135
|
+
if (techStack.frontend?.includes('React')) {
|
|
136
|
+
rules.push(`${ruleNum++}. React components use functional patterns with hooks`);
|
|
137
|
+
}
|
|
138
|
+
if (techStack.frontend?.includes('Next')) {
|
|
139
|
+
rules.push(`${ruleNum++}. Next.js App Router conventions for routing and layouts`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Always add a generic rule if nothing specific matched
|
|
143
|
+
if (rules.length === 0) {
|
|
144
|
+
rules.push('1. Environment variables never hardcoded in source code');
|
|
145
|
+
rules.push('2. Clear separation of concerns between modules');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return ['## Architecture Rules', ...rules, ''].join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate code quality rules (constant across all projects).
|
|
153
|
+
*
|
|
154
|
+
* @returns Markdown section
|
|
155
|
+
*/
|
|
156
|
+
export function getCodeQualityRules(): string {
|
|
157
|
+
return [
|
|
158
|
+
'## Code Quality',
|
|
159
|
+
'1. Maximum 500 lines per source file',
|
|
160
|
+
'2. Unit tests for every module (happy path + edge case + failure)',
|
|
161
|
+
'3. Standard logging (no unstructured print statements)',
|
|
162
|
+
'4. Docstrings/JSDoc on public functions',
|
|
163
|
+
'',
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate governance rules (pipeline invariants, constant).
|
|
169
|
+
*
|
|
170
|
+
* @returns Markdown section
|
|
171
|
+
*/
|
|
172
|
+
function getGovernanceRules(): string {
|
|
173
|
+
return [
|
|
174
|
+
'## Governance Rules',
|
|
175
|
+
'1. Consensus threshold: 0.95 with minimum 2 reviewers',
|
|
176
|
+
'2. All artifacts are immutable once stored (new versions create new files)',
|
|
177
|
+
'3. No placeholder content in production code or generated output',
|
|
178
|
+
'4. Gate failures route to RECOVERY_LOOP before phase retry',
|
|
179
|
+
'5. Constitution modifications during pipeline execution are forbidden',
|
|
180
|
+
'6. Change Requests required for scope changes after INTAKE',
|
|
181
|
+
'',
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate language-specific and session-specific constraints.
|
|
187
|
+
*
|
|
188
|
+
* @param language - Project language
|
|
189
|
+
* @param sessionGuidance - Optional session guidance text
|
|
190
|
+
* @returns Markdown section
|
|
191
|
+
*/
|
|
192
|
+
export function getConstraintsSection(
|
|
193
|
+
language: OutputLanguage,
|
|
194
|
+
sessionGuidance?: string,
|
|
195
|
+
): string {
|
|
196
|
+
const lines = ['## Project Constraints'];
|
|
197
|
+
|
|
198
|
+
const langConstraints: Record<string, string[]> = {
|
|
199
|
+
python: ['- Python 3.11+ required', '- Use virtual environment (venv) for all operations'],
|
|
200
|
+
typescript: ['- Node.js 18+ required', '- ESM modules (import/export, .js extensions)'],
|
|
201
|
+
fullstack: [
|
|
202
|
+
'- Python 3.11+ for backend, Node.js 18+ for frontend',
|
|
203
|
+
'- Monorepo structure with clear app boundaries',
|
|
204
|
+
],
|
|
205
|
+
website: ['- Node.js 18+ required', '- SSG/SSR optimization for performance and SEO'],
|
|
206
|
+
all: [
|
|
207
|
+
'- Python 3.11+ for backend, Node.js 18+ for frontend and website',
|
|
208
|
+
'- Monorepo structure with clear app boundaries',
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const constraints = langConstraints[language] ?? langConstraints.python;
|
|
213
|
+
lines.push(...constraints);
|
|
214
|
+
|
|
215
|
+
if (sessionGuidance) {
|
|
216
|
+
lines.push('', '### Session-Specific Guidance');
|
|
217
|
+
lines.push(sessionGuidance.slice(0, 500));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
lines.push('');
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate the immutability notice (constant).
|
|
226
|
+
*
|
|
227
|
+
* @returns Markdown section
|
|
228
|
+
*/
|
|
229
|
+
function getImmutabilitySection(): string {
|
|
230
|
+
return [
|
|
231
|
+
'## Immutability',
|
|
232
|
+
'This document MUST NOT be modified during pipeline execution.',
|
|
233
|
+
'Any modification triggers constitution verification failure at next gate.',
|
|
234
|
+
'',
|
|
235
|
+
].join('\n');
|
|
236
|
+
}
|