@openprd/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openprd/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
package/src/growth.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson } from './fs-utils.js';
|
|
4
|
+
|
|
5
|
+
export const OPENPRD_GROWTH_DIR = path.join('.openprd', 'growth');
|
|
6
|
+
export const OPENPRD_GROWTH_CANDIDATES = path.join(OPENPRD_GROWTH_DIR, 'candidates.jsonl');
|
|
7
|
+
export const OPENPRD_GROWTH_ACCEPTED = path.join(OPENPRD_GROWTH_DIR, 'accepted.json');
|
|
8
|
+
export const OPENPRD_GROWTH_REJECTED = path.join(OPENPRD_GROWTH_DIR, 'rejected.json');
|
|
9
|
+
export const OPENPRD_GROWTH_LOCAL_PREFERENCES = path.join(OPENPRD_GROWTH_DIR, 'preferences.local.json');
|
|
10
|
+
export const OPENPRD_STANDARDS_CONFIG = path.join('.openprd', 'standards', 'config.json');
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_GROWTH_CONFIG = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
reviewRequired: true,
|
|
15
|
+
autoApply: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
minConfidence: 0.8,
|
|
18
|
+
safeTypes: ['code-extension'],
|
|
19
|
+
},
|
|
20
|
+
candidateLimit: 200,
|
|
21
|
+
scopes: ['project', 'user-local', 'openprd-core'],
|
|
22
|
+
supportedCandidateTypes: [
|
|
23
|
+
'code-extension',
|
|
24
|
+
'exempt-path-segment',
|
|
25
|
+
'exempt-file-pattern',
|
|
26
|
+
'user-preference',
|
|
27
|
+
'workflow-gotcha',
|
|
28
|
+
'standards-rule',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SAFE_APPLY_TYPES = new Set(['code-extension', 'exempt-path-segment', 'exempt-file-pattern', 'user-preference']);
|
|
33
|
+
const AUTO_APPLY_TYPES = new Set(['code-extension']);
|
|
34
|
+
|
|
35
|
+
function normalizePosixPath(value) {
|
|
36
|
+
return String(value ?? '').split(path.sep).join('/');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeExtension(value) {
|
|
40
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
return raw.startsWith('.') ? raw : `.${raw}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function slug(value) {
|
|
46
|
+
return String(value ?? '')
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/^\./, '')
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
|| 'unknown';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function growthCandidateId(type, key) {
|
|
56
|
+
return `${slug(type)}-${slug(key)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function growthPath(projectRoot, relativePath) {
|
|
60
|
+
return cjoin(projectRoot, relativePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nowIso() {
|
|
64
|
+
return new Date().toISOString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readJsonIfExists(filePath, fallback) {
|
|
68
|
+
if (!(await exists(filePath))) return fallback;
|
|
69
|
+
return readJson(filePath).catch(() => fallback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readJsonlIfExists(filePath) {
|
|
73
|
+
if (!(await exists(filePath))) return [];
|
|
74
|
+
return readJsonl(filePath).catch(() => []);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeArray(value) {
|
|
78
|
+
return Array.isArray(value) ? value.filter((item) => item !== null && item !== undefined) : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeCandidate(raw = {}) {
|
|
82
|
+
const type = String(raw.type ?? '').trim();
|
|
83
|
+
const key = String(raw.key ?? raw.extension ?? raw.pattern ?? raw.preferenceKey ?? '').trim();
|
|
84
|
+
const id = raw.id ? String(raw.id) : growthCandidateId(type || 'candidate', key || raw.title || raw.path || 'unknown');
|
|
85
|
+
const scope = ['project', 'user-local', 'openprd-core'].includes(raw.scope) ? raw.scope : 'project';
|
|
86
|
+
const status = ['pending', 'applied', 'rejected'].includes(raw.status) ? raw.status : 'pending';
|
|
87
|
+
return {
|
|
88
|
+
version: 1,
|
|
89
|
+
id,
|
|
90
|
+
type,
|
|
91
|
+
key,
|
|
92
|
+
scope,
|
|
93
|
+
status,
|
|
94
|
+
title: String(raw.title ?? `${type}: ${key}`).trim(),
|
|
95
|
+
summary: String(raw.summary ?? '').trim(),
|
|
96
|
+
evidence: normalizeArray(raw.evidence).map((item) => {
|
|
97
|
+
if (typeof item === 'string') return { note: item };
|
|
98
|
+
return item;
|
|
99
|
+
}),
|
|
100
|
+
confidence: typeof raw.confidence === 'number' ? raw.confidence : null,
|
|
101
|
+
suggestedPatch: raw.suggestedPatch ?? null,
|
|
102
|
+
appliedAt: raw.appliedAt ?? null,
|
|
103
|
+
appliedChanges: normalizeArray(raw.appliedChanges),
|
|
104
|
+
applyMode: raw.applyMode ?? null,
|
|
105
|
+
applyReason: raw.applyReason ?? null,
|
|
106
|
+
rejectedAt: raw.rejectedAt ?? null,
|
|
107
|
+
notes: raw.notes ?? null,
|
|
108
|
+
createdAt: raw.createdAt ?? nowIso(),
|
|
109
|
+
updatedAt: raw.updatedAt ?? nowIso(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function latestCandidates(records = []) {
|
|
114
|
+
const byId = new Map();
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
const candidate = normalizeCandidate(record);
|
|
117
|
+
const previous = byId.get(candidate.id);
|
|
118
|
+
if (!previous || String(candidate.updatedAt) >= String(previous.updatedAt)) {
|
|
119
|
+
byId.set(candidate.id, candidate);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [...byId.values()].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writeCandidateEvent(projectRoot, candidate, patch = {}) {
|
|
126
|
+
const event = {
|
|
127
|
+
...candidate,
|
|
128
|
+
...patch,
|
|
129
|
+
updatedAt: nowIso(),
|
|
130
|
+
};
|
|
131
|
+
await appendJsonl(growthPath(projectRoot, OPENPRD_GROWTH_CANDIDATES), event);
|
|
132
|
+
return normalizeCandidate(event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeAutoApplyConfig(value = {}) {
|
|
136
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
137
|
+
const defaultAuto = DEFAULT_GROWTH_CONFIG.autoApply;
|
|
138
|
+
const minConfidence = Number(source.minConfidence ?? defaultAuto.minConfidence);
|
|
139
|
+
return {
|
|
140
|
+
enabled: source.enabled !== false,
|
|
141
|
+
minConfidence: Number.isFinite(minConfidence) && minConfidence >= 0 && minConfidence <= 1
|
|
142
|
+
? minConfidence
|
|
143
|
+
: defaultAuto.minConfidence,
|
|
144
|
+
safeTypes: normalizeArray(source.safeTypes ?? defaultAuto.safeTypes).map((item) => String(item).trim()).filter(Boolean),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeGrowthConfig(value = {}) {
|
|
149
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
150
|
+
return {
|
|
151
|
+
...DEFAULT_GROWTH_CONFIG,
|
|
152
|
+
...source,
|
|
153
|
+
scopes: normalizeArray(source.scopes ?? DEFAULT_GROWTH_CONFIG.scopes),
|
|
154
|
+
supportedCandidateTypes: normalizeArray(source.supportedCandidateTypes ?? DEFAULT_GROWTH_CONFIG.supportedCandidateTypes),
|
|
155
|
+
autoApply: normalizeAutoApplyConfig(source.autoApply),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function ensureGrowthFiles(projectRoot) {
|
|
160
|
+
await fs.mkdir(growthPath(projectRoot, OPENPRD_GROWTH_DIR), { recursive: true });
|
|
161
|
+
const acceptedPath = growthPath(projectRoot, OPENPRD_GROWTH_ACCEPTED);
|
|
162
|
+
const rejectedPath = growthPath(projectRoot, OPENPRD_GROWTH_REJECTED);
|
|
163
|
+
const localPreferencesPath = growthPath(projectRoot, OPENPRD_GROWTH_LOCAL_PREFERENCES);
|
|
164
|
+
if (!(await exists(acceptedPath))) {
|
|
165
|
+
await writeJson(acceptedPath, { version: 1, candidates: [] });
|
|
166
|
+
}
|
|
167
|
+
if (!(await exists(rejectedPath))) {
|
|
168
|
+
await writeJson(rejectedPath, { version: 1, candidates: [] });
|
|
169
|
+
}
|
|
170
|
+
if (!(await exists(localPreferencesPath))) {
|
|
171
|
+
await writeJson(localPreferencesPath, { version: 1, preferences: {} });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function initGrowthWorkspace(projectRoot) {
|
|
176
|
+
await ensureGrowthFiles(projectRoot);
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
action: 'growth-init',
|
|
180
|
+
projectRoot,
|
|
181
|
+
files: {
|
|
182
|
+
dir: OPENPRD_GROWTH_DIR,
|
|
183
|
+
candidates: OPENPRD_GROWTH_CANDIDATES,
|
|
184
|
+
accepted: OPENPRD_GROWTH_ACCEPTED,
|
|
185
|
+
rejected: OPENPRD_GROWTH_REJECTED,
|
|
186
|
+
localPreferences: OPENPRD_GROWTH_LOCAL_PREFERENCES,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function readGrowthState(projectRoot) {
|
|
192
|
+
await ensureGrowthFiles(projectRoot);
|
|
193
|
+
const records = await readJsonlIfExists(growthPath(projectRoot, OPENPRD_GROWTH_CANDIDATES));
|
|
194
|
+
const candidates = latestCandidates(records);
|
|
195
|
+
const accepted = await readJsonIfExists(growthPath(projectRoot, OPENPRD_GROWTH_ACCEPTED), { version: 1, candidates: [] });
|
|
196
|
+
const rejected = await readJsonIfExists(growthPath(projectRoot, OPENPRD_GROWTH_REJECTED), { version: 1, candidates: [] });
|
|
197
|
+
return {
|
|
198
|
+
candidates,
|
|
199
|
+
accepted: normalizeArray(accepted.candidates),
|
|
200
|
+
rejected: normalizeArray(rejected.candidates),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, options = {}) {
|
|
205
|
+
const candidate = normalizeCandidate(rawCandidate);
|
|
206
|
+
if (!candidate.type || !candidate.key) {
|
|
207
|
+
return { ok: false, action: 'growth-observe', skipped: true, reason: 'missing-type-or-key', candidate };
|
|
208
|
+
}
|
|
209
|
+
await ensureGrowthFiles(projectRoot);
|
|
210
|
+
const state = await readGrowthState(projectRoot);
|
|
211
|
+
const existing = state.candidates.find((item) => item.id === candidate.id);
|
|
212
|
+
if (existing?.status === 'applied' || existing?.status === 'rejected') {
|
|
213
|
+
return { ok: true, action: 'growth-observe', skipped: true, reason: `candidate-${existing.status}`, candidate: existing };
|
|
214
|
+
}
|
|
215
|
+
if (existing?.status === 'pending') {
|
|
216
|
+
const autoApplyDecision = assessAutoApplyGrowthCandidate(existing, options.autoApply);
|
|
217
|
+
if (autoApplyDecision.ok) {
|
|
218
|
+
const applied = await applyGrowthCandidate(projectRoot, existing, {
|
|
219
|
+
mode: 'auto',
|
|
220
|
+
reason: autoApplyDecision.reason,
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
...applied,
|
|
224
|
+
action: 'growth-observe',
|
|
225
|
+
skipped: false,
|
|
226
|
+
autoApplied: true,
|
|
227
|
+
autoApplyDecision,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
action: 'growth-observe',
|
|
233
|
+
skipped: true,
|
|
234
|
+
reason: 'candidate-already-pending',
|
|
235
|
+
candidate: existing,
|
|
236
|
+
autoApplied: false,
|
|
237
|
+
autoApplyDecision,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const stored = await writeCandidateEvent(projectRoot, candidate);
|
|
241
|
+
const autoApplyDecision = assessAutoApplyGrowthCandidate(stored, options.autoApply);
|
|
242
|
+
if (autoApplyDecision.ok) {
|
|
243
|
+
const applied = await applyGrowthCandidate(projectRoot, stored, {
|
|
244
|
+
mode: 'auto',
|
|
245
|
+
reason: autoApplyDecision.reason,
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
...applied,
|
|
249
|
+
action: 'growth-observe',
|
|
250
|
+
skipped: false,
|
|
251
|
+
autoApplied: true,
|
|
252
|
+
autoApplyDecision,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
action: 'growth-observe',
|
|
258
|
+
skipped: false,
|
|
259
|
+
candidate: stored,
|
|
260
|
+
autoApplied: false,
|
|
261
|
+
autoApplyDecision,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function assessAutoApplyGrowthCandidate(candidate, rawConfig = {}) {
|
|
266
|
+
const config = normalizeAutoApplyConfig(rawConfig);
|
|
267
|
+
if (!config.enabled) {
|
|
268
|
+
return { ok: false, reason: 'auto-apply-disabled' };
|
|
269
|
+
}
|
|
270
|
+
if (!AUTO_APPLY_TYPES.has(candidate.type) || !config.safeTypes.includes(candidate.type)) {
|
|
271
|
+
return { ok: false, reason: 'type-needs-review' };
|
|
272
|
+
}
|
|
273
|
+
if (candidate.scope !== 'project') {
|
|
274
|
+
return { ok: false, reason: 'scope-needs-review' };
|
|
275
|
+
}
|
|
276
|
+
if (typeof candidate.confidence !== 'number' || candidate.confidence < config.minConfidence) {
|
|
277
|
+
return { ok: false, reason: 'confidence-below-threshold' };
|
|
278
|
+
}
|
|
279
|
+
const extension = normalizeExtension(candidate.key);
|
|
280
|
+
if (!extension || candidate.key !== extension) {
|
|
281
|
+
return { ok: false, reason: 'invalid-extension' };
|
|
282
|
+
}
|
|
283
|
+
const patch = candidate.suggestedPatch ?? {};
|
|
284
|
+
if (
|
|
285
|
+
patch.file !== OPENPRD_STANDARDS_CONFIG
|
|
286
|
+
|| patch.op !== 'append'
|
|
287
|
+
|| patch.path !== 'developmentStandards.codeFileLines.codeFileExtensions'
|
|
288
|
+
|| normalizeExtension(patch.value) !== extension
|
|
289
|
+
) {
|
|
290
|
+
return { ok: false, reason: 'patch-needs-review' };
|
|
291
|
+
}
|
|
292
|
+
return { ok: true, reason: 'safe-code-extension' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function applyGrowthCandidate(projectRoot, candidate, options = {}) {
|
|
296
|
+
if (!SAFE_APPLY_TYPES.has(candidate.type)) {
|
|
297
|
+
return { ok: false, action: 'growth-apply', projectRoot, candidate, errors: [`Growth candidate type requires manual review: ${candidate.type}`] };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const applied = candidate.type === 'user-preference'
|
|
301
|
+
? await applyUserPreferenceCandidate(projectRoot, candidate)
|
|
302
|
+
: await applyStandardsCandidate(projectRoot, candidate);
|
|
303
|
+
if (!applied.ok) {
|
|
304
|
+
return { ok: false, action: 'growth-apply', projectRoot, candidate, errors: applied.errors };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stored = await writeCandidateEvent(projectRoot, candidate, {
|
|
308
|
+
status: 'applied',
|
|
309
|
+
appliedAt: nowIso(),
|
|
310
|
+
appliedChanges: applied.changed,
|
|
311
|
+
applyMode: options.mode ?? 'manual',
|
|
312
|
+
applyReason: options.reason ?? null,
|
|
313
|
+
});
|
|
314
|
+
const acceptedPath = growthPath(projectRoot, OPENPRD_GROWTH_ACCEPTED);
|
|
315
|
+
const accepted = await readJsonIfExists(acceptedPath, { version: 1, candidates: [] });
|
|
316
|
+
const acceptedCandidates = normalizeArray(accepted.candidates).filter((item) => item.id !== stored.id);
|
|
317
|
+
acceptedCandidates.push(stored);
|
|
318
|
+
await writeJson(acceptedPath, { version: 1, candidates: acceptedCandidates });
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
action: 'growth-apply',
|
|
323
|
+
projectRoot,
|
|
324
|
+
candidate: stored,
|
|
325
|
+
changed: applied.changed,
|
|
326
|
+
errors: [],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function reviewGrowthWorkspace(projectRoot) {
|
|
331
|
+
const state = await readGrowthState(projectRoot);
|
|
332
|
+
const pending = state.candidates.filter((candidate) => candidate.status === 'pending');
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
action: 'growth-review',
|
|
336
|
+
projectRoot,
|
|
337
|
+
pending,
|
|
338
|
+
applied: state.candidates.filter((candidate) => candidate.status === 'applied'),
|
|
339
|
+
rejected: state.candidates.filter((candidate) => candidate.status === 'rejected'),
|
|
340
|
+
summary: {
|
|
341
|
+
pending: pending.length,
|
|
342
|
+
applied: state.candidates.filter((candidate) => candidate.status === 'applied').length,
|
|
343
|
+
rejected: state.candidates.filter((candidate) => candidate.status === 'rejected').length,
|
|
344
|
+
},
|
|
345
|
+
nextActions: pending.length === 0
|
|
346
|
+
? ['当前没有待确认增长候选。']
|
|
347
|
+
: pending.map((candidate) => `收工复盘时确认后运行 openprd grow . --apply --id ${candidate.id};不采用则运行 openprd grow . --reject --id ${candidate.id}`),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ensureStandardsConfigShape(config) {
|
|
352
|
+
const next = config && typeof config === 'object' ? { ...config } : {};
|
|
353
|
+
next.developmentStandards = next.developmentStandards && typeof next.developmentStandards === 'object'
|
|
354
|
+
? { ...next.developmentStandards }
|
|
355
|
+
: {};
|
|
356
|
+
next.developmentStandards.codeFileLines = next.developmentStandards.codeFileLines && typeof next.developmentStandards.codeFileLines === 'object'
|
|
357
|
+
? { ...next.developmentStandards.codeFileLines }
|
|
358
|
+
: {};
|
|
359
|
+
next.growth = normalizeGrowthConfig(next.growth);
|
|
360
|
+
return next;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function appendUnique(list, value, normalize = (item) => String(item)) {
|
|
364
|
+
const next = normalizeArray(list).map((item) => normalize(item)).filter(Boolean);
|
|
365
|
+
const normalized = normalize(value);
|
|
366
|
+
if (normalized && !next.includes(normalized)) {
|
|
367
|
+
next.push(normalized);
|
|
368
|
+
}
|
|
369
|
+
return next;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function applyStandardsCandidate(projectRoot, candidate) {
|
|
373
|
+
const configPath = growthPath(projectRoot, OPENPRD_STANDARDS_CONFIG);
|
|
374
|
+
if (!(await exists(configPath))) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
errors: [`${OPENPRD_STANDARDS_CONFIG} is required. Run: openprd standards . --init`],
|
|
378
|
+
changed: [],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const config = ensureStandardsConfigShape(await readJson(configPath));
|
|
383
|
+
const lineConfig = config.developmentStandards.codeFileLines;
|
|
384
|
+
const changed = [];
|
|
385
|
+
if (candidate.type === 'code-extension') {
|
|
386
|
+
const extension = normalizeExtension(candidate.key);
|
|
387
|
+
lineConfig.codeFileExtensions = appendUnique(lineConfig.codeFileExtensions, extension, normalizeExtension);
|
|
388
|
+
changed.push(`developmentStandards.codeFileLines.codeFileExtensions += ${extension}`);
|
|
389
|
+
} else if (candidate.type === 'exempt-path-segment') {
|
|
390
|
+
lineConfig.exemptPathSegments = appendUnique(lineConfig.exemptPathSegments, candidate.key);
|
|
391
|
+
changed.push(`developmentStandards.codeFileLines.exemptPathSegments += ${candidate.key}`);
|
|
392
|
+
} else if (candidate.type === 'exempt-file-pattern') {
|
|
393
|
+
lineConfig.exemptFilePatterns = appendUnique(lineConfig.exemptFilePatterns, candidate.key);
|
|
394
|
+
changed.push(`developmentStandards.codeFileLines.exemptFilePatterns += ${candidate.key}`);
|
|
395
|
+
} else {
|
|
396
|
+
return { ok: false, errors: [`${candidate.type} cannot update standards config automatically.`], changed: [] };
|
|
397
|
+
}
|
|
398
|
+
await writeJson(configPath, config);
|
|
399
|
+
return { ok: true, errors: [], changed };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function applyUserPreferenceCandidate(projectRoot, candidate) {
|
|
403
|
+
const preferencesPath = growthPath(projectRoot, OPENPRD_GROWTH_LOCAL_PREFERENCES);
|
|
404
|
+
const current = await readJsonIfExists(preferencesPath, { version: 1, preferences: {} });
|
|
405
|
+
const preferences = current.preferences && typeof current.preferences === 'object' ? { ...current.preferences } : {};
|
|
406
|
+
preferences[candidate.key] = candidate.suggestedPatch?.value ?? candidate.summary ?? true;
|
|
407
|
+
await writeJson(preferencesPath, { version: 1, preferences, updatedAt: nowIso() });
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
errors: [],
|
|
411
|
+
changed: [`${OPENPRD_GROWTH_LOCAL_PREFERENCES} preferences.${candidate.key}`],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function applyGrowthCandidateWorkspace(projectRoot, options = {}) {
|
|
416
|
+
const id = String(options.id ?? '').trim();
|
|
417
|
+
if (!id) {
|
|
418
|
+
return { ok: false, action: 'growth-apply', projectRoot, errors: ['--id is required.'] };
|
|
419
|
+
}
|
|
420
|
+
const state = await readGrowthState(projectRoot);
|
|
421
|
+
const candidate = state.candidates.find((item) => item.id === id);
|
|
422
|
+
if (!candidate) {
|
|
423
|
+
return { ok: false, action: 'growth-apply', projectRoot, errors: [`Growth candidate not found: ${id}`] };
|
|
424
|
+
}
|
|
425
|
+
if (candidate.status !== 'pending') {
|
|
426
|
+
return { ok: false, action: 'growth-apply', projectRoot, candidate, errors: [`Growth candidate is already ${candidate.status}.`] };
|
|
427
|
+
}
|
|
428
|
+
return applyGrowthCandidate(projectRoot, candidate, { mode: 'manual' });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function rejectGrowthCandidateWorkspace(projectRoot, options = {}) {
|
|
432
|
+
const id = String(options.id ?? '').trim();
|
|
433
|
+
if (!id) {
|
|
434
|
+
return { ok: false, action: 'growth-reject', projectRoot, errors: ['--id is required.'] };
|
|
435
|
+
}
|
|
436
|
+
const state = await readGrowthState(projectRoot);
|
|
437
|
+
const candidate = state.candidates.find((item) => item.id === id);
|
|
438
|
+
if (!candidate) {
|
|
439
|
+
return { ok: false, action: 'growth-reject', projectRoot, errors: [`Growth candidate not found: ${id}`] };
|
|
440
|
+
}
|
|
441
|
+
if (candidate.status !== 'pending') {
|
|
442
|
+
return { ok: false, action: 'growth-reject', projectRoot, candidate, errors: [`Growth candidate is already ${candidate.status}.`] };
|
|
443
|
+
}
|
|
444
|
+
const stored = await writeCandidateEvent(projectRoot, candidate, {
|
|
445
|
+
status: 'rejected',
|
|
446
|
+
rejectedAt: nowIso(),
|
|
447
|
+
notes: String(options.notes ?? '').trim() || null,
|
|
448
|
+
});
|
|
449
|
+
const rejectedPath = growthPath(projectRoot, OPENPRD_GROWTH_REJECTED);
|
|
450
|
+
const rejected = await readJsonIfExists(rejectedPath, { version: 1, candidates: [] });
|
|
451
|
+
const rejectedCandidates = normalizeArray(rejected.candidates).filter((item) => item.id !== stored.id);
|
|
452
|
+
rejectedCandidates.push(stored);
|
|
453
|
+
await writeJson(rejectedPath, { version: 1, candidates: rejectedCandidates });
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
ok: true,
|
|
457
|
+
action: 'growth-reject',
|
|
458
|
+
projectRoot,
|
|
459
|
+
candidate: stored,
|
|
460
|
+
errors: [],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function checkGrowthWorkspace(projectRoot) {
|
|
465
|
+
const state = await readGrowthState(projectRoot);
|
|
466
|
+
const pending = state.candidates.filter((candidate) => candidate.status === 'pending');
|
|
467
|
+
const applied = state.candidates.filter((candidate) => candidate.status === 'applied');
|
|
468
|
+
const rejected = state.candidates.filter((candidate) => candidate.status === 'rejected');
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
action: 'growth-check',
|
|
472
|
+
projectRoot,
|
|
473
|
+
pending,
|
|
474
|
+
applied,
|
|
475
|
+
rejected,
|
|
476
|
+
summary: {
|
|
477
|
+
pending: pending.length,
|
|
478
|
+
applied: applied.length,
|
|
479
|
+
rejected: rejected.length,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function validateGrowthConfig(config, errors = []) {
|
|
485
|
+
const growth = config?.growth;
|
|
486
|
+
if (!growth) return errors;
|
|
487
|
+
if (growth.enabled !== undefined && typeof growth.enabled !== 'boolean') {
|
|
488
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.enabled must be a boolean.`);
|
|
489
|
+
}
|
|
490
|
+
if (growth.reviewRequired !== undefined && growth.reviewRequired !== true) {
|
|
491
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.reviewRequired must remain true; shared rules cannot be auto-applied.`);
|
|
492
|
+
}
|
|
493
|
+
if (growth.candidateLimit !== undefined) {
|
|
494
|
+
const limit = Number(growth.candidateLimit);
|
|
495
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
496
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.candidateLimit must be a positive integer.`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (growth.autoApply !== undefined) {
|
|
500
|
+
const autoApply = growth.autoApply;
|
|
501
|
+
if (!autoApply || typeof autoApply !== 'object' || Array.isArray(autoApply)) {
|
|
502
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.autoApply must be an object.`);
|
|
503
|
+
} else {
|
|
504
|
+
if (autoApply.enabled !== undefined && typeof autoApply.enabled !== 'boolean') {
|
|
505
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.autoApply.enabled must be a boolean.`);
|
|
506
|
+
}
|
|
507
|
+
if (autoApply.minConfidence !== undefined) {
|
|
508
|
+
const minConfidence = Number(autoApply.minConfidence);
|
|
509
|
+
if (!Number.isFinite(minConfidence) || minConfidence < 0 || minConfidence > 1) {
|
|
510
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.autoApply.minConfidence must be between 0 and 1.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (autoApply.safeTypes !== undefined && !Array.isArray(autoApply.safeTypes)) {
|
|
514
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.autoApply.safeTypes must be an array.`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return errors;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function buildCodeExtensionCandidate(relativePath, details = {}) {
|
|
522
|
+
const normalized = normalizePosixPath(relativePath);
|
|
523
|
+
const extension = normalizeExtension(path.extname(normalized));
|
|
524
|
+
return normalizeCandidate({
|
|
525
|
+
type: 'code-extension',
|
|
526
|
+
key: extension,
|
|
527
|
+
scope: 'project',
|
|
528
|
+
title: `新增代码文件扩展名 ${extension}`,
|
|
529
|
+
summary: `检测到 ${normalized} 看起来像代码文件,但 ${extension} 尚未纳入 OpenPrd dev-check 代码扩展名配置。`,
|
|
530
|
+
evidence: [
|
|
531
|
+
{
|
|
532
|
+
path: normalized,
|
|
533
|
+
lineCount: details.lineCount ?? null,
|
|
534
|
+
reason: details.reason ?? 'looks-like-code',
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
confidence: details.confidence ?? 0.74,
|
|
538
|
+
suggestedPatch: {
|
|
539
|
+
file: OPENPRD_STANDARDS_CONFIG,
|
|
540
|
+
op: 'append',
|
|
541
|
+
path: 'developmentStandards.codeFileLines.codeFileExtensions',
|
|
542
|
+
value: extension,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|