@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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cjoin, exists, readJson } from './fs-utils.js';
|
|
4
|
+
import { buildCodeExtensionCandidate, observeGrowthWorkspace } from './growth.js';
|
|
5
|
+
import { recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowledge.js';
|
|
6
|
+
|
|
7
|
+
const DEVELOPMENT_STANDARDS_CONFIG = cjoin('.openprd', 'standards', 'config.json');
|
|
8
|
+
const CODE_FILE_EXTENSIONS = new Set([
|
|
9
|
+
'.c',
|
|
10
|
+
'.cc',
|
|
11
|
+
'.cjs',
|
|
12
|
+
'.cpp',
|
|
13
|
+
'.cs',
|
|
14
|
+
'.css',
|
|
15
|
+
'.go',
|
|
16
|
+
'.h',
|
|
17
|
+
'.hpp',
|
|
18
|
+
'.html',
|
|
19
|
+
'.java',
|
|
20
|
+
'.js',
|
|
21
|
+
'.jsx',
|
|
22
|
+
'.kt',
|
|
23
|
+
'.mjs',
|
|
24
|
+
'.php',
|
|
25
|
+
'.py',
|
|
26
|
+
'.rb',
|
|
27
|
+
'.rs',
|
|
28
|
+
'.scss',
|
|
29
|
+
'.sh',
|
|
30
|
+
'.swift',
|
|
31
|
+
'.ts',
|
|
32
|
+
'.tsx',
|
|
33
|
+
'.vue',
|
|
34
|
+
]);
|
|
35
|
+
const EXEMPT_PATH_SEGMENTS = new Set([
|
|
36
|
+
'.git',
|
|
37
|
+
'.openprd',
|
|
38
|
+
'.openspec',
|
|
39
|
+
'node_modules',
|
|
40
|
+
'vendor',
|
|
41
|
+
'dist',
|
|
42
|
+
'build',
|
|
43
|
+
'out',
|
|
44
|
+
'coverage',
|
|
45
|
+
'generated',
|
|
46
|
+
'__fixtures__',
|
|
47
|
+
'fixtures',
|
|
48
|
+
'snapshots',
|
|
49
|
+
]);
|
|
50
|
+
const EXEMPT_FILE_PATTERNS = [
|
|
51
|
+
/(^|\/)package-lock\.json$/i,
|
|
52
|
+
/(^|\/)pnpm-lock\.yaml$/i,
|
|
53
|
+
/(^|\/)yarn\.lock$/i,
|
|
54
|
+
/(^|\/)bun\.lockb$/i,
|
|
55
|
+
/\.min\.(js|css)$/i,
|
|
56
|
+
/\.(generated|gen)\.[^.]+$/i,
|
|
57
|
+
/\.snap$/i,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_DEVELOPMENT_STANDARDS = {
|
|
61
|
+
codeFileLines: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
okMax: 700,
|
|
64
|
+
attentionMax: 1500,
|
|
65
|
+
appliesTo: 'agent-touched-code-files',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function normalizePathForReport(value) {
|
|
70
|
+
return String(value ?? '').split(path.sep).join('/');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function countTextLines(text) {
|
|
74
|
+
if (!text) return 0;
|
|
75
|
+
const lineCount = text.split(/\r\n|\r|\n/).length;
|
|
76
|
+
return /(\r\n|\r|\n)$/.test(text) ? lineCount - 1 : lineCount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeStringList(value) {
|
|
80
|
+
return Array.isArray(value)
|
|
81
|
+
? value.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
82
|
+
: [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeExtension(value) {
|
|
86
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
87
|
+
if (!raw) return null;
|
|
88
|
+
return raw.startsWith('.') ? raw : `.${raw}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compilePattern(value) {
|
|
92
|
+
if (value instanceof RegExp) return value;
|
|
93
|
+
const raw = String(value ?? '').trim();
|
|
94
|
+
if (!raw) return null;
|
|
95
|
+
try {
|
|
96
|
+
return new RegExp(raw, 'i');
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isCodeFile(relativePath, lineConfig) {
|
|
103
|
+
return lineConfig.codeFileExtensions.has(path.extname(relativePath).toLowerCase());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isExemptPath(relativePath, lineConfig) {
|
|
107
|
+
const normalized = normalizePathForReport(relativePath);
|
|
108
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
109
|
+
return segments.some((segment) => lineConfig.exemptPathSegments.has(segment))
|
|
110
|
+
|| lineConfig.exemptFilePatterns.some((pattern) => pattern.test(normalized));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function looksLikeCodeFile(relativePath, text) {
|
|
114
|
+
const extension = path.extname(relativePath);
|
|
115
|
+
if (!extension || !text.trim()) {
|
|
116
|
+
return { match: false, confidence: 0, reason: 'no-extension-or-empty' };
|
|
117
|
+
}
|
|
118
|
+
const checks = [
|
|
119
|
+
{ pattern: /^#!.*\b(node|deno|python|ruby|bash|sh|zsh|perl|php)\b/m, weight: 0.9, reason: 'shebang' },
|
|
120
|
+
{ pattern: /^\s*(import|export)\s.+from\s+['"][^'"]+['"]/m, weight: 0.85, reason: 'module-import' },
|
|
121
|
+
{ pattern: /^\s*(const|let|var)\s+[A-Za-z_$][\w$]*\s*=/m, weight: 0.72, reason: 'variable-declaration' },
|
|
122
|
+
{ pattern: /^\s*(function|class|interface|type|enum)\s+[A-Za-z_$][\w$]*/m, weight: 0.78, reason: 'declaration' },
|
|
123
|
+
{ pattern: /^\s*(def|class)\s+[A-Za-z_][\w_]*\s*[\(:]/m, weight: 0.78, reason: 'python-declaration' },
|
|
124
|
+
{ pattern: /^\s*package\s+[A-Za-z_][\w.]*/m, weight: 0.78, reason: 'package-declaration' },
|
|
125
|
+
{ pattern: /<script\b[^>]*>[\s\S]{0,200}(import|export|const|let|function)\b/i, weight: 0.82, reason: 'script-block' },
|
|
126
|
+
{ pattern: /[{;}]\s*$/m, weight: 0.55, reason: 'code-punctuation' },
|
|
127
|
+
];
|
|
128
|
+
let best = { match: false, confidence: 0, reason: 'no-code-signal' };
|
|
129
|
+
for (const check of checks) {
|
|
130
|
+
if (check.pattern.test(text) && check.weight > best.confidence) {
|
|
131
|
+
best = { match: true, confidence: check.weight, reason: check.reason };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return best;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeLineConfig(config = {}) {
|
|
138
|
+
const source = config?.developmentStandards?.codeFileLines ?? config?.codeFileLines ?? {};
|
|
139
|
+
const okMax = Number(source.okMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.okMax);
|
|
140
|
+
const attentionMax = Number(source.attentionMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.attentionMax);
|
|
141
|
+
const codeFileExtensions = new Set([
|
|
142
|
+
...CODE_FILE_EXTENSIONS,
|
|
143
|
+
...normalizeStringList(source.codeFileExtensions).map(normalizeExtension).filter(Boolean),
|
|
144
|
+
...normalizeStringList(source.additionalCodeFileExtensions).map(normalizeExtension).filter(Boolean),
|
|
145
|
+
]);
|
|
146
|
+
const exemptPathSegments = new Set([
|
|
147
|
+
...EXEMPT_PATH_SEGMENTS,
|
|
148
|
+
...normalizeStringList(source.exemptPathSegments),
|
|
149
|
+
...normalizeStringList(source.additionalExemptPathSegments),
|
|
150
|
+
]);
|
|
151
|
+
const customPatterns = [
|
|
152
|
+
...normalizeStringList(source.exemptFilePatterns),
|
|
153
|
+
...normalizeStringList(source.additionalExemptFilePatterns),
|
|
154
|
+
].map(compilePattern).filter(Boolean);
|
|
155
|
+
return {
|
|
156
|
+
enabled: source.enabled !== false,
|
|
157
|
+
okMax: Number.isInteger(okMax) && okMax > 0 ? okMax : DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.okMax,
|
|
158
|
+
attentionMax: Number.isInteger(attentionMax) && attentionMax > okMax
|
|
159
|
+
? attentionMax
|
|
160
|
+
: DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.attentionMax,
|
|
161
|
+
codeFileExtensions,
|
|
162
|
+
exemptPathSegments,
|
|
163
|
+
exemptFilePatterns: [...EXEMPT_FILE_PATTERNS, ...customPatterns],
|
|
164
|
+
growthEnabled: config?.growth?.enabled !== false,
|
|
165
|
+
growthAutoApply: config?.growth?.autoApply,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function validateDevelopmentStandardsConfig(config, errors = []) {
|
|
170
|
+
const lineConfig = config?.developmentStandards?.codeFileLines;
|
|
171
|
+
if (!lineConfig) return errors;
|
|
172
|
+
const okMax = Number(lineConfig.okMax);
|
|
173
|
+
const attentionMax = Number(lineConfig.attentionMax);
|
|
174
|
+
if (!Number.isInteger(okMax) || okMax < 1) {
|
|
175
|
+
errors.push(`${DEVELOPMENT_STANDARDS_CONFIG} developmentStandards.codeFileLines.okMax must be a positive integer.`);
|
|
176
|
+
}
|
|
177
|
+
if (!Number.isInteger(attentionMax) || attentionMax <= okMax) {
|
|
178
|
+
errors.push(`${DEVELOPMENT_STANDARDS_CONFIG} developmentStandards.codeFileLines.attentionMax must be greater than okMax.`);
|
|
179
|
+
}
|
|
180
|
+
for (const field of ['codeFileExtensions', 'additionalCodeFileExtensions', 'exemptPathSegments', 'additionalExemptPathSegments', 'exemptFilePatterns', 'additionalExemptFilePatterns']) {
|
|
181
|
+
if (lineConfig[field] !== undefined && !Array.isArray(lineConfig[field])) {
|
|
182
|
+
errors.push(`${DEVELOPMENT_STANDARDS_CONFIG} developmentStandards.codeFileLines.${field} must be an array.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const value of [
|
|
186
|
+
...normalizeStringList(lineConfig.exemptFilePatterns),
|
|
187
|
+
...normalizeStringList(lineConfig.additionalExemptFilePatterns),
|
|
188
|
+
]) {
|
|
189
|
+
if (!compilePattern(value)) {
|
|
190
|
+
errors.push(`${DEVELOPMENT_STANDARDS_CONFIG} developmentStandards.codeFileLines exempt file pattern is invalid: ${value}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return errors;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readDevelopmentConfig(projectRoot) {
|
|
197
|
+
const configPath = cjoin(projectRoot, DEVELOPMENT_STANDARDS_CONFIG);
|
|
198
|
+
if (!(await exists(configPath))) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
return readJson(configPath).catch(() => ({}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fileStatus(lineCount, lineConfig) {
|
|
205
|
+
if (lineCount <= lineConfig.okMax) return 'ok';
|
|
206
|
+
if (lineCount <= lineConfig.attentionMax) return 'attention';
|
|
207
|
+
return 'warning';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function nextActionForStatus(status, lineConfig) {
|
|
211
|
+
if (status === 'ok') {
|
|
212
|
+
return `结构状态正常;最终回复中可简要说明 dev-check 已回顾 touched files。`;
|
|
213
|
+
}
|
|
214
|
+
if (status === 'attention') {
|
|
215
|
+
return `最终回复说明本轮只触碰的局部职责和影响范围,避免继续新增无关职责。`;
|
|
216
|
+
}
|
|
217
|
+
if (status === 'warning') {
|
|
218
|
+
return `判断本轮是否继续扩大职责或堆叠逻辑;若扩大了,先重构、拆分或解耦后复查;若只是窄 bugfix 或小修且暂不拆,说明原因并留下后续拆分建议。`;
|
|
219
|
+
}
|
|
220
|
+
if (status === 'exempt') {
|
|
221
|
+
return `豁免治理;只记录行数,不要求拆分。`;
|
|
222
|
+
}
|
|
223
|
+
if (status === 'not-code') {
|
|
224
|
+
return `不适用;研发期行数规则只约束代码文件。`;
|
|
225
|
+
}
|
|
226
|
+
return `无法检查;请确认文件路径。`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function analyzeDevelopmentFile(projectRoot, targetPath, lineConfig) {
|
|
230
|
+
const absolutePath = path.isAbsolute(targetPath)
|
|
231
|
+
? path.resolve(targetPath)
|
|
232
|
+
: cjoin(projectRoot, targetPath);
|
|
233
|
+
const relativePath = normalizePathForReport(path.relative(projectRoot, absolutePath));
|
|
234
|
+
|
|
235
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
236
|
+
return {
|
|
237
|
+
path: targetPath,
|
|
238
|
+
status: 'error',
|
|
239
|
+
lineCount: null,
|
|
240
|
+
nextAction: '文件必须位于当前项目内。',
|
|
241
|
+
error: 'file-outside-project',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const stat = await fs.stat(absolutePath).catch(() => null);
|
|
246
|
+
if (!stat) {
|
|
247
|
+
return {
|
|
248
|
+
path: relativePath,
|
|
249
|
+
status: 'error',
|
|
250
|
+
lineCount: null,
|
|
251
|
+
nextAction: '文件不存在;请确认路径后重试。',
|
|
252
|
+
error: 'file-missing',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (!stat.isFile()) {
|
|
256
|
+
return {
|
|
257
|
+
path: relativePath,
|
|
258
|
+
status: 'error',
|
|
259
|
+
lineCount: null,
|
|
260
|
+
nextAction: '目标不是文件;请传入具体代码文件。',
|
|
261
|
+
error: 'not-a-file',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const text = await fs.readFile(absolutePath, 'utf8').catch(() => '');
|
|
266
|
+
const lineCount = countTextLines(text);
|
|
267
|
+
const codeFile = isCodeFile(relativePath, lineConfig);
|
|
268
|
+
const exempt = isExemptPath(relativePath, lineConfig);
|
|
269
|
+
const codeSignal = codeFile || exempt ? { match: false, confidence: 0, reason: null } : looksLikeCodeFile(relativePath, text);
|
|
270
|
+
const candidateCode = !codeFile && !exempt && codeSignal.match;
|
|
271
|
+
const status = exempt ? 'exempt' : (codeFile || candidateCode ? fileStatus(lineCount, lineConfig) : 'not-code');
|
|
272
|
+
let growthCandidate = null;
|
|
273
|
+
let growthObservation = null;
|
|
274
|
+
if (candidateCode) {
|
|
275
|
+
growthCandidate = buildCodeExtensionCandidate(relativePath, {
|
|
276
|
+
lineCount,
|
|
277
|
+
confidence: codeSignal.confidence,
|
|
278
|
+
reason: codeSignal.reason,
|
|
279
|
+
});
|
|
280
|
+
if (lineConfig.growthEnabled) {
|
|
281
|
+
growthObservation = await observeGrowthWorkspace(projectRoot, growthCandidate, {
|
|
282
|
+
autoApply: lineConfig.growthAutoApply,
|
|
283
|
+
});
|
|
284
|
+
growthCandidate = growthObservation.candidate ?? growthCandidate;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const baseAction = nextActionForStatus(status, lineConfig);
|
|
288
|
+
const nextAction = growthObservation?.autoApplied
|
|
289
|
+
? `${baseAction} 另外:已自动补齐 ${growthCandidate.key} 代码文件识别规则,后续同类文件会直接纳入 dev-check。`
|
|
290
|
+
: growthCandidate
|
|
291
|
+
? `${baseAction} 另外:该扩展名尚未固化为代码文件识别规则,先按代码候选处理;本轮收工复盘时运行 openprd grow . --review 集中确认。`
|
|
292
|
+
: baseAction;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
path: relativePath,
|
|
296
|
+
absolutePath,
|
|
297
|
+
status,
|
|
298
|
+
fileKind: exempt ? 'exempt' : (codeFile ? 'code' : (candidateCode ? 'candidate-code' : 'non-code')),
|
|
299
|
+
lineCount,
|
|
300
|
+
sizeBytes: stat.size,
|
|
301
|
+
thresholds: {
|
|
302
|
+
okMax: lineConfig.okMax,
|
|
303
|
+
attentionMax: lineConfig.attentionMax,
|
|
304
|
+
},
|
|
305
|
+
growthCandidate,
|
|
306
|
+
growthObservation,
|
|
307
|
+
nextAction,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function checkDevelopmentStandardsWorkspace(projectRoot, options = {}) {
|
|
312
|
+
const targets = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
|
313
|
+
const errors = [];
|
|
314
|
+
if (targets.length === 0) {
|
|
315
|
+
errors.push('No files provided. Usage: openprd dev-check [project] <file...>');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const config = await readDevelopmentConfig(projectRoot);
|
|
319
|
+
const lineConfig = normalizeLineConfig(config);
|
|
320
|
+
const files = [];
|
|
321
|
+
if (lineConfig.enabled) {
|
|
322
|
+
for (const target of targets) {
|
|
323
|
+
files.push(await analyzeDevelopmentFile(projectRoot, target, lineConfig));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const statusCounts = files.reduce((counts, file) => {
|
|
328
|
+
counts[file.status] = (counts[file.status] ?? 0) + 1;
|
|
329
|
+
return counts;
|
|
330
|
+
}, {});
|
|
331
|
+
errors.push(...files.filter((file) => file.status === 'error').map((file) => `${file.path}: ${file.nextAction}`));
|
|
332
|
+
const touchedFiles = files
|
|
333
|
+
.filter((file) => file.status !== 'error')
|
|
334
|
+
.map((file) => file.path);
|
|
335
|
+
const knowledgeSignal = {
|
|
336
|
+
kind: 'dev-check',
|
|
337
|
+
ok: errors.length === 0,
|
|
338
|
+
summary: `dev-check attention=${statusCounts.attention ?? 0}, warning=${statusCounts.warning ?? 0}`,
|
|
339
|
+
touchedFiles,
|
|
340
|
+
};
|
|
341
|
+
await recordKnowledgeReviewSignal(projectRoot, knowledgeSignal).catch(() => null);
|
|
342
|
+
const knowledgeReview = await reviewKnowledgeWorkspace(projectRoot, {
|
|
343
|
+
signal: knowledgeSignal,
|
|
344
|
+
touchedFiles,
|
|
345
|
+
}).catch((error) => ({
|
|
346
|
+
ok: false,
|
|
347
|
+
action: 'quality-knowledge-review',
|
|
348
|
+
skipped: false,
|
|
349
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
ok: errors.length === 0,
|
|
354
|
+
action: 'dev-check',
|
|
355
|
+
projectRoot,
|
|
356
|
+
enabled: lineConfig.enabled,
|
|
357
|
+
thresholds: {
|
|
358
|
+
okMax: lineConfig.okMax,
|
|
359
|
+
attentionMax: lineConfig.attentionMax,
|
|
360
|
+
warningAbove: lineConfig.attentionMax,
|
|
361
|
+
},
|
|
362
|
+
summary: {
|
|
363
|
+
total: files.length,
|
|
364
|
+
statusCounts,
|
|
365
|
+
attention: statusCounts.attention ?? 0,
|
|
366
|
+
warning: statusCounts.warning ?? 0,
|
|
367
|
+
},
|
|
368
|
+
files,
|
|
369
|
+
knowledgeReview,
|
|
370
|
+
errors,
|
|
371
|
+
};
|
|
372
|
+
}
|