@xulthekl/team-flow 0.63.0 → 0.64.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/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +28 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/docs/state-machine.md +4 -1
- package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +2 -2
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/guard/checks/_fs-utils.mjs +18 -0
- package/scripts/guard/checks/arch-design-light.mjs +39 -0
- package/scripts/guard/checks/arch-merged-light.mjs +67 -0
- package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
- package/scripts/guard/checks/artifacts-planned.mjs +38 -0
- package/scripts/guard/checks/compound-writeback-light.mjs +45 -0
- package/scripts/guard/checks/cross-change-consistency-light.mjs +75 -0
- package/scripts/guard/checks/direct-short-path.mjs +52 -0
- package/scripts/guard/checks/direct-test-result.mjs +30 -0
- package/scripts/guard/checks/execution-plan-ready.mjs +7 -1
- package/scripts/guard/checks/execution-reviews-passed-light.mjs +28 -0
- package/scripts/guard/checks/lightweight-completion-evidence.mjs +27 -0
- package/scripts/guard/checks/specs-merged.mjs +25 -1
- package/scripts/guard/checks/test-matrix-complete.mjs +27 -1
- package/scripts/guard/checks/test-matrix-ready.mjs +28 -1
- package/scripts/guard/checks/test-merged-light.mjs +36 -0
- package/scripts/guard/guard.mjs +102 -12
- package/scripts/infer-workflow.mjs +35 -4
- package/scripts/lib/arch-merge.mjs +20 -4
- package/scripts/lib/cmd-execution.mjs +44 -1
- package/scripts/lib/cmd-state.mjs +94 -4
- package/scripts/lib/execution-plan.mjs +3 -1
- package/scripts/lib/state-loader.mjs +43 -0
- package/scripts/lib/surface-scan.mjs +156 -0
- package/scripts/lib/test-merge.mjs +10 -2
- package/scripts/team-flow.mjs +3 -3
- package/skills/clean-code/SKILL.md +1 -1
- package/skills/jarvis/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +39 -13
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/test-strategy/SKILL.md +1 -1
- package/skills/workflow-start/SKILL.md +63 -5
- package/skills/workflow-start/references/routing-rules.md +4 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// scripts/guard/checks/direct-test-result.mjs — direct/lightweight 验证证据(§4.5 注记③)
|
|
2
|
+
// v0.64.0:guard 只读 tf test record 程序化证据,绝不在 guard 进程内现场跑测试
|
|
3
|
+
// (cmd-state spawnSync 10s 总超时会杀掉 guard,A-19)。证据机制与 tests-passing 同源。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
7
|
+
import { parseStructuredTestResult } from './tests-passing.mjs';
|
|
8
|
+
|
|
9
|
+
const RECORD_HINT = 'run your verification command, then record it: tf test record <change-dir> --from <runner-output>';
|
|
10
|
+
|
|
11
|
+
export function checkDirectTestResult(changeDir) {
|
|
12
|
+
const state = readState(changeDir);
|
|
13
|
+
const parsed = parseStructuredTestResult(state.test_result);
|
|
14
|
+
if (!parsed || parsed.verdict !== 'pass') {
|
|
15
|
+
return { pass: false, failures: [`no passing programmatic test evidence recorded — ${RECORD_HINT}`] };
|
|
16
|
+
}
|
|
17
|
+
if (parsed.total == null || parsed.total <= 0) {
|
|
18
|
+
return { pass: false, failures: [`recorded evidence has total=0 (empty-run rejected) — ${RECORD_HINT}`] };
|
|
19
|
+
}
|
|
20
|
+
if (!state.test_evidence_path) {
|
|
21
|
+
return { pass: false, failures: [`test_evidence_path missing — ${RECORD_HINT}`] };
|
|
22
|
+
}
|
|
23
|
+
const evidenceAbs = path.isAbsolute(state.test_evidence_path)
|
|
24
|
+
? state.test_evidence_path
|
|
25
|
+
: path.join(changeDir, state.test_evidence_path);
|
|
26
|
+
if (!fs.existsSync(evidenceAbs)) {
|
|
27
|
+
return { pass: false, failures: [`evidence file not found: ${state.test_evidence_path} — ${RECORD_HINT}`] };
|
|
28
|
+
}
|
|
29
|
+
return { pass: true, failures: [] };
|
|
30
|
+
}
|
|
@@ -7,7 +7,7 @@ import { readState } from '../../lib/state-loader.mjs';
|
|
|
7
7
|
* A non-empty DP-4 field is not sufficient: it must name the current plan
|
|
8
8
|
* revision, whose hashes, workflow, mode, and state summary remain current.
|
|
9
9
|
*/
|
|
10
|
-
export function checkExecutionPlanReady(changeDir) {
|
|
10
|
+
export function checkExecutionPlanReady(changeDir, ctx = {}) {
|
|
11
11
|
const plan = readPlan(changeDir);
|
|
12
12
|
if (!plan) {
|
|
13
13
|
return {
|
|
@@ -21,6 +21,12 @@ export function checkExecutionPlanReady(changeDir) {
|
|
|
21
21
|
return { pass: false, failures: validation.failures };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// v0.64.0(§4.5 注记⑤,R2 兼容性修复):planned 无 DP-4——豁免 DP-4 revision 精确引用,
|
|
25
|
+
// 判据 = plan 存在 + validatePlan 通过(内嵌 hash 与制品一致)。
|
|
26
|
+
if (ctx.variant === 'planned') {
|
|
27
|
+
return { pass: true, failures: [], reason: 'planned variant: DP-4 reference waived; plan file + hash validation suffice' };
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
const state = readState(changeDir);
|
|
25
31
|
const expectedRevision = `plan revision ${plan.revision}`;
|
|
26
32
|
const decision = typeof state.dp_4_result === 'string' ? state.dp_4_result : '';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// scripts/guard/checks/execution-reviews-passed-light.mjs — planned 一次最终审查(§4.5)
|
|
2
|
+
// v0.64.0(A-05①):planned 无波次无逐波 receipt——判据 = 关门前存在非空(≥5 非空行)
|
|
3
|
+
// 最终审查记录 final-review.md;内容最低契约(B-01)防空文件桩。
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const MIN_LINES = 5;
|
|
8
|
+
|
|
9
|
+
export function checkExecutionReviewsPassedLight(changeDir) {
|
|
10
|
+
const p = join(changeDir, 'final-review.md');
|
|
11
|
+
if (!existsSync(p)) {
|
|
12
|
+
return {
|
|
13
|
+
pass: false,
|
|
14
|
+
failures: [
|
|
15
|
+
'final-review.md missing — planned path requires ONE final review before closing '
|
|
16
|
+
+ '(record verdict, scope, and findings; ≥5 lines)',
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const lines = readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).length;
|
|
21
|
+
if (lines < MIN_LINES) {
|
|
22
|
+
return {
|
|
23
|
+
pass: false,
|
|
24
|
+
failures: [`final-review.md too thin (${lines} non-empty lines, need ≥${MIN_LINES}) — record verdict + scope + findings`],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { pass: true, failures: [] };
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// scripts/guard/checks/lightweight-completion-evidence.mjs — lightweight 档完成证据(§4.5)
|
|
2
|
+
// v0.64.0:类 direct-test-result——证据文件须存在且非空(独立于 test_result 解析,
|
|
3
|
+
// 兜住「test_result 被清但文件还在」与「文件被删但记录还在」两种漂移)。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
7
|
+
|
|
8
|
+
export function checkLightweightCompletionEvidence(changeDir) {
|
|
9
|
+
const state = readState(changeDir);
|
|
10
|
+
if (!state.test_evidence_path) {
|
|
11
|
+
return {
|
|
12
|
+
pass: false,
|
|
13
|
+
failures: ['completion evidence missing: test_evidence_path is null — record verification via tf test record before closing'],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const abs = path.isAbsolute(state.test_evidence_path)
|
|
17
|
+
? state.test_evidence_path
|
|
18
|
+
: path.join(changeDir, state.test_evidence_path);
|
|
19
|
+
if (!fs.existsSync(abs)) {
|
|
20
|
+
return { pass: false, failures: [`completion evidence file not found: ${state.test_evidence_path}`] };
|
|
21
|
+
}
|
|
22
|
+
const content = fs.readFileSync(abs, 'utf-8').trim();
|
|
23
|
+
if (!content) {
|
|
24
|
+
return { pass: false, failures: [`completion evidence file is empty: ${state.test_evidence_path}`] };
|
|
25
|
+
}
|
|
26
|
+
return { pass: true, failures: [] };
|
|
27
|
+
}
|
|
@@ -34,11 +34,35 @@ function hasDeltaSpecs(changeDir) {
|
|
|
34
34
|
* Passes when either spec_merged is recorded, or there are no delta specs to merge.
|
|
35
35
|
* Blocks `executing → closing` when delta specs exist but spec-merger hasn't run.
|
|
36
36
|
*/
|
|
37
|
-
export function checkSpecsMerged(changeDir) {
|
|
37
|
+
export function checkSpecsMerged(changeDir, ctx = {}) {
|
|
38
38
|
const state = readState(changeDir);
|
|
39
39
|
if (state.spec_merged === true || state.spec_merged === 'true') {
|
|
40
40
|
return { pass: true, failures: [] };
|
|
41
41
|
}
|
|
42
|
+
// v0.64.0(§4.5 注记⑦,B-02):planned 轻语义——无 specs/ 目录即过;
|
|
43
|
+
// 有 specs/ 但无 delta 头 = 手写完整形态 specs 永不合并 → FAIL(补 delta 头或移除)。
|
|
44
|
+
if (ctx.variant === 'planned') {
|
|
45
|
+
const specsDir = join(changeDir, 'specs');
|
|
46
|
+
if (!existsSync(specsDir)) {
|
|
47
|
+
return { pass: true, failures: [], reason: 'planned: no specs/ dir — merge N/A' };
|
|
48
|
+
}
|
|
49
|
+
if (!hasDeltaSpecs(changeDir)) {
|
|
50
|
+
return {
|
|
51
|
+
pass: false,
|
|
52
|
+
failures: [
|
|
53
|
+
'specs/ exists but no ## ADDED/MODIFIED/REMOVED/RENAMED Requirements delta headers — '
|
|
54
|
+
+ 'full-form specs would never merge; add delta headers (then spec-merger) or remove specs/',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 有 delta 头但未合并 → 落到下方统一失败
|
|
59
|
+
return {
|
|
60
|
+
pass: false,
|
|
61
|
+
failures: [
|
|
62
|
+
'Delta specs exist in specs/ but spec-merger has not run (spec_merged not recorded). Run spec-merger to merge ADDED/MODIFIED/REMOVED/RENAMED requirements before closing.',
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
42
66
|
if (!hasDeltaSpecs(changeDir)) {
|
|
43
67
|
return { pass: true, failures: [] };
|
|
44
68
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
/**
|
|
20
20
|
* Returns { pass, failures[], reason? }.
|
|
21
21
|
*/
|
|
22
|
-
export function checkTestMatrixComplete(changeDir) {
|
|
22
|
+
export function checkTestMatrixComplete(changeDir, ctx = {}) {
|
|
23
23
|
const state = readState(changeDir);
|
|
24
24
|
|
|
25
25
|
// Step 1 — legacy:v0.32.0 之前初始化的 change(无 schema_version 打戳)保留旧行为。
|
|
@@ -40,6 +40,32 @@ export function checkTestMatrixComplete(changeDir) {
|
|
|
40
40
|
return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// v0.64.0(§4.5 注记②,D4 无契约轻量判据):direct/planned/quick/lightweight
|
|
44
|
+
// 无契约 → 跳过 Step 3 契约声明检查;矩阵存在非空 +(若已记录)hash 一致即可。
|
|
45
|
+
const isLight = ctx.variant === 'direct' || ctx.variant === 'planned'
|
|
46
|
+
|| ctx.workflow === 'quick' || ctx.workflow === 'lightweight';
|
|
47
|
+
if (isLight) {
|
|
48
|
+
const matrix = readTestMatrixFile(changeDir);
|
|
49
|
+
if (!matrix.exists || matrix.empty) {
|
|
50
|
+
return {
|
|
51
|
+
pass: false,
|
|
52
|
+
failures: [
|
|
53
|
+
!matrix.exists
|
|
54
|
+
? 'test-matrix.md missing — light path requires the matrix file itself (no contract)'
|
|
55
|
+
: 'test-matrix.md is empty — must contain at least a Summary section and Cases table',
|
|
56
|
+
'or skip explicitly: tf state set <dir> test_matrix_skipped true + test_matrix_skip_reason "<reason>"',
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (state.test_matrix_hash) {
|
|
61
|
+
const computedLight = computeTestMatrixHash(changeDir);
|
|
62
|
+
if (computedLight !== state.test_matrix_hash) {
|
|
63
|
+
return { pass: false, failures: ['test-matrix.md changed since recorded hash — run tf state rebuild'] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { pass: true, failures: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
// Step 3 — 非存量 change 的契约必须声明矩阵(v0.13:内容型豁免删除)。
|
|
44
70
|
const contract = contractDeclaresTestMatrix(changeDir);
|
|
45
71
|
if (!contract.exists) {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
/**
|
|
18
18
|
* Returns { pass, failures[], reason? }.
|
|
19
19
|
*/
|
|
20
|
-
export function checkTestMatrixReady(changeDir) {
|
|
20
|
+
export function checkTestMatrixReady(changeDir, ctx = {}) {
|
|
21
21
|
const state = readState(changeDir);
|
|
22
22
|
|
|
23
23
|
// Exemption 1:legacy change(v0.32.0 之前初始化,无 schema_version 打戳)。
|
|
@@ -25,6 +25,33 @@ export function checkTestMatrixReady(changeDir) {
|
|
|
25
25
|
return { pass: true, failures: [], reason: 'legacy change — initialized before v0.32.0' };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// v0.64.0(§4.5 注记②,D4 无契约轻量判据):direct/planned/quick/lightweight
|
|
29
|
+
// 无 execution-contract——判据降为「test-matrix.md 存在非空 OR 显式 skip 附理由」,
|
|
30
|
+
// 不要求契约声明段。skip 仍须附理由(拒 state set 自清为常规通过路径)。
|
|
31
|
+
const isLight = ctx.variant === 'direct' || ctx.variant === 'planned'
|
|
32
|
+
|| ctx.workflow === 'quick' || ctx.workflow === 'lightweight';
|
|
33
|
+
if (isLight) {
|
|
34
|
+
if (state.test_matrix_skipped === 'true') {
|
|
35
|
+
if (skipMissingReason(state)) {
|
|
36
|
+
return { pass: false, failures: [SKIP_REASON_HINT] };
|
|
37
|
+
}
|
|
38
|
+
return { pass: true, failures: [], reason: `explicitly skipped: ${state.test_matrix_skip_reason}` };
|
|
39
|
+
}
|
|
40
|
+
const matrix = readTestMatrixFile(changeDir);
|
|
41
|
+
if (matrix.exists && !matrix.empty) {
|
|
42
|
+
return { pass: true, failures: [] };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
pass: false,
|
|
46
|
+
failures: [
|
|
47
|
+
matrix.exists
|
|
48
|
+
? 'test-matrix.md is empty — add a Summary + Cases table (light path has no contract section)'
|
|
49
|
+
: 'test-matrix.md missing — create it (light path: matrix file alone is sufficient, no contract needed)',
|
|
50
|
+
'or skip explicitly: tf state set <dir> test_matrix_skipped true + test_matrix_skip_reason "<reason>"',
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
// Exemption 2:显式 skip(必须附理由)。
|
|
29
56
|
if (state.test_matrix_skipped === 'true') {
|
|
30
57
|
// v2.1 §6.3 skip 禁令:glaf4-delegation 模式禁止 skip(唯一例外 TEST_BOOTSTRAP)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// scripts/guard/checks/test-merged-light.mjs — 测试台账轻回写(§4,横展发现的同型全局态)
|
|
2
|
+
// v0.64.0:无测试触碰 → 过;有 → docs/test-ledger/** 须含归因 change:<name>
|
|
3
|
+
//(写入走 tf test-merge --light,同 sink 同 rewriteIndex 单写入口)。
|
|
4
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
5
|
+
import { scanArchitectureSurface } from '../../lib/surface-scan.mjs';
|
|
6
|
+
import { filesContainDir } from './_fs-utils.mjs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export function checkTestMergedLight(changeDir) {
|
|
10
|
+
const state = readState(changeDir);
|
|
11
|
+
const changeName = state.change_name || changeDir.split('/').filter(Boolean).pop();
|
|
12
|
+
const scan = scanArchitectureSurface(changeDir, state);
|
|
13
|
+
|
|
14
|
+
if (scan.error === 'no-baseline' || scan.error === 'diff-failed') {
|
|
15
|
+
return { pass: false, failures: [`surface scan failed (${scan.error}) — cannot determine touched tests, fail-closed`] };
|
|
16
|
+
}
|
|
17
|
+
if (scan.error === 'aggregate-list-missing') {
|
|
18
|
+
// 测试判定不依赖聚合清单——降级为仅用文件清单分类(清单缺失不阻塞本维度)
|
|
19
|
+
// 但 scanArchitectureSurface 在清单缺失时不返回 files……此时保守 FAIL:
|
|
20
|
+
return { pass: false, failures: ['aggregate list missing (.team-flow/aggregate-dirs.txt) — shared scan precondition unmet, fail-closed'] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (scan.testFiles.length === 0) {
|
|
24
|
+
return { pass: true, failures: [], reason: 'no test files touched — test ledger writeback N/A' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ledger = join(scan.projectRoot, 'docs', 'test-ledger');
|
|
28
|
+
const anchor = `change:${changeName}`;
|
|
29
|
+
if (filesContainDir(ledger, anchor) || filesContainDir(ledger, changeName)) {
|
|
30
|
+
return { pass: true, failures: [] };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
pass: false,
|
|
34
|
+
failures: [`tests touched but docs/test-ledger/** has no '${anchor}' entry — run: tf test-merge --light <change-dir>`],
|
|
35
|
+
};
|
|
36
|
+
}
|
package/scripts/guard/guard.mjs
CHANGED
|
@@ -21,6 +21,18 @@ import { checkArchReadiness } from './checks/arch-readiness.mjs';
|
|
|
21
21
|
import { checkArchSnapshot } from './checks/arch-snapshot.mjs';
|
|
22
22
|
import { checkArchMerged } from './checks/arch-merged.mjs';
|
|
23
23
|
import { checkDelegationStatus } from './checks/delegation-status.mjs';
|
|
24
|
+
// v0.64.0 P1(spec-superflow 2.0 实施计划 §4.5):direct/planned/lightweight 短路径与轻量全局态闸门
|
|
25
|
+
import { checkDirectShortPath } from './checks/direct-short-path.mjs';
|
|
26
|
+
import { checkDirectTestResult } from './checks/direct-test-result.mjs';
|
|
27
|
+
import { checkLightweightCompletionEvidence } from './checks/lightweight-completion-evidence.mjs';
|
|
28
|
+
import { checkArtifactsPlanned } from './checks/artifacts-planned.mjs';
|
|
29
|
+
import { checkArchDesignLight } from './checks/arch-design-light.mjs';
|
|
30
|
+
import { checkArchSnapshotLight } from './checks/arch-snapshot-light.mjs';
|
|
31
|
+
import { checkArchMergedLight } from './checks/arch-merged-light.mjs';
|
|
32
|
+
import { checkCompoundWritebackLight } from './checks/compound-writeback-light.mjs';
|
|
33
|
+
import { checkTestMergedLight } from './checks/test-merged-light.mjs';
|
|
34
|
+
import { checkCrossChangeConsistencyLight } from './checks/cross-change-consistency-light.mjs';
|
|
35
|
+
import { checkExecutionReviewsPassedLight } from './checks/execution-reviews-passed-light.mjs';
|
|
24
36
|
|
|
25
37
|
// Transition matrix: <from>:<to> → required check dimensions
|
|
26
38
|
const TRANSITION_CHECKS = {
|
|
@@ -113,12 +125,52 @@ const WORKFLOW_TRANSITION_CHECKS = {
|
|
|
113
125
|
|
|
114
126
|
const TRANSITION_WORKFLOW_REQUIREMENTS = {
|
|
115
127
|
'exploring:bridging': ['hotfix'],
|
|
116
|
-
|
|
128
|
+
// v0.64.0(§4.5 注记①):quick/lightweight 短路径 + planned(full 变体)共用该捷径;
|
|
129
|
+
// checkWorkflowAllowed 经 variant 参数放行 planned。
|
|
130
|
+
'exploring:approved-for-build': ['tweak', 'quick', 'lightweight'],
|
|
117
131
|
};
|
|
118
132
|
|
|
119
|
-
|
|
133
|
+
// v0.64.0(§4.5①):direct / lightweight 短路径维度表。
|
|
134
|
+
// 必须显式列出关键跳——未列出的 key 会回落 full 基表(contract-fresh 等对轻路径恒 FAIL)。
|
|
135
|
+
const QUICK_TRANSITION_CHECKS = {
|
|
136
|
+
quick: {
|
|
137
|
+
'exploring:approved-for-build': ['direct-short-path'],
|
|
138
|
+
'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready'],
|
|
139
|
+
'executing:closing': ['direct-short-path', 'direct-test-result', 'test-matrix-complete'],
|
|
140
|
+
'executing:debugging': [],
|
|
141
|
+
'debugging:executing': ['direct-test-result'],
|
|
142
|
+
},
|
|
143
|
+
lightweight: {
|
|
144
|
+
'exploring:approved-for-build': ['direct-short-path'],
|
|
145
|
+
'approved-for-build:executing': ['direct-short-path', 'test-matrix-ready'],
|
|
146
|
+
'executing:closing': ['direct-short-path', 'direct-test-result', 'lightweight-completion-evidence', 'test-matrix-complete'],
|
|
147
|
+
'executing:debugging': [],
|
|
148
|
+
'debugging:executing': ['direct-test-result'],
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// v0.64.0(§4.5②):planned(workflow=full + variant=planned)维度表。
|
|
153
|
+
// 无契约/无 DP-3/无逐波审查;回写四灯 + 最终审查 + 测试矩阵在 closing 一次性收口。
|
|
154
|
+
const PLANNED_TRANSITION_CHECKS = {
|
|
155
|
+
// 注:不挂 schema-valid——该 checker 强制 canonical specs/<capability>/spec.md,
|
|
156
|
+
// 与 planned「specs 按需」冲突(P1 实测修正,同步方案 §4.5);反空壳由 artifacts-planned
|
|
157
|
+
// (proposal≥10 行 + checkbox)+ 最终审查内容契约承担。
|
|
158
|
+
'exploring:approved-for-build': ['artifacts-planned'],
|
|
159
|
+
'approved-for-build:executing': ['execution-plan-ready', 'test-matrix-ready'],
|
|
160
|
+
'executing:closing': [
|
|
161
|
+
'execution-plan-ready', 'execution-reviews-passed-light', 'test-matrix-complete',
|
|
162
|
+
'tests-passing', 'arch-merged-light', 'compound-writeback-light', 'test-merged-light',
|
|
163
|
+
'cross-change-consistency-light', 'delegation-status', 'specs-merged', 'tasks-complete',
|
|
164
|
+
],
|
|
165
|
+
'executing:debugging': [],
|
|
166
|
+
'debugging:executing': ['execution-plan-ready', 'tests-passing'],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function checkWorkflowAllowed(key, workflow, variant) {
|
|
120
170
|
const allowed = TRANSITION_WORKFLOW_REQUIREMENTS[key];
|
|
121
171
|
if (!allowed || allowed.includes(workflow)) return { pass: true, checks: [] };
|
|
172
|
+
// planned(full 变体)走同一捷径,但其维度由 PLANNED 表管辖(§4.5)
|
|
173
|
+
if (variant === 'planned' && key === 'exploring:approved-for-build') return { pass: true, checks: [] };
|
|
122
174
|
return {
|
|
123
175
|
pass: false,
|
|
124
176
|
checks: [{
|
|
@@ -129,7 +181,19 @@ function checkWorkflowAllowed(key, workflow) {
|
|
|
129
181
|
};
|
|
130
182
|
}
|
|
131
183
|
|
|
132
|
-
function resolveDimensions(key, workflow) {
|
|
184
|
+
function resolveDimensions(key, workflow, opts = {}) {
|
|
185
|
+
const { variant, plannedArch } = opts;
|
|
186
|
+
if (workflow === 'full' && variant === 'planned') {
|
|
187
|
+
let dims = PLANNED_TRANSITION_CHECKS[key] ?? TRANSITION_CHECKS[key];
|
|
188
|
+
// G3:planned_arch=true 时在规划段追加轻架构两维(§4.5 条件行)
|
|
189
|
+
if (plannedArch && (key === 'exploring:approved-for-build' || key === 'approved-for-build:executing')) {
|
|
190
|
+
dims = [...dims, 'arch-design-light', 'arch-snapshot-light'];
|
|
191
|
+
}
|
|
192
|
+
return dims;
|
|
193
|
+
}
|
|
194
|
+
if (workflow === 'quick' || workflow === 'lightweight') {
|
|
195
|
+
return QUICK_TRANSITION_CHECKS[workflow]?.[key] ?? TRANSITION_CHECKS[key];
|
|
196
|
+
}
|
|
133
197
|
return WORKFLOW_TRANSITION_CHECKS[workflow]?.[key] ?? TRANSITION_CHECKS[key];
|
|
134
198
|
}
|
|
135
199
|
|
|
@@ -138,13 +202,16 @@ async function main() {
|
|
|
138
202
|
options: {
|
|
139
203
|
json: { type: 'boolean', default: false },
|
|
140
204
|
workflow: { type: 'string', default: 'full' },
|
|
205
|
+
// v0.64.0 P1:cmd-state 透传的前门变体(§4.5 注记①)
|
|
206
|
+
'workflow-variant': { type: 'string', default: '' },
|
|
207
|
+
'planned-arch': { type: 'string', default: '' },
|
|
141
208
|
},
|
|
142
209
|
allowPositionals: true,
|
|
143
210
|
});
|
|
144
211
|
|
|
145
212
|
const subcommand = positionals[0];
|
|
146
213
|
if (subcommand !== 'check') {
|
|
147
|
-
console.error('Usage: guard.mjs check <change-dir> <from-state> <to-state> [--json] [--workflow <mode>]');
|
|
214
|
+
console.error('Usage: guard.mjs check <change-dir> <from-state> <to-state> [--json] [--workflow <mode>] [--workflow-variant <direct|planned|legacy>] [--planned-arch true]');
|
|
148
215
|
process.exit(2);
|
|
149
216
|
}
|
|
150
217
|
|
|
@@ -153,20 +220,27 @@ async function main() {
|
|
|
153
220
|
const toState = positionals[3];
|
|
154
221
|
const useJson = values.json;
|
|
155
222
|
const workflow = values.workflow;
|
|
223
|
+
const workflowVariant = values['workflow-variant'] || null;
|
|
224
|
+
const plannedArch = values['planned-arch'] === 'true';
|
|
156
225
|
|
|
157
|
-
|
|
226
|
+
// F1 校正(§3.3):加 quick/lightweight——direct/planned 是 variant 不是 workflow 值
|
|
227
|
+
const VALID_WORKFLOWS = ['full', 'hotfix', 'tweak', 'quick', 'lightweight'];
|
|
158
228
|
if (!VALID_WORKFLOWS.includes(workflow)) {
|
|
159
229
|
console.error(`Invalid workflow: ${workflow}. Must be one of: ${VALID_WORKFLOWS.join(', ')}`);
|
|
160
230
|
process.exit(2);
|
|
161
231
|
}
|
|
232
|
+
if (workflowVariant && !['direct', 'planned', 'legacy'].includes(workflowVariant)) {
|
|
233
|
+
console.error(`Invalid workflow variant: ${workflowVariant}. Must be one of: direct, planned, legacy`);
|
|
234
|
+
process.exit(2);
|
|
235
|
+
}
|
|
162
236
|
|
|
163
237
|
if (!changeDir || !fromState || !toState) {
|
|
164
|
-
console.error('Usage: guard.mjs check <change-dir> <from-state> <to-state> [--json]');
|
|
238
|
+
console.error('Usage: guard.mjs check <change-dir> <from-state> <to-state> [--json] [--workflow <mode>] [--workflow-variant <direct|planned|legacy>] [--planned-arch true]');
|
|
165
239
|
process.exit(2);
|
|
166
240
|
}
|
|
167
241
|
|
|
168
242
|
const key = `${fromState}:${toState}`;
|
|
169
|
-
const dimensions = resolveDimensions(key, workflow);
|
|
243
|
+
const dimensions = resolveDimensions(key, workflow, { variant: workflowVariant, plannedArch });
|
|
170
244
|
|
|
171
245
|
if (!dimensions) {
|
|
172
246
|
const valid = Object.keys(TRANSITION_CHECKS).join(', ');
|
|
@@ -176,7 +250,7 @@ async function main() {
|
|
|
176
250
|
process.exit(1);
|
|
177
251
|
}
|
|
178
252
|
|
|
179
|
-
const workflowCheck = checkWorkflowAllowed(key, workflow);
|
|
253
|
+
const workflowCheck = checkWorkflowAllowed(key, workflow, workflowVariant);
|
|
180
254
|
if (!workflowCheck.pass) {
|
|
181
255
|
if (useJson) {
|
|
182
256
|
console.log(JSON.stringify({ pass: false, checks: workflowCheck.checks }, null, 2));
|
|
@@ -198,6 +272,10 @@ async function main() {
|
|
|
198
272
|
process.exit(0);
|
|
199
273
|
}
|
|
200
274
|
|
|
275
|
+
// 变体上下文:需要 variant/plannedArch 的 runner(test-matrix-*、execution-plan-ready、
|
|
276
|
+
// specs-merged、direct-short-path 的轻量分支)从闭包 CTX 读取,保持 runner(dir) 签名兼容。
|
|
277
|
+
const CTX = { workflow, variant: workflowVariant, plannedArch };
|
|
278
|
+
|
|
201
279
|
const CHECK_RUNNERS = {
|
|
202
280
|
'artifacts-exist': (dir) => checkArtifactsExist(dir),
|
|
203
281
|
'schema-valid': async (dir) => (await import('./checks/schema-valid.mjs')).checkSchemaValid(dir),
|
|
@@ -205,20 +283,32 @@ async function main() {
|
|
|
205
283
|
'contract-current': (dir) => checkContractCurrent(dir),
|
|
206
284
|
'tasks-complete': (dir) => checkTasksComplete(dir),
|
|
207
285
|
'tests-passing': (dir) => checkTestsPassing(dir),
|
|
208
|
-
'specs-merged': (dir) => checkSpecsMerged(dir),
|
|
286
|
+
'specs-merged': (dir) => checkSpecsMerged(dir, CTX),
|
|
209
287
|
'dp-gate-passed': (dir) => checkDpGate(dir, fromState, toState),
|
|
210
288
|
'dp3-approved': (dir) => checkDp3Approved(dir),
|
|
211
|
-
'execution-plan-ready': (dir) => checkExecutionPlanReady(dir),
|
|
289
|
+
'execution-plan-ready': (dir) => checkExecutionPlanReady(dir, CTX),
|
|
212
290
|
'execution-reviews-passed': (dir) => checkExecutionReviewsPassed(dir),
|
|
213
291
|
'arch-design': (dir) => checkArchDesign(dir),
|
|
214
292
|
'compound-captured': (dir) => checkCompoundCaptured(dir),
|
|
215
|
-
'test-matrix-complete': (dir) => checkTestMatrixComplete(dir),
|
|
216
|
-
'test-matrix-ready': (dir) => checkTestMatrixReady(dir),
|
|
293
|
+
'test-matrix-complete': (dir) => checkTestMatrixComplete(dir, CTX),
|
|
294
|
+
'test-matrix-ready': (dir) => checkTestMatrixReady(dir, CTX),
|
|
217
295
|
'gates-probed': (dir) => checkGatesProbed(dir),
|
|
218
296
|
'arch-readiness': (dir) => checkArchReadiness(dir),
|
|
219
297
|
'arch-snapshot': (dir) => checkArchSnapshot(dir),
|
|
220
298
|
'arch-merged': (dir) => checkArchMerged(dir),
|
|
221
299
|
'delegation-status': (dir) => checkDelegationStatus(dir),
|
|
300
|
+
// v0.64.0 P1(§3.3 清单,11 个新 runner)
|
|
301
|
+
'direct-short-path': (dir) => checkDirectShortPath(dir, CTX),
|
|
302
|
+
'direct-test-result': (dir) => checkDirectTestResult(dir),
|
|
303
|
+
'lightweight-completion-evidence': (dir) => checkLightweightCompletionEvidence(dir),
|
|
304
|
+
'artifacts-planned': (dir) => checkArtifactsPlanned(dir),
|
|
305
|
+
'arch-design-light': (dir) => checkArchDesignLight(dir),
|
|
306
|
+
'arch-snapshot-light': (dir) => checkArchSnapshotLight(dir),
|
|
307
|
+
'arch-merged-light': (dir) => checkArchMergedLight(dir),
|
|
308
|
+
'compound-writeback-light': (dir) => checkCompoundWritebackLight(dir),
|
|
309
|
+
'test-merged-light': (dir) => checkTestMergedLight(dir),
|
|
310
|
+
'cross-change-consistency-light': (dir) => checkCrossChangeConsistencyLight(dir),
|
|
311
|
+
'execution-reviews-passed-light': (dir) => checkExecutionReviewsPassedLight(dir),
|
|
222
312
|
};
|
|
223
313
|
|
|
224
314
|
const checks = [];
|
|
@@ -57,11 +57,14 @@ function inferMode(changeDir) {
|
|
|
57
57
|
const state = readState(changeDir);
|
|
58
58
|
|
|
59
59
|
// Explicit override: honor any non-auto, non-null workflow value
|
|
60
|
+
// v0.64.0(§3.2 F1):VALID_WORKFLOWS 扩为五值,quick/lightweight 亦为合法显式档位。
|
|
60
61
|
if (state.workflow && state.workflow !== 'auto') {
|
|
61
|
-
const valid = ['hotfix', 'tweak', 'full'];
|
|
62
|
+
const valid = ['hotfix', 'tweak', 'full', 'quick', 'lightweight'];
|
|
62
63
|
if (valid.includes(state.workflow)) {
|
|
63
64
|
return {
|
|
64
65
|
mode: state.workflow,
|
|
66
|
+
// 双通道(§3.2):显式 workflow 已定时不再建议前门(显式 > 推断)
|
|
67
|
+
suggested_path: null,
|
|
65
68
|
explicit: true,
|
|
66
69
|
reason: `workflow explicitly set to '${state.workflow}' in .team-flow.yaml; skipping auto-detection`,
|
|
67
70
|
};
|
|
@@ -85,6 +88,20 @@ function inferMode(changeDir) {
|
|
|
85
88
|
'新增 capability', 'new capability',
|
|
86
89
|
]);
|
|
87
90
|
|
|
91
|
+
// v0.64.0(§3.2 双通道):前门建议信号。
|
|
92
|
+
// hasSchemaChange 已含 api/schema/接口 关键词(F-06 去重:不新建同词判定);
|
|
93
|
+
// arch/aggregate/cross_module/uncertainty 为净新增信号(现状 infer 无)。
|
|
94
|
+
const hasArchAggregate = hasKeyword(combined, [
|
|
95
|
+
'聚合', 'aggregate', 'architecture', '架构', '限界上下文', 'bounded context', 'cqrs',
|
|
96
|
+
]);
|
|
97
|
+
const hasCrossModule = hasKeyword(combined, [
|
|
98
|
+
'cross-module', '跨模块', '跨服务', 'multi-repo', '多仓',
|
|
99
|
+
]);
|
|
100
|
+
// 架构 surface 信号 → 建议 planned(轻架构);不再直接推 full(D1)
|
|
101
|
+
const archPathSignal = hasSchemaChange || hasArchAggregate;
|
|
102
|
+
// 跨模块 / 高不确定性 → full(D1:full 只留重场景)
|
|
103
|
+
const fullSignal = hasCrossModule || (taskCount > 4 && fileCount > 6);
|
|
104
|
+
|
|
88
105
|
const allExts = files.map(f => {
|
|
89
106
|
const parts = f.split('.');
|
|
90
107
|
return parts[parts.length - 1].toLowerCase();
|
|
@@ -96,6 +113,7 @@ function inferMode(changeDir) {
|
|
|
96
113
|
if (taskCount === 0 && fileCount === 0) {
|
|
97
114
|
return {
|
|
98
115
|
mode: 'full',
|
|
116
|
+
suggested_path: null,
|
|
99
117
|
explicit: false,
|
|
100
118
|
reason: 'no planning artifacts detected → full (safe default)',
|
|
101
119
|
};
|
|
@@ -105,8 +123,9 @@ function inferMode(changeDir) {
|
|
|
105
123
|
if (taskCount <= 2 && fileCount <= 2 && !hasSchemaChange && !hasNewModule) {
|
|
106
124
|
return {
|
|
107
125
|
mode: 'hotfix',
|
|
126
|
+
suggested_path: 'direct',
|
|
108
127
|
explicit: false,
|
|
109
|
-
reason: `≤2 tasks, ≤2 files, no schema/API/new-module keywords → hotfix`,
|
|
128
|
+
reason: `≤2 tasks, ≤2 files, no schema/API/new-module keywords → hotfix (trivial → suggest path direct; hotfix retained when reusing an existing contract)`,
|
|
110
129
|
};
|
|
111
130
|
}
|
|
112
131
|
|
|
@@ -114,16 +133,28 @@ function inferMode(changeDir) {
|
|
|
114
133
|
if (taskCount <= 4 && configDocOnly && !hasSchemaChange && !hasNewModule) {
|
|
115
134
|
return {
|
|
116
135
|
mode: 'tweak',
|
|
136
|
+
suggested_path: 'direct',
|
|
137
|
+
explicit: false,
|
|
138
|
+
reason: `≤4 tasks, only config/doc files, no schema/API/new-module keywords → tweak (trivial → suggest path direct)`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Arch/API/DB/聚合信号 → 建议 planned(轻架构)而非 full(§3.2 D1 修正)
|
|
143
|
+
if (archPathSignal && !fullSignal) {
|
|
144
|
+
return {
|
|
145
|
+
mode: 'full',
|
|
146
|
+
suggested_path: 'planned',
|
|
117
147
|
explicit: false,
|
|
118
|
-
reason:
|
|
148
|
+
reason: `${taskCount} tasks, ${fileCount} files${hasSchemaChange ? ', schema/API change detected' : ''}${hasArchAggregate ? ', arch/aggregate keywords detected' : ''} → suggest path planned (light architecture); front door: workflow start --path planned (new change) or tf state upgrade <dir> planned (existing change)`,
|
|
119
149
|
};
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
// Default
|
|
123
153
|
return {
|
|
124
154
|
mode: 'full',
|
|
155
|
+
suggested_path: null,
|
|
125
156
|
explicit: false,
|
|
126
|
-
reason: `${taskCount} tasks, ${fileCount} files${codeFileCount > 0 ? ` (${codeFileCount} code files)` : ''}${hasSchemaChange ? ', schema/API change detected' : ''}${hasNewModule ? ', new module detected' : ''} → full`,
|
|
157
|
+
reason: `${taskCount} tasks, ${fileCount} files${codeFileCount > 0 ? ` (${codeFileCount} code files)` : ''}${hasSchemaChange ? ', schema/API change detected' : ''}${hasNewModule ? ', new module detected' : ''}${hasCrossModule ? ', cross-module detected' : ''} → full`,
|
|
127
158
|
};
|
|
128
159
|
}
|
|
129
160
|
|
|
@@ -584,16 +584,29 @@ function updateIndex(globalArchDir, changeName) {
|
|
|
584
584
|
|
|
585
585
|
/* ============ Step 1: 预检 ============ */
|
|
586
586
|
|
|
587
|
-
function preCheck(changeDir, archDir, projectRoot) {
|
|
587
|
+
function preCheck(changeDir, archDir, projectRoot, light = false) {
|
|
588
588
|
const conflicts = [];
|
|
589
589
|
if (!existsSync(archDir)) {
|
|
590
590
|
return { pass: false, conflicts: ['architecture/ directory not found in change'] };
|
|
591
591
|
}
|
|
592
|
-
|
|
592
|
+
// v0.64.0(§4 D8,--light):light 模式持久源只要求 architecture.md(聚合注册行 +
|
|
593
|
+
// 演进日志段 = marker 区投影唯一输入);api.md/database.md 触及时才要求(调用方按
|
|
594
|
+
// touched surface 判定,此处仅对已存在文件做非空校验——缺失即该 sink 无增量,
|
|
595
|
+
// 生成式重建会安全保留旧表而非抹除)。full 模式维持三件套硬要求。
|
|
596
|
+
const required = light ? ['architecture.md'] : ['architecture.md', 'database.md', 'api.md'];
|
|
597
|
+
for (const f of required) {
|
|
593
598
|
const fp = join(archDir, f);
|
|
594
599
|
if (!existsSync(fp)) conflicts.push(`architecture/${f} missing`);
|
|
595
600
|
else if (readFileSync(fp, 'utf-8').trim().length === 0) conflicts.push(`architecture/${f} empty`);
|
|
596
601
|
}
|
|
602
|
+
if (light) {
|
|
603
|
+
for (const f of ['database.md', 'api.md']) {
|
|
604
|
+
const fp = join(archDir, f);
|
|
605
|
+
if (existsSync(fp) && readFileSync(fp, 'utf-8').trim().length === 0) {
|
|
606
|
+
conflicts.push(`architecture/${f} empty (present but blank — delete it or fill the delta)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
597
610
|
return { pass: conflicts.length === 0, conflicts };
|
|
598
611
|
}
|
|
599
612
|
|
|
@@ -705,6 +718,7 @@ export function run(args = {}) {
|
|
|
705
718
|
for (let i = 0; i < args.length; i++) {
|
|
706
719
|
if (args[i] === '--project-root' && args[i + 1] && !args[i + 1].startsWith('--')) parsed.projectRoot = args[++i];
|
|
707
720
|
else if (args[i] === '--dry-run') parsed.dryRun = true;
|
|
721
|
+
else if (args[i] === '--light') parsed.light = true;
|
|
708
722
|
else if (!args[i].startsWith('--')) parsed._.push(args[i]);
|
|
709
723
|
}
|
|
710
724
|
args = parsed;
|
|
@@ -712,8 +726,9 @@ export function run(args = {}) {
|
|
|
712
726
|
|
|
713
727
|
const changeDir = args._?.[0] || args.changeDir;
|
|
714
728
|
const dryRun = args.dryRun || false;
|
|
729
|
+
const light = args.light || false; // v0.64.0 §4 D8:planned 轻回写入口
|
|
715
730
|
if (!changeDir) {
|
|
716
|
-
console.error('Usage: tf arch-merge <change-dir> [--project-root <path>] [--dry-run]');
|
|
731
|
+
console.error('Usage: tf arch-merge <change-dir> [--project-root <path>] [--dry-run] [--light]');
|
|
717
732
|
process.exit(1);
|
|
718
733
|
}
|
|
719
734
|
|
|
@@ -733,6 +748,7 @@ export function run(args = {}) {
|
|
|
733
748
|
console.log(` Source: ${archDir}`);
|
|
734
749
|
console.log(` Target: ${globalArchDir}`);
|
|
735
750
|
if (dryRun) console.log(` Mode: DRY-RUN`);
|
|
751
|
+
if (light) console.log(` Mode: LIGHT(minimal persistent source: architecture.md + present deltas;同 sink 同生成器)`);
|
|
736
752
|
|
|
737
753
|
// 写锁(硬门禁 3)
|
|
738
754
|
const lockPath = acquireLock(projectRoot);
|
|
@@ -740,7 +756,7 @@ export function run(args = {}) {
|
|
|
740
756
|
if (!existsSync(globalArchDir)) makeDir(globalArchDir);
|
|
741
757
|
|
|
742
758
|
// Step 1: 预检
|
|
743
|
-
const preCheckResult = preCheck(changeDir, archDir, projectRoot);
|
|
759
|
+
const preCheckResult = preCheck(changeDir, archDir, projectRoot, light);
|
|
744
760
|
if (!preCheckResult.pass) {
|
|
745
761
|
console.error(' Pre-check FAILED:');
|
|
746
762
|
for (const c of preCheckResult.conflicts) console.error(` - ${c}`);
|