@cyning/harness 2.9.0 → 2.11.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/CHANGELOG.md +35 -0
- package/docs/ONBOARDING.md +15 -2
- package/docs/USER_GUIDE_v1.0_zh.md +8 -2
- package/docs/rethink/2026-07-mechanization-rate/03_coverage_matrix.md +4 -2
- package/docs/rethink/2026-07-mechanization-rate/04_next_steps.md +4 -3
- package/docs/spec/SPEC-discipline-coverage-yaml_v1.md +204 -0
- package/docs/spec/SPEC-discipline-show_v1.md +89 -0
- package/docs/spec/SPEC-lifecycle-engine-min_v1.md +246 -0
- package/docs/spec/SPEC-lifecycle-guard-expand_v1.md +100 -0
- package/docs/spec/SPEC-post-g4-debt-retro_v1.md +237 -0
- package/docs/spec/SPEC-verify-full-reviews-gate_v1.md +1 -1
- package/harness/discipline-coverage.yaml +289 -0
- package/harness/lifecycle.yaml +6 -3
- package/lib/audit.js +5 -1
- package/lib/cli.js +128 -20
- package/lib/discipline-coverage.js +148 -0
- package/lib/lifecycle.js +255 -0
- package/package.json +1 -1
- package/schema/discipline-coverage.v1.schema.json +55 -0
- package/schema/lifecycle.v1.schema.json +1 -1
package/lib/lifecycle.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
+
import { runTestCheck } from './audit.js';
|
|
4
5
|
import { resolveHarnessRoot } from './paths.js';
|
|
6
|
+
import { findReview, parseHumanGates } from './task-meta.js';
|
|
7
|
+
import { lintTaskFile } from './task-lint.js';
|
|
5
8
|
|
|
6
9
|
const SEVERITIES = new Set(['block', 'warn']);
|
|
7
10
|
|
|
@@ -119,3 +122,255 @@ export function formatLifecycleShow(data) {
|
|
|
119
122
|
}
|
|
120
123
|
return lines.join('\n').trimEnd();
|
|
121
124
|
}
|
|
125
|
+
|
|
126
|
+
/** 已接线的守卫(未登记者 → unevaluated;close_* 本波仍未接线) */
|
|
127
|
+
const GUARD_ADAPTERS = {
|
|
128
|
+
'HG-AUDIT-R1': evaluateHgGate('HG-AUDIT-R1'),
|
|
129
|
+
'HG-TASK-DRAFT': evaluateHgGate('HG-TASK-DRAFT'),
|
|
130
|
+
reviews_retention: evaluateReviewsRetention,
|
|
131
|
+
audit_D5: evaluateAuditD5,
|
|
132
|
+
task_lint: evaluateTaskLint,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function findRepoRootFromTask(absTask) {
|
|
136
|
+
let cur = path.dirname(absTask);
|
|
137
|
+
while (true) {
|
|
138
|
+
if (path.basename(cur) === 'docs') return path.dirname(cur);
|
|
139
|
+
const parent = path.dirname(cur);
|
|
140
|
+
if (parent === cur) return null;
|
|
141
|
+
cur = parent;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function flagsAllow(flags, allowFlag) {
|
|
146
|
+
if (allowFlag === '--allow-no-review') return Boolean(flags.allowNoReview);
|
|
147
|
+
if (allowFlag === '--allow-lint-fail') return Boolean(flags.allowLintFail);
|
|
148
|
+
if (allowFlag === '--allow-no-spec-review') return Boolean(flags.allowNoSpecReview);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function evaluateHgGate(gateId) {
|
|
153
|
+
return ({ taskPath }) => {
|
|
154
|
+
const content = fs.readFileSync(taskPath, 'utf8');
|
|
155
|
+
const gates = parseHumanGates(content);
|
|
156
|
+
const row = gates.find((g) => g.id === gateId);
|
|
157
|
+
if (!row) {
|
|
158
|
+
return { status: 'fail', detail: `闸表无 ${gateId} 行` };
|
|
159
|
+
}
|
|
160
|
+
if (row.status === 'approved') {
|
|
161
|
+
return { status: 'pass', detail: `${gateId}=approved` };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
status: 'fail',
|
|
165
|
+
detail: `${gateId}=${row.status}(须 approved)`,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function evaluateReviewsRetention({ taskPath, target, flags }) {
|
|
171
|
+
const review = findReview(target, taskPath);
|
|
172
|
+
if (review.found) {
|
|
173
|
+
return { status: 'pass', detail: review.latest };
|
|
174
|
+
}
|
|
175
|
+
if (flags.allowNoReview) {
|
|
176
|
+
return {
|
|
177
|
+
status: 'warn',
|
|
178
|
+
detail: 'missing R<n> review(--allow-no-review)',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { status: 'fail', detail: 'missing R<n> review' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function evaluateAuditD5({ taskPath, target }) {
|
|
185
|
+
const result = runTestCheck(target, taskPath);
|
|
186
|
+
if (result.ok) {
|
|
187
|
+
return {
|
|
188
|
+
status: 'pass',
|
|
189
|
+
detail: result.skipped
|
|
190
|
+
? `D5 skipped · ${result.reason}`
|
|
191
|
+
: result.reason,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { status: 'fail', detail: result.reason };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function evaluateTaskLint({ taskPath, target }) {
|
|
198
|
+
try {
|
|
199
|
+
const result = lintTaskFile(taskPath, { cwd: target });
|
|
200
|
+
if (result.ok) {
|
|
201
|
+
return { status: 'pass', detail: 'task lint PASS' };
|
|
202
|
+
}
|
|
203
|
+
const rules = (result.errors ?? []).map((e) => e.rule).join(',') || 'errors';
|
|
204
|
+
return { status: 'fail', detail: `task lint FAIL · ${rules}` };
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return { status: 'fail', detail: `task lint 异常: ${err.message}` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 转移 dry-run(引擎最小骨架 · v2.10+)。
|
|
212
|
+
* 默认不写盘;无 --apply。
|
|
213
|
+
*
|
|
214
|
+
* @param {{
|
|
215
|
+
* transitionId: string,
|
|
216
|
+
* fromState: string,
|
|
217
|
+
* taskPath?: string,
|
|
218
|
+
* harnessRoot?: string,
|
|
219
|
+
* flags?: { allowNoReview?: boolean, allowLintFail?: boolean, allowNoSpecReview?: boolean },
|
|
220
|
+
* cwd?: string,
|
|
221
|
+
* }} options
|
|
222
|
+
*/
|
|
223
|
+
export function dryRunTransition(options = {}) {
|
|
224
|
+
const {
|
|
225
|
+
transitionId,
|
|
226
|
+
fromState,
|
|
227
|
+
taskPath,
|
|
228
|
+
harnessRoot,
|
|
229
|
+
flags = {},
|
|
230
|
+
cwd = process.cwd(),
|
|
231
|
+
} = options;
|
|
232
|
+
|
|
233
|
+
const { data } = loadLifecycle({ harnessRoot });
|
|
234
|
+
const knownIds = data.transitions.map((t) => t.id);
|
|
235
|
+
const transition = data.transitions.find((t) => t.id === transitionId);
|
|
236
|
+
|
|
237
|
+
if (!transition) {
|
|
238
|
+
return {
|
|
239
|
+
engine: 'lifecycle-dry-run',
|
|
240
|
+
lifecycle_doc_version: data.version,
|
|
241
|
+
transition_id: transitionId ?? null,
|
|
242
|
+
from: fromState ?? null,
|
|
243
|
+
to: null,
|
|
244
|
+
hat: null,
|
|
245
|
+
structure_ok: false,
|
|
246
|
+
guards: [],
|
|
247
|
+
blocked: true,
|
|
248
|
+
unevaluated_count: 0,
|
|
249
|
+
detail: `未知 transition: ${transitionId}(已知: ${knownIds.join(', ')})`,
|
|
250
|
+
exitCode: 2,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!fromState || !transition.from.includes(fromState)) {
|
|
255
|
+
return {
|
|
256
|
+
engine: 'lifecycle-dry-run',
|
|
257
|
+
lifecycle_doc_version: data.version,
|
|
258
|
+
transition_id: transition.id,
|
|
259
|
+
from: fromState ?? null,
|
|
260
|
+
to: transition.to,
|
|
261
|
+
hat: transition.hat ?? null,
|
|
262
|
+
structure_ok: false,
|
|
263
|
+
guards: [],
|
|
264
|
+
blocked: true,
|
|
265
|
+
unevaluated_count: 0,
|
|
266
|
+
detail: `from "${fromState}" ∉ ${JSON.stringify(transition.from)}`,
|
|
267
|
+
exitCode: 2,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const absTask = taskPath
|
|
272
|
+
? path.isAbsolute(taskPath)
|
|
273
|
+
? taskPath
|
|
274
|
+
: path.resolve(cwd, taskPath)
|
|
275
|
+
: null;
|
|
276
|
+
|
|
277
|
+
if (taskPath && absTask && !fs.existsSync(absTask)) {
|
|
278
|
+
const e = new Error(`--task 不可读: ${taskPath}`);
|
|
279
|
+
e.exitCode = 1;
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const canEval = Boolean(absTask);
|
|
284
|
+
const target = absTask ? findRepoRootFromTask(absTask) ?? cwd : cwd;
|
|
285
|
+
const guards = [];
|
|
286
|
+
|
|
287
|
+
for (const g of transition.guards) {
|
|
288
|
+
const adapter = GUARD_ADAPTERS[g.id];
|
|
289
|
+
if (!canEval || !adapter) {
|
|
290
|
+
guards.push({
|
|
291
|
+
id: g.id,
|
|
292
|
+
severity: g.severity,
|
|
293
|
+
status: 'unevaluated',
|
|
294
|
+
detail: !canEval ? '无 --task · 未求值' : '本波未接线 adapter',
|
|
295
|
+
allow_flag: g.allow_flag ?? null,
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const evaluated = adapter({
|
|
301
|
+
taskPath: absTask,
|
|
302
|
+
target,
|
|
303
|
+
flags,
|
|
304
|
+
guard: g,
|
|
305
|
+
cwd,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
let status = evaluated.status;
|
|
309
|
+
let detail = evaluated.detail ?? null;
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
status === 'fail' &&
|
|
313
|
+
g.allow_flag &&
|
|
314
|
+
flagsAllow(flags, g.allow_flag)
|
|
315
|
+
) {
|
|
316
|
+
status = 'warn';
|
|
317
|
+
detail = `${detail ?? 'fail'}(${g.allow_flag} 豁免)`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
guards.push({
|
|
321
|
+
id: g.id,
|
|
322
|
+
severity: g.severity,
|
|
323
|
+
status,
|
|
324
|
+
detail,
|
|
325
|
+
allow_flag: g.allow_flag ?? null,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const unevaluated_count = guards.filter((g) => g.status === 'unevaluated').length;
|
|
330
|
+
const blocked = guards.some(
|
|
331
|
+
(g) => g.severity === 'block' && g.status === 'fail',
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
engine: 'lifecycle-dry-run',
|
|
336
|
+
lifecycle_doc_version: data.version,
|
|
337
|
+
transition_id: transition.id,
|
|
338
|
+
from: fromState,
|
|
339
|
+
to: transition.to,
|
|
340
|
+
hat: transition.hat ?? null,
|
|
341
|
+
structure_ok: true,
|
|
342
|
+
guards,
|
|
343
|
+
blocked,
|
|
344
|
+
unevaluated_count,
|
|
345
|
+
exitCode: blocked ? 2 : 0,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 人读 dry-run 报告。
|
|
351
|
+
*/
|
|
352
|
+
export function formatLifecycleDryRun(report) {
|
|
353
|
+
const lines = [
|
|
354
|
+
'=== Harness lifecycle dry-run ===',
|
|
355
|
+
`engine: ${report.engine}`,
|
|
356
|
+
`lifecycle: v${report.lifecycle_doc_version}`,
|
|
357
|
+
`transition: ${report.transition_id} · ${report.from} → ${report.to}${report.hat ? ` · hat=${report.hat}` : ''}`,
|
|
358
|
+
`structure_ok: ${report.structure_ok}`,
|
|
359
|
+
`blocked: ${report.blocked}`,
|
|
360
|
+
`unevaluated_count: ${report.unevaluated_count}`,
|
|
361
|
+
];
|
|
362
|
+
if (report.detail) lines.push(`detail: ${report.detail}`);
|
|
363
|
+
if (report.unevaluated_count > 0) {
|
|
364
|
+
lines.push('WARN: 存在未求值守卫 · unevaluated ≠ pass(勿当作已通过)');
|
|
365
|
+
}
|
|
366
|
+
lines.push('', '## guards');
|
|
367
|
+
for (const g of report.guards ?? []) {
|
|
368
|
+
const d = g.detail ? ` · ${g.detail}` : '';
|
|
369
|
+
lines.push(`- [${g.severity}] ${g.id}: ${g.status}${d}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push('');
|
|
372
|
+
lines.push(
|
|
373
|
+
'注: dry-run 为旁路资格报告 · 不替代 verify / gate-check 作为 30 硬闸 · 非 G7 runner',
|
|
374
|
+
);
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://cyning.dev/schemas/cyning-harness/discipline-coverage.v1.json",
|
|
4
|
+
"title": "cyning-harness Discipline Coverage (mechanization matrix)",
|
|
5
|
+
"description": "Starter 子集机械化率资产 · 随包版本维护 · 非 audit UI(v2.10+)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "as_of_package_version", "scope", "statements"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "type": "string", "minLength": 1 },
|
|
11
|
+
"as_of_package_version": { "type": "string", "minLength": 1 },
|
|
12
|
+
"scope": { "type": "string", "minLength": 1 },
|
|
13
|
+
"source_audit": { "type": "string" },
|
|
14
|
+
"statements": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"minItems": 1,
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["id", "source", "summary", "status"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": { "type": "string", "minLength": 1 },
|
|
23
|
+
"source": { "type": "string", "minLength": 1 },
|
|
24
|
+
"summary": { "type": "string", "minLength": 1 },
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["mechanical", "partial", "prompt-only"]
|
|
28
|
+
},
|
|
29
|
+
"mechanism": { "type": ["string", "null"] },
|
|
30
|
+
"gap": { "type": ["string", "null"] },
|
|
31
|
+
"notes": { "type": ["string", "null"] },
|
|
32
|
+
"mechanism_quality": { "type": ["string", "null"] }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"gaps": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["id", "title", "status"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": { "type": "string", "minLength": 1 },
|
|
44
|
+
"title": { "type": "string", "minLength": 1 },
|
|
45
|
+
"status": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": ["open", "closed", "deferred"]
|
|
48
|
+
},
|
|
49
|
+
"closed_in": { "type": ["string", "null"] },
|
|
50
|
+
"note": { "type": ["string", "null"] }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://cyning.dev/schemas/cyning-harness/lifecycle.v1.json",
|
|
4
4
|
"title": "cyning-harness Task Lifecycle (minimal)",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "状态/转移/守卫登记真值 · 由 lifecycle dry-run 引擎消费(yaml ≠ 引擎 · v2.7+ 登记 · v2.10+ dry-run)",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["version", "states", "transitions"],
|