@xulthekl/team-flow 0.63.0 → 0.65.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 +57 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/agents/architecture-design.md +1 -1
- package/agents/architecture-reviewer.md +2 -2
- 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-gate-exemptions.mjs +5 -3
- package/scripts/guard/checks/arch-merged-light.mjs +67 -0
- package/scripts/guard/checks/arch-readiness.mjs +1 -1
- package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
- package/scripts/guard/checks/arch-snapshot.mjs +5 -3
- 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 +404 -54
- package/scripts/lib/arch-parse.mjs +5 -2
- package/scripts/lib/arch-registry.mjs +523 -0
- package/scripts/lib/arch-scan-code.mjs +518 -0
- package/scripts/lib/cmd-arch.mjs +9 -1
- package/scripts/lib/cmd-doctor.mjs +1 -1
- package/scripts/lib/cmd-execution.mjs +44 -1
- package/scripts/lib/cmd-state.mjs +94 -4
- package/scripts/lib/config-loader.mjs +20 -0
- 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 +6 -3
- package/skills/architecture-design/SKILL.md +29 -11
- package/skills/architecture-design/chapters/ch04-entity-to-aggregate.md +18 -7
- package/skills/architecture-design/chapters/ch06-integration.md +12 -3
- package/skills/architecture-design/glossary.md +5 -1
- package/skills/architecture-design/references/adr-templates.md +56 -0
- package/skills/architecture-design/references/context-map-8.md +47 -0
- package/skills/architecture-design/references/ddd-evented-playbook.md +41 -0
- package/skills/architecture-design/references/s3.5-architecture-template.md +36 -4
- package/skills/architecture-design/references/s3.5-loading-protocol.md +5 -4
- package/skills/architecture-design/references/s3.5-product-architecture.md +6 -6
- package/skills/architecture-design/templates/architecture.md +20 -0
- package/skills/ce-compound/references/concepts-vocabulary.md +1 -1
- package/skills/ce-compound/references/full-mode-workflow.md +2 -2
- package/skills/ce-compound/references/lightweight-mode.md +1 -1
- package/skills/clean-code/SKILL.md +1 -1
- package/skills/jarvis/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +60 -13
- package/skills/release-archivist/references/closing-procedures.md +1 -1
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/test-strategy/SKILL.md +1 -1
- package/skills/workflow-orchestrator/SKILL.md +2 -2
- package/skills/workflow-start/SKILL.md +63 -5
- package/skills/workflow-start/references/routing-rules.md +4 -4
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// scripts/guard/checks/cross-change-consistency-light.mjs — 跨 change 冲突确定性初筛(§3.3)
|
|
2
|
+
// v0.64.0(A-14/S-07 重定义):guard runner 是 10s 内确定性 Node 函数,**不能调 LLM agent**。
|
|
3
|
+
// 本 runner 做机械初筛:其他在途 change(state ∈ executing/debugging/approved-for-build)
|
|
4
|
+
// 的 minimal api.md 端点路径 / architecture.md 聚合行 与本 change 的最小源求交——
|
|
5
|
+
// 交集非空 = 共享 surface 冲突。语义级冲突由 release-archivist 关门序列派发
|
|
6
|
+
// cross-change-consistency-checker agent 兜底(分工见 §3.3)。
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { join, basename, dirname } from 'node:path';
|
|
9
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
10
|
+
import { findProjectRoot } from '../../lib/surface-scan.mjs';
|
|
11
|
+
|
|
12
|
+
const IN_FLIGHT = new Set(['approved-for-build', 'executing', 'debugging']);
|
|
13
|
+
|
|
14
|
+
function extractApiPaths(file) {
|
|
15
|
+
if (!existsSync(file)) return new Set();
|
|
16
|
+
const text = readFileSync(file, 'utf-8');
|
|
17
|
+
// 端点表单元格里的路径 token(/api/xxx、/v1/xxx、{id} 形态)
|
|
18
|
+
const out = new Set();
|
|
19
|
+
for (const m of text.matchAll(/(\/[A-Za-z0-9_\-{}./]+)/g)) {
|
|
20
|
+
const p = m[1];
|
|
21
|
+
if (p.length > 1 && !p.endsWith('.md')) out.add(p.replace(/\/+$/, ''));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractAggregateNames(file) {
|
|
27
|
+
if (!existsSync(file)) return new Set();
|
|
28
|
+
const text = readFileSync(file, 'utf-8');
|
|
29
|
+
const out = new Set();
|
|
30
|
+
for (const m of text.matchAll(/聚合[::\s*`]+[`*]*([A-Za-z][A-Za-z0-9_\-]*)/g)) out.add(m[1]);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function checkCrossChangeConsistencyLight(changeDir) {
|
|
35
|
+
const state = readState(changeDir);
|
|
36
|
+
const projectRoot = findProjectRoot(changeDir);
|
|
37
|
+
const changesDir = join(projectRoot, 'changes');
|
|
38
|
+
if (!existsSync(changesDir)) return { pass: true, failures: [], reason: 'no changes/ dir' };
|
|
39
|
+
|
|
40
|
+
const selfName = state.change_name || basename(changeDir);
|
|
41
|
+
const selfApi = extractApiPaths(join(changeDir, 'architecture', 'api.md'));
|
|
42
|
+
const selfAgg = extractAggregateNames(join(changeDir, 'architecture', 'architecture.md'));
|
|
43
|
+
|
|
44
|
+
const conflicts = [];
|
|
45
|
+
for (const entry of readdirSync(changesDir, { withFileTypes: true })) {
|
|
46
|
+
if (!entry.isDirectory() || entry.name === selfName) continue;
|
|
47
|
+
const otherDir = join(changesDir, entry.name);
|
|
48
|
+
const otherStatePath = join(otherDir, '.team-flow.yaml');
|
|
49
|
+
if (!existsSync(otherStatePath)) continue;
|
|
50
|
+
const other = readState(otherDir);
|
|
51
|
+
if (!IN_FLIGHT.has(other.state)) continue;
|
|
52
|
+
|
|
53
|
+
const otherApi = extractApiPaths(join(otherDir, 'architecture', 'api.md'));
|
|
54
|
+
const otherAgg = extractAggregateNames(join(otherDir, 'architecture', 'architecture.md'));
|
|
55
|
+
|
|
56
|
+
const apiOverlap = [...selfApi].filter(p => otherApi.has(p));
|
|
57
|
+
const aggOverlap = [...selfAgg].filter(a => otherAgg.has(a));
|
|
58
|
+
if (apiOverlap.length || aggOverlap.length) {
|
|
59
|
+
conflicts.push(
|
|
60
|
+
`${entry.name}: api=[${apiOverlap.slice(0, 5).join(', ')}] aggregates=[${aggOverlap.slice(0, 5).join(', ')}]`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (conflicts.length) {
|
|
66
|
+
return {
|
|
67
|
+
pass: false,
|
|
68
|
+
failures: [
|
|
69
|
+
`shared surface conflicts with in-flight change(s):\n ${conflicts.join('\n ')} `
|
|
70
|
+
+ '— resolve ownership or stagger closing; semantic-level review via cross-change-consistency-checker agent',
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { pass: true, failures: [] };
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// scripts/guard/checks/direct-short-path.mjs — direct/lightweight 短路径 + 架构 surface 扫描
|
|
2
|
+
// v0.64.0(实施计划 §4,G1 加固):
|
|
3
|
+
// - 挂 quick/lightweight 的 exploring→approved-for-build / approved-for-build→executing / closing
|
|
4
|
+
// - G4 前提保障:无架构 surface(API/DB/聚合)→ 过(真琐碎零负担)
|
|
5
|
+
// - 命中架构 surface → FAIL 并给升级指引(禁止静默过);扫描失败/清单缺失一律 fail-closed
|
|
6
|
+
import { readState } from '../../lib/state-loader.mjs';
|
|
7
|
+
import { scanArchitectureSurface } from '../../lib/surface-scan.mjs';
|
|
8
|
+
|
|
9
|
+
const UPGRADE_HINT =
|
|
10
|
+
'architecture surface (API/DB/aggregate) touched — upgrade to planned: '
|
|
11
|
+
+ 'tf state upgrade <change-dir> planned (keeps code/test evidence, rolls back to approved-for-build, '
|
|
12
|
+
+ 're-runs planned gates + light writeback sequence; downgrades rejected)';
|
|
13
|
+
|
|
14
|
+
export function checkDirectShortPath(changeDir) {
|
|
15
|
+
const state = readState(changeDir);
|
|
16
|
+
const scan = scanArchitectureSurface(changeDir, state);
|
|
17
|
+
|
|
18
|
+
if (scan.error === 'no-baseline') {
|
|
19
|
+
return {
|
|
20
|
+
pass: false,
|
|
21
|
+
failures: [
|
|
22
|
+
'no scan baseline: base_sha missing and no origin/<default> to derive merge-base — '
|
|
23
|
+
+ 'run inside a git repo with at least one commit (tf state init stamps base_sha), fail-closed',
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (scan.error === 'diff-failed') {
|
|
28
|
+
return {
|
|
29
|
+
pass: false,
|
|
30
|
+
failures: [`git diff against base failed (base=${scan.base ?? 'unknown'}) — cannot verify architecture surface, fail-closed`],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (scan.error === 'aggregate-list-missing') {
|
|
34
|
+
return {
|
|
35
|
+
pass: false,
|
|
36
|
+
failures: [
|
|
37
|
+
'aggregate list missing: create .team-flow/aggregate-dirs.txt (one dir prefix per line; '
|
|
38
|
+
+ 'empty file = explicitly no aggregates) or run workflow-bootstrap — fail-closed, aggregate surface is blind otherwise',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (scan.architectureSurface.length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
pass: false,
|
|
46
|
+
failures: [
|
|
47
|
+
`${UPGRADE_HINT}\n touched: ${scan.architectureSurface.slice(0, 10).join(', ')}`,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { pass: true, failures: [] };
|
|
52
|
+
}
|
|
@@ -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 = [];
|