@openprd/cli 0.1.0 → 0.1.8
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 +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +344 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +402 -441
- package/README_CN.md +4 -578
- package/README_EN.md +850 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- package/skills/openprd-requirement-intake/SKILL.md +39 -23
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +265 -65
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +639 -117
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1176 -75
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/self-update.js +1 -1
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
package/src/growth.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson } from './fs-utils.js';
|
|
4
|
+
import { listBenchmarkRecommendationsWorkspace } from './benchmark.js';
|
|
4
5
|
|
|
5
6
|
export const OPENPRD_GROWTH_DIR = path.join('.openprd', 'growth');
|
|
6
7
|
export const OPENPRD_GROWTH_CANDIDATES = path.join(OPENPRD_GROWTH_DIR, 'candidates.jsonl');
|
|
7
8
|
export const OPENPRD_GROWTH_ACCEPTED = path.join(OPENPRD_GROWTH_DIR, 'accepted.json');
|
|
8
9
|
export const OPENPRD_GROWTH_REJECTED = path.join(OPENPRD_GROWTH_DIR, 'rejected.json');
|
|
9
10
|
export const OPENPRD_GROWTH_LOCAL_PREFERENCES = path.join(OPENPRD_GROWTH_DIR, 'preferences.local.json');
|
|
11
|
+
export const OPENPRD_GROWTH_LEDGER = path.join(OPENPRD_GROWTH_DIR, 'ledger.json');
|
|
12
|
+
export const OPENPRD_GROWTH_EVENTS = path.join(OPENPRD_GROWTH_DIR, 'events.jsonl');
|
|
10
13
|
export const OPENPRD_STANDARDS_CONFIG = path.join('.openprd', 'standards', 'config.json');
|
|
11
14
|
|
|
12
15
|
export const DEFAULT_GROWTH_CONFIG = {
|
|
@@ -78,6 +81,32 @@ function normalizeArray(value) {
|
|
|
78
81
|
return Array.isArray(value) ? value.filter((item) => item !== null && item !== undefined) : [];
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function defaultGrowthLedger() {
|
|
85
|
+
return {
|
|
86
|
+
version: 1,
|
|
87
|
+
updatedAt: nowIso(),
|
|
88
|
+
summary: {
|
|
89
|
+
eventCount: 0,
|
|
90
|
+
observed: 0,
|
|
91
|
+
pendingObserved: 0,
|
|
92
|
+
autoApplied: 0,
|
|
93
|
+
manualApplied: 0,
|
|
94
|
+
reconciledAutoApplied: 0,
|
|
95
|
+
rejected: 0,
|
|
96
|
+
skipped: 0,
|
|
97
|
+
completionCheckpoints: 0,
|
|
98
|
+
skippedReasons: {},
|
|
99
|
+
current: {
|
|
100
|
+
total: 0,
|
|
101
|
+
pending: 0,
|
|
102
|
+
applied: 0,
|
|
103
|
+
rejected: 0,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
recentEvents: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
function normalizeCandidate(raw = {}) {
|
|
82
111
|
const type = String(raw.type ?? '').trim();
|
|
83
112
|
const key = String(raw.key ?? raw.extension ?? raw.pattern ?? raw.preferenceKey ?? '').trim();
|
|
@@ -161,6 +190,7 @@ async function ensureGrowthFiles(projectRoot) {
|
|
|
161
190
|
const acceptedPath = growthPath(projectRoot, OPENPRD_GROWTH_ACCEPTED);
|
|
162
191
|
const rejectedPath = growthPath(projectRoot, OPENPRD_GROWTH_REJECTED);
|
|
163
192
|
const localPreferencesPath = growthPath(projectRoot, OPENPRD_GROWTH_LOCAL_PREFERENCES);
|
|
193
|
+
const ledgerPath = growthPath(projectRoot, OPENPRD_GROWTH_LEDGER);
|
|
164
194
|
if (!(await exists(acceptedPath))) {
|
|
165
195
|
await writeJson(acceptedPath, { version: 1, candidates: [] });
|
|
166
196
|
}
|
|
@@ -170,20 +200,194 @@ async function ensureGrowthFiles(projectRoot) {
|
|
|
170
200
|
if (!(await exists(localPreferencesPath))) {
|
|
171
201
|
await writeJson(localPreferencesPath, { version: 1, preferences: {} });
|
|
172
202
|
}
|
|
203
|
+
if (!(await exists(ledgerPath))) {
|
|
204
|
+
await writeJson(ledgerPath, defaultGrowthLedger());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readGrowthAutoApplyConfig(projectRoot) {
|
|
209
|
+
const configPath = growthPath(projectRoot, OPENPRD_STANDARDS_CONFIG);
|
|
210
|
+
const config = await readJsonIfExists(configPath, null);
|
|
211
|
+
return normalizeGrowthConfig(config?.growth).autoApply;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeGrowthLedgerEvent(event = {}) {
|
|
215
|
+
return {
|
|
216
|
+
version: 1,
|
|
217
|
+
at: event.at ?? nowIso(),
|
|
218
|
+
action: String(event.action ?? 'observe').trim() || 'observe',
|
|
219
|
+
outcome: String(event.outcome ?? 'recorded').trim() || 'recorded',
|
|
220
|
+
candidateId: event.candidateId ? String(event.candidateId) : null,
|
|
221
|
+
candidateType: event.candidateType ? String(event.candidateType) : null,
|
|
222
|
+
scope: event.scope ? String(event.scope) : null,
|
|
223
|
+
autoApplied: event.autoApplied === true,
|
|
224
|
+
applyMode: event.applyMode ? String(event.applyMode) : null,
|
|
225
|
+
reason: event.reason ? String(event.reason) : null,
|
|
226
|
+
changed: normalizeArray(event.changed).map((item) => String(item)),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function summarizeGrowthLedgerEvents(events = [], state = { candidates: [] }) {
|
|
231
|
+
const summary = defaultGrowthLedger().summary;
|
|
232
|
+
summary.eventCount = events.length;
|
|
233
|
+
for (const rawEvent of events) {
|
|
234
|
+
const event = normalizeGrowthLedgerEvent(rawEvent);
|
|
235
|
+
if (event.action === 'observe') {
|
|
236
|
+
summary.observed += 1;
|
|
237
|
+
if (event.outcome === 'pending-recorded') {
|
|
238
|
+
summary.pendingObserved += 1;
|
|
239
|
+
}
|
|
240
|
+
if (event.outcome.startsWith('skipped-')) {
|
|
241
|
+
summary.skipped += 1;
|
|
242
|
+
const reason = event.outcome.replace(/^skipped-/, '') || 'unknown';
|
|
243
|
+
summary.skippedReasons[reason] = (summary.skippedReasons[reason] ?? 0) + 1;
|
|
244
|
+
}
|
|
245
|
+
if (event.autoApplied) {
|
|
246
|
+
summary.autoApplied += 1;
|
|
247
|
+
}
|
|
248
|
+
} else if (event.action === 'apply' && event.applyMode === 'manual') {
|
|
249
|
+
summary.manualApplied += 1;
|
|
250
|
+
} else if (event.action === 'reconcile' && event.autoApplied) {
|
|
251
|
+
summary.reconciledAutoApplied += 1;
|
|
252
|
+
summary.autoApplied += 1;
|
|
253
|
+
} else if (event.action === 'reject') {
|
|
254
|
+
summary.rejected += 1;
|
|
255
|
+
} else if (event.action === 'checkpoint') {
|
|
256
|
+
summary.completionCheckpoints += 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const pending = state.candidates.filter((candidate) => candidate.status === 'pending').length;
|
|
260
|
+
const applied = state.candidates.filter((candidate) => candidate.status === 'applied').length;
|
|
261
|
+
const rejected = state.candidates.filter((candidate) => candidate.status === 'rejected').length;
|
|
262
|
+
summary.current = {
|
|
263
|
+
total: state.candidates.length,
|
|
264
|
+
pending,
|
|
265
|
+
applied,
|
|
266
|
+
rejected,
|
|
267
|
+
};
|
|
268
|
+
return summary;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function syncGrowthLedger(projectRoot, event = null) {
|
|
272
|
+
if (event) {
|
|
273
|
+
await appendJsonl(growthPath(projectRoot, OPENPRD_GROWTH_EVENTS), normalizeGrowthLedgerEvent(event));
|
|
274
|
+
}
|
|
275
|
+
const state = await readGrowthState(projectRoot);
|
|
276
|
+
const events = await readJsonlIfExists(growthPath(projectRoot, OPENPRD_GROWTH_EVENTS));
|
|
277
|
+
const normalizedEvents = events.map((entry) => normalizeGrowthLedgerEvent(entry));
|
|
278
|
+
const ledger = {
|
|
279
|
+
...defaultGrowthLedger(),
|
|
280
|
+
updatedAt: nowIso(),
|
|
281
|
+
summary: summarizeGrowthLedgerEvents(normalizedEvents, state),
|
|
282
|
+
recentEvents: normalizedEvents.slice(-12).reverse(),
|
|
283
|
+
};
|
|
284
|
+
await writeJson(growthPath(projectRoot, OPENPRD_GROWTH_LEDGER), ledger);
|
|
285
|
+
return ledger;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function recordGrowthCheckpointWorkspace(projectRoot, options = {}) {
|
|
289
|
+
await ensureGrowthFiles(projectRoot);
|
|
290
|
+
const outcome = String(options.outcome ?? 'quality-verify').trim() || 'quality-verify';
|
|
291
|
+
const reason = String(options.reason ?? outcome).trim() || outcome;
|
|
292
|
+
const scope = String(options.scope ?? 'project').trim() || 'project';
|
|
293
|
+
const changed = normalizeArray(options.changed).map((item) => String(item));
|
|
294
|
+
const recentEvents = await readJsonlIfExists(growthPath(projectRoot, OPENPRD_GROWTH_EVENTS));
|
|
295
|
+
const normalizedRecent = recentEvents
|
|
296
|
+
.map((entry) => normalizeGrowthLedgerEvent(entry))
|
|
297
|
+
.slice(-24)
|
|
298
|
+
.reverse();
|
|
299
|
+
const duplicate = normalizedRecent.find((event) => (
|
|
300
|
+
event.action === 'checkpoint'
|
|
301
|
+
&& event.outcome === outcome
|
|
302
|
+
&& String(event.reason ?? '') === reason
|
|
303
|
+
&& String(event.scope ?? '') === scope
|
|
304
|
+
));
|
|
305
|
+
if (duplicate) {
|
|
306
|
+
const ledger = await syncGrowthLedger(projectRoot);
|
|
307
|
+
return {
|
|
308
|
+
ok: true,
|
|
309
|
+
action: 'growth-checkpoint',
|
|
310
|
+
projectRoot,
|
|
311
|
+
recorded: false,
|
|
312
|
+
reason: 'duplicate-checkpoint',
|
|
313
|
+
event: duplicate,
|
|
314
|
+
ledger,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const event = normalizeGrowthLedgerEvent({
|
|
318
|
+
action: 'checkpoint',
|
|
319
|
+
outcome,
|
|
320
|
+
reason,
|
|
321
|
+
scope,
|
|
322
|
+
changed,
|
|
323
|
+
});
|
|
324
|
+
const ledger = await syncGrowthLedger(projectRoot, event);
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
action: 'growth-checkpoint',
|
|
328
|
+
projectRoot,
|
|
329
|
+
recorded: true,
|
|
330
|
+
event,
|
|
331
|
+
ledger,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function reconcilePendingAutoApplyCandidates(projectRoot, options = {}) {
|
|
336
|
+
const state = await readGrowthState(projectRoot);
|
|
337
|
+
const autoApply = options.autoApply ?? await readGrowthAutoApplyConfig(projectRoot);
|
|
338
|
+
const reconciled = [];
|
|
339
|
+
for (const candidate of state.candidates) {
|
|
340
|
+
if (candidate.status !== 'pending') {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const autoApplyDecision = assessAutoApplyGrowthCandidate(candidate, autoApply);
|
|
344
|
+
if (!autoApplyDecision.ok) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const applied = await applyGrowthCandidate(projectRoot, candidate, {
|
|
348
|
+
mode: 'auto',
|
|
349
|
+
reason: autoApplyDecision.reason,
|
|
350
|
+
});
|
|
351
|
+
if (applied.ok) {
|
|
352
|
+
reconciled.push(applied.candidate);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return reconciled;
|
|
173
356
|
}
|
|
174
357
|
|
|
175
358
|
export async function initGrowthWorkspace(projectRoot) {
|
|
176
359
|
await ensureGrowthFiles(projectRoot);
|
|
360
|
+
const reconciled = await reconcilePendingAutoApplyCandidates(projectRoot);
|
|
361
|
+
for (const candidate of reconciled) {
|
|
362
|
+
await syncGrowthLedger(projectRoot, {
|
|
363
|
+
action: 'reconcile',
|
|
364
|
+
outcome: 'auto-applied',
|
|
365
|
+
candidateId: candidate.id,
|
|
366
|
+
candidateType: candidate.type,
|
|
367
|
+
scope: candidate.scope,
|
|
368
|
+
autoApplied: true,
|
|
369
|
+
applyMode: 'auto',
|
|
370
|
+
reason: candidate.applyReason ?? 'safe-code-extension',
|
|
371
|
+
changed: candidate.appliedChanges,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const ledger = reconciled.length > 0
|
|
375
|
+
? await readJsonIfExists(growthPath(projectRoot, OPENPRD_GROWTH_LEDGER), defaultGrowthLedger())
|
|
376
|
+
: await syncGrowthLedger(projectRoot);
|
|
177
377
|
return {
|
|
178
378
|
ok: true,
|
|
179
379
|
action: 'growth-init',
|
|
180
380
|
projectRoot,
|
|
381
|
+
reconciledAutoApplied: reconciled,
|
|
382
|
+
ledger,
|
|
181
383
|
files: {
|
|
182
384
|
dir: OPENPRD_GROWTH_DIR,
|
|
183
385
|
candidates: OPENPRD_GROWTH_CANDIDATES,
|
|
184
386
|
accepted: OPENPRD_GROWTH_ACCEPTED,
|
|
185
387
|
rejected: OPENPRD_GROWTH_REJECTED,
|
|
186
388
|
localPreferences: OPENPRD_GROWTH_LOCAL_PREFERENCES,
|
|
389
|
+
ledger: OPENPRD_GROWTH_LEDGER,
|
|
390
|
+
events: OPENPRD_GROWTH_EVENTS,
|
|
187
391
|
},
|
|
188
392
|
};
|
|
189
393
|
}
|
|
@@ -204,13 +408,31 @@ async function readGrowthState(projectRoot) {
|
|
|
204
408
|
export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, options = {}) {
|
|
205
409
|
const candidate = normalizeCandidate(rawCandidate);
|
|
206
410
|
if (!candidate.type || !candidate.key) {
|
|
207
|
-
|
|
411
|
+
const result = { ok: false, action: 'growth-observe', skipped: true, reason: 'missing-type-or-key', candidate };
|
|
412
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
413
|
+
action: 'observe',
|
|
414
|
+
outcome: 'skipped-missing-type-or-key',
|
|
415
|
+
candidateId: candidate.id,
|
|
416
|
+
candidateType: candidate.type,
|
|
417
|
+
scope: candidate.scope,
|
|
418
|
+
reason: result.reason,
|
|
419
|
+
});
|
|
420
|
+
return result;
|
|
208
421
|
}
|
|
209
422
|
await ensureGrowthFiles(projectRoot);
|
|
210
423
|
const state = await readGrowthState(projectRoot);
|
|
211
424
|
const existing = state.candidates.find((item) => item.id === candidate.id);
|
|
212
425
|
if (existing?.status === 'applied' || existing?.status === 'rejected') {
|
|
213
|
-
|
|
426
|
+
const result = { ok: true, action: 'growth-observe', skipped: true, reason: `candidate-${existing.status}`, candidate: existing, autoApplied: false };
|
|
427
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
428
|
+
action: 'observe',
|
|
429
|
+
outcome: `skipped-candidate-${existing.status}`,
|
|
430
|
+
candidateId: existing.id,
|
|
431
|
+
candidateType: existing.type,
|
|
432
|
+
scope: existing.scope,
|
|
433
|
+
reason: result.reason,
|
|
434
|
+
});
|
|
435
|
+
return result;
|
|
214
436
|
}
|
|
215
437
|
if (existing?.status === 'pending') {
|
|
216
438
|
const autoApplyDecision = assessAutoApplyGrowthCandidate(existing, options.autoApply);
|
|
@@ -219,15 +441,27 @@ export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, opt
|
|
|
219
441
|
mode: 'auto',
|
|
220
442
|
reason: autoApplyDecision.reason,
|
|
221
443
|
});
|
|
222
|
-
|
|
444
|
+
const result = {
|
|
223
445
|
...applied,
|
|
224
446
|
action: 'growth-observe',
|
|
225
447
|
skipped: false,
|
|
226
448
|
autoApplied: true,
|
|
227
449
|
autoApplyDecision,
|
|
228
450
|
};
|
|
451
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
452
|
+
action: 'observe',
|
|
453
|
+
outcome: 'auto-applied',
|
|
454
|
+
candidateId: applied.candidate.id,
|
|
455
|
+
candidateType: applied.candidate.type,
|
|
456
|
+
scope: applied.candidate.scope,
|
|
457
|
+
autoApplied: true,
|
|
458
|
+
applyMode: 'auto',
|
|
459
|
+
reason: autoApplyDecision.reason,
|
|
460
|
+
changed: applied.changed,
|
|
461
|
+
});
|
|
462
|
+
return result;
|
|
229
463
|
}
|
|
230
|
-
|
|
464
|
+
const result = {
|
|
231
465
|
ok: true,
|
|
232
466
|
action: 'growth-observe',
|
|
233
467
|
skipped: true,
|
|
@@ -236,6 +470,15 @@ export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, opt
|
|
|
236
470
|
autoApplied: false,
|
|
237
471
|
autoApplyDecision,
|
|
238
472
|
};
|
|
473
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
474
|
+
action: 'observe',
|
|
475
|
+
outcome: 'skipped-candidate-already-pending',
|
|
476
|
+
candidateId: existing.id,
|
|
477
|
+
candidateType: existing.type,
|
|
478
|
+
scope: existing.scope,
|
|
479
|
+
reason: autoApplyDecision.reason,
|
|
480
|
+
});
|
|
481
|
+
return result;
|
|
239
482
|
}
|
|
240
483
|
const stored = await writeCandidateEvent(projectRoot, candidate);
|
|
241
484
|
const autoApplyDecision = assessAutoApplyGrowthCandidate(stored, options.autoApply);
|
|
@@ -244,15 +487,27 @@ export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, opt
|
|
|
244
487
|
mode: 'auto',
|
|
245
488
|
reason: autoApplyDecision.reason,
|
|
246
489
|
});
|
|
247
|
-
|
|
490
|
+
const result = {
|
|
248
491
|
...applied,
|
|
249
492
|
action: 'growth-observe',
|
|
250
493
|
skipped: false,
|
|
251
494
|
autoApplied: true,
|
|
252
495
|
autoApplyDecision,
|
|
253
496
|
};
|
|
497
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
498
|
+
action: 'observe',
|
|
499
|
+
outcome: 'auto-applied',
|
|
500
|
+
candidateId: applied.candidate.id,
|
|
501
|
+
candidateType: applied.candidate.type,
|
|
502
|
+
scope: applied.candidate.scope,
|
|
503
|
+
autoApplied: true,
|
|
504
|
+
applyMode: 'auto',
|
|
505
|
+
reason: autoApplyDecision.reason,
|
|
506
|
+
changed: applied.changed,
|
|
507
|
+
});
|
|
508
|
+
return result;
|
|
254
509
|
}
|
|
255
|
-
|
|
510
|
+
const result = {
|
|
256
511
|
ok: true,
|
|
257
512
|
action: 'growth-observe',
|
|
258
513
|
skipped: false,
|
|
@@ -260,6 +515,15 @@ export async function observeGrowthWorkspace(projectRoot, rawCandidate = {}, opt
|
|
|
260
515
|
autoApplied: false,
|
|
261
516
|
autoApplyDecision,
|
|
262
517
|
};
|
|
518
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
519
|
+
action: 'observe',
|
|
520
|
+
outcome: 'pending-recorded',
|
|
521
|
+
candidateId: stored.id,
|
|
522
|
+
candidateType: stored.type,
|
|
523
|
+
scope: stored.scope,
|
|
524
|
+
reason: autoApplyDecision.reason,
|
|
525
|
+
});
|
|
526
|
+
return result;
|
|
263
527
|
}
|
|
264
528
|
|
|
265
529
|
export function assessAutoApplyGrowthCandidate(candidate, rawConfig = {}) {
|
|
@@ -273,7 +537,10 @@ export function assessAutoApplyGrowthCandidate(candidate, rawConfig = {}) {
|
|
|
273
537
|
if (candidate.scope !== 'project') {
|
|
274
538
|
return { ok: false, reason: 'scope-needs-review' };
|
|
275
539
|
}
|
|
276
|
-
if (
|
|
540
|
+
if (
|
|
541
|
+
candidate.type !== 'code-extension'
|
|
542
|
+
&& (typeof candidate.confidence !== 'number' || candidate.confidence < config.minConfidence)
|
|
543
|
+
) {
|
|
277
544
|
return { ok: false, reason: 'confidence-below-threshold' };
|
|
278
545
|
}
|
|
279
546
|
const extension = normalizeExtension(candidate.key);
|
|
@@ -329,7 +596,9 @@ async function applyGrowthCandidate(projectRoot, candidate, options = {}) {
|
|
|
329
596
|
|
|
330
597
|
export async function reviewGrowthWorkspace(projectRoot) {
|
|
331
598
|
const state = await readGrowthState(projectRoot);
|
|
599
|
+
const ledger = await syncGrowthLedger(projectRoot);
|
|
332
600
|
const pending = state.candidates.filter((candidate) => candidate.status === 'pending');
|
|
601
|
+
const benchmarkRecommendations = await listBenchmarkRecommendationsWorkspace(projectRoot).catch(() => []);
|
|
333
602
|
return {
|
|
334
603
|
ok: true,
|
|
335
604
|
action: 'growth-review',
|
|
@@ -342,9 +611,14 @@ export async function reviewGrowthWorkspace(projectRoot) {
|
|
|
342
611
|
applied: state.candidates.filter((candidate) => candidate.status === 'applied').length,
|
|
343
612
|
rejected: state.candidates.filter((candidate) => candidate.status === 'rejected').length,
|
|
344
613
|
},
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
614
|
+
ledger,
|
|
615
|
+
benchmarkRecommendations,
|
|
616
|
+
nextActions: [
|
|
617
|
+
...(pending.length === 0
|
|
618
|
+
? ['当前没有待确认增长候选。']
|
|
619
|
+
: pending.map((candidate) => `收工复盘时确认后运行 openprd grow . --apply --id ${candidate.id};不采用则运行 openprd grow . --reject --id ${candidate.id}`)),
|
|
620
|
+
...benchmarkRecommendations.map((source) => `信源 ${source.sourceKey} 最近 ${source.windowDays} 天已被采纳 ${source.adoptedCount} 次(累计 ${source.totalAdoptedCount} 次),建议确认后运行 ${source.approveCommand} 纳入 benchmark。`),
|
|
621
|
+
],
|
|
348
622
|
};
|
|
349
623
|
}
|
|
350
624
|
|
|
@@ -425,7 +699,20 @@ export async function applyGrowthCandidateWorkspace(projectRoot, options = {}) {
|
|
|
425
699
|
if (candidate.status !== 'pending') {
|
|
426
700
|
return { ok: false, action: 'growth-apply', projectRoot, candidate, errors: [`Growth candidate is already ${candidate.status}.`] };
|
|
427
701
|
}
|
|
428
|
-
|
|
702
|
+
const result = await applyGrowthCandidate(projectRoot, candidate, { mode: 'manual' });
|
|
703
|
+
if (result.ok) {
|
|
704
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
705
|
+
action: 'apply',
|
|
706
|
+
outcome: 'manual-applied',
|
|
707
|
+
candidateId: result.candidate.id,
|
|
708
|
+
candidateType: result.candidate.type,
|
|
709
|
+
scope: result.candidate.scope,
|
|
710
|
+
applyMode: 'manual',
|
|
711
|
+
reason: result.candidate.applyReason,
|
|
712
|
+
changed: result.changed,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
429
716
|
}
|
|
430
717
|
|
|
431
718
|
export async function rejectGrowthCandidateWorkspace(projectRoot, options = {}) {
|
|
@@ -452,17 +739,27 @@ export async function rejectGrowthCandidateWorkspace(projectRoot, options = {})
|
|
|
452
739
|
rejectedCandidates.push(stored);
|
|
453
740
|
await writeJson(rejectedPath, { version: 1, candidates: rejectedCandidates });
|
|
454
741
|
|
|
455
|
-
|
|
742
|
+
const result = {
|
|
456
743
|
ok: true,
|
|
457
744
|
action: 'growth-reject',
|
|
458
745
|
projectRoot,
|
|
459
746
|
candidate: stored,
|
|
460
747
|
errors: [],
|
|
461
748
|
};
|
|
749
|
+
result.ledger = await syncGrowthLedger(projectRoot, {
|
|
750
|
+
action: 'reject',
|
|
751
|
+
outcome: 'rejected',
|
|
752
|
+
candidateId: stored.id,
|
|
753
|
+
candidateType: stored.type,
|
|
754
|
+
scope: stored.scope,
|
|
755
|
+
reason: stored.notes,
|
|
756
|
+
});
|
|
757
|
+
return result;
|
|
462
758
|
}
|
|
463
759
|
|
|
464
760
|
export async function checkGrowthWorkspace(projectRoot) {
|
|
465
761
|
const state = await readGrowthState(projectRoot);
|
|
762
|
+
const ledger = await syncGrowthLedger(projectRoot);
|
|
466
763
|
const pending = state.candidates.filter((candidate) => candidate.status === 'pending');
|
|
467
764
|
const applied = state.candidates.filter((candidate) => candidate.status === 'applied');
|
|
468
765
|
const rejected = state.candidates.filter((candidate) => candidate.status === 'rejected');
|
|
@@ -478,6 +775,7 @@ export async function checkGrowthWorkspace(projectRoot) {
|
|
|
478
775
|
applied: applied.length,
|
|
479
776
|
rejected: rejected.length,
|
|
480
777
|
},
|
|
778
|
+
ledger,
|
|
481
779
|
};
|
|
482
780
|
}
|
|
483
781
|
|
|
@@ -488,7 +786,7 @@ export function validateGrowthConfig(config, errors = []) {
|
|
|
488
786
|
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.enabled must be a boolean.`);
|
|
489
787
|
}
|
|
490
788
|
if (growth.reviewRequired !== undefined && growth.reviewRequired !== true) {
|
|
491
|
-
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.reviewRequired must remain true; shared rules
|
|
789
|
+
errors.push(`${OPENPRD_STANDARDS_CONFIG} growth.reviewRequired must remain true; non-whitelisted shared rules still require wrap-up review.`);
|
|
492
790
|
}
|
|
493
791
|
if (growth.candidateLimit !== undefined) {
|
|
494
792
|
const limit = Number(growth.candidateLimit);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function escapeHtml(value) {
|
|
2
|
+
return String(value ?? '')
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function listMarkup(items, emptyText = '暂无') {
|
|
11
|
+
const normalized = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
12
|
+
if (normalized.length === 0) {
|
|
13
|
+
return `<li class="empty">${escapeHtml(emptyText)}</li>`;
|
|
14
|
+
}
|
|
15
|
+
return normalized.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function slugify(value, fallback = 'artifact') {
|
|
19
|
+
const slug = String(value ?? '')
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '')
|
|
23
|
+
.slice(0, 80);
|
|
24
|
+
return slug || fallback;
|
|
25
|
+
}
|